import { ReactNode } from 'react'; /** * Reconnect backoff with a STABILITY RESET and jitter. * * Two mistakes this replaces, both measured in matrx-frontend: * * - **Resetting the attempt counter on SUBSCRIBED.** A flapping channel then * cycles at the 1s floor forever, and each cycle fires a full catch-up * fetch — a self-inflicted request storm on the exact network that is * already struggling. The counter resets only after the channel has STAYED * healthy for `stabilityMs`. * - **No jitter.** Every tab of every user reconnects on the same doubling * schedule after a Realtime deploy, so the service gets a synchronized * thundering herd at 1s, 2s, 4s… Jitter spreads them. */ interface BackoffOptions { /** First delay in ms; doubles up to `maxDelayMs`. Default 1000. */ initialDelayMs?: number; /** Ceiling for the delay. Default 30000. */ maxDelayMs?: number; /** * How long a connection must HOLD before its next failure starts from the * floor again. Default 30000. */ stabilityMs?: number; /** Fraction of the delay applied as random jitter, 0..1. Default 0.25. */ jitter?: number; /** Injectable for tests. Default `Math.random`. */ random?: () => number; /** Injectable for tests. Default `Date.now`. */ now?: () => number; } interface Backoff { /** Delay for the next attempt, advancing the attempt counter. */ nextDelayMs(): number; /** Record that the connection came up (starts the stability window). */ markConnected(): void; /** Consecutive failed attempts since the last stability reset. */ attempts(): number; /** True once `attempts` passes the alarm threshold — for loud diagnostics. */ isAlarming(): boolean; reset(): void; } /** * Past this many consecutive failures the channel is not "retrying", it is * BROKEN, and the diagnostics sink escalates from warn to error. Silence here * is how a permanently dead subscription hides behind a spinner. */ declare const RECONNECT_ALARM_ATTEMPTS = 6; /** * The bounded policy: after this many consecutive failed attempts WITHOUT a * stable connection in between, the manager stops retrying a channel, says so * once (`channel.reconnect.abandoned`), and leaves it `disconnected` until the * tab wakes or the network returns. A channel for a conversation that was never * created (or whose first send was refused) otherwise reconnects forever. */ declare const RECONNECT_GIVE_UP_ATTEMPTS = 10; declare function createBackoff(options?: BackoffOptions): Backoff; /** * Idempotency: at-least-once in, exactly-once out (R3). * * Realtime delivery is at-least-once by nature — a resubscribe after a flap, * a channel that rejoined while the old one was still draining, or a consumer * that also broadcasts a row it INSERTed will all deliver the same logical * event twice. Every duplicate that reaches a reducer is a doubled message in * a thread, a doubled row in a list, or a doubled network call. * * The consumer supplies an event key (or lets the package use the envelope's * `eid`); this seen-set drops the repeat. It is time-bounded, not unbounded: * a session that runs for a day must not grow a set of every event it ever saw. */ interface DedupeOptions { /** How long a key stays "seen". Default 300000 (5 minutes). */ ttlMs?: number; /** * Hard cap on retained keys. Default 5000. On overflow the OLDEST keys are * evicted — the newest events are the ones a duplicate is likely to shadow. */ maxKeys?: number; /** Injectable for tests. Default `Date.now`. */ now?: () => number; } interface Dedupe { /** `true` if this key has NOT been seen (i.e. deliver it); records the key. */ admit(key: string): boolean; /** Number of keys currently retained. */ size(): number; /** Total keys rejected as duplicates since creation. */ rejected(): number; clear(): void; } declare function createDedupe(options?: DedupeOptions): Dedupe; /** * THE ENVIRONMENT BRIDGE (R4) — tab sleep, app background, and network flaps. * * A realtime channel does not usually die loudly. It dies because the laptop * closed, because a background tab was throttled until the socket idled out, * or because Wi-Fi handed off to LTE. In every one of those the page comes back * looking fine and silently showing stale data — the worst possible failure, * because the user trusts it. * * So the manager needs to know two things about its host: is the surface * VISIBLE, and is the device ONLINE. This port supplies them. It ships a * working default for every platform we run on — the browser one here, the * React Native one in `../native/environment`, and an inert one for SSR and * node tests. A host never has to fill this in to get correct behavior. */ type EnvironmentSignal = "visible" | "hidden" | "online" | "offline"; interface EnvironmentBridge { /** Name of the bridge, for the diagnostics surface. */ readonly name: string; isVisible(): boolean; isOnline(): boolean; subscribe(listener: (signal: EnvironmentSignal) => void): () => void; } /** * The SSR / node / worker bridge: always visible, always online, no events. * It is inert by design and says so — importing this package during SSR must * never touch `document` or `window`. */ declare function createInertEnvironment(): EnvironmentBridge; /** The browser bridge: `visibilitychange` + `online`/`offline`. */ declare function createBrowserEnvironment(): EnvironmentBridge; /** * The default: the browser bridge where a DOM exists, the inert one otherwise. * This is what makes `import`ing the package safe during SSR (contract point 4 * of the independence contract). */ declare function defaultEnvironment(): EnvironmentBridge; /** * THE WRITE LEDGER — echo suppression for Postgres Changes, and the named * solution to autosave↔realtime coexistence (R2 + R6). * * The problem, stated exactly (it is not the one people expect): * * Supabase sends you your OWN writes. Postgres Changes has no "not to self" * option — the echo of your UPDATE arrives 50–500ms AFTER your REST response * already returned the fresh row. By the time the echo lands, any "save in * flight" flag has been cleared. **Flag-only suppression therefore always * misses the echo.** * * What an unsuppressed echo does (the notes freeze, 2026-07): the echo of save * N arrives while the user has typed toward N+1 → dirty-local ≠ server → a * false "conflict" → the conflict UI does O(n²) diff work every render → the * next save clears it → its echo re-arms it. Self-sustaining main-thread * saturation, from one missing comparison. * * And what OVER-suppression does (the notes phantom conflict, 2026-09-13): * every platform table carries an integer `version` that `_touch_row` bumps on * EVERY update — a desktop sync writing `file_path`, an ingest job stamping * metadata, a folder move. Such a write changes nothing the editor compares * (same content, same label), and it lands under the same actor. A ledger that * decides "same content + same actor + our write pending ⇒ own echo" swallows * it — and the client keeps the OLD version. Its next save's compare-and-swap * misses on a row whose text is identical to the client's own base, and the * user is shown a conflict dialog for an edit nobody made. The client had * suppressed the one event that carried the number it needed. * * So the ledger is REVISION-MONOTONIC FIRST, then timestamp, then flags: * * R1. A revision strictly older than what we hold → `stale`. * R2. A revision equal to the one an unsettled write of ours will produce, * with matching content (or, with no content to compare, the same actor) * → `own-echo` (this is the echo that beats the REST response). A newer * revision carrying exactly the content of an unsettled write of ours is * also our echo (our write landed above the number we expected, after a * compare-and-swap rebase). * R3. A revision equal to what we already settled, with matching content → * `own-echo`; with different content → `remote` (a real divergence). * R4. A revision NEWER than anything we hold or expect → `remote`, ALWAYS — * same actor, same content, pending write or not. We did not produce that * revision, or we have not learned it yet; either way the consumer must * see it, because a version the client never adopts is the next false * conflict. Delivering it is harmless: a consumer that compares content * finds nothing to merge and simply advances its base. * * Only when neither side carries a revision does the older doctrine apply: * * 1. Strictly older `updatedAt` than what we already hold → `stale`. An event * that is not newer carries zero information, and merging it REGRESSES the * row's `updated_at`, which then fails the next save's optimistic lock and * produces a sticky false conflict. * 2. Content we are known to have written and not yet settled → `own-echo`. * 3. Equal `updatedAt` → an echo only if the content fingerprint also matches. * A same-millisecond collaborator write must still land. * 4. While one of OUR writes is unsettled, an event attributed to this client * (by `requestId`, by client id, or by actor id) is an echo — but a * DIVERGENT payload still comes through as `remote`, because that is a real * conflict and hiding it is worse than showing it. * 5. If either timestamp is unparseable, fall through to the fingerprint and * otherwise DELIVER. Degrade to delivering, never to silently dropping. * * `localWritePending` on the verdict is R6's answer: the consumer can tell an * optimistic local write still in flight from its echo from a genuine remote * write, using package APIs alone. */ type EventOrigin = "own-echo" | "remote" | "stale"; type OriginReason = "request-id-match" | "older-revision" | "pending-write-same-revision" | "equal-revision-same-content" | "newer-revision" | "older-timestamp" | "equal-timestamp-same-content" | "pending-write-same-actor" | "pending-write-same-content" | "no-local-record" | "newer-timestamp" | "divergent-content"; interface OriginVerdict { origin: EventOrigin; /** Stable machine reason — the diagnostics surface counts these. */ reason: OriginReason; /** True while THIS client has an unsettled write for the row (autosave in flight). */ localWritePending: boolean; } interface LocalWrite { /** Fully-qualified table, e.g. `workbench.notes`. */ table: string; /** Row primary key as a string. */ id: string; /** Content fingerprint of what we wrote — any stable string the consumer builds. */ fingerprint?: string | undefined; /** The `updated_at` we expect the server to stamp, once known. */ updatedAt?: string | undefined; /** * The integer revision this write will PRODUCE (the row's `version` after * the server's `_touch_row` bump — on a platform table that is the * compare-and-swap `expectedVersion + 1`). Supplying it is what lets the * ledger recognize the echo that beats the REST response by number instead * of by content, and — the important half — refuse to call a HIGHER number * an echo. */ revision?: number | undefined; /** * The id the SERVER will stamp on the row's metadata for this write, when * the write goes through a backend that stamps one (the `metadata.request_id` * convention). An exact-match echo test, and the only fully reliable one. */ requestId?: string | undefined; } interface WriteTicket { readonly key: string; readonly requestId: string | undefined; } interface ClassifyInput { table: string; id: string; updatedAt?: string | null | undefined; fingerprint?: string | undefined; /** `updated_by` from the row, if the table carries the actor stamp. */ updatedBy?: string | null | undefined; /** `metadata.request_id` from the row, if the backend stamps one. */ requestId?: string | null | undefined; /** The row's integer `version`, if the table carries one. */ revision?: number | null | undefined; } interface WriteLedgerOptions { /** * The user id this client acts as, when known. Enables the `updated_by` * echo test — the cheapest and most reliable one for tables carrying the * platform `_stamp_actor` trigger. */ actorId?: string | undefined; /** * How long an unsettled write stays on the ledger before it is assumed lost. * Default 30000. Without a TTL a failed request holds suppression open * forever and the row stops updating from anyone. */ ttlMs?: number; /** Injectable for tests. Default `Date.now`. */ now?: () => number; } interface SettleResult { updatedAt?: string | undefined; fingerprint?: string | undefined; /** The revision the server actually stamped (the REST response's `version`). */ revision?: number | undefined; } interface ObservedRecord { table: string; id: string; updatedAt?: string | null | undefined; fingerprint?: string | undefined; /** The row's `version` as read. */ revision?: number | null | undefined; } interface WriteLedger { /** Register a write BEFORE the optimistic reducer and the REST call. */ begin(write: LocalWrite): WriteTicket; /** Record what the server actually returned. Keeps the monotonic floor honest. */ settle(ticket: WriteTicket, result?: SettleResult): void; /** The request failed; stop suppressing for it. */ abandon(ticket: WriteTicket): void; /** Record server state we learned some other way (a list fetch, a backfill). */ observe(record: ObservedRecord): void; /** Classify one incoming payload. */ classify(input: ClassifyInput): OriginVerdict; /** Is one of OUR writes still unsettled for this row? */ isPending(table: string, id: string): boolean; /** The highest revision the ledger holds for a row, if it holds one. Diagnostics and tests. */ heldRevision(table: string, id: string): number | undefined; /** Rows currently tracked. Diagnostics. */ size(): number; clear(): void; } /** A revision is meaningful only as a finite integer; anything else is "absent". */ declare function canonicalRevision(value: number | null | undefined): number | undefined; declare function createWriteLedger(options?: WriteLedgerOptions): WriteLedger; /** * Structural contracts for @ai-matrx/realtime. * * THE PACKAGE NEVER IMPORTS `@supabase/supabase-js`. Every Supabase surface it * touches is described here STRUCTURALLY, so the package installs into a Vite * app, a Next.js app, an Expo app, a Chrome extension, and a node test harness * with one dependency graph and no version skew against the host's client. * * The signatures below must match Supabase's real overloads PRECISELY — a * wider filter type or a `never`-typed callback makes the real client fail to * satisfy these interfaces, and the HOST discovers it at its own typecheck * instead of us discovering it here. `src/core/supabase-shape.test.ts` carries * the compile-time proof against a stand-in that mirrors the real declarations. */ /** Where a channel currently stands, from the consumer's point of view. */ type RealtimeStatus = "connecting" | "connected" | "reconnecting" | "disconnected"; /** Aggregate connection health across every channel this manager owns. */ type ConnectionHealth = "live" | "degraded" | "down"; type PostgresChangeEvent = "*" | "INSERT" | "UPDATE" | "DELETE"; /** The payload `postgres_changes` delivers. */ interface PostgresChangePayload { eventType?: string; schema?: string; table?: string; new?: unknown; old?: unknown; commit_timestamp?: string; errors?: unknown; } /** The `postgres_changes` filter, exactly as Supabase declares it. */ interface PostgresChangeFilter { event: PostgresChangeEvent; schema: string; table?: string; filter?: string; } /** A presence roster keyed by presence key, as `presenceState()` returns it. */ type PresenceStateLike = Record; /** Structural stand-in for `RealtimeChannel`. */ interface RealtimeChannelLike { on(type: "postgres_changes", filter: PostgresChangeFilter, callback: (payload: PostgresChangePayload) => void): RealtimeChannelLike; on(type: "broadcast", filter: { event: string; }, callback: (payload: { event: string; payload?: unknown; }) => void): RealtimeChannelLike; on(type: "presence", filter: { event: "sync" | "join" | "leave"; }, callback: (payload: unknown) => void): RealtimeChannelLike; subscribe(callback?: (status: string, error?: Error) => void): unknown; send(args: { type: "broadcast"; event: string; payload?: unknown; }): unknown; track(state: Record): unknown; untrack(): unknown; presenceState(): PresenceStateLike; } /** Structural stand-in for `SupabaseClient` (realtime surface only). */ interface RealtimeClientLike { channel(topic: string, params?: unknown): RealtimeChannelLike; removeChannel(channel: RealtimeChannelLike): unknown; /** * The realtime socket handle. Optional because the package accepts a client * STRUCTURALLY and must never require more of it than it uses: this is read * only for a PRIVATE channel, where Supabase authorizes the topic against * `realtime.messages` RLS and refuses the join unless the socket is carrying * the current access token. A client without it simply cannot open a private * channel, and the manager says so rather than joining a channel that will * silently receive nothing. */ realtime?: { setAuth(token?: string): unknown; }; } /** * The diagnostic sink. Background failures NEVER go to a swallowed catch and * never to a bare `console.log` a host cannot route — they come out here, typed, * with a remedy sentence. The default sink writes to `console.warn`/`error` so * a host that injects nothing still sees everything. */ type DiagnosticLevel = "info" | "warn" | "error"; interface DiagnosticEvent { level: DiagnosticLevel; /** Stable machine code, e.g. `channel.reconnect.scheduled`. */ code: string; message: string; /** What the operator or developer should DO about it. Never empty for warn/error. */ remedy?: string; topic?: string; detail?: Record; } type DiagnosticsSink = (event: DiagnosticEvent) => void; /** The Matrx envelope every package-sent broadcast carries. */ interface MatrxBroadcastEnvelope { /** Envelope version — bump only with a documented consumer action. */ v: 1; /** The sending client session. This is what makes echo suppression possible. */ cid: string; /** Unique per emitted event; the dedup key when the consumer supplies none. */ eid: string; /** Wall-clock send time (ms). Diagnostics + ordering hints only. */ ts: number; /** The consumer's actual payload. */ data: TPayload; } /** * PRESENCE (R5) — who is here, without ghosts. * * Supabase presence removes a member when its socket closes cleanly. The * interesting cases are the ones where it does NOT close cleanly: a closed * laptop, a killed tab, a phone that lost signal in a tunnel. The entry then * lingers until the server's own timeout, and a room shows a participant who * left twenty minutes ago. Worse, a rejoin during that window shows the same * person twice. * * Two mechanisms fix that here, and neither depends on clocks agreeing between * devices (they never do): * * - **Heartbeat.** This client re-`track()`s itself every `heartbeatMs`. That * is a positive liveness signal, not an absence of one. * - **Local-clock expiry.** Every member's `lastSeenAt` is stamped with OUR * clock when WE received their state. A member we have not heard from in * `expireAfterMs` is pruned from the roster we publish, whatever the server * still holds. A ghost therefore cannot outlive one expiry window. * * Join/leave bursts are debounced: a rejoining channel emits a full `sync` plus * a `join` per member, and a roster that re-renders once per member is the * O(N²) selector storm this package exists to prevent. */ interface PresenceMember { /** The presence key this member tracked under. */ key: string; /** The member's client session id (distinct per tab, even for one user). */ clientId: string; state: TState; /** OUR clock, first time we saw this member. */ joinedAt: number; /** OUR clock, most recent time we saw this member's state. */ lastSeenAt: number; isSelf: boolean; } interface PresenceOptions> { /** The presence key for THIS client. Defaults to the client session id. */ key?: string; /** What this client publishes about itself. */ state: TState; /** How often this client re-announces itself. Default 15000. */ heartbeatMs?: number; /** A member unheard-from this long is pruned locally. Default 45000. */ expireAfterMs?: number; /** Coalesce sync/join/leave bursts into one emission. Default 150. */ debounceMs?: number; /** * Whether THIS holder announces itself. Default true. * * One channel carries ONE presence entry per key, so a topic must have * exactly one publisher: a second holder that also tracks overwrites the * first's state on every heartbeat, and the platform's own typing indicator * is what that breaks. A surface that only wants to READ the roster (a * typists label, an online-dot list) sets `publish: false` — it still sees * everyone, it just stops fighting over the entry. */ publish?: boolean; onChange?: (members: readonly PresenceMember[]) => void; } interface PresenceRoom> { readonly key: string; members(): readonly PresenceMember[]; /** Publish new state for this client. Merged, not replaced. */ update(state: Partial): void; subscribe(listener: (members: readonly PresenceMember[]) => void): () => void; dispose(): void; } interface PresenceRuntime> extends PresenceRoom { /** Called by the manager once the channel is SUBSCRIBED. */ onSubscribed(channel: RealtimeChannelLike): void; /** Called by the manager when the channel drops (roster is no longer trustworthy). */ onDisconnected(): void; /** Called by the manager whenever the channel reports a presence event. */ onPresenceEvent(channel: RealtimeChannelLike): void; } interface PresenceTimers { setInterval: (run: () => void, ms: number) => unknown; clearInterval: (handle: unknown) => void; setTimeout: (run: () => void, ms: number) => unknown; clearTimeout: (handle: unknown) => void; now: () => number; } declare function createPresenceRuntime>(options: PresenceOptions, timers?: PresenceTimers): PresenceRuntime; /** * THE REALTIME MANAGER — the one object that owns channels. * * Everything a consumer used to hand-roll lives here, and none of it is * optional: unique topics, enforced teardown, echo suppression, dedup, the * decoupled ordered handler queue, jittered reconnect with a stability reset, * a BACKFILL door on every recovery path, tab-sleep and network awareness, and * live diagnostics. Opening a channel is one call; there is no correct-usage * checklist to forget. * * THE RULE THAT MADE THIS PACKAGE: realtime has NO REPLAY. Every event that * happened while the socket was down is gone forever. A reconnect without a * re-read leaves a permanently wrong screen that looks perfectly healthy — so * reconnecting IS re-reading here, not something each caller remembers. */ /** Why the package is asking the consumer to re-read from the database. */ type BackfillReason = "reconnect" | "wake" | "network-restored" | "queue-overflow" | "manual"; interface BackfillContext { reason: BackfillReason; topic: string; /** ms the channel was not receiving events, when known. */ gapMs: number | null; } interface PostgresChangeDelivery> { payload: PostgresChangePayload; /** The row the event carries (`new` for INSERT/UPDATE, `old` for DELETE). */ row: TRow | null; /** Where this event came from, per the write ledger. Always `remote` here. */ origin: OriginVerdict; } interface PostgresChangeBinding> { event: PostgresChangeEvent; schema: string; table: string; /** PostgREST filter, e.g. `file_id=eq.`. */ filter?: string; /** * The row's primary key, as a string. REQUIRED for echo suppression and for * the default dedup key — without it the package cannot tell your write from * anyone else's, and it says so once through the diagnostics sink rather than * silently degrading. */ rowId?: (row: Record) => string | undefined; /** A stable string summarizing the row's content. Powers content-aware echo tests. */ fingerprint?: (row: Record) => string | undefined; /** Column carrying the row's `updated_at`. Default `updated_at`. */ updatedAtField?: string; /** Column carrying the actor stamp. Default `updated_by`. */ updatedByField?: string; /** * Column carrying the row's integer revision. Default `version` — every * platform base-contract table has one, bumped by `_touch_row` on EVERY * update. A revision the ledger has never seen is never suppressed as an * echo, whatever the content or actor says (see `origin.ts` R4). Set to * `null` for a table whose `version` column means something else. */ revisionField?: string | null; onChange: (delivery: PostgresChangeDelivery) => void | Promise; } interface BroadcastMessage { event: string; data: TData; /** The sending client session id. */ from: string; eventId: string; sentAt: number; /** False when the message did not carry a Matrx envelope (a foreign sender). */ enveloped: boolean; } interface BroadcastBinding { event: string; onMessage: (message: BroadcastMessage) => void | Promise; } /** * RAW WIRE — for a channel whose payload shape is owned by a FOREIGN PEER. * * Normally this package wraps every broadcast in the Matrx envelope * (`{v, cid, eid, ts, data}`), and that envelope is what makes echo suppression * and dedup possible at all. But some channels do not get to choose their wire: * the other end is a separately released program that reads a payload shape it * defined — the deployed Chrome extension's `matrx-extension-bridge:` * channel is the case that produced this mode. Wrapping there would put a shape * on the wire the peer does not recognize, and the peer would simply stop * hearing us with nothing failing loudly on either side. * * So raw mode means EXACTLY this, in both directions: * * - **Send:** the consumer's payload goes on the wire verbatim. Byte-identical * to what a hand-rolled `channel.send({type:"broadcast", event, payload})` * would have put there. * - **Receive:** no envelope is read, ever — not even from a payload that * happens to look enveloped. `BroadcastMessage.enveloped` is always `false` * and `data` is the payload exactly as it arrived. * * WHAT THAT COSTS, AND WHY IT IS NEVER SILENT. Two protections ride the * envelope, and raw mode has to get them back explicitly or announce their * absence: * * 1. **Client-side echo suppression** used `cid`. There is no `cid` on a raw * wire, so either supply {@link RawWireOptions.isOwnMessage} (the peer's * shape carries something identifying the sender) or set * {@link RawWireOptions.acceptEchoFromSelf} to say in code that you accept * the server-side `broadcast.self:false` as the only protection. Supplying * neither is legal and produces a `channel.raw.echo-unguarded` warning — the * package will not quietly downgrade a guarantee. * 2. **Dedup** keyed off `eid`. Without one, every arrival is a distinct key * and dedup does nothing. Supply {@link ChannelSpec.eventKey} — it receives * the raw payload — or accept a `channel.raw.dedup-unguarded` warning. * * Raw mode is TRANSITIONAL by intent: it exists so both ends of a foreign wire * can ride this package today, and so the day the peer ships an envelope-aware * release, the only change is deleting `wire` from the spec. */ interface RawWireOptions { /** * Does this inbound payload come from THIS client? Return `true` and the * message is suppressed exactly as an envelope echo would have been. Supply it * whenever the foreign shape carries anything identifying the sender. */ isOwnMessage?: (payload: unknown) => boolean; /** * Set `true` to state, in code, that this channel runs without client-side * echo suppression because the foreign shape carries no sender identity. The * package still announces the fact (at `info`), because a reader of the * diagnostics stream must be able to see which channels are unguarded. */ acceptEchoFromSelf?: boolean; } /** How this channel's broadcasts ride the wire. Default: the Matrx envelope. */ type BroadcastWire = { mode: "matrx"; } | ({ mode: "raw"; } & RawWireOptions); interface ChannelSpec = Record> { /** Build it with a channel namespace — never a hand-written string. */ topic: string; postgresChanges?: readonly PostgresChangeBinding[]; broadcast?: readonly BroadcastBinding[]; presence?: PresenceOptions; /** * THE BACKFILL DOOR. Re-read whatever the missed events would have changed. * Called on every recovery path: reconnect, tab wake, network restore, and * queue overflow. Omitting it is allowed and LOUD — the package warns once, * because a channel with no reconciliation is a screen that will eventually * lie. */ onBackfill?: (context: BackfillContext) => void | Promise; onStatusChange?: (status: RealtimeStatus) => void; /** * Echo suppression is ON by default and opting out is the discouraged path * (R2). Set `false` only when the consumer genuinely needs to see its own * writes come back over the wire. */ echoSuppression?: boolean; /** * A PRIVATE channel — Supabase authorizes the topic against `realtime.messages` * RLS before allowing the join, which is how Database Broadcast * (`realtime.broadcast_changes()` from a Postgres trigger) delivers rows only * to the users entitled to them. * * Two things follow, and both are handled here rather than by every consumer: * the channel is created with `config.private: true`, and the socket must be * carrying the current access token, so `client.realtime.setAuth()` is awaited * BEFORE subscribing. Skipping that second step is the silent failure this * exists to prevent — the join is refused and the channel receives nothing * while looking perfectly healthy. * * A client with no `realtime.setAuth` cannot open a private channel; the * manager emits `channel.private.unsupported` and connects anyway as a public * channel would, so the failure is visible instead of mysterious. */ private?: boolean; /** * THE WIRE MODE for this channel's broadcasts. Defaults to `{mode:"matrx"}`. * Use `{mode:"raw", …}` ONLY when a foreign peer owns the payload shape — * see {@link RawWireOptions} for what raw mode costs and how to buy it back. * A raw channel's `topic` is likewise the peer's contract, so declare its * namespace with `foreignTopic` rather than inventing an `mx:` name. */ wire?: BroadcastWire; /** * Idempotency key for an event. Defaults to the broadcast envelope's `eid`, * and for Postgres Changes to `table:rowId:eventType:commitTimestamp`. * On a raw-wire channel there is no `eid`, so supplying this is the ONLY way * to keep dedup — `payload` is the raw payload exactly as it arrived. */ eventKey?: (source: "broadcast" | "postgres_changes", payload: unknown) => string | undefined; /** * Called when this holder gains or loses TAB LEADERSHIP of its topic. Several * holders of one topic in one tab (two Board tiles of one document) share one * client id and one channel; the work that must happen once per tab — the * autosave, the host election — belongs to the leader. See * {@link ChannelHandle.isLocalLeader}. */ onLeaderChange?: (isLeader: boolean) => void; } interface ChannelHandle = Record> { readonly topic: string; status(): RealtimeStatus; /** * Send a broadcast, wrapped in the Matrx envelope so receivers can suppress * it — unless the spec declared `wire: {mode:"raw"}`, in which case `data` * goes on the wire verbatim. */ send(event: string, data: TData): void; /** The presence room, when the spec declared one. */ presence: PresenceRoom | null; /** Ask for a reconciliation pass right now. */ requestBackfill(reason?: BackfillReason): void; /** * True for exactly ONE open holder of this topic per manager (per tab): the * longest-open one. When it closes, the next-oldest becomes leader and hears * `onLeaderChange(true)`. Use it to run once-per-tab work (autosave, host * election) — same-tab holders share a client id, so nothing else can tell * them apart. A holder with no peers is the leader. */ isLocalLeader(): boolean; /** Enforced teardown. Idempotent. */ close(): void; } interface ManagerTimers extends PresenceTimers { schedule: (run: () => void) => void; } interface RealtimeManagerOptions { /** Any Supabase client. The package never constructs one — hosts own identity. */ client: RealtimeClientLike; /** The signed-in user id, when known. Enables the `updated_by` echo test. */ actorId?: string | undefined; /** Visibility/online bridge. Defaults to the browser one, or inert under SSR. */ environment?: EnvironmentBridge; /** Background-failure sink. Defaults to a console sink that states the remedy. */ diagnostics?: DiagnosticsSink; backoff?: BackoffOptions; /** * Consecutive failed reconnects (no stable connection between) after which a * channel is abandoned: one `channel.reconnect.abandoned` diagnostic, status * `disconnected`, no more timers. A tab wake or network restore starts a * fresh bounded round. Default 10; 0 or Infinity disables the bound. */ giveUpAfterAttempts?: number; dedupe?: DedupeOptions; queue?: { maxDepth?: number; sliceMs?: number; }; /** Share one write ledger with the host's write path. One is created if omitted. */ ledger?: WriteLedger; /** * How long a surface may be hidden before waking triggers a reconcile. * Default 10000 — shorter than any realistic socket idle-out. */ wakeBackfillAfterMs?: number; timers?: ManagerTimers; } interface RealtimeManager { readonly clientId: string; /** The shared write ledger — register optimistic writes on it (R6). */ readonly ledger: WriteLedger; open = Record>(spec: ChannelSpec): ChannelHandle; health(): ConnectionHealth; /** Reconcile every open channel (a manual "I don't trust the screen" button). */ backfillAll(reason?: BackfillReason): void; openChannelCount(): number; dispose(): void; } declare function createRealtimeManager(options: RealtimeManagerOptions): RealtimeManager; /** * `` — the one place an app wires realtime. * * The host injects IDENTITY and nothing else: a Supabase client and the signed-in * user id. Every hard part (topics, echo suppression, dedup, the handler queue, * reconnect, backfill, tab-sleep, presence expiry, diagnostics) is already * inside the manager (C22). * * The manager is torn down on unmount, which tears down every channel it owns. * That is the enforced lifecycle from R1: a leaked channel is not something a * consumer can cause from here. */ interface RealtimeProviderProps { /** Any Supabase client. `null` while the host is still resolving one. */ client: RealtimeClientLike | null | undefined; /** The signed-in user id, when known. Enables the `updated_by` echo test. */ actorId?: string | undefined; /** Defaults to the browser bridge (inert under SSR). `/native` supplies the RN one. */ environment?: EnvironmentBridge; /** Defaults to a console sink that always states the remedy. */ diagnostics?: DiagnosticsSink; backoff?: BackoffOptions; dedupe?: DedupeOptions; queue?: { maxDepth?: number; sliceMs?: number; }; /** Share the host's write ledger so optimistic writes suppress their own echoes. */ ledger?: WriteLedger; children: ReactNode; } declare function RealtimeProvider(props: RealtimeProviderProps): ReactNode; /** * The manager, or `null` while the host has no client yet. Every hook in this * package tolerates `null` by staying disconnected — a surface rendered before * auth resolves must not throw. */ declare function useRealtimeManager(): RealtimeManager | null; /** * Is a `` mounted above this component? * * For the ONE decision that needs it: a package that owns realtime work of its * own (messaging, meet) must ride the host's single manager when the host has * one — a second manager means a second write ledger, so one manager's * optimistic echo looks remote to the other — and mount its own only in an app * that wires none. `useRealtimeManager() !== null` cannot answer that question: * it is also null while a mounted provider waits for its Supabase client, and a * package that guessed "no provider" there would mount one, then tear it (and * every channel under it) down the moment the host's client arrived. */ declare function useIsRealtimeProviderMounted(): boolean; /** * The manager, or a thrown error naming the fix. Use it in code that genuinely * cannot proceed without realtime — never to "handle" a missing provider. */ declare function useRequiredRealtimeManager(): RealtimeManager; /** Aggregate connection health, polled. For a live/degraded badge. */ declare function useRealtimeHealth(pollMs?: number): ConnectionHealth; interface UseChannelOptions { /** Turn the subscription off without unmounting. Default `true`. */ enabled?: boolean; } interface UseChannelResult = Record> { status: RealtimeStatus; /** Send a broadcast on this channel, enveloped so receivers suppress the echo. */ send: (event: string, data: TData) => void; requestBackfill: (reason?: BackfillReason) => void; /** The live handle, or `null` while disconnected/disabled. */ channel: ChannelHandle | null; } declare function useChannel = Record>(spec: ChannelSpec | null, options?: UseChannelOptions): UseChannelResult; interface UsePresenceArgs> { /** Build it with a channel namespace — never a hand-written string. */ topic: string; presence: PresenceOptions; /** Extra bindings to share this channel with (R1: one channel per feature). */ spec?: Omit, "topic" | "presence">; } interface UsePresenceResult> { members: readonly PresenceMember[]; /** Everyone but this client. The usual thing a UI wants. */ others: readonly PresenceMember[]; status: RealtimeStatus; /** Publish new state for this client (merged, not replaced). */ update: (state: Partial) => void; } declare function usePresence>(args: UsePresenceArgs, options?: UseChannelOptions): UsePresenceResult; /** * THE TYPING LIFECYCLE (R5) — start, heartbeat, stop, timeout. * * "X is typing…" that never goes away is the most-reported realtime bug in * every chat product ever built, and it has exactly one cause: the STOP is an * event, and events get lost. The tab closed, the network dropped, the user * navigated away mid-word — and the last thing anyone heard was "started". * * The cure is to stop treating "typing" as a latch and treat it as a LEASE: * * - Typing is re-announced every `heartbeatMs` while the user is typing. * - A remote typist is shown only while their lease is fresh, measured on OUR * clock at OUR receipt time — so no clock skew, and no dependence on a stop * message ever arriving. * - This client stops advertising `idleMs` after the last keystroke, and sends * an explicit stop as a courtesy, never as the mechanism. * * A crashed sender's indicator therefore clears in at most one expiry window, * automatically, with no message from anyone. */ interface TypingState extends Record { typing: boolean; } interface TypingMember { key: string; clientId: string; /** Whatever the consumer put on the presence state (name, avatar, userId…). */ state: Record; } interface TypingOptions { /** Stop advertising this long after the last keystroke. Default 3000. */ idleMs?: number; /** Re-announce interval while actively typing. Default 2000. */ heartbeatMs?: number; /** * Drop a remote typist we have not heard from in this long. Must exceed * `heartbeatMs` with margin for a slow network. Default 6000. */ expireAfterMs?: number; onChange?: (typists: readonly TypingMember[]) => void; } interface TypingTracker { /** Call on EVERY keystroke. Cheap, idempotent, self-throttling. */ keystroke(): void; /** Explicit stop (send pressed, blur, unmount). A courtesy, not the mechanism. */ stop(): void; typists(): readonly TypingMember[]; subscribe(listener: (typists: readonly TypingMember[]) => void): () => void; dispose(): void; } declare function createTypingTracker(room: PresenceRoom, options?: TypingOptions, timers?: PresenceTimers): TypingTracker; interface UseTypingArgs { /** Build it with a channel namespace — never a hand-written string. */ topic: string; /** * What other clients should know about this typist (userId, display name, * avatar). It rides the presence state and comes back on every `TypingMember`. */ identity: Record; /** Presence key for this client. Defaults to the client session id. */ presenceKey?: string; /** * Whether this holder ANNOUNCES its own typing. Default true. * * A composer publishes; a typists label only reads. One channel carries one * presence entry per key, so mounting both as publishers on one topic makes * them overwrite each other — the read-only one passes `publish: false`. */ publish?: boolean; typing?: TypingOptions; } interface UseTypingResult { typists: readonly TypingMember[]; /** Call on EVERY keystroke. Self-throttling. */ onKeystroke: () => void; /** Explicit stop — on send, blur, or navigation. */ stopTyping: () => void; status: RealtimeStatus; } declare function useTyping(args: UseTypingArgs, options?: UseChannelOptions): UseTypingResult; /** * THE CHANNEL-NAMING REGISTRY — one place where channels are named, so two * features can never collide (R1). * * Two failures live here, and they look nothing alike: * * 1. **Two features choosing the same topic string.** Supabase's * `RealtimeClient.channel(topic)` DEDUPES BY TOPIC: a second call with a * topic that is still registered returns the SAME channel object. Two * features that both picked `"messages"` therefore silently share one * channel, and the second one's `.on(...)` calls land after the first * one's `subscribe()` — which throws, or (worse) attaches nothing. The cure * is a declared namespace per feature, refused at registration time if it * is already taken by a different declaration. * * 2. **One feature colliding with ITSELF across a remount.** React 19 * double-invokes effects and Fast Refresh re-runs them, while * `removeChannel` is async and NOT awaited by React. The remount's * `.channel(topic)` therefore hands back the previous mount's still-joined * channel, and `.on("postgres_changes", ...)` on it throws "cannot add * postgres_changes callbacks after subscribe()". The cure is * `uniqueChannelTopic()`. * * 🚨 **`uniqueChannelTopic()` IS FOR POSTGRES CHANGES ONLY.** For that transport * the topic really is nothing but a client-side connection key — the server * pushes on the declared filter — so making every INSTANCE unique costs nothing. * For **broadcast and presence the topic IS the room**: it is the routing key * the server fans messages out on, and a unique instance topic silently puts * every participant in a room of one. That was the 0.1.x defect. Those channels * go on the wire with their DECLARED topic and are shared+ref-counted inside the * client by `rooms.ts`, which is where failure 2 is handled for them. * * Both are applied automatically by the manager — a consumer names a channel * through a namespace and never thinks about any of it again. */ /** Every Matrx channel topic starts here, so a topic is recognizable in logs. */ declare const TOPIC_PREFIX = "mx"; interface ChannelNamespaceSpec { /** Feature-level name, unique platform-wide. Lowercase, `[a-z0-9-]`. */ namespace: string; /** * Ordered part names this namespace requires, e.g. `["userId"]` or * `["conversationId"]`. A namespace with zero parts is a singleton channel. */ parts: readonly string[]; /** What this channel carries. Shown by the diagnostics surface. */ description: string; /** * A FOREIGN topic root, used verbatim in place of `mx:`. * * Only for a channel whose topic string is a contract owned by a separately * released peer — the deployed Chrome extension's `matrx-extension-bridge` * is the case this exists for. Renaming such a topic to `mx:…` does not * migrate anything; it puts this client in a room of its own while the peer * keeps listening where it always did, silently. * * The namespace is still declared and still collision-checked, so nothing * else can claim it and the diagnostics surface still lists it. Pair it with * `wire: {mode:"raw"}` on the channel spec — a foreign topic almost always * comes with a foreign payload shape. */ foreignTopic?: string; } interface ChannelNamespace { readonly spec: ChannelNamespaceSpec; /** Build the stable topic for one instance of this channel. */ topic(parts?: Readonly>): string; } declare class ChannelNamespaceCollisionError extends Error { readonly namespace: string; readonly existing: ChannelNamespaceSpec; readonly attempted: ChannelNamespaceSpec; constructor(namespace: string, existing: ChannelNamespaceSpec, attempted: ChannelNamespaceSpec); } /** * Declare a channel namespace. Idempotent for an identical declaration (so * Fast Refresh and dual module graphs are fine); throws on a genuine collision. * * Outside production, the SAME channel (same parts and foreign topic) re-declared * with a new description is a newer copy of its own module — Fast Refresh, or a * package reinstalled under a long-running dev server whose globalThis registry * still holds the old declaration. It replaces the old declaration and warns; * it never throws (live 2026-10-07: one in-place description change 500'd every * page on the shared dev server). A different shape under the same name is two * channels and is always refused; in production a description mismatch is a * second declarer and is refused too. */ declare function defineChannelNamespace(spec: ChannelNamespaceSpec): ChannelNamespace; /** Every namespace declared in this realm — the diagnostics surface reads it. */ declare function listChannelNamespaces(): readonly ChannelNamespaceSpec[]; /** * Make a topic unique to THIS channel instance. **Postgres Changes only** — see * the file header. Calling it for a broadcast or presence channel splits the * room. * * Ported from `@ai-matrx/data`'s `uniqueChannelTopic` (40 call sites in * matrx-frontend). Channel NAMING belongs to this package, so this is its home; * `@ai-matrx/data` keeps its copy only until its consumers move here (see the * README § Migrating off hand-rolled channels). * * The counter lives in a globalThis slot, not a module `let`: with dual * ESM/CJS output an unsplit module can be instantiated twice, and two counters * both starting at 1 hand out the same "unique" topic twice. */ declare function uniqueChannelTopic(base: string): string; /** Test-only: forget every declaration so a suite can prove collision behavior. */ declare function resetChannelRegistryForTests(): void; /** * THE DIAGNOSTICS SURFACE (R7) — realtime bugs must be observable, not guessed. * * Every realtime bug we have ever chased was diagnosed by adding console.logs * to production code and shipping them. This is that, permanently and for free: * a live inventory of every channel this realm has open, its subscription * state, what it has received, and — the numbers nobody ever had — how many * events were suppressed as echoes, dropped as duplicates, refused as stale, * and shed under queue pressure. * * It is DATA, not UI (D3: this package is headless). `useRealtimeDiagnostics` * in `/react` polls it; an admin surface renders it. */ interface ChannelDiagnostics { topic: string; namespace: string | undefined; status: RealtimeStatus; /** ms since epoch of the most recent successful SUBSCRIBED. */ subscribedAt: number | null; /** How many times this channel has come back after dropping. */ reconnects: number; /** Consecutive failed connect attempts right now. */ failedAttempts: number; /** Raw events handed over by the socket. */ received: number; /** Events that actually reached a consumer handler. */ delivered: number; /** Dropped as this client's own echo. */ suppressedEcho: number; /** Dropped as a duplicate of an event already delivered. */ deduped: number; /** Dropped as strictly older than state we already hold. */ stale: number; /** Currently queued, waiting for the handler slice. */ queueDepth: number; /** Shed because the queue hit its bound (each shed triggers a backfill). */ queueDropped: number; /** Consumer handlers that threw or rejected. */ handlerErrors: number; /** Backfill/reconcile passes this channel has run. */ backfills: number; lastBackfillAt: number | null; lastEventAt: number | null; /** Events received in the last 60 seconds. */ eventsPerMinute: number; } interface DiagnosticsSnapshot { clientId: string; health: ConnectionHealth; channels: readonly ChannelDiagnostics[]; /** Every channel namespace declared in this realm, live or not. */ namespaces: readonly ChannelNamespaceSpec[]; capturedAt: number; } interface ChannelMetrics { readonly topic: string; status: RealtimeStatus; namespace: string | undefined; subscribedAt: number | null; reconnects: number; failedAttempts: number; received: number; delivered: number; suppressedEcho: number; deduped: number; stale: number; queueDepth: () => number; queueDropped: () => number; handlerErrors: number; backfills: number; lastBackfillAt: number | null; lastEventAt: number | null; recordEvent(): void; snapshot(): ChannelDiagnostics; } declare function createChannelMetrics(args: { topic: string; namespace: string | undefined; queueDepth: () => number; queueDropped: () => number; now?: () => number; }): ChannelMetrics; /** * THE CONSOLE HANDLE — the name the docstring below promises. * * `realtimeDiagnostics()` has always been documented as "safe to call from * anywhere, including a devtools console", and in every BUNDLED host that * sentence was false: the function existed only as a module binding inside a * Turbopack/Vite chunk, so on a live page nobody — operator, support engineer, * or verifying agent — could reach the counters at all. Measured 2026-09-11 on * matrx-frontend `/tasks`: no realtime global existed on `window`, and the * repo contained ZERO callers of `realtimeDiagnostics` or * `useRealtimeDiagnostics`, so the entire R7 surface was unobservable in the * product it was built for. * * So the package publishes the handle itself rather than asking every host to * remember to. It is installed from `registerChannelMetrics` — NOT at module * scope — because this package declares `sideEffects: false`, which lets a * bundler drop a top-level call; a channel registering its metrics is the * moment there is something to report and is impossible to tree-shake away. */ declare const DIAGNOSTICS_GLOBAL = "__matrxRealtimeDiagnostics"; /** Idempotent: re-registering channels must not redefine the handle. */ declare function installDiagnosticsConsoleHandle(): void; declare function registerChannelMetrics(metrics: ChannelMetrics): () => void; /** The live snapshot. Safe to call from anywhere, including a devtools console. */ declare function realtimeDiagnostics(): DiagnosticsSnapshot; /** * The default diagnostic sink. A host that injects nothing STILL sees every * background failure, with the remedy attached — silence is never the default. */ declare function createConsoleDiagnostics(prefix?: string): DiagnosticsSink; declare function useRealtimeDiagnostics(pollMs?: number): DiagnosticsSnapshot; /** * THE AMBIENT MANAGER — the door for code that is not a React component. * * `` builds exactly ONE manager per app, and `useRealtimeManager()` * hands it to components. But a large share of realtime ownership in a real app * is deliberately NOT in a component: * * - Redux middleware, which is where this doctrine says a feature's * subscription SHOULD live (a component effect re-subscribes on identity * churn and multiplies channels across mounts). * - Plain service modules and thunks that own a module-level subscription. * * None of those can call a hook. Before this module they had exactly two * options, and both are defects: * * 1. `createRealtimeManager(...)` of their own — a SECOND manager, and * therefore a second write ledger, so the app's own optimistic writes look * REMOTE to that manager and every one of them costs a refetch. This is the * failure the "one provider per app" rule exists to prevent, and a * middleware author reaching for the constructor cannot see it happening. * 2. Hand-roll `client.channel(...)` — which is the thing this package exists * to delete. * * So the provider PUBLISHES its manager here, and non-React owners read it. * There is no second manager and no second ledger: this is a pointer to the one * the provider already built. * * WHY IT IS SUBSCRIBABLE. The manager's lifetime is `client` + `actorId`: it is * `null` before auth resolves and it is REBUILT when the signed-in user changes * (and disposed, taking every channel with it). A middleware that read * `currentRealtimeManager()` once at module load would hold null forever on a * cold start, and one that ignored the change would hold a disposed manager * after a user switch — silently, with a screen that looks healthy. So the * contract is `onRealtimeManagerChange(listener)`: open on arrival, drop your * handle on departure. `subscribeToRealtimeManager` below is the whole recipe * in one call, so no consumer has to remember the shape. * * No module-level mutable state (the package ban): the registry lives in a * `globalThis` slot, shared across the ESM and CJS module graphs. */ /** * Publish the app's manager. Called by `` ONLY — a consumer * that calls this is claiming to be the app's provider, which is the thing the * one-provider rule forbids. */ declare function setAmbientRealtimeManager(manager: RealtimeManager | null): void; /** * The app's manager right now, or `null` when no provider is mounted or its * Supabase client has not resolved yet. * * Reading this ONCE is almost always wrong — see the header. Prefer * {@link subscribeToRealtimeManager}. */ declare function currentRealtimeManager(): RealtimeManager | null; /** * Observe the app's manager. The listener fires immediately with the current * value (which may be `null`), and again on every change. Returns an * unsubscribe. */ declare function onRealtimeManagerChange(listener: (manager: RealtimeManager | null) => void): () => void; /** * THE RECIPE, IN ONE CALL — for a Redux middleware or module-level owner that * wants one channel open for as long as the app has a manager. * * Opens `spec` when a manager exists, closes the handle when it goes away, and * re-opens on the new manager when the provider rebuilds one. Returns a stop * function that closes the channel and unsubscribes. * * `spec` is a FACTORY so the caller can capture whatever it needs at open time * (the current user id, a fresh `dispatch`) rather than at module load. */ declare function subscribeToRealtimeManager(spec: (manager: RealtimeManager) => ChannelSpec): () => void; /** * The client session id — the single fact echo suppression is built on. * * Every broadcast this package sends carries it (`MatrxBroadcastEnvelope.cid`), * and every write a consumer registers on the write ledger is attributed to it. * It is STABLE for the lifetime of the JS realm and UNIQUE per tab/app instance: * two tabs of the same user are two clients, because a user watching her own * edit appear in the other tab is a real remote event, not an echo. * * It lives in a globalThis slot (see `slot.ts`) so the ESM and CJS graphs of * this package agree on it. A per-module `let` here would hand the sender and * the receiver two different ids and disable echo suppression entirely. */ /** RFC-4122-shaped id, with a documented fallback for runtimes without WebCrypto. */ declare function randomId(): string; /** The stable id of THIS client session. */ declare function clientSessionId(): string; /** * THE DECOUPLED ORDERED HANDLER QUEUE (R3) — the freeze-class killer. * * A Supabase channel invokes your handler ON THE SOCKET'S CALLBACK PATH. If * that handler does real work — a Redux dispatch that re-runs every sorted * selector, an O(content) diff, an RPC — then a burst of events executes that * work back-to-back with no yield, the main thread never returns to the * browser, and the tab freezes. Every historical Matrx browser lockup is a * variation of this (a background tab waking with 500 queued events is the * classic trigger). * * This queue puts a scheduler between the socket and the consumer: * * - Handlers run OFF the socket callback (the socket loop returns immediately). * - Order is preserved PER CHANNEL — one queue per channel, strictly FIFO, * and an async handler is awaited before the next event starts, so a * consumer never sees event N+1 applied before event N finished. * - The drain YIELDS to the event loop every `sliceMs`, so even a 10,000-event * burst keeps the tab interactive instead of trading correctness for a * frozen window. * - Depth is BOUNDED. An unbounded queue does not prevent the freeze, it just * moves it and adds an out-of-memory ending. On overflow the queue drops * the oldest work, SAYS SO through `onOverflow`, and the manager turns that * into a backfill — reconciling from the database is exactly the right * recovery for "we could not keep up with the stream". */ type QueueTask = () => void | Promise; interface EventQueueOptions { /** Max pending tasks before the oldest are dropped. Default 500. */ maxDepth?: number; /** Max ms of uninterrupted draining before yielding to the event loop. Default 8. */ sliceMs?: number; /** Called with the number of tasks dropped. The manager triggers a backfill. */ onOverflow?: (dropped: number) => void; /** Called when a consumer handler throws or rejects. Never swallowed. */ onError?: (error: unknown) => void; /** Injectable for tests. Default `Date.now`. */ now?: () => number; /** Injectable for tests. Default `setTimeout(fn, 0)`. */ schedule?: (run: () => void) => void; } interface EventQueue { push(task: QueueTask): void; depth(): number; /** Total tasks dropped by overflow since creation. */ dropped(): number; /** Total tasks completed since creation. */ processed(): number; /** Resolves when the queue is empty. Tests and teardown use it. */ idle(): Promise; /** Stop draining and discard pending work (channel closed). */ dispose(): void; } declare function createEventQueue(options?: EventQueueOptions): EventQueue; export { type BackfillContext, type BackfillReason, type Backoff, type BackoffOptions, type BroadcastBinding, type BroadcastMessage, type BroadcastWire, type ChannelDiagnostics, type ChannelHandle, type ChannelMetrics, type ChannelNamespace, ChannelNamespaceCollisionError, type ChannelNamespaceSpec, type ChannelSpec, type ClassifyInput, type ConnectionHealth, DIAGNOSTICS_GLOBAL, type Dedupe, type DedupeOptions, type DiagnosticEvent, type DiagnosticLevel, type DiagnosticsSink, type DiagnosticsSnapshot, type EnvironmentBridge, type EnvironmentSignal, type EventOrigin, type EventQueue, type EventQueueOptions, type LocalWrite, type ManagerTimers, type MatrxBroadcastEnvelope, type ObservedRecord, type OriginReason, type OriginVerdict, type PostgresChangeBinding, type PostgresChangeDelivery, type PostgresChangeEvent, type PostgresChangeFilter, type PostgresChangePayload, type PresenceMember, type PresenceOptions, type PresenceRoom, type PresenceRuntime, type PresenceStateLike, type PresenceTimers, type QueueTask, RECONNECT_ALARM_ATTEMPTS, RECONNECT_GIVE_UP_ATTEMPTS, type RawWireOptions, type RealtimeChannelLike, type RealtimeClientLike, type RealtimeManager, type RealtimeManagerOptions, RealtimeProvider, type RealtimeProviderProps, type RealtimeStatus, type SettleResult, TOPIC_PREFIX, type TypingMember, type TypingOptions, type TypingState, type TypingTracker, type UseChannelOptions, type UseChannelResult, type UsePresenceArgs, type UsePresenceResult, type UseTypingArgs, type UseTypingResult, type WriteLedger, type WriteLedgerOptions, type WriteTicket, canonicalRevision, clientSessionId, createBackoff, createBrowserEnvironment, createChannelMetrics, createConsoleDiagnostics, createDedupe, createEventQueue, createInertEnvironment, createPresenceRuntime, createRealtimeManager, createTypingTracker, createWriteLedger, currentRealtimeManager, defaultEnvironment, defineChannelNamespace, installDiagnosticsConsoleHandle, listChannelNamespaces, onRealtimeManagerChange, randomId, realtimeDiagnostics, registerChannelMetrics, resetChannelRegistryForTests, setAmbientRealtimeManager, subscribeToRealtimeManager, uniqueChannelTopic, useChannel, useIsRealtimeProviderMounted, usePresence, useRealtimeDiagnostics, useRealtimeHealth, useRealtimeManager, useRequiredRealtimeManager, useTyping };