Rindle

API index and search · Build metadata

Supporting declarations

packages/client/src/store.ts. These declarations explain referenced types. Only package-page symbols are package exports.

Exact source

DehydratedQuery

/** One query's SSR snapshot, keyed by its `viewKey` ({@link stableKey} of the AST): the
 *  pre-projected first-paint `rows` plus the `cvMin` watermark they reflect (SSR-DESIGN.md §6.2).
 *  Serializable as-is into the HTML — `rows` are already JSON values (json columns parsed). */
export interface DehydratedQuery {
    rows: unknown[];
    cvMin: number;
}

DehydratedState

/** The whole dehydrated cache: every preloaded query's snapshot, keyed by `viewKey`. The server
 *  builds it with {@link Store.dehydrate}; the browser seeds it with {@link Store.hydrate}. */
export type DehydratedState = Record<string, DehydratedQuery>;

AssembledNode

/** A single assembled (nested-by-name) row from `POST /query` (SSR-DESIGN.md §3.3): the cells
 *  under `cols`, each in-view relationship inlined by its alias (a nested array / object, or a
 *  scalar for a `countAs` aggregate). {@link Store.assembleSnapshot} converts these to the
 *  view's projected result shape. */
export interface AssembledNode {
    cols: Record<string, WireValue>;
    [rel: string]: unknown;
}

WriteTx

/** The write transaction handed to `store.write(tx => …)`. Rows are objects keyed by column;
 *  the Store positionalizes them (and stringifies json columns) before the backend sees them.
 *
 *  `add` takes an {@link InsertOf} row — a nullable column may be omitted (it is filled with `null`,
 *  design 206 §7). `remove`/`edit` take a full {@link RowOf} row: they identify an EXISTING row, so
 *  every column (nullable ones as their actual `T | null` value) must be present. */
export interface WriteTx<S extends ColsMap> {
    add<N extends keyof S & string>(table: N, row: InsertOf<S[N]>): void;
    remove<N extends keyof S & string>(table: N, row: RowOf<S[N]>): void;
    edit<N extends keyof S & string>(table: N, oldRow: RowOf<S[N]>, newRow: RowOf<S[N]>): void;
}

CachedQueryView

export interface CachedQueryView<Q extends Query<any, any, any>> {
    readonly view: ReturnType<Q["materialize"]>;
    /** Retain this query's named remote footprint and release it later. Ad-hoc local queries
     *  return a no-op release function. */
    retain(query: Q): () => void;
    destroy(): void;
}

SyncQueryLease

export interface SyncQueryLease {
    readonly resultType: ResultType;
    subscribe(listener: () => void): () => void;
    release(): void;
}

QueryInspect

/** One live materialized view's read-only summary for a devtools pane (DEBUG-TOOLS-BROWSER-DESIGN
 *  §4.2 — "surface, not instrument"). All fields are already held by the {@link Store}; this is the
 *  single read-only accessor over the otherwise-private `views`/`asts` maps. */
export interface QueryInspect {
    /** The Store-assigned query id (also the backend's qid — the Store passes it straight through). */
    qid: QueryId;
    /** The query's AST (`Store.asts`), for the inspector's pretty-print / table-derivation. */
    ast: Ast;
    /** The view's SERVER-CHANNEL state (`unknown` while loading, `complete` once authoritative). */
    resultType: ResultType;
    /** Current materialized row count (`view.data.length`). */
    rowCount: number;
    /** A capped peek at the projected rows (reference-stable objects off the live view). */
    sample: readonly unknown[];
}

StoreInspect

/** A frozen snapshot of the Store's live query state for a devtools pane ({@link Store.__inspect}). */
export interface StoreInspect {
    queries: QueryInspect[];
}

Store

