SERVER ECOSYSTEM
WebSocket Gateways
@rasenganjs/ws adds a NestJS-inspired Gateway class on top of app.websocket() — rooms, broadcasting, and a { event, data } message envelope, resolved through the same DI container as your HTTP controllers.
npm install @rasenganjs/ws
Registering the Plugin
Gateway is declared through the module plugin system — register the plugin once, before any module that declares gateways:
import { bootstrap } from '@rasenganjs/server'; import { createWsPlugin } from '@rasenganjs/ws'; import appModule from './app.module'; bootstrap((app) => { app.registerPlugin(createWsPlugin()); app.registerModule(appModule); // may declare gateways: [ChatGateway] });
Writing a Gateway
import { Gateway, GatewayRouter, type GatewayClient, type GatewayMessageHandler, } from '@rasenganjs/ws'; export class ChatGateway extends Gateway { path = '/chat'; onConnect(client: GatewayClient) { client.join('lobby'); } onDisconnect(client: GatewayClient) { console.log(`${client.id} disconnected`); } messages(router: GatewayRouter) { router.on('sendMessage', this.handleSendMessage); } handleSendMessage: GatewayMessageHandler<{ text: string }> = ( client, data ) => { client.to('lobby').emit('newMessage', { text: data.text, from: client.id }); }; }
import { defineModule } from '@rasenganjs/server'; import { ChatGateway } from './chat.gateway'; export default defineModule({ gateways: [ChatGateway], });
Because Gateway extends Provider, constructor injection and onInit()/onDestroy() work exactly like any other provider — inject a service, subscribe to something in onInit(), and it fires like any other module resolution.
GatewayClient
Broadcasting from Outside a Connection
Gateway.server (set by the plugin after the gateway is resolved) lets you broadcast from an HTTP controller, a queue job, or anywhere else outside a live connection's context:
export class NotificationController extends Controller { constructor(private chat: ChatGateway) { super(); } routes(router: Router) { router.post('/announce', this.announce); } announce: RouteHandler = async (ctx) => { await this.chat.server.emit('announcement', { text: ctx.body.text }); return ctx.res.json({ ok: true }); }; }
Wire Format
Messages are JSON { event, data } frames — this is not Socket.IO-wire-compatible, it's a minimal envelope this package defines itself (plain WebSocket has no built-in named-event concept). Binary frames bypass the envelope entirely — handle them via Gateway.onBinaryMessage.
Scaling Across Processes
createWsPlugin({ adapter }) accepts a GatewayAdapter — a pub/sub relay, not a room-membership registry (a client is always connected to exactly one process, so room membership itself never needs to be shared state):
import { createWsPlugin, RedisGatewayAdapter } from '@rasenganjs/ws'; import Redis from 'ioredis'; app.registerPlugin( createWsPlugin({ adapter: new RedisGatewayAdapter({ client: new Redis() }), }) );
The default MemoryGatewayAdapter is single-process only — fine for development or a single instance, not for horizontally-scaled deployments.
Heartbeat
Browsers can't send protocol-level pings, so the plugin runs its own app-level dead-connection detection, on by default:
app.registerPlugin( createWsPlugin({ heartbeat: { interval: 30_000, timeout: 15_000 } }) ); app.registerPlugin(createWsPlugin({ heartbeat: false })); // disable entirely
Any inbound frame counts as liveness — active clients are never pinged into disconnection.
