Skip to main content

stacks-realtime

Use when implementing real-time features in Stacks - WebSocket broadcasting, public/private/presence channels, emit to users, the Channel class, broadcast discovery, server lifecycle, or realtime configuration. Covers @stacksjs/realtime and config/realtime.ts.

Datos de origen

Repositorio
stacksjs/stacks
Última actividad en el origen
6 de octubre de 2026 a las 01:49
Idioma detectado de SKILL.md
inglés
Estrellas
631
Forks
20

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
stacks-realtime
description
Use when implementing real-time features in Stacks - WebSocket broadcasting, public/private/presence channels, emit to users, the Channel class, broadcast discovery, server lifecycle, or realtime configuration. Covers @stacksjs/realtime and config/realtime.ts.
license
MIT
compatibility
Bun >= 1.3.0, TypeScript
allowed-tools
Read Edit Write Bash Grep Glob
# Stacks Realtime WebSocket broadcasting via `ts-broadcasting` with channel-based messaging. Provides a Laravel-like API for real-time features. ## Key Paths - Core package: `storage/framework/core/realtime/src/` - Configuration: `config/realtime.ts` - Application broadcasts: `app/Broadcasts/` ## Source Files ``` realtime/src/ index.ts # re-exports ts-broadcasting + all Stacks-specific exports emit.ts # emit(), emitToUser(), emitToUsers() channel.ts # Channel class + channel() factory broadcast.ts # Broadcast class + runBroadcast() + broadcast() ws.ts # setWsAuthenticator() - gates upgrades on the broadcast server replay-buffer.ts # setReplayBuffer() / replaySince() - per-channel replay with seq numbers server-instance.ts # server lifecycle (createServer/getServer/setServer/stopServer) ``` ## Exports (from index.ts) The package re-exports everything from `ts-broadcasting` plus Stacks-specific APIs: ```typescript // Re-exported from ts-broadcasting export * from 'ts-broadcasting' // Server instance management export { getServer, setServer, createServer, stopServer } from './server-instance' // Stacks-specific exports export { emit, emitToUser, emitToUsers } from './emit' export type { EmitOptions } from './emit' export { channel as createChannel, Channel as StacksChannel } from './channel' export { broadcast as dispatchBroadcast, runBroadcast, Broadcast as LegacyBroadcast } from './broadcast' export type { BroadcastInstance } from './broadcast' export { debugSnapshot, getReplayBuffer, pruneExpired, recordBroadcast, replaySince, setReplayBuffer } from './replay-buffer' export { storeWebSocketEvent, setWsAuthenticator, getWsAuthenticator } from './ws' export type { WsAuthenticator, WsAuthResult } from './ws' ``` Note the renamed exports: - `channel()` is exported as `createChannel` - `Channel` class is exported as `StacksChannel` - `broadcast()` is exported as `dispatchBroadcast` - `Broadcast` class is exported as `LegacyBroadcast` --- ## Emit Functions The primary API for broadcasting events. ### `emit(channel, event, data?, options?)` - Broadcast to a channel ```typescript import { emit, emitToUser, emitToUsers } from '@stacksjs/realtime' // Broadcast to a public channel emit('chat-room', 'new-message', { text: 'Hello', sender: 'John' }) // Broadcast to a private channel (auto-prefixes 'private-') emit('orders', 'updated', { status: 'shipped' }, { private: true }) // Broadcast to a presence channel (auto-prefixes 'presence-') emit('room-1', 'user-joined', { userId: 42 }, { presence: true }) // Exclude specific socket(s) from receiving - socket IDs (the `socket_id` // a client gets in `connection_established`), not user IDs. Every ID in an // array is excluded. emit('chat', 'typing', data, { exclude: 'socket-id-1' }) emit('chat', 'typing', data, { exclude: ['socket-id-1', 'socket-id-2'] }) ``` ### EmitOptions interface ```typescript interface EmitOptions { private?: boolean // prefix channel with 'private-' presence?: boolean // prefix channel with 'presence-' exclude?: string | string[] // socket IDs to exclude (all of them) driver?: string // broadcast driver override } ``` ### `emitToUser(userId, event, data?, options?)` - Emit to specific user Sends to `private-user.{userId}` channel. The target user must be subscribed to their own private user channel. ```typescript // Emit to a specific user (sends to 'private-user.42') emitToUser(42, 'notification', { message: 'You have a new order' }) emitToUser('user-123', 'alert', { type: 'warning', text: 'Session expiring' }) ``` ### `emitToUsers(userIds, event, data?, options?)` - Emit to multiple users Iterates over userIds and calls `emitToUser` for each. ```typescript // Emit to multiple users emitToUsers([42, 43, 44], 'announcement', { text: 'Server maintenance at 3pm' }) ``` --- ## Channel Class Provides a fluent API for broadcasting to channels with explicit channel types. ### `channel(name)` - Factory function ```typescript import { createChannel } from '@stacksjs/realtime' // or within the package: import { channel } from './channel' const ch = createChannel('orders') // Broadcast to public channel (no prefix) await ch.public('new-order', { id: 1, total: 99.99 }) // Broadcast to private channel (auto-prefixes 'private-') await ch.private('status-update', { status: 'shipped' }) // Broadcast to presence channel (auto-prefixes 'presence-') await ch.presence('user-online', { userId: 42 }) // Broadcast with explicit type await ch.broadcast('event', data, 'private') // type: 'public' | 'private' | 'presence' await ch.broadcast('event', data) // defaults to 'public' ``` ### Channel class internals ```typescript class Channel { private channelName: string constructor(channel: string) async private(event: string, data?: any): Promise<void> // Prefixes with 'private-' if not already prefixed async public(event: string, data?: any): Promise<void> // Uses channelName as-is async presence(event: string, data?: any): Promise<void> // Prefixes with 'presence-' if not already prefixed async broadcast(event: string, data?: any, type: ChannelType = 'public'): Promise<void> // Dispatches to private(), presence(), or public() based on type } ``` All methods throw `Error('Broadcast server not initialized')` if `getServer()` returns null. --- ## Server Lifecycle The server instance is stored as a module-level singleton (`serverInstance`). ```typescript import { createServer, getServer, setServer, stopServer } from '@stacksjs/realtime' import type { ServerConfig, BroadcastServer } from 'ts-broadcasting' // Create and start a new broadcast server // Internally: new BroadcastServer(config) -> server.start() -> setServer(server) const server: BroadcastServer = await createServer(config) // Get existing server instance (or null) const server: BroadcastServer | null = getServer() // Set server instance manually setServer(server) // Stop server and set instance to null await stopServer() ``` `createServer()` dynamically imports `ts-broadcasting`, instantiates `BroadcastServer`, calls `start()`, stores the instance via `setServer()`, and returns it. That server is the WebSocket endpoint (`/app` and `/ws` on its own host/port); there is no upgrade handler for the app's HTTP server. ### Connection auth ```typescript import { setWsAuthenticator } from '@stacksjs/realtime' // Read on every upgrade, so it applies whether installed before or after createServer(). setWsAuthenticator(async (req) => { const token = new URL(req.url).searchParams.get('token') const user = token ? await verify(token) : null if (!user) return { ok: false, status: 401, message: 'Unauthorized' } // user -> socket.data.user, data -> socket.data.data (what channel authorizers read) return { ok: true, user: { id: user.id }, data: { team: user.team } } }) ``` A refusal answers the upgrade with its status; a throw refuses with a 500 and is logged, not sent. An `authorizeConnection` in the `createServer()` config still runs after it. Per-channel access is still `Broadcast.channel('private-orders.{id}', (socket, params) => ...)`. `/stats` and `/metrics` are off unless `createServer({ endpoints: { stats: true, metrics: true, token } })`. ### Dead sockets and slow consumers Bun's own WebSocket options, passed through `createServer()`: ```typescript await createServer({ host: '0.0.0.0', port: 6001, websocket: { idleTimeout: 60, // Bun pings idle sockets; no pong within this -> closed (rounded up to 4s steps) sendPings: true, backpressureLimit: 1024 * 1024, closeOnBackpressureLimit: true, // drop a consumer that falls 1 MB behind }, }) ``` ### Replay buffer ```typescript import { replaySince, setReplayBuffer } from '@stacksjs/realtime' setReplayBuffer({ channels: ['orders.*'], maxPerChannel: 100, ttlMs: 5 * 60_000 }) ``` Every broadcast on a matching channel, from any API, is recorded and reaches clients as `{ event, channel, data, seq }`. Patterns match with or without the `private-`/`presence-` prefix (`orders.*` covers `private-orders.1`); `replaySince()` takes the full name. The client keeps the last `seq` per channel and sends it back after reconnecting (through a route of the app's), which answers with `replaySince('private-orders.1', seq)`. The buffer and its numbering are per process. --- ## Broadcast Discovery Dynamically loads broadcast files from `app/Broadcasts/` and executes them. ### `runBroadcast(name, payload?)` - Run a broadcast file ```typescript import { runBroadcast, dispatchBroadcast } from '@stacksjs/realtime' // Loads app/Broadcasts/OrderStatusChanged.ts and executes it await runBroadcast('OrderStatusChanged', { orderId: 1 }) // Alias - identical to runBroadcast await dispatchBroadcast('NewMessage', { text: 'Hello' }) ``` ### Broadcast file interface ```typescript interface BroadcastInstance { channel?: () => string | string[] // channels to broadcast on broadcastOn?: () => string | string[] // alias for channel() event?: () => string // event name broadcastAs?: () => string // alias for event() data?: () => any // data payload broadcastWith?: () => any // alias for data() handle?: (payload?: any) => Promise<void> | void // custom handler } ``` ### How broadcast discovery works 1. Scans `app/Broadcasts/**/*.ts` using `bun.globSync()` 2. Finds file ending with `{name}.ts` 3. Imports the module and reads its `default` export 4. If `handle()` exists, calls it with the payload and returns 5. Otherwise, reads channel/event/data from the interface methods 6. Constructs a `BroadcastEvent` and calls `server.broadcaster.broadcast(event)` ### Example broadcast file ```typescript // app/Broadcasts/OrderCreated.ts export default { broadcastOn: () => ['orders', 'private-admin'], broadcastAs: () => 'order.created', broadcastWith: () => ({ orderId: 123, total: 99.99 }), } ``` Or with a custom handler: ```typescript // app/Broadcasts/UserNotification.ts export default { handle: async (payload) => { const { emit } = await import('@stacksjs/realtime') emit(`private-user.${payload.userId}`, 'notification', { message: payload.message, }, { private: true }) }, } ``` --- ## Legacy/Backward Compatibility ### Broadcast class (exported as `LegacyBroadcast`) ```typescript class Broadcast { async connect(): Promise<void> // no-op async disconnect(): Promise<void> // no-op subscribe(channel, callback): void // warns: use BroadcastClient instead unsubscribe(channel): void // warns: use BroadcastClient instead broadcast(channel, event, data?, type?): void // delegates to server.broadcast() isConnected(): boolean // returns getServer() !== null } ``` ### ws.ts ```typescript storeWebSocketEvent(type, socket, details): Promise<void> // no-op, kept for compatibility ``` --- ## config/realtime.ts Full configuration with all options and their defaults: ```typescript export default { enabled: true, // Deployment mode mode: 'server' as 'server' | 'serverless', // 'server': ts-broadcasting (high-performance Bun WebSocket server) // 'serverless': API Gateway WebSocket + Lambda + DynamoDB // Legacy driver option (backward compat) driver: 'bun' as 'socket' | 'pusher' | 'bun' | 'reverb' | 'ably', // Server mode config (ts-broadcasting) server: { host: env.BROADCAST_HOST || '0.0.0.0', port: Number(env.BROADCAST_PORT || 6001), scheme: 'ws' as 'ws' | 'wss', driver: 'bun', redis: { enabled: Boolean(env.BROADCAST_REDIS_ENABLED || false), host: env.REDIS_HOST || 'localhost', port: Number(env.REDIS_PORT || 6379), password: env.REDIS_PASSWORD || '',
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub