/** * Client-side state for the graph-mutation channel. * * `sendGraphMutation` is the wire primitive; this module absorbs the repeated * client boilerplate around it: track the current base, propose ops against it, * serialize submits, refresh on structured stale-base refusals, and adopt the * applied graph the channel already verified. Server authority is unchanged: * nothing here can skip `handleGraphMutation`'s validation or the host store's * compare-and-swap. * * @module */ import type { DocumentGraph } from './document-graph.js'; import { type PatchOp } from './graph-patch.js'; import { type GraphMutationResponse } from './graph-mutation.js'; /** Configuration for {@link createGraphMutationClient} — endpoint, initial base, and stale-recovery policy. */ export interface GraphMutationClientOptions { /** The mutation endpoint `sendGraphMutation` POSTs to. */ readonly url: string; /** The initial client-side base graph (e.g. decoded from an initial GET or inlined SSR data). */ readonly base: DocumentGraph; /** Injectable fetch for tests / non-browser hosts. Defaults to global fetch. */ readonly fetchImpl?: typeof fetch; /** * Host-owned base reloader (e.g. GET the host's graph endpoint and decode). When present, * a `staleBase` refusal triggers reload + re-propose up to `maxStaleRetries` times. * LiteShip does not dictate the read endpoint's shape — the host owns it (ADR-0015). */ readonly refreshBase?: () => Promise; /** Bounded stale-base retries. Default: 1 when `refreshBase` is provided, else 0. */ readonly maxStaleRetries?: number; /** * Abort a submit's request after this many milliseconds, settling it to the channel's * `{ status: 'error' }` shape. Without it, a hung request holds the SERIALIZED submit * queue for as long as the runtime's own fetch deadline (minutes in some browsers) — * every queued submit on this client waits behind it. Default: no client-side timeout. */ readonly timeoutMs?: number; } /** * The ops a submit proposes: a fixed op array, or a builder invoked with the CURRENT base — * the builder form re-derives ops after a stale-base refresh, so retried proposals never * carry nodes computed against a graph the server has already moved past. */ export type GraphMutationOps = readonly PatchOp[] | ((base: DocumentGraph) => readonly PatchOp[]); /** * The client-side half of the mutation channel: a base-tracking state machine over * `sendGraphMutation`. Submits are strictly serialized (no self-inflicted CAS races), * an `applied` response advances the base, and a `staleBase` refusal reloads + * re-proposes within the configured bound. */ export interface GraphMutationClient { /** The current client-side base (advances on every applied submit / adopt). */ readonly base: () => DocumentGraph; /** * Adopt an externally-obtained graph as the new base (e.g. from an SSE snapshot/patch stream). * If a submit is already in flight, that submit keeps the base it already captured; whichever * adopt/applied result writes last is the current base. */ readonly adopt: (next: DocumentGraph) => void; /** * Propose ops against the current base, send, and settle to the channel's one-shape * response. NEVER rejects — every failure (ops builder throw, propose throw, transport, * refreshBase throw) maps to `{ status: 'error' }`, mirroring the channel contract. */ readonly submit: (ops: GraphMutationOps) => Promise; } /** * Build a {@link GraphMutationClient}. The returned client never rejects: every failure — * ops-builder throw, propose throw, transport error, `refreshBase` throw — settles to the * channel's `{ status: 'error' }` shape, mirroring `sendGraphMutation`'s one-shape contract. */ export declare function createGraphMutationClient(options: GraphMutationClientOptions): GraphMutationClient; //# sourceMappingURL=graph-mutation-client.d.ts.map