Transmit
Transmit is a native opinionated Server-Sent-Event (SSE) module built for AdonisJS. It is a simple and efficient way to send real-time updates to the client, such as notifications, live chat messages, or any other type of real-time data.
The data transmission occurs only from server to client, not the other way around. You have to use a form or a fetch request to achieve client to server communication.
Installation
Install and configure the package using the following command :
node ace add @adonisjs/transmit
-
Installs the
@adonisjs/transmit
package using the detected package manager. -
Registers the
@adonisjs/transmit/transmit_provider
service provider inside theadonisrc.ts
file. -
Creates a new
transmit.ts
file inside theconfig
directory.
You will also have to install the Transmit client package to listen for events on the client-side.
npm install @adonisjs/transmit-client
Configuration
The configuration for the transmit package is stored within the config/transmit.ts
file.
See also: Config stub
import { defineConfig } from '@adonisjs/transmit'
export default defineConfig({
pingInterval: false,
transport: null,
})
-
pingInterval
-
The interval used to send ping messages to the client. The value is in milliseconds or using a string
Duration
format (i.e:10s
). Set tofalse
to disable ping messages. -
transport
-
Transmit supports syncing events across multiple servers or instances. You can enable the feature by referencing the wanted transport layer (only
redis
is supported for now). Set tonull
to disable syncing.import env from '#start/env'import { defineConfig } from '@adonisjs/transmit'import { redis } from '@adonisjs/transmit/transports'export default defineConfig({transport: {driver: redis({host: env.get('REDIS_HOST'),port: env.get('REDIS_PORT'),password: env.get('REDIS_PASSWORD'),keyPrefix: 'transmit',})}})Ensure you have
ioredis
installed when using theredis
transport.
Register Routes
You have to register the transmit routes to allow the client to connect to the server. The routes are registered manually.
import transmit from '@adonisjs/transmit/services/main'
transmit.registerRoutes()
You can also register each route manually by binding the controller by hand.
const EventStreamController = () => import('@adonisjs/transmit/controllers/event_stream_controller')
const SubscribeController = () => import('@adonisjs/transmit/controllers/subscribe_controller')
const UnsubscribeController = () => import('@adonisjs/transmit/controllers/unsubscribe_controller')
router.get('/__transmit/events', [EventStreamController])
router.post('/__transmit/subscribe', [SubscribeController])
router.post('/__transmit/unsubscribe', [UnsubscribeController])
If you want to modify the route definition, for example, to use the Rate Limiter
and auth middleware to avoid abuse of some transmit routes, you can either change the route definition or pass a callback to the transmit.registerRoutes
method.
import transmit from '@adonisjs/transmit/services/main'
transmit.registerRoutes((route) => {
// Ensure you are authenticated to register your client
if (route.getPattern() === '__transmit/events') {
route.middleware(middleware.auth())
return
}
// Add a throttle middleware to other transmit routes
route.use(throttle)
})
Channels
Channels are used to group events. For example, you can have a channel for notifications, another for chat messages, and so on. They are created on the fly when the client subscribes to them.
Channel Names
Channel names are used to identify the channel. They are case-sensitive and must be a string. You cannot use any special characters or spaces in the channel name except /
. The following are some examples of valid channel names:
import transmit from '@adonisjs/transmit/services/main'
transmit.broadcast('global', { message: 'Hello' })
transmit.broadcast('chats/1/messages', { message: 'Hello' })
transmit.broadcast('users/1', { message: 'Hello' })
Channel names use the same syntax as route in AdonisJS but are not related to them. You can freely define a http route and a channel with the same key.
Channel Authorization
You can authorize or reject a connection to a channel using the authorize
method. The method receives the channel name and the HttpContext
. It must return a boolean value.
import transmit from '@adonisjs/transmit/services/main'
import Chat from '#models/chat'
import type { HttpContext } from '@adonisjs/core/http'
transmit.authorize<{ id: string }>('users/:id', (ctx: HttpContext, { id }) => {
return ctx.auth.user?.id === +id
})
transmit.authorize<{ id: string }>('chats/:id/messages', async (ctx: HttpContext, { id }) => {
const chat = await Chat.findOrFail(+id)
return ctx.bouncer.allows('accessChat', chat)
})
Broadcasting Events
You can broadcast events to a channel using the broadcast
method. The method receives the channel name and the data to send.
import transmit from '@adonisjs/transmit/services/main'
transmit.broadcast('global', { message: 'Hello' })
You can also broadcast events to any channel except one using the broadcastExcept
method. The method receives the channel name, the data to send, and the UID you want to ignore.
transmit.broadcastExcept('global', { message: 'Hello' }, 'uid-of-sender')
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.
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.
import { Transmit } from '@adonisjs/transmit-client'
export const transmit = new Transmit({
baseUrl: window.location.origin
})
You should create only one instance of the Transmit
class and reuse it throughout your application.
Configuring the Transmit Instance
The Transmit
class accepts an object with the following properties:
-
baseUrl
-
The base URL of the server. The URL must include the protocol (http or https) and the domain name.
-
uidGenerator
-
A function that generates a unique identifier for the client. The function must return a string. It defaults to
crypto.randomUUID
. -
eventSourceFactory
-
A function that creates a new
EventSource
instance. It defaults to the WebAPIEventSource
. You need to provide a custom implementation if you want to use the client onNode.js
,React Native
or any other environment that does not support theEventSource
API. -
eventTargetFactory
-
A function that creates a new
EventTarget
instance. It defaults to the WebAPIEventTarget
. You need to provide a custom implementation if you want to use the client onNode.js
,React Native
or any other environment that does not support theEventTarget
API. Returnnull
to disable theEventTarget
API. -
httpClientFactory
-
A function that creates a new
HttpClient
instance. It is mainly used for testing purposes. -
beforeSubscribe
-
A function that is called before subscribing to a channel. It receives the channel name and the
Request
object sent to the server. Use this function to add custom headers or modify the request object. -
beforeUnsubscribe
-
A function that is called before unsubscribing from a channel. It receives the channel name and the
Request
object sent to the server. Use this function to add custom headers or modify the request object. -
maxReconnectAttempts
-
The maximum number of reconnection attempts. It defaults to
5
. -
onReconnectAttempt
-
A function that is called before each reconnection attempt and receives the number of attempts made so far. Use this function to add custom logic.
-
onReconnectFailed
-
A function that is called when the reconnection attempts fail. Use this function to add custom logic.
-
onSubscribeFailed
-
A function that is called when the subscription fails. It receives the
Response
object. Use this function to add custom logic. -
onSubscription
-
A function that is called when the subscription is successful. It receives the channel name. Use this function to add custom logic.
-
onUnsubscription
-
A function that is called when the unsubscription is successful. It receives the channel name. Use this function to add custom logic.
Creating a Subscription
You can create a subscription to a channel using the subscription
method. The method receives the channel name.
const subscription = transmit.subscription('chats/1/messages')
await subscription.create()
The create
method registers the subscription on the server. It returns a promise that you can await
or void
.
If you don't call the create
method, the subscription will not be registered on the server, and you will not receive any events.
Listening for Events
You can listen for events on the subscription using the onMessage
method that receives a callback function. You can call the onMessage
method multiple times to add different callbacks.
subscription.onMessage((data) => {
console.log(data)
})
You can also listen to a channel only once using the onMessageOnce
method which receives a callback function.
subscription.onMessageOnce(() => {
console.log('I will be called only once')
})
Stop Listening for Events
The onMessage
and onMessageOnce
methods return a function that you can call to stop listening for a specific callback.
const stopListening = subscription.onMessage((data) => {
console.log(data)
})
// Stop listening
stopListening()
Deleting a Subscription
You can delete a subscription using the delete
method. The method returns a promise that you can await
or void
. This method will unregister the subscription on the server.
await subscription.delete()
Avoiding GZip Interference
When deploying applications that use @adonisjs/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