import { R as RealtimeManager, C as ChannelSpec, a as RealtimeStatus, b as ConnectionHealth, D as DiagnosticsSink, P as PresenceRoom, c as PresenceTimers } from './manager-CwqlSmAA.js'; export { B as BackfillContext, d as BackfillReason, e as Backoff, f as BackoffOptions, g as BroadcastBinding, h as BroadcastMessage, i as BroadcastWire, j as ChannelHandle, k as ClassifyInput, l as Dedupe, m as DedupeOptions, n as DiagnosticEvent, o as DiagnosticLevel, E as EnvironmentBridge, p as EnvironmentSignal, q as EventOrigin, L as LocalWrite, M as ManagerTimers, r as MatrxBroadcastEnvelope, O as ObservedRecord, s as OriginReason, t as OriginVerdict, u as PostgresChangeBinding, v as PostgresChangeDelivery, w as PostgresChangeEvent, x as PostgresChangeFilter, y as PostgresChangePayload, z as PresenceMember, A as PresenceOptions, F as PresenceRuntime, G as PresenceStateLike, H as RECONNECT_ALARM_ATTEMPTS, I as RECONNECT_GIVE_UP_ATTEMPTS, J as RawWireOptions, K as RealtimeChannelLike, N as RealtimeClientLike, Q as RealtimeManagerOptions, S as SettleResult, W as WriteLedger, T as WriteLedgerOptions, U as WriteTicket, V as canonicalRevision, X as createBackoff, Y as createBrowserEnvironment, Z as createDedupe, _ as createInertEnvironment, $ as createPresenceRuntime, a0 as createRealtimeManager, a1 as createWriteLedger, a2 as defaultEnvironment } from './manager-CwqlSmAA.js'; /** * 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 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; /** * 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; /** * 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; export { type ChannelDiagnostics, type ChannelMetrics, type ChannelNamespace, ChannelNamespaceCollisionError, type ChannelNamespaceSpec, ChannelSpec, ConnectionHealth, DIAGNOSTICS_GLOBAL, DiagnosticsSink, type DiagnosticsSnapshot, type EventQueue, type EventQueueOptions, PresenceRoom, PresenceTimers, type QueueTask, RealtimeManager, RealtimeStatus, TOPIC_PREFIX, type TypingMember, type TypingOptions, type TypingState, type TypingTracker, clientSessionId, createChannelMetrics, createConsoleDiagnostics, createEventQueue, createTypingTracker, currentRealtimeManager, defineChannelNamespace, installDiagnosticsConsoleHandle, listChannelNamespaces, onRealtimeManagerChange, randomId, realtimeDiagnostics, registerChannelMetrics, resetChannelRegistryForTests, setAmbientRealtimeManager, subscribeToRealtimeManager, uniqueChannelTopic };