/** * PersistentChannel — a durable, convergent view over one broadcast channel. * * Wraps a {@link RealtimeRoom} channel as a **grow-only set** keyed by message * id: every record is deduplicated by id and ordered by `(ts, id)`, so the same * message arriving from any source (optimistic local send, live broadcast, or a * relay replay on reconnect) lands exactly once and in a stable order. * * There is no single source of truth. Each peer holds its own replica (a * pluggable {@link ChannelStore} — `localStorage`, IndexedDB, …) and the relay * holds another. They converge on reconnect because union + id-dedup is * commutative and idempotent. The store is an instant first-paint cache, not an * authority: a refresh repaints from it immediately, then merges whatever the * relay replays. * * const chat = PersistentChannel.create(room, 'chat', { * store: localStorageChannelStore(`chat:${roomUrl}`), * resolveMeta: (e) => presenceById()[e.from], // capture name/color * }); * * chat.messages.subscribe((msgs) => render(msgs)); // deduped + sorted * chat.send({ text }); // optimistic + broadcast + persist * * Only the channel's "message" event (configurable) is persisted; transient * events like typing indicators stay the caller's concern via `room.on`. */ import { Signal } from '../client/reactive.js'; import type { RealtimeRoom } from './room.js'; import type { BroadcastEvent } from './types.js'; /** A persisted member of the grow-only set. */ export interface ChannelRecord { /** Stable dedup key (sender-assigned message id). */ id: string; /** Originating peer id. */ from: string; /** Sender clock (epoch ms) — primary sort key. */ ts: number; /** Application payload (e.g. `{ text }`). */ payload: T; /** Optional sender metadata captured at send/receipt (name, color, …). */ meta?: Record; } /** * Durable backing store for a peer's local replica. Implementations may be sync * (`localStorage`) or async (IndexedDB/OPFS); `load` may return a promise. */ export interface ChannelStore { load(): ChannelRecord[] | Promise[]>; save(records: ChannelRecord[]): void | Promise; } export interface PersistentChannelOptions { /** Broadcast event name treated as a persisted message. Default `'message'`. */ event?: string; /** Durable replica for this peer. Omit for in-memory only. */ store?: ChannelStore; /** Keep at most the N most recent records. Default 200. */ max?: number; /** Capture sender metadata for an inbound message (e.g. from presence). */ resolveMeta?: (event: BroadcastEvent) => Record | undefined; /** Injectable clock for local sends. Default `Date.now`. */ now?: () => number; } export declare const DEFAULT_MAX_RECORDS = 200; export declare class PersistentChannel { /** Reactive, deduped, `(ts, id)`-sorted message list. */ readonly messages: Signal[]>; private readonly room; private readonly channel; private readonly event; private readonly store?; private readonly max; private readonly resolveMeta?; private readonly now; private records; private byId; private unsubscribe; private disposed; private constructor(); /** Create a persistent view over `room`'s `channel`. */ static create(room: RealtimeRoom, channel: string, opts?: PersistentChannelOptions): PersistentChannel; /** * Optimistically record locally, broadcast to peers, and persist. Returns the * stable message id. The room dedups its own id, so the broadcast never * echoes back into this channel. */ send(payload: T, meta?: Record): string; /** Current records (deduped + sorted). Read without subscribing. */ snapshot(): ChannelRecord[]; /** Stop listening. Does not clear the durable store. */ dispose(): void; private handleEvent; private hydrate; /** Insert one record and publish if it was new. */ private commitOne; /** Pure insert with dedup + cap. Returns true when a new record was added. */ private add; private commit; } /** * A {@link ChannelStore} backed by Web Storage (`localStorage` by default). * Degrades to in-memory if storage is unavailable or over quota — durability is * best-effort, since the relay replica remains authoritative for catch-up. */ export declare function localStorageChannelStore(key: string, storage?: Storage | undefined): ChannelStore; //# sourceMappingURL=persistent-channel.d.ts.map