@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.
npm install @boringnode/transmitThis 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.
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'})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)})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',})}})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})constsubscription=transmit.subscription('chats/1/messages')awaitsubscription.create()subscription.onMessage((data)=>{console.log(data)})subscription.onMessageOnce(()=>{console.log('I will be called only once')})conststopListening=subscription.onMessage((data)=>{console.log(data)})// Stop listeningstopListening()awaitsubscription.delete()Here are the available adapters for specific frameworks:
- AdonisJS (Official)
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})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.
traefik.http.middlewares.gzip.compress=true
traefik.http.middlewares.gzip.compress.excludedcontenttypes=text/event-stream
traefik.http.routers.my-router.middlewares=gzip