API index and search · Build metadata
Supporting declarations
packages/optimistic/src/client.ts. These declarations explain referenced types. Only package-page symbols are package exports.
HeadersInit
export type HeadersInit = Record<string, string>;RealtimeLeaseTableSpec
/** One footprint table's spec on the lease wire (mirror of the api-server's `RoomTableSpec`).
* `footprintWhere` (H-iii lease-wire flip) is the EXACT footprint-membership predicate from the
* ONE unified compiler (`compileRoomScopeSpecs` — the same output the boot wire ships the room
* gate): present only for an exact footprint ROOT (lossless row-local extraction; the vacuous-true
* empty AND for an unconstrained one), ABSENT for child/correlated tables. It feeds the §3
* router's pk-membership read proof (`OptimisticBackend`'s routing table) — never authorization. */
export interface RealtimeLeaseTableSpec {
table: string;
footprintWhere?: Condition;
writable: {
kind: "none";
} | {
kind: "predicate";
where?: Condition;
joinKeyCols: string[];
};
}RealtimeLeaseBlock
/** The room-serve block on a query lease (mirror of the api-server's `QueryLeaseRealtime`). */
export interface RealtimeLeaseBlock {
/** The store's gate/domain key for this room source (`connectSource`) — `"room:<profile>/<key>"`. */
sourceKey: string;
/** Where the ROOM ws opens — the lease's DEDICATED field. Never confuse it with the TOP-LEVEL
* `wsEndpoint` (the read-router's whole-DAEMON-session migration signal). */
wsEndpoint: string;
/** The room shell's self-authorizing signed lease (seals the APPROVED query AST) — presented as
* the room subscribe's `leaseToken`. */
roomToken: string;
/** Token expiry (ms epoch) — the renewal clock (renewal = a fresh lease through the app route). */
exp: number;
doc: string;
tables: RealtimeLeaseTableSpec[];
}LifecycleLeaseEntry
/** One minted SYSTEM-STREAM lease on the `lifecycle` block (mirror of the api-server's
* `QueryLeaseLifecycleLease`; Slice I-iii): an ordinary daemon materialization over one of the
* four `_rindle_*` lifecycle tables, presented on the wire exactly like the primary lease
* (subscribe-with-`leaseToken`). The identity fields document the minted predicate — this client
* keys its retains (idempotence per (table, scope/doc/clientId)) and the backend keys its
* release-time row filters on them. */
export interface LifecycleLeaseEntry {
table: string;
leaseToken: string;
wsEndpoint?: string;
/** DOORBELL only: the §4.1 occupancy scope (= the wire room doc, `"<profile>/<key>"`). */
scope?: string;
/** FENCE entries only: the room doc. */
doc?: string;
/** FENCE ledger/outcomes when the server could client-scope the predicate. */
clientId?: string;
}LifecycleLeaseBlock
/** The §4 lifecycle block on a query lease (mirror of the api-server's `QueryLeaseLifecycle`):
* `doorbell` on every labeled lease under the opt-in server config, `fence` (watermark + ledger
* + outcomes) only when the lease is ALSO room-served. Absent ⇒ this client behaves exactly as
* today — the whole plane is inert-until-fed. */
export interface LifecycleLeaseBlock {
doorbell: LifecycleLeaseEntry;
fence?: LifecycleLeaseEntry[];
}RealtimeFenceBlock
/** The §4.2 downgrade fence block on a query lease (mirror of the api-server's
* `QueryLeaseRealtimeFence`, Slice I-v): rides a labeled reply whose occupancy gate CLOSED
* (no `realtime` block) when the server could drain the room — `finalFlushSeq` is the room's
* last COMMITTED flush seq, the value the client's ghost holds against
* (`_rindle_room_watermark(doc) ≥ finalFlushSeq` through the daemon plane). A room-attached
* query receiving it runs the GRACEFUL downgrade dance instead of the loud legacy anomaly. */
export interface RealtimeFenceBlock {
/** The retiring room source's gate/domain key (`"room:" + doc`). */
sourceKey: string;
doc: string;
finalFlushSeq: number;
}RealtimeAnomalyKind
export type RealtimeAnomalyKind =
/** A re-lease (renewal / reconnect re-resolution) came back WITHOUT a realtime block AND
* without a §4.2 fence — the query is no longer room-served but the server gave nothing to
* downgrade behind (a legacy/pre-I-v server, or `lifecycle.drainRoom` unconfigured). Surfaced
* loudly; a reply WITH a `realtimeFence` takes the graceful I-v dance instead. */
"downgrade"
/** The I-v ghost is STUCK (§7.5): its watermark fence cleared but sent room-domain mids never
* resolved (sent-but-undelivered when the socket died — undecidable in general). The ghost
* holds — no timeout-retire is invented — and the mids are named once, actionably. */
| "downgrade-stuck"
/** A lease named a DIFFERENT `sourceKey` than the query's live room sub — surfaced loudly, no
* re-attach. Deliberately NOT composed from demote+upgrade (deferred to §7.6's rare-case
* follow-up): a sourceKey-change reply carries a realtime block for the NEW room but NO
* fence for the OLD one, and without `finalFlushSeq` the old slice cannot be ghosted soundly. */
| "source-key-changed"
/** The lease POST failed or the room attach threw. The initial-materialize case fails OPEN to
* the daemon path (indistinguishable from an unlabeled query's recovery). */
| "lease-failed";RealtimeAnomaly
/** A loud realtime lease anomaly (always ALSO `console.error`'d). */
export interface RealtimeAnomaly {
kind: RealtimeAnomalyKind;
name: string;
args: unknown;
message: string;
}RealtimeClientOptions
/** Rindle Realtime client knobs (Slice G-v). All optional — an app with no labeled queries never
* touches any of this. */
export interface RealtimeClientOptions {
/** The DECLARED room mutators (302 §5: declared, not derived). A mutator named here routes to
* the attached room — it stages onto the room's own tables and ships on the room socket —
* whenever exactly ONE room is attached; solo (no room) it takes the ordinary daemon path,
* and with several rooms attached it routes daemon too (explicit multi-room binding is a
* later slice). Every mutator NOT named here is a daemon mutator. A misdeclaration fails
* SOFT (302 §5.1): the write lands on the other authority's tables, so the view just stops
* feeling instant until the echo relays it — never a divergence. An explicit top-level
* `domainPolicy` overrides this entirely. */
mutators?: readonly string[];
/** Build the ROOM ws transport for a lease's `realtime.wsEndpoint`. Default
* `(endpoint) => new WsTransport(endpoint)`. Injectable for tests / custom ws impls. */
transport?: (endpoint: string) => Transport;
/** Loud anomaly surface — see {@link RealtimeAnomaly}. Every anomaly is also `console.error`'d. */
onAnomaly?: (anomaly: RealtimeAnomaly) => void;
/** How long before a room lease's `exp` the proactive token renewal fires (default 30s). The
* renewal is a FRESH lease through the app query route (renewal-as-reauthorization), and the
* live room sub proactively re-subscribes with the fresh token so the shell's TTL backstop
* never fires on a healthy session. */
renewMarginMs?: number;
}RealtimeInspect
/** Read-only realtime bookkeeping snapshot ({@link RindleClient.__realtimeInspect}) — test/devtools
* introspection, mirroring the backend's `__inspect` convention. */
export interface RealtimeInspect {
rooms: Record<string, {
wsEndpoint: string;
/** The room's OWNED tables (302 §2): wire table → its namespaced engine table — read back
* from the BACKEND's registry (`backend.roomTablesFor`, the one source of truth; the
* client keeps no shadow copy). */
promoted: Record<string, string>;
/** Live room-retained queries on this room, by remote key. */
queries: Record<string, {
name: string;
sourceQid: QueryId;
exp: number;
refCount: number;
}>;
}>;
}RindleClientOptions
export interface RindleClientOptions<S extends ColsMap, R extends ClientRegistry> {
schema: Schema<S>;
/** The PREDICTED mutators (the API server holds the authoritative twins by name). */
mutators: R;
/** The acting principal for a shared (generator) mutator's `ctx.user` — the local identity the
* optimistic prediction writes under (the server injects its OWN authenticated user for the
* authoritative run). Re-read on each run, including replay. Keep this identity stable for
* the client's lifetime and recreate the client on account changes. */
user?: () => string;
/** The app API server: named queries resolve to leases here, mutations push here. */
api: {
url: string;
routes?: {
query?: string;
mutate?: string;
};
/** Extra headers per request (auth). A function is re-evaluated per call. */
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
fetch?: typeof fetch;
};
/** Optional subscription transport override. Omit it in the normal unified setup: the first
* query lease carries `wsEndpoint` + a fresh affinity ticket and opens the transport lazily.
* - `{ wsUrl }` — a static endpoint (single daemon), opened eagerly; in a routed deploy this is
* the SSR-injected bootstrap endpoint (READ-ROUTER-DESIGN.md §2.4). A routed lease naming a
* different follower migrates the connection there.
* - `{ wsUrl }` omitted (or this whole option omitted) — pure-lazy: the first lease's
* `wsEndpoint` opens the connection (a
* routed SPA with no SSR bootstrap).
* - `{ transport }` — a pre-built transport (tests / in-process); fixed, no migration.
*
* With a fixed `wsUrl`, set `affinity: true` to opt into FOLLOWER-AFFINITY mode (design §2).
* With no daemon option, a lease that returns an affinity ticket enables the same mode
* automatically before opening its socket. The ticket is persisted per-tab and forwarded on
* later leases so both legs pin the same nearby follower. Ignored for `{ transport }`. */
daemon?: {
wsUrl?: string;
affinity?: boolean;
} | {
transport: Transport;
};
/** Stable client identity. Default: a per-origin base (localStorage) plus per-tab and per-instance
* suffixes, so each tab — and each client instance within a tab — gets its own mid sequence yet a
* reload keeps it; falls back to a fresh random id when web storage is unavailable. Pass a value
* to override. */
clientID?: string;
/** A policy rejection's reason (the prediction's snap-back rides the lmid release). Fires for
* BOTH planes since H-v: the HTTP mutate route's per-envelope rejections AND a room's
* `mutationOutcome {kind:"rejected"}` frames — one surface, whichever authority said no. */
onRejected?: (envelope: MutationEnvelope, reason: string) => void;
/** A failed mutate FLUSH — the transport/authority leg, not a policy verdict: the batch is
* retried with backoff and nothing has been confirmed yet, so the pending mutations stay
* predicted and the queue is head-of-line blocked until it succeeds. Fires once per attempt.
* This is the twin of {@link onRejected}: `onRejected` is "the authority said no" (final,
* lmid already advanced), this is "the authority never answered" (retrying).
*
* LOUD by contract — every attempt reaching this hook is ALSO `console.error`'d (backed off
* to attempts 1, 2, 4, 8, … so a long outage doesn't flood the console), because an
* indefinitely retried flush is indistinguishable from a hung app if it stays silent. */
onMutationError?: (err: unknown, attempt: number) => void;
/** Persist `local: true` tables across reloads and keep them live-coherent across tabs
* (`207-LOCAL-TABLE-PERSISTENCE-DESIGN.md`). `user` is the storage identity (§3.2) — one IDB
* database per (origin, user); pass a sentinel like `"anon"` for a signed-out mode. When set,
* `createRindleClient` awaits the initial restore attempt before returning. Storage failures
* can leave tables empty while local writes remain usable. Close the old client before calling
* `deleteLocalPersistence(user)` at logout; deletion is never implicit.
* Per-table opt-out: declare `table(name, { local: "session" })` for local state that must stay
* ephemeral and per-tab (e.g. selection) even with persistence on (§5.4). */
persistLocal?: PersistLocalOptions;
queue?: {
maxBatch?: number;
retryDelayMs?: (attempt: number) => number;
};
/** Explicitly selects a mutation's confirming stream. Its domain supplies the mutation ID,
* transport, and confirmation watermark. Returning `undefined` selects `"daemon"`.
* If this policy is omitted, `realtime.mutators` supplies the declared room routing policy;
* without either declaration, mutations route to the daemon. Routes are not inferred from
* reads or writes. A room deopt can re-enqueue the mutation on the daemon stream. */
domainPolicy?: (name: string, args: unknown) => string | undefined;
/** Rindle Realtime client knobs (G-v resolve-then-register) — see {@link RealtimeClientOptions}. */
realtime?: RealtimeClientOptions;
/** Development-only recovery knobs. Keep off in production: a mutation gap means state loss
* or two writers sharing a clientID, and should be investigated. */
dev?: {
/** On a mutation-gap response, clear the persisted clientID and hard reload the page. This
* recovers from dev DB wipes while making the reset visible to the developer. */
resetOnMutationGap?: boolean;
};
}RindleClient
export interface RindleClient<S extends ColsMap, R extends ClientRegistry> {
store: Store<S>;
backend: OptimisticBackend<S>;
/** Retain a named query for navigation/prefetch. By default waits for server authority; pass
* `{ until: "present" }` to continue as soon as the local view has a result. */
ensure<Q extends AnyQuery>(query: Q, options?: EnsureQueryOptions): Promise<void>;
/** Call `mutate.foo(args)` for a normal optimistic write, or `mutate.foo.folded(opts, args)` for a
* debounced, last-value-wins folded write (FOLDED-MUTATIONS-DESIGN §3). */
mutate: {
[K in keyof R]: MutateFn<Parameters<R[K]>[1]>;
};
/** Assign IDs and enqueue every outstanding fold immediately. Also called on
* `beforeunload`/`pagehide` as a best-effort flush; neither path confirms delivery. */
flushFolds(): void;
clientID: string;
/** Release retained prefetch queries, transports, and lifecycle listeners; close local persistence.
* Flushes folded arguments into the queue first; does not await server confirmation. */
close(): void;
/** Read-only realtime bookkeeping snapshot (rooms, promoted tables + their client-held
* `joinKeyCols`, live room queries) — the `__inspect`-convention test/devtools hook. */
__realtimeInspect(): RealtimeInspect;
}createRindleClient
/** Create a synced application client over the local WASM engine. Connects named query leases,
* WebSocket subscriptions, and the optimistic mutation queue to the application's API routes.
* Await construction before using the store; when configured, it awaits the local-table restore
* attempt as well. Construction does not mean remote queries are synchronized: retain queries
* or use `ensure` for the required readiness boundary.
*
* Keep one client per application session. Call `close()` when the session ends, and create a
* new client when its authenticated principal changes. The application server owns authorization.
*/
export declare function createRindleClient<S extends ColsMap, R extends ClientRegistry>(opts: RindleClientOptions<S, R>): Promise<RindleClient<S, R>>;