clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

fix/175-template-quality-pass
lofi / package / testing / scenario / peer.ts
peer.ts 4.8K
1import type { InsertOf, RowOf, WhereOf } from "jazz-tools";23/**4 * How a peer's view is read for convergence comparison. Headless peers expose5 * their tables; browser peers expose the app-supplied value-free snapshot.6 * Registered by the adapters; not part of the public surface.7 */8export type ConvergeSource =9 | {10 kind: "tables";11 tableKeys: readonly string[];12 counterColumns(table: string): ReadonlySet<string>;13 setColumns(table: string): ReadonlySet<string>;14 readTable(key: string, tier: "local" | "global"): Promise<readonly Record<string, unknown>[]>;15 }16 | {17 kind: "snapshot";18 snapshot(): Promise<unknown>;19 };2021const convergeSources = new WeakMap<object, ConvergeSource>();2223/** Attach a peer's converge source. Called by the adapters when a peer boots. */24export function registerConvergeSource(peer: object, source: ConvergeSource): void {25 convergeSources.set(peer, source);26}2728/** Look up a peer's converge source, or `undefined` for foreign objects. */29export function convergeSourceOf(peer: object): ConvergeSource | undefined {30 return convergeSources.get(peer);31}3233/**34 * The table names of an app: every key whose value is a table handle. The35 * app object's non-table members (`union`, `wasmSchema`) are excluded by36 * shape, not by name, so future non-table members stay excluded too.37 */38export type ScenarioTableKey<A> =39 & {40 [K in keyof A]: A[K] extends { readonly _table: string } ? K : never;41 }[keyof A]42 & string;4344/** Options for reads through a {@link ScenarioTable}. */45export interface ScenarioReadOptions {46 /**47 * Durability tier to read at. `"local"` (the default) returns the peer's48 * current local view and works offline; `"global"` waits until the read49 * reflects globally durable data and requires the peer to be online.50 */51 tier?: "local" | "global";52}5354/**55 * One table of a headless peer's typed facade: the app's insert/update/remove56 * writes, applied to the peer's local view immediately (online or offline),57 * plus local-view reads. Writes issued here are tracked by the owning peer so58 * {@link ScenarioPeerControls.settle} can wait for their global durability.59 */60export interface ScenarioTable<Row, Init, Where> {61 /** Insert a row and resolve with it as applied to the local view. */62 insert(values: Init): Promise<Row>;63 /** Update a row by id in the local view. */64 update(id: string, patch: Partial<Init>): Promise<void>;65 /** Delete a row by id from the local view. */66 remove(id: string): Promise<void>;67 /** Read every row this peer can currently see. */68 all(where?: Where, options?: ScenarioReadOptions): Promise<readonly Row[]>;69 /** Read one row by id, or `undefined` when this peer cannot see it. */70 get(id: string, options?: ScenarioReadOptions): Promise<Row | undefined>;71}7273/** The app's tables as scenario facades, keyed by the app's own table names. */74export type ScenarioDb<A> = {75 readonly [K in ScenarioTableKey<A>]: ScenarioTable<RowOf<A[K]>, InsertOf<A[K]>, WhereOf<A[K]>>;76};7778/** Options for {@link ScenarioPeerControls.settle}. */79export interface ScenarioSettleOptions {80 /** Defensive deadline for the settle wait. Defaults to 20 seconds. */81 timeoutMs?: number;82}8384/**85 * The controls every scenario peer implements, headless or browser: a stable86 * name, an offline window toggle, and a durability barrier for issued writes.87 */88export interface ScenarioPeerControls {89 /** The peer's name in the scenario, e.g. `"alice"`. */90 readonly name: string;91 /** Whether the peer is currently inside an offline window. */92 readonly isOffline: boolean;93 /** Open an offline window: the peer keeps working locally without syncing. */94 offline(): Promise<void>;95 /** Close the offline window and resume syncing. */96 online(): Promise<void>;97 /**98 * Resolve once every write issued through this peer has reached global99 * durability. The peer must be online; offline peers cannot settle and the100 * wait fails at its deadline with a reminder to call101 * {@link ScenarioPeerControls.online} first.102 */103 settle(options?: ScenarioSettleOptions): Promise<void>;104}105106/**107 * A headless scenario peer: a real synced client with the scenario controls108 * plus the app's typed table API. Writes issued through {@link db} apply to109 * this peer's local view immediately — pair them with110 * {@link ScenarioPeerControls.settle} or a convergence assertion when the111 * scenario needs them visible elsewhere.112 */113export interface ScenarioPeer<A> extends ScenarioPeerControls {114 /** The app's tables, scoped to this peer's replica. */115 readonly db: ScenarioDb<A>;116 /**117 * Restart the peer: tear the client down and boot a fresh one with the same118 * identity. Headless peers run on a memory driver, so unsynced local state119 * is lost — what survives a restart is what had already synced.120 */121 restart(): Promise<void>;122}