trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
105 lines • 4.74 kB
TypeScript
/**
* 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<T = unknown> {
/** 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<string, unknown>;
}
/**
* 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<T = unknown> {
load(): ChannelRecord<T>[] | Promise<ChannelRecord<T>[]>;
save(records: ChannelRecord<T>[]): void | Promise<void>;
}
export interface PersistentChannelOptions<T = unknown> {
/** Broadcast event name treated as a persisted message. Default `'message'`. */
event?: string;
/** Durable replica for this peer. Omit for in-memory only. */
store?: ChannelStore<T>;
/** 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<string, unknown> | undefined;
/** Injectable clock for local sends. Default `Date.now`. */
now?: () => number;
}
export declare const DEFAULT_MAX_RECORDS = 200;
export declare class PersistentChannel<T = unknown> {
/** Reactive, deduped, `(ts, id)`-sorted message list. */
readonly messages: Signal<ChannelRecord<T>[]>;
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<T = unknown>(room: RealtimeRoom, channel: string, opts?: PersistentChannelOptions<T>): PersistentChannel<T>;
/**
* 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, unknown>): string;
/** Current records (deduped + sorted). Read without subscribing. */
snapshot(): ChannelRecord<T>[];
/** 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<T = unknown>(key: string, storage?: Storage | undefined): ChannelStore<T>;
//# sourceMappingURL=persistent-channel.d.ts.map