Broadcast
Channel-based real-time broadcasting over WebSocket. One connection per client, multiplexed channels, pattern-based authorization, auto-reconnect.
Setup
Using a service provider (recommended)
import { BroadcastProvider } from '@strav/signal'
import { session } from '@strav/http'
app.use(new BroadcastProvider({
middleware: [session()],
pingInterval: 30_000,
path: '/_broadcast',
}))
The BroadcastProvider calls BroadcastManager.boot() with the router and shuts down connections on application shutdown.
Manual setup
import { broadcast } from '@strav/signal'
broadcast.boot(router, {
middleware: [session()], // optional — run middleware on each WS connection
pingInterval: 30_000, // keepalive interval in ms (default: 30s, 0 to disable)
path: '/_broadcast', // WS endpoint path (default: /_broadcast)
})
Channels
Define channels to control which topics clients can subscribe to. Channel patterns use the same:param syntax as routes.
import { broadcast } from '@strav/signal'
Public channel
Anyone can subscribe — no authorization:
broadcast.channel('announcements')
Authorized channel
The callback receives the connection'sContext (with session/user from middleware) and the extracted params. Return true to allow, false to deny:
broadcast.channel('chats/:id', async (ctx, { id }) => {
const user = ctx.get('user')
return !!user
})
Channel with message handlers
For bidirectional communication, define message handlers that receive events from clients:
broadcast.channel('chat/:id', {
authorize: async (ctx, { id }) => !!ctx.get('user'),
messages: {
async send(ctx, { id }, data) {
const user = ctx.get('user')
await Message.create({ chatId: id, text: data.text, userId: user.id })
broadcast.to(`chat/${id}`).send('new_message', { text: data.text, userId: user.id })
},
typing(ctx, { id }, data) {
broadcast.to(`chat/${id}`).except(ctx.clientId).send('typing', data)
}
}
})
Each handler receives:
ctx— the connection's Context (withctx.clientIdset to the sender's ID)params— extracted route parameters from the channel patterndata— the payload sent by the client
Broadcasting
Send events to channel subscribers from anywhere — controllers, services, event listeners:
import { broadcast } from '@strav/signal'
// Broadcast to all subscribers
broadcast.to('announcements').send('news', { title: 'v2 released!' })
// Broadcast to a dynamic channel
broadcast.to(`chats/${chatId}`).send('message', { text, userId })
// Exclude a specific client (e.g. the sender)
broadcast.to(`chats/${chatId}`).except(senderId).send('message', data)
Broadcasting to a channel with no subscribers is a no-op.
Client
The browser client manages a single WebSocket connection with automatic reconnection and multiplexed channel subscriptions.
import { Broadcast } from '@strav/signal/broadcast'
Connect
const bc = new Broadcast() // auto-detects ws(s)://host/_broadcast
Or with explicit options:
const bc = new Broadcast({
url: 'wss://api.example.com/_broadcast',
maxReconnectAttempts: 10, // default: Infinity
})
Subscribe to a channel
const chat = bc.subscribe('chat/1')
chat.on('new_message', (data) => {
console.log(data.text)
})
chat.on('typing', (data) => {
showTypingIndicator(data.userId)
})
The on method returns a cleanup function:
const stop = chat.on('new_message', handler)
stop() // remove this specific listener
Send messages to the server
For channels with message handlers:
chat.send('send', { text: 'Hello!' })
chat.send('typing', { active: true })
Unsubscribe
chat.leave()
Connection lifecycle
bc.on('connected', () => {
console.log('Online')
})
bc.on('disconnected', () => {
console.log('Offline')
})
bc.on('reconnecting', (attempt) => {
console.log(`Reconnecting... attempt ${attempt}`)
})
bc.on('subscribed', (channel) => {
console.log(`Subscribed to ${channel}`)
})
bc.on('error', ({ channel, reason }) => {
console.log(`Subscription to ${channel} denied: ${reason}`)
})
Client ID
Each connection receives a unique ID from the server. Available after the firstconnected event:
bc.clientId // string | null
Close
bc.close() // closes connection, no reconnect
Reconnection
The client reconnects automatically with exponential backoff (1s, 2s, 4s, ... up to 30s). On reconnect, all active subscriptions are re-established automatically.
Wire protocol
All messages are JSON over a single WebSocket. Short keys minimize overhead:
Client → Server:
{ "t": "sub", "c": "chat/1" } subscribe
{ "t": "unsub", "c": "chat/1" } unsubscribe
{ "t": "msg", "c": "chat/1", "e": "send", "d": {...} } channel message
Server → Client:
{ "t": "welcome", "id": "uuid" } connection established
{ "t": "ok", "c": "chat/1" } subscription confirmed
{ "t": "err", "c": "chat/1", "r": "unauthorized" } subscription denied
{ "t": "msg", "c": "chat/1", "e": "message", "d": {...} } broadcast event
{ "t": "ping" } keepalive
Introspection
broadcast.clientCount // number of active connections
broadcast.subscriberCount('chat/1') // subscribers on a specific channel
Integration with events
Bridge application events to broadcast channels:
import { Emitter } from '@strav/kernel'
import { broadcast } from '@strav/signal'
Emitter.on<{ task: Task }>('task.created', ({ task }) => {
broadcast.to(`projects/${task.projectId}/tasks`).send('created', task)
})
Full example
// index.ts
import { broadcast } from '@strav/signal'
broadcast.boot(router, { middleware: [session()] })
broadcast.channel('notifications', (ctx) => !!ctx.get('user'))
broadcast.channel('chat/:id', {
authorize: (ctx, { id }) => !!ctx.get('user'),
messages: {
async send(ctx, { id }, data) {
broadcast.to(`chat/${id}`).send('message', {
text: data.text,
userId: ctx.get('user').id,
})
}
}
})
// client
import { Broadcast } from '@strav/signal/broadcast'
const bc = new Broadcast()
const chat = bc.subscribe('chat/1')
chat.on('message', (data) => {
addMessage(data.text, data.userId)
})
chat.send('send', { text: 'Hello!' })