npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

batchql

v1.1.18

Published

Don't just graphql, batchql.

Downloads

93

Readme

BatchQL

logo

NPM

BatchQL is a language-level query optimizer for GraphQL.

Note: Not all GraphQL features are perfectly supported, there will be some caveats that come with the usage of a tool like this. Right now, you should handle mutations and subscriptions through standard means, as the batching logic has been written with queries in mind. But mutations are really close to working :)

Installation

npm install --save batchql
# or
yarn add batchql

Playground & Usage

You can play with the code by copy+pasting the following into https://matthiasak.github.io/arbiter-frame. This example uses the GitHub GraphQL API to demonstrate the effectiveness of logical query batching :)

const token = `${'4eb1f1a7b25729f6'}${'27d570e88688ef866017388e'}`

const app = $ => {
    const {mux, batch, debug} = batchql
    
    const fetcher = (url) => (query, args) =>
    (debug() && log({batchedQuery: query}, '')) ||
    fetch(
        url, 
        { 
            method: 'POST', 
            headers: {
                "Content-Type": 'application/json',
                "Authorization": `Bearer ${token}`
            }, 
            body: JSON.stringify({ query, variables: args }) 
        })
    .then(r => r.json())
    .then(f => {
        if(f.errors) throw new Error(f.errors.map(e => '\n- '+e.message).join(''))
        return f.data
    })

    // register a free GraphQL endpoint at https://graph.cool
    const f = mux(fetcher('https://api.github.com/graphql'), 100)

    // want debug messages for parsing graphQL syntax? uncomment below
    debug(true)
    
    f(`($name: String!){ 
      topic(name: $name) {
        name
        relatedTopics {
          id
          name
        }
      }
    }`, {name: 'typescript'})
    .then(d => log(d, ''))
    
    f(`($name: String!){ 
      topic(name: $name) {
        name
        relatedTopics {
          id
          name
        }
      }
    }`, {name: 'ruby'})
    .then(d => log(d, ''))
    
    f(`($name: String!){ 
      topic(name: $name) {
        name
        relatedTopics {
          id
          name
        }
      }
    }`, {name: 'ruby'})
    .then(d => log(d, ''))
    
    f(`($name: String!){ 
      topic(name: $name) {
        name
        relatedTopics {
          id
          name
        }
      }
    }`, {name: 'ruby'})
    .then(d => log(d, ''))
}

require('[email protected]').then(app).catch(e => log(e))

This is Dark Magic...

Not really :-) For a given time-slice, one part of BatchQL's algorithm (the mux()) can take all calls for a given X ms and then send all of those query strings to the batch() method.

The old method (BatchQL pre-1.0) used to take a best guess on batching the queries together by finding a general query statement and matching on parens or curly braces, but if the input query had invalid syntax then the algorithm kinda just blew up.

It used to kinda do something like this:

const mergedQueries = batch('query { allPersons { name } }', 'query { allPersons { email } }', 'query { allPersons { age } }')
mergedQueries
// query { 
//  item0: allPersons { name } 
//  item1: allPersons { email } 
//  item2: allPersons { age } 
// }

This merely batched the queries together, but didn't do an actual logical merge of the queries. But no more! Now we have an actual tree-merging batching mechanisms that doesn't batch messages together, it batches the logical queries. Yey!

const mergedQueries = batch('query { allPersons { name } }', 'query { allPersons { email } }', 'query { allPersons { age } }')
mergedQueries
// query { allPersons { name email age }  }
// sooooooooooooo much more efficient as queries get larger

How?

Parsers, combinators, and parser-combinators.

Parser combinators propose a middle ground between hand-written parsers and generated parsers. They are made of small functions that handle basic parts of a format, like recognizing a word in ASCII characters or a null byte. Those functions are then composed in more useful building blocks like the pair combinator that applies two parsers in sequence, or the choice combinator that tries different parsers until one of them succeeds.

The functions are completely deterministic and hold no mutable state. The deterministic behaviour of parser combinators simplifies writing the state machine that manages data accumulation.

We can actually parse GraphQL query strings into a lightweight Abstract Syntax Tree (AST), and then with multiple ASTs merge them into a single tree with some quick recursive logic.

You can check out each piece in the various files under the /src folder.

  • /src/parsers.ts - code for parsing tokens
  • /src/combinators.ts - code for the parser combinators that contain the logic to parse entire GraphQL queries
  • /src/merge.ts - code for merging multiple ASTs into a single AST, generating extraction maps for parsing out the tree of data requested by each parallelized query, and logic for renaming query variables and aliases/query-fields that may have collisions (with built-in reverse maps)
  • /src/regenerate.ts - code for generating a GraphQL string from an AST
  • /src/batchql.ts - code for the muxer, a general fetcher, and the batch method, plus applying extraction maps to the returned data

Embracing parser combinators

Changing the language is important, but it is not enough. Another component is required to help fix logical bugs, and make sure parsers are both easier to write and do not contain errors.

