Rindle

API index and search · Build metadata

@rindle/room

0.0.0 · Public export map; development manifest version (0.0.0).

Source revision 05d0bf2c2e56 · build details
Source revision: 05d0bf2c2e56.
TypeScript input SHA-256: aabe6cfcc4172b870d5e272142958e9ea8d8784c2aa23133156e5a7ee633318e
Generated 2026-09-04T23:58:25.590Z with TypeScript 6.0.3. Public TypeScript checks and declaration emit passed. Package runtime tests are separate.

Entry point source

AuthorityApplyResult

TypeAliasDeclaration · Source: packages/room/src/authority.ts:27 · Supporting declarations

The apply outcome. Anything else — network failure, a 5xx — is a throw: plain errors retry (same body, same id; dedup absorbs a landed retry), errors marked fatal: true (an identity mismatch — same id, different body — or a malformed refusal) kill the incarnation loudly instead.

export type AuthorityApplyResult = {
    kind: "ok";
    applied: boolean;
    cv?: number;
} | {
    kind: "conflict";
    conflicts: AuthorityConflict[];
} | {
    kind: "fenced";
    currentEpoch?: number;
};

AuthorityConflict

InterfaceDeclaration · Source: packages/room/src/authority.ts:17 · Supporting declarations

One CAS rejection in the authority's 409 conflict body: the row's authoritative current image (null = absent), keyed by table + pk cells.

export interface AuthorityConflict {
    table: string;
    pk: unknown[];
    current: unknown[] | null;
}

createRoomShell

FunctionDeclaration · Source: packages/room/src/shell.ts:1810 · Supporting declarations

Boot a room shell: init the wasm, mint the upstream lease, connect the upstream leg, and serve the downstream ws (+ the private control plane, if configured). Returns once the ports are bound and the upstream connection is underway — await shell.awaitLive() for the seed.

export declare function createRoomShell(opts: RoomShellOptions): Promise<RoomShell>;

DownstreamOptions

InterfaceDeclaration · Source: packages/room/src/shell.ts:96 · Supporting declarations

The downstream half: how clients are authorized and served (§4/§10.1).

export interface DownstreamOptions {
    /** This room's document id — lease tokens for any other doc are refused. */
    docId: string;
    /** Token key ring: `kid` → shared secret (the API server signs with the same ring). */
    tokenKeys: Record<string, string>;
    /** Idle grace before an unsubscribed query's pipeline is reclaimed (default 30s). */
    idleTtlMs?: number;
    /** Lease-expiry / idle-sweep cadence (default 1s). */
    sweepIntervalMs?: number;
    /** How long a revocation keeps refusing pre-revocation tokens (default 30min — set
     *  it ≥ the longest token TTL the API server mints). */
    revocationWindowMs?: number;
    /** The room's private control plane (`POST /revoke`, `GET /stats`). Omit to run
     *  without one (no revocation surface). */
    control?: {
        authToken: string;
        port?: number;
    };
    /** The write plane (§5.1). Omit to run read-only (writes are refused). */
    writes?: WritesOptions;
    /** Per-socket downstream send budget in bytes (§9; default 4 MiB). A socket whose
     *  queued bytes would exceed it is terminated with a gap — re-subscribing is the
     *  repair. */
    sendBudgetBytes?: number;
}

fatalAuthorityError

FunctionDeclaration · Source: packages/room/src/authority.ts:51 · Supporting declarations

export declare function fatalAuthorityError(message: string): FatalAuthorityError;

httpAuthority

FunctionDeclaration · Source: packages/room/src/authority.ts:73 · Supporting declarations

The HTTP [RoomAuthority]: the API-server host as the room's sole counterpart (§5.3.1). Maps 409 {error:"fenced"} / 409 {error:"conflict"} to their results, a 500 mentioning a batch-identity mismatch to a fatal error, and anything else non-OK to a retryable throw.

export declare function httpAuthority(opts: HttpAuthorityOptions): RoomAuthority;

HttpAuthorityOptions

InterfaceDeclaration · Source: packages/room/src/authority.ts:57 · Supporting declarations

export interface HttpAuthorityOptions {
    /** The API server's apply endpoint (e.g. `http://…/api/rindle/apply-row-change-txn`). */
    applyUrl: string;
    /** The epoch-claim endpoint (e.g. `http://…/api/rindle/claim-room-epoch`). */
    claimUrl: string;
    /** The lmid-probe endpoint (e.g. `http://…/api/rindle/room-lmids`). */
    lmidsUrl: string;
    /** Extra headers on every call — the epoch-bound flush credential rides here. */
    headers?: Record<string, string>;
    fetch?: typeof fetch;
}

initRoomWasm

FunctionDeclaration · Source: packages/room/src/wasm.ts:15 · Supporting declarations

