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.

معلومات المصدر

المستودع
stacksjs/stacks
آخر نشاط في المصدر
٦ أكتوبر ٢٠٢٦ في ٠١:٤٩
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٦٣١
التفرعات
٢٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
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 || '',
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub