Rindle

API index and search · Build metadata

Supporting declarations

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

Exact source

WireValue

/** A bare wire cell. (JSON columns arrive as their raw JSON string.) */
export type WireValue = number | string | boolean | null;

ColType

/** Declared column type (from the typed schema) — drives the comparator + JSON parsing.
 *  `"int64"` is the exact-i64 column plane (design 226): the vocabulary exists from
 *  Stage C4 (codegen can name it), but no exact integer cell crosses this wire — and
 *  `WireValue` gains no `bigint` — until Stage E lands the browser bigint plane; until
 *  then the daemon refuses IVM queries whose footprint touches such a column. */
export type ColType = "string" | "number" | "boolean" | "json" | "int64";

WireProjection

/** A scalar-projection annotation on a relationship slot (REDUCE-DESIGN.md §9): a
 *  relationship aggregate (`issue { commentCount: count(comments) }`) lives as a singular
 *  one-row relationship whose child is the aggregate row; this tells the receiver to unwrap
 *  it into a scalar field — `commentCount: 5` instead of `commentCount: [{ count: 5 }]`. */
export interface WireProjection {
    /** Which CHILD column to surface as the scalar value (the aggregate column). */
    col: number;
    /** The value to emit when the relationship is empty (a childless parent) — the
     *  aggregate identity (`0` for count, `null` for sum/avg). */
    identity: WireValue;
}

WireRel

export interface WireRel {
    name: string;
    slot: number;
    /** `null` ⇒ an out-of-view / gating slot (the in-view gate drops `Child`s addressed here). */
    child: WireSchema | null;
    /** Non-null ⇒ a scalar-projected relationship aggregate the receiver unwraps to a scalar
     *  (REDUCE-DESIGN.md §9). Absent/`null` for an ordinary (plural or `.one()`) relationship. */
    project?: WireProjection | null;
}

WireSchema

export interface WireSchema {
    columns: string[];
    primaryKey: number[];
    /** Resolved, PK-completed sort: `[columnIndex, ascending]` pairs (the comparator input). */
    sort: [
        number,
        boolean
    ][];
    singular: boolean;
    relationships: WireRel[];
}

WireNode

export interface WireNode {
    row: WireValue[];
    rels: {
        rel: number;
        children: WireNode[];
    }[];
}

PathSeg

export interface PathSeg {
    rel: number;
    parentRow: WireValue[];
}

FlatOp

/** A positional change at one level of the view tree. A `remove` ships only the leaving `row`
 *  (the receiver already holds the subtree and locates it by key) — `node` is NEVER on the wire;
 *  it is an OPTIONAL client-side enrichment carrying the full removed subtree, attached by the
 *  ArrayView when a change consumer opts in (`Store.subscribeChanges(_, { removedSubtree: true })`),
 *  so a narrator can resolve a removed row's nested subs just as it can on an `add`. */
export type FlatOp = {
    tag: "add";
    node: WireNode;
} | {
    tag: "remove";
    row: WireValue[];
    node?: WireNode;
} | {
    tag: "edit";
    old: WireValue[];
    new: WireValue[];
};

FlatChange

export interface FlatChange {
    path: PathSeg[];
    op: FlatOp;
}

ChangeEvent

/**
 * What a {@link Backend} pushes per query: the handshake (`hello`), the (possibly
 * chunked) hydrate `snapshot`, then incremental `batch`es. The core builds an ArrayView
 * on `hello`, hydrates on `snapshot`, folds on `batch` — identically for any backend.
 */
export type ChangeEvent = {
    type: "hello";
    schema: WireSchema;
    comparatorVersion: number;
} | {
    type: "snapshot";
    adds: FlatChange[];
    last: boolean;
} | {
    type: "batch";
    events: FlatChange[];
    catchUp?: boolean;
};

Mutation

export type Mutation = {
    op: "add";
    table: string;
    row: WireValue[];
} | {
    op: "remove";
    table: string;
    row: WireValue[];
} | {
    op: "edit";
    table: string;
    old: WireValue[];
    new: WireValue[];
};

NormalizedOp

/**
 * A table-tagged, path-free row delta — the **normalized** wire payload (the path-free twin
 * of {@link FlatChange}; NORMALIZED-CHANGES-DESIGN.md §3). Rows are positional (bare cells; a
 * json column is its raw JSON string). `op` is the discriminant. Lives here (not in
 * `@rindle/normalized`) so both the protocol (`@rindle/remote`) and the sync layer share one type.
 */
export type NormalizedOp = {
    table: string;
    op: "add";
    row: WireValue[];
} | {
    table: string;
    op: "remove";
    row: WireValue[];
} | {
    table: string;
    op: "edit";
    old: WireValue[];
    new: WireValue[];
};

NormalizedTableSchema

/** One base table's flat schema on the normalized `hello` (§3): column names (in order) +
 *  primary-key column indices. Wire rows are positional against `columns`. */
export interface NormalizedTableSchema {
    name: string;
    columns: string[];
    primaryKey: number[];
}

NormalizedEvent

/**
 * A per-query NORMALIZED stream event — the path-free twin of {@link ChangeEvent}. The
 * `hello` carries the flat per-table schemas (no nested view schema, §3); `snapshot`/`batch`
 * carry table-tagged {@link NormalizedOp}s. The `NormalizedSync` layer folds these into the
 * local engine's base tables.
 *
 * `cv` (the global commit version the frame's data reflects — OPTIMISTIC-WRITES-DESIGN.md
 * §8.3/§8.6) is present on sources that speak the optimistic protocol; the plain normalized
 * path may omit it.
 */
export type NormalizedEvent = {
    type: "hello";
    tables: NormalizedTableSchema[];
    comparatorVersion: number;
    normalizedFp: string;
} | {
    type: "snapshot";
    ops: NormalizedOp[];
    cv?: number;
} | {
    type: "batch";
    ops: NormalizedOp[];
    cv?: number;
};

QueryId

export type QueryId = number;

BackendServerDelta

/** A dev-only authoritative server delta exposed by backends that can distinguish the upstream
 *  server stream from their local view/IVM stream. Flat remote backends surface clean
 *  {@link ChangeEvent}s; local-first normalized/optimistic backends surface the path-free
 *  {@link NormalizedEvent}s before they are folded into the local engine. */
export type BackendServerDelta = {
    format: "flat";
    event: ChangeEvent;
} | {
    format: "normalized";
    event: NormalizedEvent;
};

BackendDevObserver

/** Dev-only passive tap over a backend's authoritative server stream. Optional because in-process
 *  backends have no server stream, and older/custom backends may only expose the Store tap. */
export interface BackendDevObserver {
    onServerDelta?(qid: QueryId, ev: BackendServerDelta): void;
}

ResultType

/** A query's SERVER-CHANNEL state, surfaced on its {@link ArrayView} (FOLDED-MUTATIONS-DESIGN §7 —
 *  formerly conflated with pending-ness, OPTIMISTIC-WRITES-DESIGN.md §6):
 *   - `unknown`  — not hydrated: the server has not produced a first result for this query yet;
 *   - `complete` — the server has answered. STAYS `complete` while a local mutation is pending (the
 *                  prediction is the client's best current answer); reversion on rejection is an
 *                  event (`onRejected`), not a downgrade of completeness;
 *   - `error`    — RESERVED for a future server-side, query-level error signal
 *                  (see `designs/101-QUERY-ERRORS-DESIGN.md`); no longer produced by a pending mutation.
 *  "Is a prediction pending here?" is now a separate reactive axis (the backend's `pending(qid)` /
 *  `onPending`), not folded into this type. A backend with no server lifecycle (the in-process
 *  engine) leaves every view `complete`. */
export type ResultType = "unknown" | "complete" | "error";

RemoteQuery

/** The network identity for a named query subscription. The local AST remains local; remote
 *  normalized/optimistic sources use only this `(name,args)` pair upstream. */
export interface RemoteQuery {
    name: string;
    args: unknown;
}

Backend

/**
 * The seam the core talks to. Backend-agnostic: the same interface for the in-process
 * WASM backend and a remote (network) backend. The core never knows which. A backend
 * pushes a per-query {@link ChangeEvent} stream via {@link Backend.onEvent}; the remote
 * backend additionally owns the epoch/seq/gap protocol and emits only clean, in-order
 * events (so the ArrayView never sees the wire protocol).
 */
export interface Backend {
    registerQuery(queryId: QueryId, ast: unknown, remote?: RemoteQuery): void;
    unregisterQuery(queryId: QueryId): void;
    /** Optional split path for local-first backends: retain/release a named remote footprint
     *  without creating another local materialized view. `localQueryId`, when provided, names
     *  the local AST view this remote footprint feeds. `ast`, when provided, gives the backend
     *  the local schema context for sync-only retains that still need aggregate/table setup. */
    retainRemoteQuery?(queryId: QueryId, remote: RemoteQuery, localQueryId?: QueryId, ast?: Ast): void;
    releaseRemoteQuery?(queryId: QueryId): void;
    /** Local: applies now (changes flow back on the stream). Remote: sends to the server. */
    mutate(mutations: Mutation[]): Promise<void>;
    /** Optional DIRECT-COMMIT path for LOCAL-only tables (`201-LOCAL-ONLY-TABLES-DESIGN.md` §6):
     *  applies the writes straight to the engine, OUTSIDE the optimistic pending stack (a local
     *  table is untracked, so it never rebases). The backend rejects any synced/tracked table (M2).
     *  Backends with no local-table support (a plain remote sync backend) omit it.
     *
     *  `onCommitted` (207 §5.1) runs after the engine commit is applied but BEFORE subscriber
     *  delivery: a subscriber throwing during delivery re-raises out of this call, and a caller
     *  that must stay coherent with the engine (the persistence tap) cannot tell that throw from
     *  a pre-commit rejection — the callback can, because it fires exactly when the commit is in. */
    writeLocal?(mutations: Mutation[], onCommitted?: () => void): void;
    onEvent(handler: (queryId: QueryId, event: ChangeEvent) => void): void;
    /** Optional: the backend pushes per-query {@link ResultType} changes here and the core routes
     *  each to the matching view (so `view.resultType` tracks it). Backends with no lifecycle (the
     *  in-process engine) omit it and every view stays `complete`. */
    onResultType?(handler: (queryId: QueryId, resultType: ResultType) => void): void;
    /** Optional: brackets one commit's coherent multi-query delivery so the Store can fold every
     *  affected view BEFORE notifying any subscriber — cross-view-atomic notification. The
     *  in-process engine derives the whole commit's per-query deltas against one consistent post-
     *  commit snapshot, then dispatches them one query at a time; without a barrier a subscriber on
     *  the first-dispatched view, re-reading a sibling view in its callback, would observe that
     *  sibling's PRE-commit state (it has not folded yet). The backend calls the handler with
     *  `"begin"` before delivering a commit's per-query `batch` events (via {@link onEvent}) and
     *  `"end"` after the last one; between them the Store folds each view but DEFERS its
     *  notification, firing every affected view's subscribers together at `"end"`. Backends with no
     *  synchronous multi-query commit boundary (a plain remote backend, where each query's frame
     *  arrives in its own message) omit it — the Store then notifies per event, exactly as before. */
    onCommitBoundary?(handler: (phase: "begin" | "end") => void): void;
    /** Optional dev-only authoritative server stream tap. It is additive, like
     *  `Store.subscribeChanges`, and must not displace the backend's normal event handler. */
    __attachDevtoolsServerDeltas?(observer: BackendDevObserver): () => void;
}

NormalizedSource

/**
 * The server side of the **normalized** local-first path (NORMALIZED-CHANGES-DESIGN.md §5/§7):
 * registers queries and pushes each one's normalized footprint stream ({@link NormalizedEvent}s).
 * Both the in-process native (`@rindle/replica`) source and the ws (`@rindle/remote`
 * `RemoteNormalizedSource`) implement it; `@rindle/normalized`'s `NormalizedBackend` consumes one,
 * never knowing which — a sibling seam of {@link Backend} for the normalized composition.
 */
export interface NormalizedSource {
    registerQuery(queryId: QueryId, remote: RemoteQuery): void;
    unregisterQuery(queryId: QueryId): void;
    /** Send base-table writes to the server; the authoritative stream flows back. */
    mutate(mutations: Mutation[]): Promise<void>;
    onNormalized(handler: (queryId: QueryId, event: NormalizedEvent) => void): void;
    /** Optional: the backend hands its OWN typed per-table schemas so the source can validate
     *  each server `hello` against them (column order / PK by name) and reject a schema skew
     *  rather than silently transposing positional cells (CRIT#4 / §3 "drift ⇒ re-subscribe").
     *  Sources that can't skew (the in-process native source) may omit it. */
    expectClientSchema?(tables: NormalizedTableSchema[]): void;
}

MutationEnvelope

/** The upstream named-mutator envelope (OPTIMISTIC-WRITES-DESIGN.md §8.1): the wire
 *  carries the name + JSON args, never code; `mid` totally orders a client's mutations. */
export interface MutationEnvelope {
    clientID: string;
    mid: number;
    name: string;
    args: unknown;
}

MutationOutcomeFrame

/** A room authority's verdict for a NON-applied mutation (RINDLE-REALTIME-QUERY-ENABLEMENT
 *  §3.3, the deopt handshake; Slice H-iv-b server half / H-v client half). Sent on the author's
 *  own socket for every mutation the room did NOT apply, always BEFORE the lmid ack that burns
 *  the `mid` (same-socket ordering only — the ack may reach the client through another path
 *  first, e.g. a replayed lmid snapshot). Applied mutations send NOTHING: silence + lmid
 *  coverage ⇒ applied.
 *
 *  - `kind: "deopt"` — the room's §3.3 commit gate refused the routed mutation (or its
 *    environment fell short, e.g. `tx.query`); the mid is burnt in the ROOM ledger with zero
 *    effects and the client re-enqueues the same logical mutation onto the daemon stream.
 *    `name`/`args` are echoed so the frame is SELF-CONTAINED: a client that already retired the
 *    entry (the burnt-mid confirm won the race, or the frame is a replay re-answer) re-invokes
 *    from the frame alone.
 *  - `kind: "rejected"` — FINAL (authz/validation): the mid is burnt the same way and the
 *    prediction snaps back on the ordinary lmid release; no re-route.
 *
 *  `reason` may be absent on a re-answered frame whose record was seeded from journal replay
 *  (the verdict is journaled; the reason is not). */
export interface MutationOutcomeFrame {
    mid: number;
    kind: "deopt" | "rejected";
    reason?: string;
    /** Echoed on DEOPT frames only (self-contained re-invoke — see above). */
    name?: string;
    args?: unknown;
}

ProgressFrame

/** The connection-level progress frame (§8.6): advances the coherent-apply release point
 *  (`cvMin`). A pure release signal — mutation confirmation does NOT ride it: `lmid` is a
 *  row in {@link CLIENT_MUTATIONS_TABLE}, delivered through the client's own per-client
 *  system query ({@link LMID_QUERY_NAME}) like any data, so it is released by the same
 *  `cvMin` as the commit's effects (transactionally coherent by construction). */
export interface ProgressFrame {
    cvMin: number;
}

CLIENT_MUTATIONS_TABLE

/** The replicated bookkeeping table carrying each client's high-water mutation id
 *  (`lmid`). Engine-hosted and served like any base table; reserved (never part of a
 *  user schema). Columns: `[client_id, last_mutation_id]`, PK `client_id`. */
export declare const CLIENT_MUTATIONS_TABLE = "_rindle_client_mutations";

LMID_QUERY_NAME

