Skip to content

Repository files navigation

@boringnode/transmit

typescript-imagegh-workflow-imagenpm-imagenpm-download-imagelicense-image

@boringnode/transmit is a framework-agnostic opinionated library to manage Server-Sent Events (SSE) in Node.js.

Here are a few things you should know before using this module.

👉 Unidirectional Communication: The data transmission occurs only from server to client, not the other way around.
👉 Textual Data Only: SSE only supports the transmission of textual data, binary data cannot be sent.
👉 HTTP Protocol: The underlying protocol used is the regular HTTP, not any special or proprietary protocol.

Installation

npm install @boringnode/transmit

Usage

This module is designed to be used with any HTTP server framework. If you wish to write an adapter for a specific framework, please refer to the Adapters section for examples.

Broadcasting Data

Once the connection is established, you can send data to the client using the transmit.broadcast method.

// Given the "transmit" instance from the adaptertransmit.broadcast('global',{message: 'Hello'})transmit.broadcast('chats/1/messages',{message: 'Hello'})transmit.broadcast('users/1',{message: 'Hello'})

Authorization

You can authorize the client to subscribe to a specific channel by using the authorize function. In the following example, we are using the AdonisJS Framework.

importtransmitfrom'@adonisjs/transmit/services/main'importChatfrom'#models/chat'importtype{HttpContext}from'@adonisjs/core/http'transmit.authorize<{id: string}>('users/:id',(ctx: HttpContext,{ id })=>{returnctx.auth.user?.id===+id})transmit.authorize<{id: string}>('chats/:id/messages',async(ctx: HttpContext,{ id })=>{constchat=awaitChat.findOrFail(+id)returnctx.bouncer.allows('accessChat',chat)})

Syncing across multiple servers or instances

By default, broadcasting events works only within the context of an HTTP request. However, you can broadcast events from the background using the transmit service if you register a transport in your configuration.

The transport layer is responsible for syncing events across multiple servers or instances. It works by broadcasting any events (like broadcasted events, subscriptions, and un-subscriptions) to all connected servers or instances using a Message Bus.

The server or instance responsible for your client connection will receive the event and broadcast it to the client.

import{Transmit}from'@boringnode/transmit'import{redis}from'@boringnode/transmit/transports'consttransmit=newTransmit({transport: {driver: redis({host: process.env.REDIS_HOST,port: process.env.REDIS_PORT,password: process.env.REDIS_PASSWORD,keyPrefix: 'transmit',})}})

Transmit Client

You can listen for events on the client-side using the @adonisjs/transmit-client package. The package provides a Transmit class. The client use the EventSource API by default to connect to the server.

Note

Even if you are not working with AdonisJS, you can still use the @adonisjs/transmit-client package.

import{Transmit}from'@adonisjs/transmit-client'exportconsttransmit=newTransmit({baseUrl: window.location.origin})

Subscribing to Channels

constsubscription=transmit.subscription('chats/1/messages')awaitsubscription.create()

Listening for Events

subscription.onMessage((data)=>{console.log(data)})subscription.onMessageOnce(()=>{console.log('I will be called only once')})

Stop Listening for Events

conststopListening=subscription.onMessage((data)=>{console.log(data)})// Stop listeningstopListening()

Unsubscribing from Channels

awaitsubscription.delete()

Adapters

Here are the available adapters for specific frameworks:

Writing an Adapter

To write an adapter for a specific framework, you need to implement the following routes:

  • GET /__transmit/events: This route is used to establish a connection between the client and the server. It returns a stream that will be used to send data to the client.
  • POST /__transmit/subscribe: This route is used to subscribe the client to a specific channel.
  • POST /__transmit/unsubscribe: This route is used to unsubscribe the client from a specific channel.

Here is an example of how you can implement the adapter for fastify:

importFastifyfrom'fastify'import{Transmit}from'@boringnode/transmit'constfastify=Fastify({logger: true})consttransmit=newTransmit({pingInterval: false,transport: null})/** * Register the client connection and keep it alive. */fastify.get('__transmit/events',(request,reply)=>{constuid=request.query.uidasstringif(!uid){returnreply.code(400).send({error: 'Missing uid'})}conststream=transmit.createStream({
uid,context: { request, reply }request: request.raw,response: reply.raw,injectResponseHeaders: reply.getHeaders()})returnreply.send(stream)})/** * Subscribe the client to a specific channel. */fastify.post('__transmit/subscribe',async(request,reply)=>{constuid=request.body.uidasstringconstchannel=request.body.channelasstringconstsuccess=awaittransmit.subscribe({
uid, channel,context: { request, reply }})if(!success){returnreply.code(400).send({error: 'Unable to subscribe to the channel'})}returnreply.code(204).send()})/** * Unsubscribe the client from a specific channel. */fastify.post('__transmit/unsubscribe',async(request,reply)=>{constuid=request.body.uidasstringconstchannel=request.body.channelasstringconstsuccess=awaittransmit.unsubscribe({
uid, channel,context: { request, reply }})if(!success){returnreply.code(400).send({error: 'Unable to unsubscribe to the channel'})}returnreply.code(204).send()})fastify.listen({port: 3000})

Avoiding GZip Interference

When deploying applications that use @boringnode/transmit, it’s important to ensure that GZip compression does not interfere with the text/event-stream content type used by Server-Sent Events (SSE). Compression applied to text/event-stream can cause connection issues, leading to frequent disconnects or SSE failures.

If your deployment uses a reverse proxy (such as Traefik or Nginx) or other middleware that applies GZip, ensure that compression is disabled for the text/event-stream content type.

Example Configuration for Traefik

traefik.http.middlewares.gzip.compress=true
traefik.http.middlewares.gzip.compress.excludedcontenttypes=text/event-stream
traefik.http.routers.my-router.middlewares=gzip

About

A framework agnostic Server-Sent-Event library

Topics

Resources

Stars

48 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages