/** * Realtime channel (SPEC.md §8) — transport-agnostic. * * A `RealtimeSession` wraps a connected socket's send/receive callbacks: * the host feeds inbound text frames to `handleMessage`, binary frames to * `handleBinary`, and wires `session.close()` to socket close. Initial * subscription registration comes from the client's most recent pull * (§8.1, loaded from the client record); a sync round completed on the * connection replaces it at round end (§8.7). Deltas are complete SSP2 * response messages pushed as `0x00`-tagged binary (§8.2/§8.7); sync * rounds ride the same socket as `0x01`-tagged byte-stream chunks driven * through the SAME `createSyncResponseStream` as `POST /sync` (§8.7 — * one handler, two framings); the only JSON data-plane server event is * the `sync` wake-up (§8.3). */ import { type PresenceKind, type ScopeMap, type WakeReason } from '@syncular/core'; import type { SyncRequestContext, SyncServerConfig } from './context.js'; import { type SyncularServerEvents } from './events.js'; import type { ServerStorage, StoredCommit } from './storage.js'; /** * Realtime adds fanout/presence tuning to the canonical sync-server config; * socket rounds must never have a narrower push/pull capability set than HTTP. */ export interface RealtimeHubConfig extends Omit { /** Deltas larger than this become `delta-too-large` wake-ups (§8.2). */ readonly maxDeltaBytes?: number; /** * §8.6 presence: cap on the serialized size (bytes) of a published * presence document. An over-cap publish is rejected loudly to the * publisher with `presence.too_large` and fans out nothing. Default 16 KiB. */ readonly maxPresenceBytes?: number; /** * §8.6.4 presence rate cap: minimum ms between fanned-out publishes per * connection per scope key. Absent ⇒ off (the reference default): every * publish fans out immediately. When set, publishes exceeding the cap * coalesce latest-wins into at most one `update` per window (never an * error, never a stale or lost latest document). */ readonly presenceMinIntervalMs?: number; } export interface RealtimeConnectOptions { readonly partition: string; readonly actorId: string; readonly clientId: string; /** * Socket send: JSON control messages as text, tagged binary otherwise * (§8.7). A host MAY return a promise that resolves when the socket * has drained — the session awaits it between round-response chunks * (§8.7 backpressure: bounded buffering, mechanics host-owned). */ readonly send: (data: string | Uint8Array) => void | Promise; /** * Close the underlying socket — invoked on §8.7 protocol violations * (pipelined rounds, unframed streams). The host must still call * `session.close()` from its socket-close handler as usual. */ readonly closeSocket?: () => void; } interface Registration { readonly id: string; readonly table: string; readonly effective: ScopeMap; } type PresenceDoc = Record; /** * §8.6 presence registry — pure in-memory ephemeral state, keyed per * `(partition, scopeKey)` → the set of present sessions and their * documents. It never touches `ServerStorage`; a server restart loses all * presence (§8.6.1). Fanout is scoped to registered peers only — the * privacy floor (§8.6.3). */ declare class PresenceRegistry { #private; /** Current documents on a key, excluding one session (the snapshot, * §8.6.4). */ snapshot(partition: string, scopeKey: string, exclude: RealtimeSession): Array<{ session: RealtimeSession; doc: PresenceDoc; }>; /** Store/replace a session's document for a key. Returns whether this is * the session's FIRST document on the key (join) or a replacement * (update). */ set(partition: string, scopeKey: string, session: RealtimeSession, doc: PresenceDoc): 'join' | 'update'; /** Remove a session's document for a key. Returns true if one existed. */ clear(partition: string, scopeKey: string, session: RealtimeSession): boolean; /** Every key a session currently holds a document on (for leave-on-drop * and registration-change re-derivation). */ keysOf(partition: string, session: RealtimeSession): string[]; /** Sessions currently registered as peers on a key (for fanout). */ peers(partition: string, scopeKey: string): RealtimeSession[]; } export declare class RealtimeSession { #private; readonly sessionId: string; readonly partition: string; readonly actorId: string; readonly clientId: string; readonly logEpoch: string; /** Response/delta layout selected by the most recent socket round. */ wireVersion: number; /** Highest contiguously applied commitSeq acknowledged by the client. */ cursor: number; /** Suppress deltas until the client catches up via pull + ack (§8.2). */ wakePending: boolean; lastKnownSeq: number; /** Replaced at socket-round completion (§8.7); initial set from §8.1. */ registrations: readonly Registration[]; /** Epoch-ms (hub clock) at registration, for `realtime.closed`. */ readonly openedAtMs: number; constructor(hub: RealtimeHub, options: RealtimeConnectOptions, registrations: readonly Registration[], cursor: number, latestSeq: number, clock: () => number, maxDeltaBytes: number, storage: ServerStorage, logEpoch: string, wireVersion: number, events: SyncularServerEvents | undefined); /** Feed an inbound text frame (client → server control message, §8.2 * ack, §8.6.2 presence). */ handleMessage(text: string): void; /** Deliver a fanout event to this session (called by the hub for peers, * §8.6.3 receive authorization checked by the caller). */ receivePresence(scopeKey: string, kind: PresenceKind, actorId: string, clientId: string, doc: PresenceDoc | null): void; /** §8.6.3: drop presence on keys the connection no longer holds and * deliver the snapshot on keys it newly holds. Called after a * registration change (§8.7 round end, §8.1 reconnect handled by * connect's snapshot). */ reconcilePresence(previousKeys: ReadonlySet): void; /** §8.6.1: on disconnect, leave every key this session was present on. */ dropAllPresence(): void; /** Snapshot delivery on connect (§8.6.4) — the newly-registered * connection sees who is already present on each of its keys. */ deliverInitialPresence(): void; /** * Feed an inbound binary frame: a `0x01`-tagged chunk of the sync * round's request byte stream (§8.7). Synchronous entry — assembly and * violation detection happen inline so a pipelined chunk arriving * while a response streams is caught deterministically; the round * itself runs async once the request is complete. The returned promise, when * present, resolves only after response streaming and registration refresh; * coordinated hosts await it to retain their partition FIFO through commit. */ handleBinary(bytes: Uint8Array): Promise | undefined; sendHeartbeat(): void; sendWake(reason: WakeReason): void; /** Called by the hub for every applied commit, in commitSeq order. */ deliverCommit(commit: StoredCommit): void; close(): void; } export declare class RealtimeHub { #private; /** §8.6 presence registry — ephemeral, in-memory, never persisted. */ readonly presence: PresenceRegistry; constructor(config: RealtimeHubConfig); get sessionCount(): number; /** §8.6.2 published-document size cap (bytes). */ get maxPresenceBytes(): number; /** §8.6.4 presence rate cap (ms); 0 = off (reference default). */ get presenceMinIntervalMs(): number; /** §8.6.3: the scope keys a set of registrations covers. */ scopeKeysOf(registrations: readonly Registration[]): Set; /** * §8.6.3 fanout: deliver a presence change to every OTHER session * registered on the key (the privacy floor — only current scope-mates). * The publisher does not receive its own fanout. */ fanoutPresence(origin: RealtimeSession, scopeKey: string, kind: PresenceKind, doc: PresenceDoc | null): void; /** * Resolve the client record's subscription list into per-connection * registrations (§8.1 at upgrade; §8.7 at socket-round end). */ loadRegistrations(partition: string, actorId: string, clientId: string): Promise; /** * Build the per-round request context for a socket sync round (§8.7): * the same shape the HTTP adapter builds, so the round drives the * SAME handler with zero semantic divergence. */ requestContextFor(identity: { readonly partition: string; readonly actorId: string; }): SyncRequestContext; requestContext(session: RealtimeSession): SyncRequestContext; /** * Register a connected socket (§8.1): load the client's last pull's * subscription list, resolve + intersect scopes, send `hello`. */ connect(options: RealtimeConnectOptions): Promise; disconnect(session: RealtimeSession): void; /** RealtimeNotifier: fan an applied commit out to matching sessions. */ notifyCommit(partition: string, commit: StoredCommit): Promise; /** Broadcast a wake-up (host-initiated resync, schema rollover, §8.3). */ wake(partition: string, reason: WakeReason): void; } export declare function createRealtimeHub(config: RealtimeHubConfig): RealtimeHub; export {};