Skip to content

Instantly share code, notes, and snippets.

@benjie
Last active March 18, 2024 10:55
Show Gist options
  • Save benjie/f696f494878ddebb423c978ccb3a39df to your computer and use it in GitHub Desktop.
Save benjie/f696f494878ddebb423c978ccb3a39df to your computer and use it in GitHub Desktop.
Alternative ways of representing incremental delivery over HTTP

Streamed JSONL

Each payload is just appended as a condensed JSON (no newlines!) payload, with a newline at the end:

HTTP/1.1 200 OK
Content-Type: application/graphql-response+jsonl
Cache-Control: no-cache

{"data":{...},"hasNext":true}
{"incremental":[...],"hasNext":true}
{}
{"incremental":[...],"hasNext":true}
{"hasNext":false}

{} is a keepalive message.

The parser for JSONL would be trivial-keep accumulating data until you hit a \n, then parse that as JSON and throw it away. Incredibly easy to implement for clients.

We would need to add any multiplexing information to the response payloads themselves.

Here's an example of an incredibly trivial parser for it (may or may not work, I wrote it direct into GitHub):

async function handleEvents(url, cb) {
  const response = await fetch(url);
  const decoder = new TextDecoderStream();
  const stream = response.body.pipeThrough(decoder);

  let current = '';
  for await (const chunk of stream) {
    current += chunk;
    let i;
    while ((i = current.indexOf("\n")) >= 0) {
      const json = JSON.parse(current.substring(0, i));
      current = current.substring(i + 1);
      if (Object.keys(json).length === 0) {
        // Keepalive
      } else {
        cb(json);
      }
    }
  }
}

For HTTP/1.1, for greater efficiency you could use Transfer-Encoding: chunked such that the connection is not terminated once the request is finished. I don't believe this is a concern in HTTP2+.

HTTP Multipart encoding

HTTP/1.1 200 OK
Accept: multipart/mixed;boundary=--graphql;subscriptionSpec="1.0"
Cache-Control: no-cache
Connection: keep-alive

--graphql
Content-Type: application/json

{"data":{...},"hasNext":true}

--graphql
Content-Type: application/json

{"incremental":[...],"hasNext":true}

--graphql
Content-Type: application/json

{}

--graphql
Content-Type: application/json

{
  "incremental": [...],
  "hasNext": true
}

--graphql
Content-Type: application/json

{"hasNext":false}

--graphql--

I'm not writing a parser for this into GitHub, but you'd just use one of the many off-the-shelf multipart parsers.

Event Stream

Each payload is wrapped in an event stream body (i.e. event: next\ndata: prefix added and \n\n suffix appended; plus any newlines are replaced with \ndata: ):

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

event: next
data: {"data":{...},"hasNext":true}

event: next
data: {"incremental":[...],"hasNext":true}

:

event: next
data: {
data:   "incremental": [...],
data:   "hasNext": true
data: }

event: next
data: {"hasNext":false}

event: complete

The : payload is a "comment" used for keepalive.

Note I formatted the third payload to indicate what it would look like if it spanned multiple lines.

Parsing this would be relatively straightforward (again, this is untested pseudocode!):

async function handleEvents(url, cb) {
  const response = await fetch(url);
  const decoder = new TextDecoderStream();
  const stream = response.body.pipeThrough(decoder);

  let current = '';
  for await (const chunk of stream) {
    current += chunk;
    let i;
    while ((i = current.indexOf("\n\n")) >= 0) {
      const payload = parseSSE(current.substring(0, i));
      current = current.substring(i + 1);
      if (Object.keys(payload.data).length === 0) {
        // Keepalive
      } else {
        cb(payload.data);
      }
    }
  }
}
function parseSSE(text) {
  const lines = text.split("\n");
  const obj = Object.create(null);
  for (const line of lines) {
    if (line.startsWith(":")) continue;
    const c = line.indexOf(":");
    const key = line.substring(0, c);
    let value = line.substring(c+1);
    if (value.startsWith(" ")) value = value.substring(1);
    if (!obj[key]) {
      obj[key] = value;
    } else {
      obj[key] += '\n';
      obj[key] += value
    }
  }
  return obj;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment