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

@toverux/expresse

v2.4.0

Published

ExpreSSE: A better module for working with Server-Sent Events in Express

Downloads

1,233

Readme

ExpreSSE npm version license Travis Build npm total downloads

ExpreSSE is a set of middlewares - with a simple and elegant API - for working with Server-Sent Events (SSE) in Express. SSE is a simple unidirectional protocol that lets an HTTP server push messages to a client that uses window.EventSource. It's HTTP long-polling, without polling!

From the MDN:

Traditionally, a web page has to send a request to the server to receive new data; that is, the page requests data from the server. With server-sent events, it's possible for a server to send new data to a web page at any time, by pushing messages to the web page.



:package: Installation & Usage

Requirements:

  • Node.js 5+ because ExpreSSE is transpiled down to ES 6 ;
  • Express 4

Install it via the npm registry:

yarn add @toverux/expresse

TypeScript users: the library as distributed on npm already contains type definitions for TypeScript. :sparkles:

sse() middleware

  • Using ES 2015 imports:

    ISseResponse is a TypeScript interface. Don't try to import it when using JavaScript.

    import { ISseResponse, sse } from '@toverux/expresse';
       
    // named export { sse } is also exported as { default }:
    import sse from '@toverux/expresse';
  • Using CommonJS:

    const { sse } = require('@toverux/expresse');
interface ISseMiddlewareOptions {
    /**
     * Serializer function applied on all messages' data field (except when you direclty pass a Buffer).
     * SSE comments are not serialized using this function.
     *
     * @default JSON.stringify
     */
    serializer?: (value: any) => string | Buffer;

    /**
     * Whether to flush headers immediately or wait for the first res.write().
     *  - Setting it to false can allow you or 3rd-party middlewares to set more headers on the response.
     *  - Setting it to true is useful for debug and tesing the connection, ie. CORS restrictions fail only when headers
     *    are flushed, which may not happen immediately when using SSE (it happens after the first res.write call).
     *
     * @default true
     */
    flushHeaders: boolean;

    /**
     * Determines the interval, in milliseconds, between keep-alive packets (neutral SSE comments).
     * Pass false to disable heartbeats (ie. you only support modern browsers/native EventSource implementation and
     * therefore don't need heartbeats to avoid the browser closing an inactive socket).
     *
     * @default 5000
     */
    keepAliveInterval: false | number;
    
    /**
     * If you are using expressjs/compression, you MUST set this option to true.
     * It will call res.flush() after each SSE messages so the partial content is compressed and reaches the client.
     * Read {@link https://github.com/expressjs/compression#server-sent-events} for more.
     *
     * @default false
     */
    flushAfterWrite?: boolean;
}

:arrow_right: Read more about serializer

Usage example (remove ISseResponse when not using TypeScript):

// somewhere in your module
router.get('/events', sse(/* options */), (req, res: ISseResponse) => {
    let messageId = parseInt(req.header('Last-Event-ID'), 10) || 0;
    
    someModule.on('someEvent', (event) => {
        //=> Data messages (no event name, but defaults to 'message' in the browser).
        res.sse.data(event);
        //=> Named event + data (data is mandatory)
        res.sse.event('someEvent', event);
        //=> Comment, not interpreted by EventSource on the browser - useful for debugging/self-documenting purposes.
        res.sse.comment('debug: someModule emitted someEvent!');
        //=> In data() and event() you can also pass an ID - useful for replay with Last-Event-ID header.
        res.sse.data(event, (messageId++).toString());
    });
    
    // (not recommended) to force the end of the connection, you can still use res.end()
    // beware that the specification does not support server-side close, so this will result in an error in EventSource.
    // prefer sending a normal event that asks the client to call EventSource#close() itself to gracefully terminate.
    someModule.on('someFinishEvent', () => res.end());
});

sseHub() middleware

This one is very useful for pushing the same messages to multiples users at a time, so they share the same "stream".

It is based on the sse() middleware, meaning that you can still use res.sse.* functions, their behavior don't change. For broadcasting to the users that have subscribed to the stream (meaning that they've made the request to the endpoint), use the req.sse.broadcast.* functions, that are exactly the same as their 1-to-1 variant.

  • Using ES 2015 imports:

    ISseHubResponse is a TypeScript interface. Don't try to import it when using JavaScript.

    import { Hub, ISseHubResponse, sseHub } from '@toverux/expresse';
  • Using CommonJS:

    const { Hub, sseHub } = require('@toverux/expresse');

The options are the same from the sse() middleware (see above), plus another, hub:

interface ISseHubMiddlewareOptions extends ISseMiddlewareOptions {
    /**
     * You can pass a Hub instance for controlling the stream outside of the middleware.
     * Otherwise, a Hub is automatically created.
     * 
     * @default Hub
     */
    hub: Hub;
}

First usage example - where the client has control on the hub (remove ISseHubResponse when not using TypeScript):

// somewhere in your module
router.get('/events', sseHub(/* options */), (req, res: ISseHubResponse) => {
    //=> The 1-to-1 functions are still there
    res.sse.event('welcome', 'Welcome!');
    
    //=> But we also get a `broadcast` property with the same functions inside.
    //   Everyone that have hit /events will get this message - including the sender!
    res.sse.broadcast.event('new-user', `User ${req.query.name} just hit the /channel endpoint`);
});

More common usage example - where the Hub is deported outside of the middleware:

const hub = new Hub();

someModule.on('someEvent', (event) => {
    //=> All the functions you're now used to are still there, data(), event() and comment().
    hub.event('someEvent', event);
});

router.get('/events', sseHub({ hub }), (req, res: ISseHubResponse) => {
    //=> The 1-to-1 functions are still there
    res.sse.event('welcome', 'Welcome! You\'ll now receive realtime events from someModule like everyone else');
});

RedisHub – Redis support for sseHub()

In the previous example you can notice that we've created the Hub object ourselves. This also means that you can replace that object with another that has a compatible interface (implement IHub in src/hub.ts to make your own :coffee:).

expresse provides an alternative subclass of Hub, RedisHub that uses Redis' pub/sub capabilities, which is very practical if you have multiple servers, and you want res.sse.broadcast.* to actually broadcast SSE messages between all the nodes.

// connects to localhost:6379 (default Redis port)
const hub = new RedisHub('channel-name');
// ...or you can pass you own two ioredis clients to bind on a custom network address
const hub = new RedisHub('channel-name', new Redis(myRedisNodeUrl), new Redis(myRedisNodeUrl));

router.get('/channel', sseHub({ hub }), (req, res: ISseHubResponse) => {
    res.sse.event('welcome', 'Welcome!'); // 1-to-1
    res.sse.broadcast.event('new-user', `User ${req.query.name} just hit the /channel endpoint`);
});

:bulb: Notes

About browser support

The W3C standard client for Server-Sent events is EventSource. Unfortunately, it is not yet implemented in Internet Explorer or Microsoft Edge.

You may want to use a polyfill on the client side if your application targets those browsers (see eventsource package on npm for Node and older browsers support).

See complete support report on Can I use

| | Chrome | IE / Edge | Firefox | Opera | Safari | |---------------------|--------|-----------|---------|-------|--------| | EventSource Support | 6 | No | 6 | 11 | 5 |

Using a serializer for messages' data fields

When sending a message, the data field is serialized using JSON.stringify. You can override that default serializer to use your own format.

The serializer must be compatible with the signature (value: any) => string|Buffer;.

For example, to format data using the toString() format of the value, you can use the String() constructor:

app.get('/events', sse({ serializer: String }), yourMiddleware);

// or, less optimized:
app.get('/events', sse({ serializer: data => data.toString() }), yourMiddleware);

Using Compression

If you are using a dynamic HTTP compression middleware, like expressjs/compression, expresse won't likely work out of the box.

This is due to the nature of compression and how compression middlewares work. For example, express' compression middleware will patch res.write and hold the content written in it until res.end() or an equivalent is called. Then the body compression can happen and the compressed content can be sent.

Therefore, res.write() must not be buffered with SSEs. That's why ExpreSSE offers expressjs/compression support through the flushAfterWrite option. It must be set when using the compression middleware:

app.use(compression());

app.get('/events', sse({ flushAfterWrite: true }), (req, res: ISseResponse) => {
    res.sse.comment('Welcome! This is a compressed SSE stream.');
});