/** * Trellis Realtime — Relay Hub (server side) * * The server counterpart to {@link WebSocketRelayTransport}: a dumb broadcast * mesh that fans every inbound frame out to all *other* connected peers, with * optional in-memory replay ({@link RelayPersistence}) so a late joiner catches * up on presence / chat tail / text state. * * This is intentionally NOT {@link RealtimeRoom}. `RealtimeRoom` is a *client* * abstraction — one peer's view of a room (its own presence + observed peers). * The relay holds no presence of its own; it only moves bytes between peers and * remembers enough to replay. Keep the two roles distinct: clients run * `RealtimeRoom`, the server runs a relay. * * Room-scoped: peers are fanned out (and replayed) only to others in the *same* * room. The room is taken from the upgrade path — `${path}/{room}` — matching * {@link DurableObjectRelayTransport}'s `${url}/{room}` convention (and the * reference Durable Object worker, which maps one object per room). A bare * connection to `path` lands in the `'default'` room. This makes the bundled * relay a faithful local stand-in for the hosted DO: distinct rooms stay * isolated, so `joinPresence({ relayUrl })` Just Works against it. * * Node-only (uses `node:http` + the optional `ws` package). Exposed through * `trellis/server`, never `trellis/realtime` (which is browser-bundled). * * import { createServer } from 'node:http'; * import { attachRealtimeRelay } from 'trellis/server'; * * const server = createServer(handler); * await attachRealtimeRelay(server, { path: '/rt' }); * server.listen(8231); * // client: joinPresence({ relayUrl: 'ws://localhost:8231/rt', room: 'doc:42' }) * // → connects to ws://localhost:8231/rt/doc:42 * * @module trellis/server */ import type { Server as HttpServer, IncomingMessage } from 'node:http'; import { RelayPersistence } from '../realtime/relay-persistence.js'; import type { BlobStore } from '../vcs/blob-store.js'; import { type BlobRequestHandlerOptions } from './blob-handler.js'; export { createBlobRequestHandler, BLOB_CORS, } from './blob-handler.js'; export type { BlobRequestHandlerOptions } from './blob-handler.js'; /** Mark set on `IncomingMessage` when the blob handler claims the request. */ export declare const TRELLIS_BLOB_CLAIMED: unique symbol; export declare function isBlobRequestClaimed(req: IncomingMessage): boolean; export interface RealtimeRelayOptions { /** * Base WebSocket upgrade path this relay claims. Default `/rt`. The relay * accepts `path` (→ room `'default'`) and `${path}/{room}` (→ that room); * other upgrade paths are left untouched so it can coexist with path-scoped * WebSocket handlers. */ path?: string; /** * Per-room replay store factory for late joiners. A fresh store is created * the first time a room is seen. Pass `false` to disable replay entirely * (pure fan-out). Default: `() => new RelayPersistence()`. */ persistence?: false | (() => RelayPersistence); /** * Grace window (ms) before replay is delivered to a new connection when no * `hello` frame arrives first (UI mounting subscribers can race the socket). * Default 250. */ replayGraceMs?: number; /** Injectable `ws` WebSocketServer ctor for tests. */ WebSocketServerImpl?: unknown; /** * Optional BlobStore factory for content-addressed blob serving * (`GET`/`HEAD /blob/:sha256`, `PUT /blob`). Pass `false` or omit to leave * the blob surface off (default). Mirrors {@link persistence} injection. */ blobStore?: false | (() => BlobStore); /** Reject PUT bodies larger than this. Default 64 MiB → 413. */ maxBlobBytes?: number; /** * Gate blob writes. Return false → 401. Default: allow all. * Production embedders should pass a real check. */ authorizeBlobWrite?: BlobRequestHandlerOptions['authorizeBlobWrite']; } export interface RealtimeRelay { /** Connected peers — across all rooms, or in `room` when given. */ clientCount(room?: string): number; /** Currently active room ids (rooms drop when their last peer leaves). */ rooms(): string[]; /** The replay store for `room`, or `null` when replay is disabled. */ persistenceFor(room: string): RelayPersistence | null; /** Close every peer socket and tear down the WS server. */ close(): Promise; } /** * Mount a realtime relay on an existing HTTP server. Only handles upgrades on * `opts.path`; other upgrade paths are left untouched, so this can coexist with * other WebSocket handlers (e.g. the graph-subscription socket) **provided * those handlers are themselves path-scoped**. */ export declare function attachRealtimeRelay(server: HttpServer, opts?: RealtimeRelayOptions): Promise; export interface StandaloneRealtimeRelayOptions extends RealtimeRelayOptions { /** Port to bind. Default 8231. Use 0 for an OS-assigned ephemeral port. */ port?: number; /** Host to bind. Default `0.0.0.0`. */ hostname?: string; } export interface StandaloneRealtimeRelay extends RealtimeRelay { /** The bound port (resolved, so `port: 0` reports the real ephemeral port). */ port: number; /** The underlying HTTP server (health check on `/`, 404 otherwise). */ server: HttpServer; } /** * Spin up a standalone relay hub on its own HTTP server. The HTTP surface is * health (`/` / `/health`) plus optional blob routes when `blobStore` is set; * the relay lives on the WebSocket upgrade path. For embedding in an existing * app server, use {@link attachRealtimeRelay} instead. * * const relay = await createRealtimeRelay({ port: 8231 }); * // ws://localhost:8231/rt → room 'default' * // ws://localhost:8231/rt/doc:42 → room 'doc:42' (isolated fan-out) * await relay.close(); */ export declare function createRealtimeRelay(opts?: StandaloneRealtimeRelayOptions): Promise; //# sourceMappingURL=relay-server.d.ts.map