Initialize the room wasm module (idempotent). Call once before WasmRoom.open. Node: bytes are read from the package. Browser/bundler: the wasm is fetched. Pass moduleOrPath to override (a WebAssembly.Module, URL, or bytes).

export declare function initRoomWasm(moduleOrPath?: unknown): Promise<void>;

journalEntryOutcome

FunctionDeclaration · Source: packages/room/src/journal.ts:50 · Supporting declarations

The ONE reading rule for an entry's verdict across journal generations: outcome when present, else the legacy rejected flag, else applied.

export declare function journalEntryOutcome(entry: RoomJournalEntry): "applied" | "rejected" | "deopt";

KeyedRow

TypeAliasDeclaration · Source: packages/room/src/mutation-tx.ts:22 · Supporting declarations

A row keyed by column name.

export type KeyedRow = Record<string, WireValue>;

memoryJournal

FunctionDeclaration · Source: packages/room/src/journal.ts:84 · Supporting declarations

An in-process journal: survives incarnations within one shell process (and, handed to a second shell, a simulated process crash — the T2 harness). Not durable beyond the process, by definition.

export declare function memoryJournal(): RoomJournal;

mintRoomToken

FunctionDeclaration · Source: packages/room/src/token.ts:126 · Supporting declarations

Sign a room lease token. This runs on the API-server side (or a test playing it).

export declare function mintRoomToken(opts: MintRoomTokenOptions): Promise<string>;

MintRoomTokenOptions

InterfaceDeclaration · Source: packages/room/src/token.ts:46 · Supporting declarations

export interface MintRoomTokenOptions {
    doc: string;
    ast: unknown;
    /** The subject (user id) this token authorizes — the §4.1 revocation key. */
    sub: string;
    /** Key id + its secret (utf-8; give every room the same `keys` map). */
    kid: string;
    key: string;
    /** Expiry, ms from `now`. Keep short (minutes) — the §4.1 TTL backstop. */
    ttlMs: number;
    /** Mint time; defaults to `Date.now()`. Injectable for tests. */
    now?: number;
    /** The room's {@link scopeSpecsHash} for the profile this lease serves — stamped so the
     *  shell can flag scope skew (see {@link RoomTokenPayload.scopesHash}). Omit to not stamp. */
    scopesHash?: string;
}

MutationTx

InterfaceDeclaration · Source: packages/room/src/mutation-tx.ts:28 · Supporting declarations

The §4.2 write handle. Keyed methods are schema-checked; positional methods are the raw wire shape (cells in schema column order, pk cells in primaryKey order). query is not available in room mutators yet (it throws) — typed so a client registry that uses it still registers, and fails loudly at run time.

export interface MutationTx {
    /** Read one row by primary key (e.g. `tx.row("issue", { id: 1 })`). */
    row(table: string, pk: KeyedRow): KeyedRow | undefined;
    /** Insert a FULL row (every column named; missing or unknown columns throw). */
    insert(table: string, row: KeyedRow): void;
    /** Update the row identified by the pk columns; only the named non-pk columns
     *  change. A missing row is a NO-OP (rebase-friendly). */
    update(table: string, row: KeyedRow): void;
    /** Insert, or fully replace when the pk already exists (a FULL row, like insert). */
    upsert(table: string, row: KeyedRow): void;
    /** Delete the row identified by the pk columns. A missing row is a NO-OP. */
    delete(table: string, pk: KeyedRow): void;
    /** NOT SUPPORTED in room mutators yet — throws. (Typed to accept the client
     *  builder's queries so shared registries typecheck.) */
    query(query: {
        ast(): unknown;
    }): never;
    get(table: string, pk: WireValue[]): WireValue[] | undefined;
    add(table: string, row: WireValue[]): void;
    remove(table: string, row: WireValue[]): void;
    edit(table: string, oldRow: WireValue[], newRow: (WireValue | undefined)[]): void;
}

RoomAuthority

InterfaceDeclaration · Source: packages/room/src/authority.ts:32 · Supporting declarations

export interface RoomAuthority {
    /** Claim the placement epoch for `doc` (§2.5) — once per shell process, at boot,
     *  AFTER resubmitting the previous incarnation's unconfirmed batches (their recorded
     *  epochs must still be current to land). */
    claimEpoch(doc: string): Promise<number>;
    /** The authority's ledger lmid per client (0 = none) — the boot probe that lets
     *  journal replay absorb already-durable mutations as dedup. */
    lmids(doc: string, clients: string[]): Promise<Record<string, number>>;
    /** Apply one batch: `body` is the exact journaled `/apply-row-change-txn` body
     *  string, sent verbatim. */
    applyRowChangeTxn(body: string): Promise<AuthorityApplyResult>;
}

RoomFlushRecord

InterfaceDeclaration · Source: packages/room/src/journal.ts:57 · Supporting declarations

One journaled flush batch: the room's flush-stream position, the placement epoch it was built under, and the EXACT /apply-row-change-txn body string — resubmitted verbatim, never rebuilt (§5.3 step 4).

export interface RoomFlushRecord {
    seq: number;
    epoch: number;
    body: string;
}

RoomJournal

InterfaceDeclaration · Source: packages/room/src/journal.ts:63 · Supporting declarations

export interface RoomJournal {
    /** Append `entries` durably, in order. Resolving is the ack gate (§8.1): the shell
     *  advances lmid rows only after this resolves. A rejection is fatal to the
     *  incarnation — an ack that might not survive must never be sent. */
    append(entries: RoomJournalEntry[]): Promise<void>;
    /** Every entry ever appended, in append order — the boot-time replay source. */
    replay(): Promise<RoomJournalEntry[]>;
    /** Journal one built flush batch, BEFORE its first send. Same durability contract
     *  as `append`: a rejection is fatal (an unjournaled batch must never reach the
     *  wire — a retry could otherwise rebuild different bytes under the same id). */
    appendFlush(record: RoomFlushRecord): Promise<void>;
    /** The authority settled flush `seq` (committed, deduped, or dead) — drop it. */
    confirmFlush(seq: number): Promise<void>;
    /** Unconfirmed flush records in seq order, plus the highest seq ever appended
     *  (0 = none) — the boot-time resubmission source and the seq seed. */
    replayFlushes(): Promise<{
        records: RoomFlushRecord[];
        maxSeq: number;
    }>;
}

RoomJournalEntry

InterfaceDeclaration · Source: packages/room/src/journal.ts:22 · Supporting declarations

One journaled mutation: the wire envelope plus its recorded outcome. Replay applies the OUTCOME — a non-applied (rejected/deopt) entry consumes its mid without running the mutator.

export interface RoomJournalEntry {
    clientID: string;
    mid: number;
    name: string;
    args: unknown;
    /** The connection's authenticated subject at push time (the lease token's `sub`,
     *  shell-stamped — managed-writes §3.3): recovery is re-invocation, so identity is
     *  an input that must survive the crash. `""` = an entry journaled before the
     *  identity plane existed (mutators see it as unauthenticated). */
    sub: string;
    /** The recorded verdict (H-iv-b): `"applied"` replays by RE-INVOKING the mutator
     *  (§3.3 — and against a moved base the re-invocation may legitimately reject or
     *  DEOPT; the journal record is never rewritten, the shell's recorded-outcome map
     *  reflects what the replaying incarnation produced); `"rejected"` (final — authz /
     *  validation / unknown mutator) and `"deopt"` (the §3.3 commit gate refused; the
     *  client was told to re-route the mutation) both replay as a consumed-mid-no-effect
     *  WITHOUT running anything — re-judging a deopt could invent effects the client
     *  already re-routed elsewhere. Absent = a legacy pre-H-iv-b entry: read through
     *  {@link journalEntryOutcome}. */
    outcome?: "applied" | "rejected" | "deopt";
    /** Legacy pre-H-iv-b flag, superseded by {@link outcome} but still WRITTEN (`true`)
     *  alongside BOTH non-applied outcomes: a legacy reader replays either kind as a
     *  consumed-mid-no-effect, which is exactly right. */
    rejected?: boolean;
}

RoomMutator

TypeAliasDeclaration · Source: packages/room/src/mutation-tx.ts:67 · Supporting declarations

A room mutator: deterministic, replayable — re-invoked on journal replay against the freshly re-subscribed base (§3.3), so the same purity rules as a client mutator apply (no clock, no randomness, a pure function of (base, args, ctx)). An auth check against ctx re-runs on replay against the freshly rebuilt base — a mutation that passed before a crash can replay as rejected if the permission row changed in between; that is §3.3's intended rebase behavior.

export type RoomMutator = (tx: MutationTx, args: never, ctx: RoomMutatorCtx) => void;

RoomMutatorCtx

InterfaceDeclaration · Source: packages/room/src/mutation-tx.ts:55 · Supporting declarations

The ambient authorization context a room mutator runs under (managed-writes design §3.2). Shell-stamped from the connection's DO-verified lease token — NEVER client-supplied — so per-row/per-field rules checked against it are trustworthy. The shape deliberately matches the shared-generator drivers' ctx.user, so one registry body runs identically in all three homes (client / api-server / room).

export interface RoomMutatorCtx {
    /** The authenticated subject (the lease token's `sub`). `""` on replay of an entry
     *  journaled before the identity plane — treat as unauthenticated. */
    user: string;
}

RoomScopeSpec

InterfaceDeclaration · Source: packages/room/src/shell.ts:124 · Supporting declarations

One table's §3.3 scope spec (H-iv-b): an element of the api-server's RoomBootResponse.scopes, passed VERBATIM to the wasm room's enableWritesV2. Structurally identical to @rindle/api-server's RoomScopeSpec (declared locally — the shell must not depend on the api-server package; TS structural typing keeps the two in lockstep at the host's threading site).

export interface RoomScopeSpec {
    table: string;
    /** The footprint's row-local predicate for this table (a wire `Condition`) — drives
     *  the commit gate's absent-read proof. Absent ⇒ absent reads on this table always
     *  deopt (fail closed). */
    footprintWhere?: unknown;
    writable: {
        kind: "none";
    } | {
        kind: "predicate";
        where?: unknown;
        joinKeyCols: string[];
    };
}

RoomShell

InterfaceDeclaration · Source: packages/room/src/shell.ts:182 · Supporting declarations

export interface RoomShell {
    /** The bound downstream port. */
    readonly port: number;
    /** The bound control-plane port (0 when no control plane was configured). */
    readonly controlPort: number;
    /** Resolves when the CURRENT incarnation is live (seq-0 snapshot applied);
     *  immediately if it already is. */
    awaitLive(): Promise<void>;
    /** The last-applied upstream commit version, if live. */
    cv(): number | undefined;
    /** The upstream subscription epoch of the current incarnation, if any. */
    upstreamEpoch(): number | undefined;
    /** This incarnation's downstream bootId (rotates on every re-subscribe). */
    bootId(): string;
    /** Fire the write-behind flush now (instead of the debounce) and await its
     *  settlement — deterministic flushing for tests and drain-before-close. No-op
     *  without an authority. */
    flushNow(): Promise<void>;
    close(): Promise<void>;
}

RoomShellOptions

InterfaceDeclaration · Source: packages/room/src/shell.ts:173 · Supporting declarations

export interface RoomShellOptions {
    upstream: UpstreamOptions;
    downstream: DownstreamOptions;
    /** Downstream ws port (default 0 = ephemeral; bound on 127.0.0.1). */
    port?: number;
    /** Diagnostic sink (default: silent). */
    log?: (line: string) => void;
}

RoomTokenError

ClassDeclaration · Source: packages/room/src/token.ts:149 · Supporting declarations

Why a token was refused. The reason is terse and safe to echo in a queryError.

export declare class RoomTokenError extends Error {
    readonly reason: string;
    constructor(reason: string);
}

RoomTokenPayload

InterfaceDeclaration · Source: packages/room/src/token.ts:29 · Supporting declarations

The room's self-authorizing signed lease token (RINDLE-REALTIME-DESIGN.md §10.1).

The API server is the authority (§4): it authenticates the user, resolves the named query to an approved AST, and signs this token. The token then IS the lease — the room materializes on first presentation, so no pre-placement /materialize control call ever touches it (the §10.1 inversion; on the DO shell the Worker verifies the same signature statelessly before get(id)).

Shape: rt1.<base64url(payload)>.<base64url(hmac-sha256(prefix.payload))> with payload { v: 1, doc, ast, sub, iat, exp, kid }:

  • doc — the room/document id the token authorizes (a token for one room presented to another is refused);
  • ast — the approved wire Ast (the client never composes this; §4's "ASTs never cross the public wire" holds because the token is opaque TO THE CLIENT — it carries it, it cannot mint or alter it);
  • sub — the subject (user id): the revocation key (§4.1);
  • iat/exp — issued-at / expiry, ms epoch. Renewal is re-authorization: clients obtain a fresh token through the API server, never extend this one. iat is what lets a revocation refuse pre-revocation tokens while a genuine re-grant (a newer token) passes immediately;
  • kid — which shared secret signed it (rotation).

HMAC via WebCrypto (crypto.subtle) so the exact same code verifies in Node (the test shell) and in a Cloudflare Worker/DO (P4) — no node:crypto import.

export interface RoomTokenPayload {
    v: 1;
    doc: string;
    ast: unknown;
    sub: string;
    iat: number;
    exp: number;
    kid: string;
    /** A short fingerprint of the room's compiled scope specs at mint time
     *  ({@link scopeSpecsHash}). Advisory, not a credential: the room's §3.3 gate is the
     *  contract regardless. It lets the shell detect SCOPE SKEW — a room profile edited
     *  while a room is already live arms the gate with the OLD specs (a one-shot at boot)
     *  while fresh leases prove against the NEW ones, so every routed write silently
     *  deopt-loops. Optional so a pre-stamp api-server / older token still verifies. */
    scopesHash?: string;
}

scopeSpecsHash

FunctionDeclaration · Source: packages/room/src/token.ts:87 · Supporting declarations

A stable short fingerprint of the compiled scope specs — the scope-skew tripwire. NOT security (the gate re-proves every write): FNV-1a-32 over the {@link canonicalJson} form, 8 hex chars. Both the api-server (stamping the lease token) and the room shell (hashing the boot-wire scopes it armed the gate with) run this over the SAME compiler output, so an unchanged profile ⇒ equal hash and a profile edited under a live room ⇒ mismatch. A collision only costs a missed diagnostic, never correctness. Accepts either wire's spec array (RoomScopeSpec[] / RoomTableSpec[] — structurally identical).

export declare function scopeSpecsHash(specs: readonly unknown[]): string;

UpstreamOptions

InterfaceDeclaration · Source: packages/room/src/shell.ts:80 · Supporting declarations

The upstream half of the shell's config: where rindled lives and what to follow.

export interface UpstreamOptions {
    /** rindled's public subscription plane, e.g. `ws://127.0.0.1:7601`. */
    wsUrl: string;
    /** rindled's private control plane, e.g. `http://127.0.0.1:7600` (lease minting). */
    controlUrl: string;
    /** Bearer token for the control plane (required unless rindled runs unauthenticated). */
    authToken?: string;
    /** The document footprint — a wire `Ast` (§3.1). What this room follows and serves from. */
    footprintAst: unknown;
    /** Lease TTL passed to `/materialize` (rindled's default when omitted). */
    leaseTtlMs?: number;
    /** The `init` clientID on the upstream socket (diagnostic identity). */
    clientId?: string;
}

verifyRoomToken

FunctionDeclaration · Source: packages/room/src/token.ts:172 · Supporting declarations

Verify a token's signature and claims; returns the payload (with the approved AST) or throws {@link RoomTokenError}. Signature is checked FIRST — no claim is trusted (not even kid's existence beyond the key lookup) before the MAC passes.

export declare function verifyRoomToken(token: string, opts: VerifyRoomTokenOptions): Promise<RoomTokenPayload>;

VerifyRoomTokenOptions

InterfaceDeclaration · Source: packages/room/src/token.ts:158 · Supporting declarations

export interface VerifyRoomTokenOptions {
    /** The room's own doc id — a token for any other doc is refused. */
    doc: string;
    /** kid → shared secret. Unknown kids are refused (never "try them all"). */
    keys: Record<string, string>;
    /** Verification time; defaults to `Date.now()`. Injectable for tests. */
    now?: number;
}

WasmRoom

ClassDeclaration · Source: packages/room/pkg/rindle_room.d.ts:11 · Supporting declarations

One room instance: the upstream-fed base store plus its downstream serving state (materialized queries deduped by QueryKey, subscribers addressed by the host's opaque keys) and — once enableWrites runs — the §5.1 write plane, with at most one mutation transaction open at a time (the room is an actor; the host serializes mutations onto it).

/* tslint:disable */
/* eslint-disable */
/**
 * One room instance: the upstream-fed base store plus its downstream serving state
 * (materialized queries deduped by `QueryKey`, subscribers addressed by the host's
 * opaque keys) and — once [`enableWrites`](Self::enable_writes) runs — the §5.1
 * write plane, with **at most one mutation transaction open at a time** (the room is
 * an actor; the host serializes mutations onto it).
 */
export class WasmRoom {
    private constructor();
    free(): void;
    [Symbol.dispose](): void;
    /**
     * Advance the ledger rows for journal-committed mutations — **the ack** (§5.1
     * step 4; call once the journal append resolves). `entries_json` is a JSON array
     * of `{clientID, mid}`. Returns `{"headCv": n}` — drain
     * [`commitAll`](Self::commit_all) to ship the lmid frames — or `{"headCv": null}`
     * when everything was already covered.
     */
    ack(entries_json: string): string;
    /**
     * The ledger row's current value for `clientID` (0 if none) — what the wire has
     * confirmed.
     */
    ackedLmid(client_id: string): number;
    /**
     * The highest mid applied to the head for `clientID` (0 if none) — the dedup
     * watermark; diagnostics and the crash-replay tests read it.
     */
    appliedMid(client_id: string): number;
    /**
     * Apply one upstream `nbatch` frame's `batch` object (JSON text). Returns a status
     * object (JSON text): `{applied: "snapshot", rows}` | `{applied: "live", ops}` |
     * `{applied: "duplicate"}` | `{applied: "staleEpoch", expected, got}`. Throws when
     * the frame is garbage or the apply poisons the store — in both cases the host
     * tears down and re-subscribes under a new epoch (§3.4); there is no repair call.
     *
     * After a `live`/`snapshot` apply, drain the fan-out with
     * [`commitAll`](Self::commit_all).
     */
    apply(batch_json: string): string;
    /**
     * Build the §5.3 net-effect batch from the dirty entries + ledger co-edits.
     * Returns `undefined` when there is nothing to flush, else JSON text
     * `{changes, batchHash, lmids}`: `changes` is the `/apply-row-change-txn`
     * `changes[]` array (ingest shape, old-images = the CAS base chain), `batchHash`
     * the batch identity computed over its canonical bytes, `lmids` the
     * `{clientID, mid}` advances the batch covers. At most one flush may be in
     * flight — resolve with [`flushOk`](Self::flush_ok) or
     * [`flushConflict`](Self::flush_conflict); retries resubmit the SAME journaled
     * body, never a rebuilt one.
     */
    beginFlush(): string | undefined;
    /**
     * Open the transaction for `(clientID, mid)`. Returns `{"begin": "tx"}` (run the
     * mutator, then [`commitMutation`](Self::commit_mutation) or
     * [`rejectMutation`](Self::reject_mutation)) or `{"begin": "dedup"}` (an absorbed
     * redelivery — send nothing). Throws on a mid gap with the exact
     * `mutation gap for …` message the client's recovery keys on — forward it on an
     * `error` frame. At most one transaction may be open.
     *
     * `name`/`args_json` (optional, Slice I-ii) are the wire envelope's mutator name
     * and JSON-text args: should this mid end non-applied, its durable outcome row
     * echoes them on a DEOPT (self-contained re-invoke, mirroring the H-iv-b frame).
     * Omitting them degrades deopt rows to NULL name/args, nothing else.
     */
    beginMutation(client_id: string, mid: number, name?: string | null, args_json?: string | null): string;
    /**
     * Drain every materialized query once and return the fan-out as JSON text:
     * `[{sub, batch}, …]` — one entry per attached subscriber per query with a
     * non-empty net delta, each `batch` stamped with that subscriber's own epoch/seq.
     * Empty array when nothing changed (or the store is no longer live — a dead
     * incarnation must not ship a torn frame).
     */
    commitAll(): string;
    /**
     * Apply the open transaction to the head as one local commit: dirty entries are
     * captured, the applied watermark advances, and every attached query sees the ops.
     * Returns `{"headCv": n}` — drain [`commitAll`](Self::commit_all) and fan out NOW
     * (§5.1 step 2: data fanout is never gated on durability), then journal the
     * envelope and [`ack`](Self::ack) once the append commits. Throws only when the
     * commit tears the head (store poisoned — tear down).
     *
     * Under [`enableWritesV2`](Self::enable_writes_v2) the §3.3 commit gate runs
     * first; a containment violation returns `{"deopt": {reason, table, pk, …}}`
     * instead (`reason`: `"writeOutOfScope"` (+ `op`) | `"joinKeyChanged"`
     * (+ `column`) | `"absentReadUnproven"`): **nothing was applied**, the tx is
     * consumed and the mid burnt exactly like [`rejectMutation`](Self::reject_mutation)
     * (do NOT call it too) — journal the envelope as rejected and [`ack`](Self::ack)
     * as usual, answering the client with a deopt. Under the v1 enable the return is
     * always `{"headCv": n}`, byte-identical to before.
     */
    commitMutation(): string;
    /**
     * The last-applied upstream commit version (`undefined` before the seq-0 snapshot).
     * This is the journal cursor's `cv` half — persist `(epoch, cv)` to resume (§3.3).
     */
    cv(): number | undefined;
    /**
     * How many `(table, pk)` keys are dirty (§5.3 — P3's flush input).
     */
    dirtyLen(): number;
    /**
     * The highest mid the write authority has committed for `clientID` (0 if none) —
     * §8.1's `durable` level.
     */
    durableLmid(client_id: string): number;
    /**
     * Enable the §5.1 write plane over a live store: `owned_json` is a JSON array of
     * the tables mutators may write (§3.2's owned set — must all be in the upstream
     * hello). Registers the `_rindle_client_mutations` ledger; call once, right after
     * the seq-0 snapshot applies and before serving. Idempotence is deliberate-ly NOT
     * provided: enabling twice is a shell bug and throws.
     */
    enableWrites(owned_json: string): void;
    /**
     * Enable the write plane with per-table **scope specs** — the §3.3 room commit
     * gate (H-iv-a). `specs_json` is a JSON array of
     * `{ table, footprintWhere?, writable }` where `writable` is the lease block's
     * `RoomTableSpec.writable` shape (`{kind:"none"}` for a context table,
     * `{kind:"predicate", where?, joinKeyCols}` for a writable one) and
     * `footprintWhere` is the footprint's row-local wire `Condition` for the table
     * (drives the absent-read proof). Predicates compile NOW, loudly — a malformed or
     * non-row-local spec (e.g. a `correlatedSubquery`) throws and nothing is enabled.
     * With the gate armed, [`commitMutation`](Self::commit_mutation) can return a
     * `{"deopt": …}` verdict. Same once-only/live-store rules as
     * [`enableWrites`](Self::enable_writes), which remains the legacy table-granular
     * entry (its gate proves nothing and never deopts).
     */
    enableWritesV2(specs_json: string): void;
    /**
     * The upstream subscription epoch this store was opened under.
     */
    epoch(): number;
    /**
     * The authority rejected the in-flight batch on CAS (§5.4 `flush_conflict`):
     * `conflicts_json` is its `409` body's `conflicts` array —
     * `[{table, pk, current}]`, `current: null` for an absent row. Each conflicted
     * key converges to the authority's image (the local net effect is dropped);
     * everything else stays dirty and retries next flush. Returns
     * `{"headCv": n | null}` — drain [`commitAll`](Self::commit_all) after a
     * non-null cv to ship the corrective frames.
     */
    flushConflict(conflicts_json: string): string;
    /**
     * Whether a §5.3 flush is in flight (diagnostics; the shell also guards).
     */
    flushInFlight(): boolean;
    /**
     * The authority committed the in-flight batch (§5.3 step 6): consumed entries
     * retire (or re-base onto the image just written, if re-dirtied mid-flight),
     * durable watermarks advance, `pending ≤` them retires. Returns
     * `{"retired": n, "dirty": n}` — a non-zero `dirty` means writes landed during
     * the flight; schedule the next flush.
     */
    flushOk(): string;
    /**
     * The room's own commit order — what downstream frames are stamped with and what
     * the shell's `progress {cvMin}` release points are computed from. Advances on
     * every upstream apply and every local commit (see `RoomStore::head_cv`).
     */
    headCv(): number;
    /**
     * Baseline present and not poisoned — the store is serving.
     */
    isLive(): boolean;
    /**
     * A failed apply left this incarnation dead (§3.4): tear down, re-subscribe.
     */
    isPoisoned(): boolean;
    /**
     * Subscribe `sub` to the reserved per-client lmid query (`_rindle/clientLmid`):
     * the room composes the `_rindle_client_mutations WHERE client_id = <clientID>`
     * AST itself — identity comes from the connection's `init`, never client args.
     * Same return shape and envelope rules as [`subscribe`](Self::subscribe).
     */
    lmidSubscribe(sub: string, epoch: number, client_id: string, now_ms: number): string;
    /**
     * How many downstream queries are currently materialized (post-dedup).
     */
    materializationCount(): number;
    /**
     * Open the room's base store from the upstream `nhello` frame's `hello` object
     * (JSON text). `idle_ttl_ms` is the downstream retention grace: a materialized
     * query with no subscribers for that long is reclaimed by [`sweep`](Self::sweep).
     * Rejects malformed hellos loudly — nothing is constructed.
     */
    static open(hello_json: string, idle_ttl_ms: number): WasmRoom;
    /**
     * Applied-but-not-retired mutations (everything, until P3's write-behind).
     */
    pendingLen(): number;
    /**
     * Drop the open transaction's staged ops and consume its mid anyway — the reject
     * path (unknown mutator, mutator threw, staging refusal, a replayed non-applied
     * journal entry). The stream stays contiguous; the eventual [`ack`](Self::ack)
     * advances the ledger with no effects, snapping the author's prediction back.
     *
     * `kind` (`"rejected"` (default) | `"deopt"`) + `reason` are the shell's H-iv-b
     * classification of the verdict: the outcome is RECORDED (Slice I-ii) and its
     * durable row rides the next flush beside the covering lmid co-edit — one
     * authority transaction, so a post-downgrade client resolving through the daemon
     * never reads a burnt non-applied mid as applied. A deopt row echoes the
     * [`beginMutation`](Self::begin_mutation) envelope's `name`/`args`.
     */
    rejectMutation(kind?: string | null, reason?: string | null): void;
    /**
     * Seed the durable watermarks from the write authority's ledger — the boot probe
     * (§3.3; T2's committed-before-crash row). Call after [`enableWrites`] and
     * **before** the journal replay: seeded mids replay as dedup instead of
     * re-invoking. `seeds_json` is a JSON array of `{clientID, lmid}`. Returns
     * `{"headCv": n | null}` — the ledger rows ride a local commit so the client's
     * lmid slice confirms them.
     *
     * [`enableWrites`]: Self::enable_writes
     */
    seedDurable(seeds_json: string): string;
    /**
     * Materialize-on-presentation (§10.1): attach subscriber `sub` (the host's opaque
     * routing key) at `epoch` to the query for this approved wire `Ast` (JSON text) —
     * reusing the pipeline on a `QueryKey` hit. Returns
     * `{queryKey, reused, hello, snapshot}` (JSON text): send `nhello(hello)` then
     * `nbatch(snapshot)` (its `seq` is 0), then this subscriber's entries from each
     * [`commitAll`](Self::commit_all). Re-subscribing an existing `sub` replaces its
     * envelope — pass the bumped `epoch`, exactly as before.
     */
    subscribe(sub: string, epoch: number, ast_json: string, now_ms: number): string;
    /**
     * How many downstream subscribers are currently attached.
     */
    subscriberCount(): number;
    /**
     * Reclaim downstream queries idle past their grace window, on the host's clock.
     * Returns how many were destroyed.
     */
    sweep(now_ms: number): number;
    /**
     * Stage an add of a full-width row (JSON-array text). Clean refusal (throw, head
     * untouched) when the key exists, the table is not owned, or the width is wrong —
     * catch it and reject the mutation.
     */
    txAdd(table: string, row_json: string): void;
    /**
     * Stage an edit to a full-width row (JSON-array text) keyed by its pk cells. The
     * shell resolves partial updates against [`txGet`](Self::tx_get) first — only
     * concrete cells cross this boundary (JSON has no `undefined`). The pk must not
     * change; the row must exist.
     */
    txEdit(table: string, row_json: string): void;
    /**
     * The effective row for `pk_json` (a JSON array of pk cells, `primaryKey` order):
     * the live head under this transaction's own staged writes — read-your-writes for
     * read-dependent mutators, and what the keyed `update`/`delete`/`upsert` layers
     * probe before composing their writes. Returns the row as JSON-array text, or
     * `undefined` when absent.
     */
    txGet(table: string, pk_json: string): string | undefined;
    /**
     * Stage a remove of the row keyed `pk_json` (JSON array of pk cells). The change
     * is composed from the effective row; a missing key is a clean refusal (the keyed
     * `delete` layer no-ops before calling this).
     */
    txRemove(table: string, pk_json: string): void;
    /**
     * Detach subscriber `sub` (unsubscribe, socket close, lease expiry, revocation —
     * the host decides why). Idempotent: returns whether it was attached. Its query
     * stays materialized for `idle_ttl_ms` (a quick re-subscribe is cheap), then
     * [`sweep`](Self::sweep) reclaims it.
     */
    unsubscribe(sub: string, now_ms: number): boolean;
}

WireValue

TypeAliasDeclaration · Source: packages/room/src/mutation-tx.ts:20 · Supporting declarations

A bare wire cell (the client stack's WireValue).

export type WireValue = number | string | boolean | null;

WritesOptions

InterfaceDeclaration · Source: packages/room/src/shell.ts:136 · Supporting declarations

The §5.1 write plane: the room's own mutator registry over its owned tables.

export interface WritesOptions {
    /** The server registry (§4.2): named mutators run against the shared head. PLAIN
     *  synchronous `(tx, args, ctx)` functions only — a `shared(...)` GENERATOR registry
     *  does NOT register verbatim (nothing drives it here; the shell rejects a mutator
     *  that returns a generator/promise — see `assertSyncMutatorReturn`). */
    mutators: Record<string, RoomMutator>;
    /** §3.2's owned set — the only tables mutators may write. Must all be in the
     *  upstream footprint; followed tables and the ledger are never writable. */
    ownedTables: string[];
    /** The §3.3 per-table scope specs from the boot wire (`RoomBootResponse.scopes` —
     *  H-iv-b). Present ⇒ the write plane enables in v2 GATED mode (`enableWritesV2`):
     *  staged writes validate against the writable predicates, join-key edits refuse,
     *  absent reads must prove against `footprintWhere`, context (`kind:"none"`) tables
     *  become txGet-READABLE, and a violating commit returns a structured DEOPT instead
     *  of applying. The scopes' writable tables must be a SUBSET of {@link ownedTables}
     *  (the host's own declaration) — a wider server scope throws at construction, so a
     *  self-hoster's owned set can never be extended from the wire. Absent ⇒ the v1
     *  table-granular write plane, byte-identical to before. */
    scopes?: RoomScopeSpec[];
    /** The durable sidecar an ack means (§8.1). Defaults to `memoryJournal()` — the
     *  "survives nothing beyond the process" class; hosts bring their own. */
    journal?: RoomJournal;
    /** The journal group-commit window (default 5ms): mutations arriving within it
     *  share one append, and their acks ride one ledger commit. */
    groupCommitMs?: number;
    /** The write authority (§5.3.1) — the API server's `/apply-row-change-txn` host
     *  (or the P3 gate's mock). Omit to run journal-only (P2 semantics: nothing is
     *  ever durable upstream). With an authority, the shell claims a placement epoch
     *  at boot, probes durable lmids before replay, and write-behinds on the flush
     *  cadence (§5.3). */
    authority?: RoomAuthority;
    /** The flush debounce (§5.3; default 250ms, within the design's ≤1s budget). */
    flushDebounceMs?: number;
    /** Flush immediately once this many keys are dirty (default 512). */
    flushDirtyMax?: number;
}