/** * Channel bus over Postgres LISTEN/NOTIFY. * * Chosen because it needs nothing that a Rebase deployment does not already * have — the same database, the same direct URL the CDC listener uses. Three * properties of `NOTIFY` shape everything below: * * - **8000 bytes per payload.** Presence and cursors fit with room to spare; a * scene snapshot does not. Rather than truncate or drop, an oversized frame * on a *retained* channel is published as a pointer — the body is already in * `rebase.channel_messages` with a sequence number, so the receiver reads it * back. That is the same trick the entity path uses (notify an address, * refetch the row), applied to a different table. On an ephemeral channel * there is nothing to point at, so the publish is refused loudly instead of * reaching some instances and not others. * * - **A notify is a query on the primary database.** Not a slow one, but it * competes with the application's real queries, and that — not throughput — * is what actually limits this transport. Measured, it carried ~10k * cross-instance messages/second and stayed flat out to eight instances; what * it should not do is spend 10k queries/second of the database's budget on * cursor movement. Hence the batching below. * * - **Delivery is best-effort.** Retained channels repair themselves through * the client's history replay, so a lost frame costs a live update rather * than correctness. That is what makes coalescing safe. */ import { NodePgDatabase } from "drizzle-orm/node-postgres"; import { ChannelBus, ChannelBusFrame, ChannelBusHandler } from "./ChannelBus"; /** NOTIFY channel carrying channel-bus frames. */ export declare const CHANNEL_BUS_NOTIFY_CHANNEL = "rebase_channel_bus"; /** * Postgres refuses a NOTIFY payload of 8000 bytes or more. The margin below it * is for nothing in particular — it is there so that a payload which passes this * check cannot fail at the server for being a few bytes over. */ export declare const PG_NOTIFY_MAX_PAYLOAD_BYTES = 7500; /** * How long a batching window stays open. * * Ten milliseconds is below the threshold where a human notices a cursor lag, * and it is the difference between one query per message and one query per * window under load. Set to 0 to disable coalescing entirely. */ export declare const DEFAULT_BATCH_WINDOW_MS = 10; export declare class PostgresChannelBus implements ChannelBus { private readonly db; private readonly connectionString; readonly kind: "postgres"; readonly maxFrameBytes = 7500; private listener?; private readonly batchWindowMs; /** * Frames waiting for the current window to close. * * The window is opened by a publish that found none open, and that publish * is sent *immediately* rather than joining a batch — see {@link publish}. */ private pending; private pendingBytes; private windowTimer?; private stopped; constructor(db: NodePgDatabase>, connectionString: string, options?: { batchWindowMs?: number; }); start(handler: ChannelBusHandler): Promise; /** * Publish, coalescing under load. * * The window is *leading edge*: a publish arriving when no window is open is * sent straight away and opens one, so an idle channel pays no added latency * at all. Frames arriving while it is open are collected and leave together * when it closes. The effect is that cost tracks elapsed time rather than * message count — one query per window instead of one per message — which is * the same shape as the retention pruning throttle, for the same reason. * * The returned promise settles when the frame has actually left, not when it * was queued, so the contract ("reaches the other instances, or rejects") * still holds. */ publish(frame: ChannelBusFrame): Promise; stop(): Promise; private openWindow; /** Send everything queued and settle the promises waiting on it. */ private flush; /** * One NOTIFY. * * A single frame goes out in the plain, unwrapped shape. That is not just * economy: during a rolling deploy an instance running the previous build * understands only that shape, and low-rate traffic — presence, the tail of * a session — is exactly what is flowing while pods restart. Batching only * appears under load, which shrinks the mixed-version window to almost * nothing. */ private send; } /** * Parse a bus payload into the frames it carries. * * Accepts both wire shapes — a bare frame and a `{ batch: [...] }` envelope — * so an instance on the new build understands one on the old. Returns an empty * array for anything unrecognisable: a malformed or future-versioned message * must never take the listener down. */ export declare function parseChannelBusPayload(payload: string): ChannelBusFrame[]; /** * Parse a single bus frame, returning null for anything that is not a frame we * understand. */ export declare function parseChannelBusFrame(payload: string): ChannelBusFrame | null;