export declare class Store<S extends ColsMap> {
    /** Type-safe query entry: `store.query.issue.where.closed(false).materialize()`. */
    readonly query: QueryRoot<S>;
    private readonly schema;
    private readonly backend;
    private nextId;
    private readonly views;
    private readonly asts;
    private readonly syncLeases;
    private readonly seeds;
    private changeListeners?;
    private removedSubtreeWanted;
    private resultTypeListeners?;
    private readonly hasResultTypeLifecycle;
    private commitDepth;
    private readonly pendingFlush;
    private readonly pendingChanges;
    constructor(schema: Schema<S>, backend: Backend);
    /** Materialize any fluent query object. Named queries subscribe remotely by `(name,args)`;
     *  ad-hoc builder queries are local-only for local-first backends.
     *
     *  `opts.onChanges` binds a narrator to this view's DIFF stream ({@link ArrayView.onChanges}) — the
     *  per-view seam that replaces filtering the store-global {@link subscribeChanges} by `qid`. It is
     *  wired BEFORE the backend registers the query, so a synchronous backend's first `snapshot` (fired
     *  inside `registerQuery`, before this returns) is delivered too. */
    materialize<Q extends Query<any, any, any>>(query: Q, opts?: {
        onChanges?: ViewChangeListener;
    }): ReturnType<Q["materialize"]>;
    /** One-shot AUTHORITATIVE read: materialize `query`, wait until its result is server-authoritative
     *  ({@link ResultType} `"complete"`), read the data once, then destroy the view — resolving with the
     *  plain result rather than a live subscription. Rejects if the query enters the `"error"` state. Use
     *  it for exports, imports, undo snapshots — anywhere that wants the current answer as a value.
     *
     *  A synchronous local-first backend (wasm/replica) has already delivered the first snapshot inside
     *  {@link materialize}, so the view is `"complete"` on entry and this settles on the next microtask
     *  without ever attaching a listener; a remote backend settles when the first live snapshot lands.
     *  The query is NEVER left subscribed — the view is destroyed before the promise settles either way.
     *  (A remote query that never completes leaves the promise pending, exactly as a `resultType` poll
     *  would; race a timeout at the call site if you need one.) */
    readOnce<Q extends Query<any, any, any>>(query: Q): Promise<ReturnType<Q["materialize"]>["data"]>;
    /** True when the backend can retain a remote named query independently from the local
     *  materialized AST view. React uses this to keep one local view per AST while still sending
     *  every mounted `(name,args)` lease through the backend. */
    canRetainRemoteQueries(): boolean;
    /** Build one local AST view, with remote syncing retained separately through the returned
     *  handle. This is a lower-level API for UI bindings; ordinary app code should keep using
     *  `materialize(query)`. */
    createCachedQueryView<Q extends Query<any, any, any>>(query: Q): CachedQueryView<Q>;
    /** Retain a named remote query purely for normalized/local-first coverage. This does not
     *  register or materialize the query AST locally, so React can keep server sync coverage alive
     *  without subscribing to the broad coverage result tree. */
    retainSyncQuery<Q extends Query<any, any, any>>(query: Q): SyncQueryLease;
    /** Apply a batch of mutations (object rows → positional). Resolves when the backend has
     *  accepted them (local: applied; remote: sent). The resulting view updates flow back via
     *  the backend's event stream. */
    write(fn: (tx: WriteTx<S>) => void): Promise<void>;
    /** Direct-commit write to LOCAL-only tables (`201-LOCAL-ONLY-TABLES-DESIGN.md` §6): the
     *  client-authoritative path for selection state, draft text, view prefs, scratch rows. It
     *  bypasses the optimistic pending stack entirely — a local table is untracked, so it never
     *  rebases, reverts, or waits on a server confirmation (it "moves on its own").
     *
     *  Rejects a synced/tracked table (M2): a direct write to one would be un-applied on the very
     *  next server rewind. Local writes also must NOT live inside a replayable mutator (M1) — the
     *  server runs the mutator from `args` alone and cannot see local tables; use this instead.
     *  Same keyed `WriteTx` shape as {@link write}. `async` so the no-seam / M2 guards surface as a
     *  REJECTED promise (the `Promise<void>` contract) rather than a synchronous throw that escapes a
     *  caller's `.catch` and crashes the event handler / render frame. */
    writeLocal(fn: (tx: WriteTx<S>) => void): Promise<void>;
    /** Drain a keyed `WriteTx` callback into positional {@link Mutation}s (shared by
     *  {@link write} / {@link writeLocal}; json columns stringified, rows in column order). */
    private collectMutations;
    /** Seed a query's first-paint snapshot from a `POST /query` response (server side): convert the
     *  assembled rows to the view's projected shape and stash them by `viewKey`. A view materialized
     *  for this AST (during the synchronous render) reads the seed; {@link dehydrate} serializes it. */
    seedAssembled(ast: Ast, rows: AssembledNode[], cvMin: number): void;
    /** The dehydrated first-paint cache for every preloaded query — embed it in the HTML and pass it
     *  to {@link hydrate} in the browser (SSR-DESIGN.md §6.2). */
    dehydrate(): DehydratedState;
    /** Seed the browser store from the server's {@link dehydrate} output (SSR-DESIGN.md §6.2): each
     *  view materialized for a hydrated AST shows these rows until its first live `hello` reconciles. */
    hydrate(state: DehydratedState): void;
    /** A query's hydrated first-paint snapshot, by `viewKey` — what React's `getServerSnapshot`
     *  reads so an SSR render (and the matching client hydration pass) sees the seeded rows without
     *  opening a subscription. */
    seedSnapshot(viewKey: string): DehydratedQuery | undefined;
    primaryKeyFor(table: string): readonly string[];
    /** Convert assembled (nested-by-name) rows (SSR-DESIGN.md §3.3) into the view's projected result
     *  shape: spread `cols` (parsing json columns), recurse into each relationship by its alias
     *  (plural → array, `.one()` → object/null, `countAs` → bare scalar). */
    assembleSnapshot(ast: Ast, rows: AssembledNode[]): unknown[];
    private assembleNode;
    private registerMaterialized;
    private retainRemote;
    private onEvent;
    /** Retire a view's SSR seed — from the view (so `data` switches from the seed to the maintained
     *  tree) AND from the seeds map (so no later mount re-seeds a now-live query) — but ONLY once the
     *  query is AUTHORITATIVE (`resultType === "complete"`). Called BEFORE the fold it accompanies, so
     *  that fold's notify already reflects the live tree with no empty gap. Idempotent.
     *
     *  The gate is the fix for the synchronous optimistic/wasm backend: it fires a query's FIRST snapshot
     *  from LOCAL, not-yet-synced state while the query is still `unknown` (`registerMaterialized` marks a
     *  lifecycle-backed remote view `unknown` up front for exactly this), then delivers the authoritative
     *  rows one event later as a `catchUp` batch — having already flipped the query to `complete`. So the
     *  seed survives the pre-sync snapshot (`unknown` ⇒ skip) and retires on the catch-up (`complete` ⇒
     *  retire). A lifecycle-LESS backend (pure wasm, the SSR one-shot, tests) is `complete` from creation,
     *  so its first snapshot retires the seed exactly as before this gate existed. */
    private retireSeedIfLive;
    /** Retire the SSR seed (if authoritative) and fold the accompanying hydration delta — BEFORE the
     *  fold so its notify already reflects the live tree with no empty gap. The subtlety: a hydration
     *  can fold NOTHING — a 0-row authoritative result, or one whose rows are already present in `top`
     *  (a query whose result is fully covered by an already-hydrated sibling: the shared rows dedup to
     *  zero net base mutations). Then {@link FlatArrayView.applyChanges} notifies nothing, so the
     *  seed→tree switch would never reach subscribers and the view freezes on the stale seed. Guard
     *  against that: if the seed retired but the fold was a no-op, force the handoff notify (inline, or
     *  via the commit-boundary flush). Flash-safe — the forced notify only fires when there was nothing
     *  to fold, so `data` is already the correct live tree by then. */
    private foldHydration;
    /** Fold a batch into its view, then notify now or — inside a commit bracket — defer the view's
     *  notification to the commit boundary, so all sibling views fold first (cross-view-atomic
     *  notification; see `commitDepth`). */
    private applyAndTrack;
    /** Deliver everything deferred during the just-ended commit, after every affected view has folded
     *  (cross-view-atomic notification): view subscribers first, then the raw change stream
     *  (narrators/devtools), each frame in arrival order. A throwing listener does not stop the others
     *  — the first error is re-raised only once the whole flush completes (mirroring the backend's
     *  per-query isolation). View subscribers run before change listeners, preserving the per-event
     *  order that held before coalescing (a view's subscribers fired before its change frame). */
    private flushCommit;
    /** Subscribe to the raw per-query {@link ChangeEvent} stream (hello / snapshot / batch) the Store
     *  routes to its views, tagged by `qid`. Fired AFTER the event is folded — and, for a commit that
     *  fans out to several queries (the in-process engine's `onCommitBoundary`), after EVERY view in
     *  that commit has folded — so a listener that re-reads ANY view (its own or a sibling) sees
     *  post-commit state, never a torn mid-commit one. Frames keep their arrival (engine-dispatch)
     *  order, one per affected query. This is the supported way to drive change-derived layers
     *  (e.g. {@link resolveChange} → @rindle/narrator, or a devtools pane) off a live store — attach
     *  BEFORE `materialize` to catch a synchronous backend's first `hello`+`snapshot`.
     *  The per-query view `WireSchema` rides the `hello` frame (also readable via `view.schema`).
     *
     *  The third listener arg is the post-fold {@link ArrayView} for this `qid` (so a template wanting
     *  list context — current `data`, `schema`, `resultType` — needn't look it up). It is ALWAYS the
     *  plural view, even for a top-level `.one()` query: the Store retains the list-shaped view, not the
     *  SingularView wrapper handed back from `materialize`. `undefined` only if the view is mid-teardown.
     *
     *  `opts.removedSubtree` enriches every `remove` op on this stream with the full removed subtree
     *  ({@link FlatOp.node}), so a consumer can resolve a removed row's nested subs exactly as on an
     *  `add` (a bare remove carries only the leaving row). It is reconstructed client-side from the
     *  view — no wire/engine cost — and paid only on real evictions while at least one subscriber asks.
     *
     *  Returns a detach function; multiple listeners may attach. */
    subscribeChanges(listener: (qid: QueryId, ev: ChangeEvent, view?: ArrayView<unknown>) => void, opts?: {
        removedSubtree?: boolean;
    }): () => void;
    /** Subscribe to per-query {@link ResultType} transitions (the server-channel lifecycle the backend
     *  pushes — `unknown` → `complete`, etc.), tagged by `qid`. Fired only on a CHANGE (never replayed
     *  on attach; read `view.resultType` for the current value). The supported seam for a status-driven
     *  layer (a devtools pane). Returns a detach function; multiple listeners may attach. */
    subscribeResultType(listener: (qid: QueryId, rt: ResultType) => void): () => void;
    /** A read-only snapshot of every live materialized view (DEBUG-TOOLS-BROWSER-DESIGN §4.2): its
     *  qid, AST, {@link ResultType}, row count, and a capped row sample. Built fresh on each call from
     *  the live `views`/`asts` maps — never cached, never mutating. `sampleRows` caps the per-query
     *  peek (default 50) so a large view doesn't bloat the snapshot. */
    __inspect(sampleRows?: number): StoreInspect;
    private columns;
    /** An object row → a positional cell array in the table's column order (json → string). */
    private positionalize;
    /** The per-level column types parallel to the WireSchema, so the view parses json columns. */
    private viewTypes;
}