Many low-level parsers ‘in the wild’ are written by hand, often because they need to deal with binary formats, or in a quest for better performance. Often these end up with hard to maintain code. At the other end of the spectrum there are parser generators, which tend to produce parsers of good quality but still present challenges in writing the grammar and integrating context specific libraries.

In between these two choices lie parser combinators.

Parser combinators propose a middle ground between hand-written parsers and generated parsers. They are made of small functions that handle basic parts of a format, like recognizing a word in ASCII characters or a null byte. Those functions are then composed in more useful building blocks like the pair combinator that applies two parsers in sequence, or the choice combinator that tries different parsers until one of them succeeds.

The functions are completely deterministic and hold no mutable state. The deterministic behaviour of parser combinators simplifies writing the state machine that manages data accumulation.

Strengths of this approach

If you choose to create "Service Oriented Components", meaning your components can request their own data (such as onMount), you run into sometimes 5 or 10 parallel graphql requests heading to your server:


  +----------------------------+
  |                            |
  |    header                  |  header -> `{ user { ...menuItems }}`
  |                            |
  |                            |
  |                            |
  +----------------------------+
  |                            |
  |  +-------------------------+
  |  |     ||     ||     ||   ||
  |  |  a  ||  b  ||  c  ||   ||  a, b, c, d, e -> `{ content(id:???) { title, date, summary } }
  |  |     ||     ||     ||   ||
  |  +--------------------|   ||
  |  +--------------------| e ||
  |  |                   ||   ||
  |  |                   ||   ||
  |  |        d          ||   ||
  |  |                   ||   ||
  |  |                   ||   ||
  |  +-------------------------|
  +----------------------------+

With the 6 queries above it could take a lot longer to get the data you need to render to the screen quickly.

BatchQL treats your queries like they are meant to be: logically independent (as much as I could do in a few days' time :D). So, using BatchQL is pretty straightforward and hopefully not too leaky of an abstraction:

// step 1. import batchql
// mux :: (string -> object -> Promise) -> (string -> object -> Promise)
import mux from 'batchql'

// step 2. create your function the posts to your graphql endpoint
const get = (url, query, args) =>
	fetch(
		url
		, {
			method: 'POST'
			, headers: Object.assign(
				{'Content-Type': 'application/json'}
				, token ? {'authorization': `Bearer ${token}`} : {})
			, body: JSON.stringify({ query, variables: args })
		})
		.then(r => r.json()))

// step 3. batch it!
const batchedGet = batchql(get.bind(null, 'https://mygraphqlendpoint.com/graphql'))

// step 4. use it err'where?
batchedGet(`{ user { id } }`).then(response => console.log(response.data.user))
batchedGet(`{ notifications { comments } }`).then(response => console.log(response.data.comments))
batchedGet(`{ messages { text, from } }`).then(response => console.log(response.data.messages))

The end result of using batchql:



   with batchql:



  +----------------------------+
  |                            |
  |    header                  |  header +> `{ user { ...menuItems }}`+
  |                            |                                      |
  |                            |                                      |
  |                            |                                      |
  +----------------------------+                                      |
  |                            |                                      |
  |  +-------------------------+                                      |
  |  |     ||     ||     ||   ||                                      +
  |  |  a  ||  b  ||  c  ||   ||  a, b, c, d, e +> `{ content(id:???) { title, date, summary } }
  |  |     ||     ||     ||   ||  +-------------------------------+   +
  |  +--------------------|   ||                      |  |  |  |  |   |
  |  +--------------------| e ||                      |  |  |  |  |   |
  |  |                   ||   ||                      |  |  |  |  |   |
  |  |                   ||   ||                      |  |  |  |  |   |
  |  |        d          ||   ||                      |  |  |  |  |   |
  |  |                   ||   ||                      |  |  |  |  |   |
  |  |                   ||   ||                      |  |  |  |  |   |
  |  +-------------------------|                      |  |  |  |  |   |
  +----------------------------+               +------v--v--v--v--v---v------------+
                                               |                                   |
                                               |         batched querying          |
                                               |                                   |
                                               |                                   |
                                               +-----------------------------------+
                                                                |
                                                         SINGLE QUERY!! :D
                                                                |
                                                            XXXX|XX  XXXXX
                                                  XXXXXXXXXXX   v         XXX
                                                 X                          X
                                           XXXX XX                          XXXXXX X
                                        XXX                                        X
                                        X                                          XX
                                        X             graphql endpoint              X
                                        X                                            X
                                        X                                            XX
                                          XXXXXXXXXXXXXXXXXX                          X
                                                           XX                        XX
                                                            XX         XXXXXXX      XX
                                                              XXXXXXXXX      XXXX XXX
                                                                                XXX

Caught a bug?

  1. Fork this repository to your own GitHub account and then clone it to your local device
  2. Install the dependencies: yarn
  3. Bundle the source code and watch for changes: npm start

After that, you'll find the code in the ./build folder!

Authors

  • Matthew Keas, @matthiasak. Need help / support? Create an issue or ping on Twitter.

Credits

Thanks to Amy Morgan on The Noun Project for the logo!