Extensions
Realtime
One API to push live updates, on the server and in the browser alike. @stone-js/realtime gives you a single Broadcaster: publish an event on a channel with to(channel).emit(event, payload), subscribe with on(channel, listener), and read members(channel) for presence. It runs over an in-process broadcaster (zero-config) or Redis pub/sub, and the exact same API backs the isomorphic client.
#Install
npm i @stone-js/realtime
npm i ioredis # only for the Redis (multi-node) fan-out
npm i ws # only for the Node client (the browser uses the global WebSocket)#Enable it
The principle
Emitting a live update is part of the domain. Which socket carries it, and whether it fans out across one node or a fleet, is context. The domain should write the intent once and let the runtime resolve the transport.
In Stone.js
One agnostic Broadcaster contract backs every driver. The provider binds the manager as realtimeManager and the default connection as realtime. Gateways are routed by the light key-router from @stone-js/router (a WS adapter runs each socket event through the kernel). The core is never touched.
import { StoneApp } from '@stone-js/core'
import { Realtime } from '@stone-js/realtime'
@Realtime({ driver: 'redis', url: 'redis://localhost:6379' })
@StoneApp({ name: 'app' })
export class Application {}import { defineConfig } from '@stone-js/core'
import { defineRealtime } from '@stone-js/realtime'
export const AppConfig = defineConfig(defineRealtime({
default: 'redis',
connections: [
{ name: 'redis', driver: 'redis', url: 'redis://localhost:6379', prefix: 'rt' },
{ name: 'local', driver: 'memory' }
]
}))#Broadcast
export class OrderService {
constructor (private readonly realtime) {} // the default broadcaster
async ship (order) {
// every subscriber of the channel receives it, on this node or across the Redis fleet
await this.realtime.to(`order:${order.id}`).emit('shipped', { at: Date.now() })
}
}#Listen with a gateway
A gateway is a plain, dependency-injected class. Its methods react to connection lifecycle events and to specific channel events, one method each. Like every keyed handler they receive (payload, event); reach the originating connection with connectionOf(event).
import { RealtimeGateway, OnConnect, OnDisconnect, OnEvent, connectionOf } from '@stone-js/realtime'
@RealtimeGateway()
export class Chat {
constructor (private readonly realtime) {}
@OnConnect() onConnect (_, event) { const connection = connectionOf(event) /* greet, authorize… */ }
@OnDisconnect() onLeave (_, event) { /* clean up presence… */ }
@OnEvent('room:1', 'message')
async onMessage (payload, event) {
await this.realtime.to('room:1').emit('message', payload)
}
}The full set of method decorators: @OnConnect, @OnDisconnect, @OnMessage, @OnError, @OnSubscribe, @OnUnsubscribe and @OnEvent(channel, event). Each is a thin alias of the light router's @OnKey; @RealtimeGateway is @KeyHandler.
#Presence
export class Room {
constructor (private readonly realtime) {}
async who (channel: string) {
return await this.realtime.members(channel) // [{ connectionId, info }]
}
}#On the frontend
The isomorphic client speaks the same Broadcaster API, so the code that emits and listens is written once. The browser uses the global WebSocket; on Node it loads ws lazily.
import { RealtimeClient } from '@stone-js/realtime'
const realtime = RealtimeClient.create({ url: 'wss://api.example.com/ws' })
realtime.on('room:1', (message) => render(message.payload))
await realtime.to('room:1').emit('message', { text: 'hi' })#Drivers
| driver | Type | Description |
|---|---|---|
| memory | built-in | In-process fan-out to local listeners and held connections. Zero-config default; single node. |
| redis | ioredis | Pub/sub fan-out across every node: publish on a channel, each instance delivers to its local sockets. |
| provider | coming next | Cloud fan-out (EventBridge, Momento, Ably) wired as a driver, never grafted onto core. |