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
최근 소스 활동
2026년 10월 6일 01:49
감지된 SKILL.md 언어
영어
스타
631
포크
20

설치 방법

기본적으로 소스를 먼저 확인하는 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에서 보기