@stone-js/realtime
v0.8.15
Published
Agnostic, plug-and-play realtime for Stone.js: one Broadcaster API for backend and frontend, channels and presence, memory and Redis fan-out, an isomorphic client, and @Realtime/@RealtimeGateway/@On* decorators.
Maintainers
Readme
Stone.js - Realtime
Agnostic, plug-and-play realtime for Stone.js. One Broadcaster API for the backend and the frontend: publish an event on a channel, subscribe a listener, read presence. One connection contract, pluggable drivers (memory now, Redis via ioredis, provider fan-out next), gateways with @OnConnect/@OnEvent, an isomorphic client, and a realtime broadcaster injected in the container. The core is never touched.
Installation
npm install @stone-js/realtime
# for the Redis (multi-node) fan-out (optional):
npm install ioredis
# for the Node client (optional; the browser uses the global WebSocket):
npm install wsPeer dependency:
@stone-js/core.ioredisandwsare optional peers, imported lazily only when used.
Enable it
Declarative (single connection):
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 {}Imperative / multi-connection via stone.realtime:
import { defineConfig } from '@stone-js/core'
export const AppConfig = defineConfig((blueprint) => blueprint.set('stone.realtime', {
default: 'redis',
connections: [
{ name: 'redis', driver: 'redis', url: 'redis://localhost:6379', prefix: 'rt' },
{ name: 'local', driver: 'memory' }
]
}))Broadcast
The default broadcaster is injected as realtime:
export class OrderService {
constructor (private readonly realtime) {}
async ship (order) {
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 lifecycle and channel events:
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) /* … */ }
@OnDisconnect() onLeave (_, event) { /* … */ }
@OnEvent('room:1', 'message')
async onMessage (payload, event) {
await this.realtime.to('room:1').emit('message', payload)
}
}Every keyed handler receives (payload, event); reach the originating connection with connectionOf(event). The full set of method decorators: @OnConnect, @OnDisconnect, @OnMessage, @OnError, @OnSubscribe, @OnUnsubscribe and @OnEvent(channel, event), thin aliases of the light router's @OnKey (and @RealtimeGateway is @KeyHandler).
On the frontend
The isomorphic client speaks the same Broadcaster API, so the code that emits and listens is written once:
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 | backing | notes |
| ---------- | -------------- | ---------------------------------------------------------------------------------- |
| memory | built-in | In-process fan-out. Zero-config default; single node. |
| redis | ioredis | Pub/sub fan-out across every node. |
| provider | coming next | Cloud fan-out (EventBridge, Momento, Ably) wired as a driver, never grafted onto core. |
The WebSocket server that holds connections ships as a separate adapter (
node-ws,aws-apigw-ws); it populates the connection store and dispatches lifecycle events into the same router.
