Frontend Dad Blog.

Streams, Sockets, Protocols...

Cover Image for Streams, Sockets, Protocols...

React has a lot going on right now when it comes to getting data from the server to the client. A lot of this centers on the concept of streaming, which requires a different mental model than the typical pattern of fetching data, waiting for it all to arrive, and then processing it. This got me thinking a bit, and I wanted to unpack some of my concepts of protocols to make sure that they still comply with Al Gore's initial vision of the Internet.

Background

The web lives and dies (mostly) via the HTTP protocol. This is the one with the methods, the "stateless", short lived connections. Data gets sent in a big one-off chunk and then the clients deal with it once the connection is closed right? WRONG (kind of).

Streaming already happens

Browsers actually stream everything. Think of an image loading in, or even document markup. It's tough to catch these days on a decent connection, but browsers process information in real time. They don't actually wait for everything to have been transferred before acting on it. Although the HTTP connection is still considered stateless and short lived, it's not so stateless and short lived that clients have to wait until it's over/closed before doing anything with the packets they receive. Modern Javascript streaming APIs simply let us tap into these connections and start acting immediately, and the use of Fetch to do that IS new.

TCP vs HTTP and "real time" connections

Traditionally, there have been two ways to create a perceived "real time" connection between client and server.

Polling is the process of simply setting a timer and sending HTTP requests to a listening server for new information. The server takes no active part in pushing updates here - it simply responds with whatever information it has at the time.

The ol' WebSocket is actually a TCP connection. Take this time to refresh yourself on the 7 layer OSI DIP. The TCP socket allows a persistent, long lived connection between server and client that lasts until one of the two decides to end it. This scheme lets servers actively push updates to clients without clients requesting them, and it's the basis for most real time apps out there today.

I'll throw a mention for WebHooks in here as well, as they can facilitate a kind of real time connection, but this is just a server POSTing some data to an endpoint defined by a client. The endpoint is in charge of reacting to any updates.

Streams (just like it sounds)

Alright- so the whole point of this post was to explore streams, specifically how it works with Fetch. Before we involve a network, however, it's interesting to think through how we can create a stream to pipe some data around a program. Check out the below example, ripped from Vercel's docs:

const decoder = new TextDecoder();
const encoder = new TextEncoder();

const readableStream = new ReadableStream({
  start(controller) {
    const text = "Stream me!";
    controller.enqueue(encoder.encode(text));
    controller.close();
  },
});

const transformStream = new TransformStream({
  transform(chunk, controller) {
    const text = decoder.decode(chunk);
    controller.enqueue(encoder.encode(text.toUpperCase()));
  },
});

const writableStream = new WritableStream({
  write(chunk) {
    console.log(decoder.decode(chunk));
  },
});

readableStream
   .pipeThrough(transformStream)
   .pipeTo(writableStream); // STREAM ME!

The example here simply creates a stream of text that is encoded, "piped" through a transformer to decode it, and then finally sent somewhere to be written. It's a contrived example, but it does represent the 3 phases of a stream, as well as the JS APIs we use to interact with / manipulate data.

With that in mind, we can look at how Fetch can natively implement these concepts. This is again an example from Vercel's docs

const decoder = new TextDecoder();

const response = await fetch('/api/stream');
const reader = response.body.getReader();

let done = false;

while (!done) {
  const { value, done: doneReading } = await reader.read();
  done = doneReading;
  const data = JSON.parse(decoder.decode(value));
  // Do something with data
} 

Here, we tap into the Fetch API and use the getReader method on the response body. It's important to note that getReader is not guaranteed to exist on the body - the server needs to be configured such that the response will provide it. Most text or JSON responses should have this defined.

The program then simply sets a loop, and reads the stream until the doneReading value is passed as true. We can see how an application could process the incoming stream and append it to the DOM, for example. This is how the ChatGPT webapp works.

Further

Wow this post spun out of control. There is a ton more to streaming and how it's actually used in production applications. Read about the concepts of backpressure or "tees" to really get into it.

Sources

Vercel

MDN