Send event (SSE) streams using the servers of Node and Koa

Keywords: Javascript node.js

            

  When you want to update your Web application in real time, you can rely on old-fashioned periodic polling or try some modern technology with push function. Your first impulse may be to use WebSockets . However, if you only want to receive data from the server, you can use Server Sent Events.

Traditionally, web pages must send a request to the server to receive new data; That is, the page requests data from the server. use server-sent events , the server can send new data to the web page at any time by pushing the message to the web page. These incoming messages can be regarded as events + data in the web page.

You can view This article To understand the difference between SSE and Websocket, and express their views on when to use one of them. For my use case, receive updates from the server regularly, and I will stick to SSE.

Use Koa's SSE Basics

Let's start by building a Koa based HTTP server.

  • It will have a 200 state response to capture all routes.
  • It will have a / SSE endpoint. When receiving a request, it will adjust some socket parameters to ensure that our connection remains open and return the appropriate HTTP header to start a new SSE stream.
  • We will create a new one PassThrough Stream (a stream that simply passes input bytes to output) and passes it as our response body.
  • Finally, we will generate a data feed using a simple interval that will periodically write the current timestamp to the stream; Therefore, the data is pushed to the client through an open connection.
const Koa = require("koa");
const { PassThrough } = require("stream");

new Koa().
  use(async (ctx, next) => {
    if (ctx.path !== "/sse") {
      return await next();
    }

    ctx.request.socket.setTimeout(0);
    ctx.req.socket.setNoDelay(true);
    ctx.req.socket.setKeepAlive(true);

    ctx.set({
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      "Connection": "keep-alive",
    });

    const stream = new PassThrough();

    ctx.status = 200;
    ctx.body = stream;

    setInterval(() => {
      stream.write(`data: ${new Date()}\n\n`);
    }, 1000);
  })
  .use(ctx => {
    ctx.status = 200;
    ctx.body = "ok";
  })
  .listen(8080, () => console.log("Listening"));

Note two points: first, the output data must comply with SSE format . Second, a flow must be returned as the principal response to ensure that Koa does not close the connection. You can drill down into the Koa source code (check this) method )To see how Koa handles the response. If you look at it, you will see that Koa will send the body content to the HTTP response stream unless you use another stream as the body. In this case, it will transmit the flow through the pipeline; Therefore, the response flow will not be closed until we close the PassThrough flow.

To test our SSE stream in the browser, we should use EventSource API.

http://localhost:8080 Access in the browser, open the console and delete the following code snippet to use the server message.

const source = new EventSource("http://localhost:8080/sse");
source.onopen = () => console.log("Connected");
source.onerror = console.error;
source.onmessage = console.log;

Close flow

If you reload the browser or close the source (using the close() method), your server will be interrupted. The interval will try to write on the stream, and then... It disappears!

We must be careful when dealing with closures, which is our flow in this case. Our interval does not know that it must stop providing data to the stream.

To solve this problem, we can attach ourselves to flowcloseevent And "unsubscribe" from the data feed.

const interval = setInterval(() => {
  stream.write(`data: ${new Date()}\n\n`);
}, 1000);

stream.on("close", () => {
  clearInterval(interval);
});

Broadcast data

The previous example generates a new data feed for each connected client. In real-world scenarios, we also want to broadcast the same data to different clients.

We can add a simple EventEmitter And move the data generation out of the connection code to see how it works.

const Koa = require("koa");
const { PassThrough } = require("stream")
const EventEmitter = require("events");

const events = new EventEmitter();
events.setMaxListeners(0);

const interval = setInterval(() => {
  events.emit("data", new Date() });
}, 1000);

new Koa().
  use(async (ctx, next) => {
    if (ctx.path !== "/sse") {
      return await next();
    }

    ctx.request.socket.setTimeout(0);
    ctx.req.socket.setNoDelay(true);
    ctx.req.socket.setKeepAlive(true);

    ctx.set({
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      "Connection": "keep-alive",
    });

    const stream = new PassThrough();
    ctx.status = 200;
    ctx.body = stream;

    const listener = (data) => {
      stream.write(`data: ${data}\n\n`);
    }

    events.on("data", listener);

    stream.on("close", () => {
      events.off("data", listener);
    });
  })
  .use(ctx => {
    ctx.status = 200;
    ctx.body = "ok";
  })
  .listen(8080, () => console.log("Listening"));

Format stream data

    As I mentioned earlier, SSE stream data must conform to a standardized format. In order to reduce the conversion from data object to SSE message       Pain, we will use custom Stream Transformer Swap our PassThrough stream.

  • The converter (line 5) converts the object to SSE text format. To simplify the example, we will only deal with pure data messages (line 13).
  • Our data feed will emit a data object with a timestamp key (line 22).
  • Our event listener writes the raw data to the converter (line 46).
    const Koa = require("koa");
    const { Transform } = require("stream");
    const EventEmitter = require("events");
    
    class SSEStream extends Transform {
      constructor() {
        super({
          writableObjectMode: true,
        });
      }
    
      _transform(data, _encoding, done) {
        this.push(`data: ${JSON.stringify(data)}\n\n`);
        done();
      }
    }
    
    const events = new EventEmitter();
    events.setMaxListeners(0);
     
    const interval = setInterval(() => {
      events.emit("data", { timestamp: new Date() });
    }, 1000);
    
    new Koa().
      use(async (ctx, next) => {
        if (ctx.path !== "/sse") {
          return await next();
        }
    
        ctx.request.socket.setTimeout(0);
        ctx.req.socket.setNoDelay(true);
        ctx.req.socket.setKeepAlive(true);
    
        ctx.set({
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
          "Connection": "keep-alive",
        });
    
        const stream = new SSEStream();
        ctx.status = 200;
        ctx.body = stream;
    
        const listener = (data) => {
          stream.write(data);
        };
    
        events.on("data", listener);
    
        stream.on("close", () => {
          events.off("data", listener);
        });
      })
      .use(ctx => {
        ctx.status = 200;
        ctx.body = "ok";
      })
      .listen(8080, () => console.log("Listening"));

    We must also make minor adjustments to our client to handle the data in the new JSON format.

  Add up

    This is just a simple example to illustrate SSE Technology: how easy it is to use it in nodes and how to use streams to           SSE is built into the Koa service. You can extend the SSEStream of the sample to support all Event flow format wait.

    As I mentioned at the beginning of this article, when dealing with one-way communication from server to client, you can use websocket and service         You can choose between sending events. Each has advantages and disadvantages. Learn them and choose one that suits your problem.

  writer

  David Barral

Posted by trrobnett on Fri, 19 Nov 2021 12:04:47 -0800