/** * LISTEN/NOTIFY multi-instance fanout lets * N server instances behind a load balancer wake each other's realtime * sessions when a commit lands on any one of them. * * ## The multi-instance problem * * A commit applied on instance A fans out to A's *local* RealtimeHub * sessions immediately (in-memory `notifyCommit`, full delta, no re-read). * But a client whose socket lives on instance B never saw it. LISTEN/NOTIFY * closes that gap: * * 1. after a commit lands, the originating instance calls `notify(payload)` * → `NOTIFY syncular_commit, ':'`; * 2. every instance runs a `listen()` loop on a dedicated connection; * on a notification it parses the payload and calls * `hub.wake(partition, 'sync')`. * * ## Why wake, not re-broadcast the delta * * Postgres NOTIFY payloads are capped (~8000 bytes) and are not an ordered, * lossless delta channel — so we do NOT try to ship commit bytes through * them. Remote sessions receive a `sync` wake and pull the delta from the * shared Postgres storage they already read from (§8.3 wake semantics). The * originating instance keeps the in-memory fast path (its own sessions get * the full delta with no extra read); only *cross-instance* delivery pays * the re-pull. Single-instance deployments never install a fanout at all. * * The payload carries `commitSeq` purely for observability/ordering in logs; * the wake itself is seq-agnostic (a session pulls whatever it is behind). * * pglite cannot exercise cross-connection NOTIFY, so the integration * (`postgres-fanout.integration.test.ts`) is env-gated on `SYNCULAR_PG_URL` * and skips cleanly; the payload encode/parse is unit-tested hermetically. */ import type { WakeReason } from '@syncular/core'; /** The channel every syncular instance LISTENs on. */ export declare const FANOUT_CHANNEL = "syncular_commit"; export interface FanoutPayload { readonly partition: string; readonly commitSeq: number; } /** * Encode a fanout payload as `:`. The partition is * base64url-encoded so a partition string containing `:` cannot corrupt the * frame; commitSeq is a plain decimal integer. */ export declare function encodeFanoutPayload(payload: FanoutPayload): string; /** Parse a fanout payload; returns `undefined` for a malformed frame. */ export declare function parseFanoutPayload(text: string): FanoutPayload | undefined; /** The hub surface the fanout wakes (a `RealtimeHub` satisfies this). */ export interface FanoutWakeTarget { wake(partition: string, reason: WakeReason): void; } /** * A raw notification connection. Production wires a driver's LISTEN support: * - node-postgres: a dedicated `Client` with `client.on('notification', …)` * after `LISTEN syncular_commit`; * - Bun.sql: `sql.listen(channel, handler)`. * The `notify` side can reuse the main pool (`SELECT pg_notify($1,$2)`), but * LISTEN needs its own long-lived connection. */ export interface PgNotificationConnection { /** Register a payload handler for the channel and begin listening. */ listen(channel: string, handler: (payload: string) => void): Promise | void; /** Send a notification on the channel. */ notify(channel: string, payload: string): Promise; close?(): Promise; } /** * The fanout primitive. `install(hub)` starts the LISTEN loop that wakes the * hub on every remote commit; `notifyCommit` is called after a local commit * lands to fan it to the other instances. */ export declare class PostgresFanout { #private; constructor(conn: PgNotificationConnection); /** Start the LISTEN loop, waking `hub` on every notification received. */ install(hub: FanoutWakeTarget): Promise; /** Fan an applied commit out to the other instances. */ notifyCommit(partition: string, commitSeq: number): Promise; close(): Promise; }