Rindle

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.

Exact source

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>>;