/** The reserved server-query name every optimistic client subscribes at connect: the
 *  one-row system query `CLIENT_MUTATIONS_TABLE WHERE client_id = <me>`. The server
 *  derives the identity from the connection (args are ignored). */
export declare const LMID_QUERY_NAME = "_rindle/clientLmid";

CLIENT_MUTATIONS_SCHEMA

/** The system table's wire schema — appended to the client's expected schemas so the
 *  lmid query's `hello` passes CRIT#4 validation. */
export declare const CLIENT_MUTATIONS_SCHEMA: NormalizedTableSchema;

OptimisticSource

/**
 * The server side of the OPTIMISTIC path (OPTIMISTIC-WRITES-DESIGN.md §8): the
 * {@link NormalizedSource} stream with `cv`-tagged data frames, plus the connection-level
 * {@link ProgressFrame} channel and the named-mutator upstream. The client buffers data
 * frames by `cv` and applies all `cv ≤ cvMin` as one coherent release (§8.5).
 */
export interface OptimisticSource {
    registerQuery(queryId: QueryId, remote: RemoteQuery): void;
    unregisterQuery(queryId: QueryId): void;
    /** Ship one named-mutator invocation upstream (§8.1). Confirmation rides the progress frames. */
    pushMutation(envelope: MutationEnvelope): Promise<void>;
    onNormalized(handler: (queryId: QueryId, event: NormalizedEvent) => void): void;
    onProgress(handler: (frame: ProgressFrame) => void): void;
    /** Optional: the backend hands its OWN typed per-table schemas so the source validates each
     *  server `hello` against them and rejects a schema skew (CRIT#4); see {@link NormalizedSource}. */
    expectClientSchema?(tables: NormalizedTableSchema[]): void;
    /** Optional: fired when the server restarts (a transport that can detect it, e.g. via a daemon
     *  boot id). The backend resets its `cv` watermark so the server's reset `cv` sequence is
     *  accepted rather than dropped as stale. In-process sources never restart and omit it. */
    onRestart?(handler: () => void): void;
    /** Optional (Slice H-v): the channel's {@link MutationOutcomeFrame} stream — the room deopt
     *  handshake's client half. **OUT-OF-BAND BY DESIGN**: the frame carries no `cv` and the source
     *  MUST dispatch it immediately on arrival, never behind the cv buffer — a deopt has to migrate
     *  its pending entry BEFORE the buffered lmid release that would otherwise retire it as a
     *  success (and the §7.3 hold-back trigger, keyed on the entry's confirming domain, would then
     *  park its staged writes the wrong way). Sources whose authority never deopts (the in-process
     *  native source, a plain daemon) omit it. */
    onMutationOutcome?(handler: (frame: MutationOutcomeFrame) => void): void;
    /** Optional (Slice H-v, the §7.5 rule-3 crash-window closer): fired when the transport
     *  RE-establishes its session (reconnect → re-`init`), BEFORE any post-reconnect frame is
     *  processed — the ordering is load-bearing: the re-subscribed lmid stream's fresh snapshot may
     *  cover a mid whose `mutationOutcome` frame died with the old socket, and once that release
     *  retires the entry as an apparent success there is nothing left to re-send. The backend
     *  re-sends this domain's unconfirmed pending envelopes with their ORIGINAL mids, in mid order;
     *  the source may DEFER their delivery until the session is re-authorized (a room's
     *  `pushMutation` requires the lease-token subscribe's subject). Idempotent under the domain's
     *  own ledger — a processed mid dedups silently (silence + lmid coverage ⇒ applied), a
     *  non-applied mid is re-answered from the authority's recorded-outcome map (resolving even an
     *  already-retired entry through the handshake's not-found arm). Distinct from
     *  {@link onRestart} (a NEW server incarnation): a same-incarnation socket drop re-syncs
     *  without restarting, and envelopes sent into the dead socket are exactly what this recovers.
     *  In-process sources never drop a session and omit it. */
    onResync?(handler: () => void): void;
}