API index and search · Build metadata
@rindle/api-server
0.0.0 · Public export map; development manifest version (0.0.0).
ApiContext
InterfaceDeclaration · Source: packages/api-server/src/index.ts:183 · Supporting declarations
Request context supplied by the application after authentication. The HTTP adapter or trusted
caller owns user; do not populate it from an unverified request body.
export interface ApiContext<User> {
user: User;
request?: unknown;
}ApiMutator
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:276 · Supporting declarations
A server mutator: a plain async function against the live {@link ServerMutationTx}. Two ways to
write one (MUTATORS-ISOMORPHIC): drive it directly (the raw escape hatch — an owner-gated cascade,
a NOT EXISTS dedup — plus a returned SqlStatement[]/SqlTxn), OR delegate to a SHARED generator
(the SAME body the client predicts) via {@link runSharedMutation}, keeping only the server-only
authority (arg parse, principal, policy) in the wrapper.
export type ApiMutator<User, Args> = (tx: ServerMutationTx, args: Args, ctx: MutationContext<User>) => MaybePromise<ApiMutatorResult>;ApiMutatorResult
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:270 · Supporting declarations
export type ApiMutatorResult = void | SqlStatement[] | SqlTxn;ApiMutators
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:401 · Supporting declarations
export type ApiMutators<User> = Record<string, ApiMutator<User, any>>;ApiQueries
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:190 · Supporting declarations
export type ApiQueries<User> = Record<string, ApiQuery<User, any>>;ApiQuery
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:189 · Supporting declarations
export type ApiQuery<User, Args> = (ctx: ApiContext<User>, args: Args) => MaybePromise<ApiQueryResult>;ApiQueryResult
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:188 · Supporting declarations
export type ApiQueryResult = Ast | Query<any, any, any>;ArgSchema
InterfaceDeclaration · Source: packages/client/src/mutation-ops.ts:161 · Supporting declarations
The minimal arg validator a shared mutator can carry so the SERVER can parse UNTRUSTED wire args
before driving it (the client trusts its typed callsites and never calls this). Structural on
purpose — a zod schema satisfies it as-is — so @rindle/client stays validator-library-agnostic.
export interface ArgSchema<Args> {
parse(raw: unknown): Args;
}assembleDurableText
FunctionDeclaration · Source: packages/client/src/stream.ts:87 · Supporting declarations
The durable half of the splice for the mapped-table layout: the compacted body followed by
whatever chunk rows have not been folded into it yet.
Chunks are ALWAYS the suffix after body — the closing checkpoint rewrites body and drops the
chunks it absorbed in ONE transaction — so a reader never observes a torn state where a chunk both
is and is not in the body.
export declare function assembleDurableText(message: {
body?: string | null;
} | null | undefined, chunks?: ReadonlyArray<{
seq: number;
text: string;
}>): string;AuthorizeMutationInput
InterfaceDeclaration · Source: packages/api-server/src/index.ts:209 · Supporting declarations
export interface AuthorizeMutationInput<User> {
user: User;
envelope: MutationEnvelope;
context: ApiContext<User>;
}AuthorizeQueryInput
InterfaceDeclaration · Source: packages/api-server/src/index.ts:202 · Supporting declarations
export interface AuthorizeQueryInput<User> {
user: User;
name: string;
args: unknown;
context: ApiContext<User>;
}Authorizer
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:217 · Supporting declarations
An optional request gate. Return false or throw to deny; true and undefined allow.
This gate does not add row predicates or replace access checks inside a mutator.
export type Authorizer<T> = (input: T) => MaybePromise<boolean | void>;AuthorizeStreamInput
InterfaceDeclaration · Source: packages/api-server/src/streams.ts:169 · Supporting declarations
export interface AuthorizeStreamInput<User> {
user: User;
streamId: string;
/** Where the subscriber claims to be. */
from: number;
/** The `meta` this stream was opened with — `undefined` when the stream is not hosted here, which
* is precisely when the app must decide from `streamId` and its own durable state. */
meta: unknown;
request?: unknown;
}BackendError
ClassDeclaration · Source: packages/api-server/src/index.ts:1371 · Supporting declarations
Thrown (wrapping the driver error) by a server tx's DB calls, so the seam can tell an INFRA failure (retry) from a mutator-body throw (business rejection).
export declare class BackendError extends Error {
readonly driverError: unknown;
constructor(driverError: unknown);
}buildRenderIndex
FunctionDeclaration · Source: packages/api-server/src/index.ts:1252 · Supporting declarations
Build the {@link RenderIndex} from a typed schema (schema.tables[name] is a TableMeta).
export declare function buildRenderIndex(schema: Schema): RenderIndex;createRindleApiServer
FunctionDeclaration · Source: packages/api-server/src/index.ts:2783 · Supporting declarations
export declare function createRindleApiServer<User = unknown>(options: RindleApiServerOptions<User>): RindleApiServer<User>;daemonBackend
FunctionDeclaration · Source: packages/api-server/src/index.ts:2165 · Supporting declarations
Legacy/private-plane adapter. Kept for existing deployments; its mutation policy is shared with {@link sqlBackend}, so the two transports cannot drift.
export declare function daemonBackend(daemon: RindleDaemonClient): MutationBackend;DEFAULT_RINDLE_API_ROUTES
VariableDeclaration · Source: packages/api-server/src/index.ts:163 · Supporting declarations
export declare const DEFAULT_RINDLE_API_ROUTES: {
readonly query: "/api/rindle/query";
readonly read: "/api/rindle/read";
readonly mutate: "/api/rindle/mutate";
readonly applyRowChangeTxn: "/api/rindle/apply-row-change-txn";
readonly claimRoomEpoch: "/api/rindle/claim-room-epoch";
readonly roomLmids: "/api/rindle/room-lmids";
readonly roomBoot: "/api/rindle/room-boot";
readonly stream: "/api/rindle/stream";
};defineApiMutators
FunctionDeclaration · Source: packages/api-server/src/index.ts:2400 · Supporting declarations
export declare function defineApiMutators<User, M extends ApiMutators<User>>(mutators: M): M;defineApiQueries
FunctionDeclaration · Source: packages/api-server/src/index.ts:2363 · Supporting declarations
export declare function defineApiQueries<User, Q extends ApiQueries<User>>(queries: Q): Q;dumpQueryShapes
FunctionDeclaration · Source: packages/api-server/src/index.ts:2507 · Supporting declarations
Dump every registered named query's wire AST — feeder 1 ("exemplar enumeration") of
rindle indices suggest (docs/INDEXING.md applied mechanically to the query set).
Because named queries are FUNCTIONS of (args, ctx), one query can build structurally
different ASTs on different args; each exemplar invocation contributes its shape, and shapes
that differ only in literal values (a limit, a filter string) dedupe to one entry. A query
with no configured exemplars is invoked once with no args. The registry is the app's whole
server-side query surface, so the resulting document is the complete static shape set —
modulo arg-value-dependent branches, which need an exemplar (or runtime shape recording) to
surface.
export declare function dumpQueryShapes<User>(opts: {
schema: Schema;
queries: ApiQueries<User>;
exemplars?: Partial<Record<string, ReadonlyArray<ShapeExemplar<User>>>>;
}): Promise<QueryShapesDoc>;frameResumePoint
FunctionDeclaration · Source: packages/client/src/stream.ts:51 · Supporting declarations
The resume point a frame implies — what rides an SSE id: line so a reconnecting EventSource
hands it straight back as Last-Event-ID. undefined for frames that are not a position.
export declare function frameResumePoint(frame: StreamFrame): number | undefined;guardMutator
FunctionDeclaration · Source: packages/api-server/src/index.ts:2463 · Supporting declarations
Wrap a SHARED (generator) mutator with a row-level ACCESS GUARD — the multi-tenant authz twin of
{@link sharedApiMutators}. It parses the untrusted wire args, derives the {@link MutatorCtx}
principal (the SAME mapping you pass to sharedApiMutators), evaluates predicate against the OPEN
mutation txn (so it can READ the rows the write depends on), and throws forbidden (403 — the
client's optimistic write snaps back) when access is denied; otherwise it drives the SAME body the
client predicts ({@link runSharedMutation}). Use it for the entries that need server-only authority
the client cannot predict, OVERRIDING the auto-wrapped default (spread sharedApiMutators(...)
first, then the guarded overrides win by key):
const principal = (ctx) => ({ user: requireUser(ctx.user) });
mutators: defineApiMutators({
...sharedApiMutators(sharedMutators, principal),
updateSlide: guardMutator(sharedMutators.updateSlide, principal,
async (tx, a, { user }) =>
(await tx.query(q.slide.where.id(a.slideId).where(editableBy(user)).one())) != null,
{ message: "not permitted to edit this slide" }),
}),
The predicate keeps the shared body READ-FREE, so the client's .folded hot paths (drag/keystroke)
still fold — the read is server-side only. Return false to deny (→ the default or opts.message
forbidden); return true/nothing to allow. To reject with a different status/message (a business
rejection, a not-found), throw a {@link RindleApiError} from inside the predicate instead. principal
runs before the predicate, so it too may throw forbidden for an anonymous caller.
export declare function guardMutator<User, Args>(gen: SharedMutatorWithArgs<Args>, principal: (ctx: MutationContext<User>) => MutatorCtx, predicate: (tx: ServerMutationTx, args: Args, ctx: MutatorCtx) => boolean | void | Promise<boolean | void>, opts?: {
message?: string;
}): ApiMutator<User, unknown>;isoTx
VariableDeclaration · Source: packages/client/src/mutation-ops.ts:127 · Supporting declarations
The one shared effect factory (stateless — see {@link IsoTx}). Its methods just BUILD a
{@link MutationOp}, so the single instance serves every schema; the generic {@link IsoTx} view is
applied at the authoring site (a json<T> cell is a parsed object here and is stringified by the
funnels, {@link toCell }), hence the cast — the runtime shape is schema-agnostic.
export declare const isoTx: IsoTx;IsoTx
InterfaceDeclaration · Source: packages/client/src/mutation-ops.ts:102 · Supporting declarations
The tier-AGNOSTIC effect factory a generator mutator writes against. Every method just BUILDS an
effect to yield — it performs no I/O and holds no state, so the single {@link isoTx} instance is
shared by every mutator on both tiers; only the driver differs. insert/upsert/insertIgnore
require non-nullable columns and permit nullable omissions, which become null.
update/delete require the PK columns; update also names the columns to change.
export interface IsoTx<S extends ColsMap = ColsMap, P extends Record<string, string> = PkMap<S>> {
/** Insert a row. Omitted nullable columns become `null`; database defaults are not applied. */
insert<N extends keyof S & string>(table: N, row: InsertOf<S[N]>): MutationOp;
/** Update the row identified by its pk columns (REQUIRED); only the named non-pk columns change. */
update<N extends keyof S & string>(table: N, row: UpdateOf<S[N], PkColsOf<S, P, N>>): MutationOp;
/** Insert, or replace non-PK columns on PK conflict, with {@link IsoTx.insert}'s omission rules. */
upsert<N extends keyof S & string>(table: N, row: InsertOf<S[N]>): MutationOp;
/** Insert with {@link IsoTx.insert}'s omission rules, or do nothing on PK conflict. */
insertIgnore<N extends keyof S & string>(table: N, row: InsertOf<S[N]>): MutationOp;
/** Delete the row identified by its pk columns. */
delete<N extends keyof S & string>(table: N, pk: PkOf<S[N], PkColsOf<S, P, N>>): MutationOp;
/** Read one row by primary key (read-your-writes). */
row<N extends keyof S & string>(table: N, pk: PkOf<S[N], PkColsOf<S, P, N>>): ReadEffect;
/** Run a full query (`where`/`orderBy`/`limit`/join) over the state this mutator is mutating —
* read-your-writes, like {@link row} but for arbitrary shapes. Pass a query from the tier-agnostic
* builder, e.g. `tx.query(q.issue.where("ownerId", "=", ctx.user))` where `q = newQueryBuilder(schema)`.
* The `yield` evaluates to {@link QueryResultRow}`[]` (cast it — the generator's single next-type is a row). */
query(query: QueryArg): QueryEffect;
all(effects: readonly YieldEffect[]): BatchEffect;
}MaybePromise
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:179 · Supporting declarations
export type MaybePromise<T> = T | PromiseLike<T>;mintRoomFlushCredential
FunctionDeclaration · Source: packages/api-server/src/index.ts:840 · Supporting declarations
Sign the default epoch-bound flush credential (/room-boot mints one per placement).
export declare function mintRoomFlushCredential(opts: {
shellSecret: string;
doc: string;
epoch: number;
/** Mint time; defaults to `Date.now()`. Injectable for tests. */
now?: number;
}): Promise<string>;MutationBackend
InterfaceDeclaration · Source: packages/api-server/src/index.ts:587 · Supporting declarations
Where a mutation runs and who stamps lmid — the seam that makes the mutator authoring surface
backend-agnostic (BYO-POSTGRES-LMID-CONTRACT-DESIGN.md §6; MUTATORS-ISOMORPHIC plan). Three
implementations ship: {@link sqlBackend} is the preferred managed-SQL path; {@link daemonBackend}
keeps the private control-plane compatibility path; and {@link postgresBackend} runs a real
interactive PG transaction with confirmation riding the CDC loop down.
The load-bearing invariant is that a mutation ALWAYS advances the client's last_mutation_id:
runMutationruns the mutator inside the backend's transaction; on success it advanceslmidtoenvelope.midin the SAME atomic unit (OPTIMISTIC-WRITES-DESIGN.md §8.2); on a BUSINESS rejection it rolls the data back but STILL advanceslmid(§2.4 — else the client's pending queue never drains and the optimistic stack wedges).rejectis the pre-flight path (unknown mutator, failed authorization): NO data,lmidalone.- An infrastructure failure THROWS from
runMutation— never a user rejection (the client retries; mid dedup absorbs any applied prefix).
export interface MutationBackend {
/** The SQL dialect this backend renders logical ops to (drives placeholder style). */
readonly dialect: SqlDialect;
/** Optional raw-SQL surface outside the mutation transaction. Built-in backends provide it;
* custom backends may omit it, in which case `scope.sql` fails as an infrastructure error. */
readonly outsideSql?: ServerSql;
runMutation(input: MutationRunInput): Promise<MutationOutcome>;
reject(input: {
envelope: MutationEnvelope;
reason: string;
}): Promise<unknown>;
}MutationContext
InterfaceDeclaration · Source: packages/api-server/src/index.ts:219 · Supporting declarations
export interface MutationContext<User> {
user: User;
envelope: MutationEnvelope;
daemon: RindleDaemonClient;
request?: unknown;
}MutationGen
TypeAliasDeclaration · Source: packages/client/src/mutation-ops.ts:148 · Supporting declarations
What a generator mutator IS: yield tx.<op>() on every side effect; a yield tx.row() expression
evaluates to the row. Neither sync nor async — the tier's driver decides, which is what lets one
body run synchronously on the client and against a live async transaction on the server.
export type MutationGen = Generator<YieldEffect, void, KeyedRow | undefined>;MutationOutcome
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:567 · Supporting declarations
The result of {@link MutationBackend.runMutation}: either the data+lmid committed together, or a
business rejection whose data was rolled back but whose lmid still advanced (§2.4).
export type MutationOutcome = {
accepted: true;
output: SqlTxnOutput;
} | {
accepted: false;
reason: string;
output?: unknown;
};MutationRejected
ClassDeclaration · Source: packages/api-server/src/index.ts:297 · Supporting declarations
Thrown by {@link MutationScope.transact} when the transacted body BUSINESS-rejects: the data
rolled back and lmid advanced alone (§2.4). Catch it to COMPENSATE an outside-tx side effect
(refund the charge), then rethrow or return — the mutation's protocol outcome is already sealed
as rejected, so a post-reject throw can't change it. A DB/infra failure is NOT this — it
propagates as the raw driver error (the client retries; lmid did not advance).
export declare class MutationRejected extends Error {
readonly reason: string;
constructor(reason: string);
}MutationRunInput
InterfaceDeclaration · Source: packages/api-server/src/index.ts:554 · Supporting declarations
The context a {@link MutationBackend} needs to run one mutation inside its transaction.
export interface MutationRunInput {
envelope: MutationEnvelope;
/** Schema-derived render metadata (from {@link RindleApiServerOptions.schema}). A logical op on a
* table absent here throws loudly — never a silent dropped write. `{}` when no schema is set. */
render: RenderIndex;
/** Invoke the (authorized) mutator against the backend-provided tx. A THROW that is NOT a
* {@link BackendError} is a BUSINESS rejection (roll the data back, then advance `lmid` alone,
* §2.4); a {@link BackendError} (a DB-layer failure) is INFRA and rejects the returned promise. */
run(tx: ServerMutationTx): Promise<void>;
}MutationScope
InterfaceDeclaration · Source: packages/api-server/src/index.ts:312 · Supporting declarations
The per-mutation server handle a {@link ScopedMutator} runs against. Code before {@link transact}
runs OUTSIDE the transaction; code after a clean transact runs AFTER the commit. The
lmid-always-advances invariant is the HARNESS's, not the author's: {@link RindleApiServer.pushMutation}
seals the response from this handle's recorded outcome, so an early return, a never-called
transact, or a swallowed {@link MutationRejected} still advances lmid and never wedges the
client's pending queue.
export interface MutationScope {
/** Raw SQL OUTSIDE the mutation transaction. Every call commits independently and therefore may
* be observed even if {@link transact} later rejects or fails. Calls may also repeat when an
* envelope is retried, so outside writes need their own idempotency key/unique constraint. */
readonly sql: ServerSql;
/** Open the ONE atomic write transaction and drive `body` inside it, committing (stamping `lmid`
* co-transactionally) on a clean return. MAY be called at most once — a second call throws.
*
* Two forms:
* - `transact(sharedMutator, args, ctx)` — drive a SHARED (generator) mutator (the same body the
* client predicts); pass the already-parsed `args` and the server `ctx` (fold server-only
* values like a charge id into `ctx` here).
* - `transact(run)` — a raw callback receiving the live {@link ServerMutationTx} (the escape
* hatch: `tx.exec`, logical writes, read-your-writes reads).
*
* A THROW from the body that is not a {@link BackendError} is a BUSINESS rejection: the data rolls
* back, `lmid` advances alone, and this method throws {@link MutationRejected} (so surrounding
* code can compensate). A {@link BackendError} is INFRA: it propagates (the client retries).
*
* **The callback form RETURNS ITS BODY'S VALUE**, and only on the committed path — a rejection
* throws, so there is never a value to act on for a transaction that rolled back. That is what
* lets a post-commit effect be decided by a TRANSACTIONAL read instead of a second, racy one
* afterwards:
*
* ```ts
* const kick = await scope.transact(async (tx) => {
* const prior = await tx.row("message", { id: a.assistantMessageId });
* if (prior) return undefined; // a replayed envelope — do NOT re-fire the effect
* tx.insert("message", …);
* return { streamId: a.assistantMessageId, history: await readHistory(tx, a.chatId) };
* });
* if (kick) void startGeneration(kick); // committed, and decided against committed state
* ```
*
* A tx-form mutator cannot do this: its return value is already the logical-write channel
* ({@link ApiMutatorResult}). A post-commit effect chosen by a transactional read is exactly what
* the scoped form is for. */
transact<T>(run: (tx: ServerMutationTx) => T | Promise<T>): Promise<T>;
transact<A, C extends MutatorCtx>(mutator: SharedMutator<A, C>, args: A, ctx: C): Promise<void>;
}MutatorCtx
InterfaceDeclaration · Source: packages/client/src/mutation-ops.ts:141 · Supporting declarations
The minimal per-invocation context a shared mutator sees on BOTH tiers: the acting principal. The
server injects its AUTHENTICATED user; the client injects its local user. (The server's own
MutationContext is a superset of this.)
export interface MutatorCtx {
user: string;
}OpenStreamInput
InterfaceDeclaration · Source: packages/api-server/src/streams.ts:260 · Supporting declarations
export interface OpenStreamInput<User> {
user: User;
/** The app's message-row id: the durable pointer AND the live plane's key. The row must already
* exist (the app's own mutator wrote it, alongside the user's prompt) with `seq = 0`. */
streamId: string;
/** Opaque app payload, passed through to a `commit` callback. Unused in `tables` mode. */
meta?: unknown;
request?: unknown;
}PgPoolLike
InterfaceDeclaration · Source: packages/api-server/src/index.ts:2278 · Supporting declarations
The slice of a node-postgres Pool the plugger needs — structural, so pg stays a
dependency of the APP, never of this package.
export interface PgPoolLike {
connect(): Promise<{
query(sql: string, params?: unknown[]): Promise<{
rows: Array<Record<string, unknown>>;
}>;
release(err?: unknown): void;
}>;
}pgPoolPlugger
FunctionDeclaration · Source: packages/api-server/src/index.ts:2287 · Supporting declarations
Adapt a node-postgres Pool (or anything pool-shaped) to a {@link PostgresPlugger}:
one client per transaction, BEGIN/COMMIT bracketing, ROLLBACK + rethrow on failure.
export declare function pgPoolPlugger(pool: PgPoolLike): PostgresPlugger;PgQuery
InterfaceDeclaration · Source: packages/api-server/src/index.ts:2178 · Supporting declarations
The query surface a {@link PostgresPlugger} transaction exposes. exec runs one statement;
query returns rows keyed by column name (read-your-own-writes inside the txn).
export interface PgQuery {
exec(sql: string, params?: unknown[]): Promise<void>;
query(sql: string, params?: unknown[]): Promise<Array<Record<string, unknown>>>;
}PinFanout
InterfaceDeclaration · Source: packages/api-server/src/index.ts:643 · Supporting declarations
The explicit fleet pin fan-out seam (READ-ROUTER-DESIGN.md §4.2). The api-server resolves each
pin's authoritative AST under pinUser and hands the ready {@link MaterializeInput}s here; the
implementation (the read router) fans EACH across all live followers. Distinct, on purpose, from
a per-viewer materialize (which routes ONE) — a pin-assert always sprays ALL.
export interface PinFanout {
assertPins(pins: readonly MaterializeInput[]): Promise<void>;
}PinnedQuery
InterfaceDeclaration · Source: packages/api-server/src/index.ts:634 · Supporting declarations
A named query to keep permanently materialized (warm with zero subscribers).
export interface PinnedQuery {
name: string;
args?: unknown;
}postgresBackend
FunctionDeclaration · Source: packages/api-server/src/index.ts:2216 · Supporting declarations
The BYO-Postgres {@link MutationBackend} (BYO-POSTGRES-LMID-CONTRACT-DESIGN.md §6.3): one PG
transaction runs the mutator's statements and ALWAYS upserts _rindle_client_mutations —
the upsert sits outside any acceptance guard by construction, so the §2.4 footgun (a rejection
that forgets to advance lmid and wedges the client's pending queue) cannot be written.
Confirmation does NOT come from this call's response: the lmid row rides the same PG commit
through CDC → relay → follower and reaches the client in the same coherent release as the
data (§8.2 relocated upstream). A rejection's reason still returns on the HTTP reply, but
nothing rejection-shaped travels the replication path — the optimistic prediction snaps back
when the advanced lmid arrives.
export declare function postgresBackend(plugger: PostgresPlugger, opts?: PostgresBackendOptions): MutationBackend;PostgresBackendOptions
InterfaceDeclaration · Source: packages/api-server/src/index.ts:2190 · Supporting declarations
export interface PostgresBackendOptions {
/** Rewrite each MUTATOR statement's SQL before it runs (never the lmid upsert). The intended
* use is dialect bridging for a dual-topology app whose mutators are written SQLite-style:
* pass {@link questionToDollarParams} to convert `?` placeholders to `$1..$n`. */
rewriteSql?: (sql: string) => string;
}postgresDialect
VariableDeclaration · Source: packages/api-server/src/index.ts:1233 · Supporting declarations
export declare const postgresDialect: SqlDialect;PostgresPlugger
InterfaceDeclaration · Source: packages/api-server/src/index.ts:2186 · Supporting declarations
The thin driver adapter that keeps pg / postgres.js out of this package's dependencies
(BYO-POSTGRES-LMID-CONTRACT-DESIGN.md §6.2): run fn inside ONE transaction — commit on
resolve, roll back on throw. {@link pgPoolPlugger} adapts a node-postgres Pool.
export interface PostgresPlugger {
transaction<T>(fn: (q: PgQuery) => Promise<T>): Promise<T>;
}PushMutationRequest
InterfaceDeclaration · Source: packages/api-server/src/index.ts:597 · Supporting declarations
export interface PushMutationRequest<User> {
user: User;
envelope: MutationEnvelope;
request?: unknown;
}PushMutationResponse
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:609 · Supporting declarations
export type PushMutationResponse = {
accepted: true;
rejected: false;
output: SqlTxnOutput;
} | {
accepted: false;
rejected: true;
reason: string;
output?: unknown;
};PushMutationsRequest
InterfaceDeclaration · Source: packages/api-server/src/index.ts:603 · Supporting declarations
export interface PushMutationsRequest<User> {
user: User;
envelopes: MutationEnvelope[];
request?: unknown;
}QueryLeaseLifecycle
InterfaceDeclaration · Source: packages/api-server/src/index.ts:484 · Supporting declarations
The §4 lifecycle block on a query lease (Slice I-iii): present only when BOTH the realtime
lifecycle config is on AND the query is realtime-labeled. doorbell rides EVERY labeled
lease (occupancy is counted whether or not the query is room-served — the 1→2 upgrade trigger
needs solo watchers subscribed BEFORE any room exists, §4.1); fence rides only a ROOM-SERVED
lease (the §4.2/§7.1/§3.3 downgrade surfaces are meaningful only where a room domain exists).
The §4.2 fence VALUE (finalFlushSeq) is deliberately NOT here — it arrives with the I-v
downgrade response; I-iii only stands up the streams.
export interface QueryLeaseLifecycle {
doorbell: QueryLeaseLifecycleLease;
fence?: QueryLeaseLifecycleLease[];
}QueryLeaseLifecycleLease
InterfaceDeclaration · Source: packages/api-server/src/index.ts:463 · Supporting declarations
One minted SYSTEM-STREAM lease on a query lease's lifecycle block (RINDLE-REALTIME-QUERY-
ENABLEMENT §4, Slice I-iii): an ordinary daemon materialization over one of the four
_rindle_* lifecycle system tables (registered by the daemon's enable_realtime_lifecycle —
rust/rindle-replica/src/mutations.rs), attachable by the EXISTING client subscribe path
(present leaseToken on a subscribe frame, exactly like the primary lease). The identity
fields (scope/doc/clientId) document the minted AST's predicate — the client keys its
retains and its release-time row filters on them.
export interface QueryLeaseLifecycleLease {
/** Which system table this lease's subscription serves. */
table: string;
leaseToken: string;
/** DOORBELL only: the §4.1 occupancy scope — the wire room doc (`"<profile>/<key>"`). */
scope?: string;
/** FENCE entries only: the room doc the predicate is scoped to. */
doc?: string;
/** FENCE ledger/outcome entries: present iff the predicate was ALSO client-scoped (the lease
* request carried `clientId`). Absent ⇒ doc-only predicate — the client filters to its own
* rows regardless (defense in depth). */
clientId?: string;
}QueryLeaseRealtime
InterfaceDeclaration · Source: packages/api-server/src/index.ts:430 · Supporting declarations
The room-serve block on a query lease (RINDLE-REALTIME-QUERY-ENABLEMENT §2.1 step 5 / §2.4, slice G-iv-b): present when the named query carries a realtime label naming a configured room profile (302 §5 — declared, not derived; no coverage proof). The G-v client uses it to open the room transport for THIS query beside — never instead of — its daemon session.
It is a dedicated block on purpose: the ROOM ws is a SEPARATE connection this query opens beside its daemon session (never a migration of the daemon session — the daemon ws host is fixed and placed by the affinity ticket). A room-served lease's top-level fields are byte-identical to the daemon-served ones.
export interface QueryLeaseRealtime {
/** The client store's gate/domain key for this room source (`connectSource`) AND the string the
* wasm engine's `parse_source_key` accepts: any string other than the reserved `"daemon"`
* parses as a room source, and the established convention is `"room:" + doc`
* (e.g. `room:document/doc:d1`). */
sourceKey: string;
/** Where the client opens the ROOM ws for this query (from `realtime.locateRoom`) — its OWN
* connection, distinct from the daemon session's fixed ws host. */
wsEndpoint: string;
/** The room's self-authorizing signed lease (`@rindle/room/token`): the APPROVED query AST +
* doc + subject, HMAC-signed with `realtime.roomTokenKey` so the room shell's
* `downstream.tokenKeys` ring verifies it. The room materializes on first presentation. */
roomToken: string;
/** Token expiry (ms epoch) — the client's renewal clock (renewal = a fresh lease). */
exp: number;
/** The wire room doc (`"<profile>/<key>"`, minted server-side — never client-derived). */
doc: string;
/** Per-footprint-table specs: the §2.2 owned/followed split + §3.2 routing metadata. Since
* H-iii each spec also carries `footprintWhere` — the same exact membership predicate the boot
* wire ships the room gate (one compiler, `compileRoomScopeSpecs`) — feeding the client's §3
* prove-or-slow-path router. Advisory routing metadata, never a credential (§3.2). */
tables: RoomTableSpec[];
}QueryLeaseRealtimeFence
InterfaceDeclaration · Source: packages/api-server/src/index.ts:495 · Supporting declarations
The §4.2 downgrade fence on a query lease (Slice I-v): rides a labeled reply whose §4.1
occupancy gate CLOSED (so there is NO realtime block) when the server could drain the room.
A SIBLING of realtime, never nested inside it — block-ABSENCE is the downgrade signal, and
the fence rides alongside that absence. Its finalFlushSeq is the room's last COMMITTED flush
seq; the client's frozen room ghost holds visible until the daemon plane has provably absorbed
it (_rindle_room_watermark(doc) ≥ finalFlushSeq). Absent from every non-downgrade reply.
export interface QueryLeaseRealtimeFence {
/** The retiring room source's gate/domain key — `"room:" + doc`, matching what the room-served
* lease's {@link QueryLeaseRealtime.sourceKey} carried. */
sourceKey: string;
doc: string;
finalFlushSeq: number;
}QueryLeaseRequest
InterfaceDeclaration · Source: packages/api-server/src/index.ts:403 · Supporting declarations
export interface QueryLeaseRequest<User> {
user: User;
name: string;
args: unknown;
request?: unknown;
/** The browser's stable `clientId` (sent on the query POST) — the anonymous routing-key fallback
* when there is no authenticated subject and no session cookie (READ-ROUTER-DESIGN.md §1.5/§2.2).
* A routing HINT only, never authorization. */
clientId?: string;
/** The browser's opaque follower-affinity ticket (FOLLOWER-AFFINITY-DESIGN.md §3), read off the
* query POST and forwarded opaquely on `materialize` so the fleet edge selects the follower
* the browser's ws is pinned to (§2, §4) — both legs co-locate. The api-server does NOT verify
* it (the fleet does); it holds no signing key. Absent ⇒ single daemon / affinity off. */
affinity?: string;
}QueryLeaseResponse
InterfaceDeclaration · Source: packages/api-server/src/index.ts:503 · Supporting declarations
export interface QueryLeaseResponse {
leaseToken: string;
materializationId: string;
queryKey?: string;
reused?: boolean;
/** The public daemon/fleet WebSocket endpoint. With the unified `rindle` connection this is
* derived from the same ingress URL (or `rindle.wsUrl`) so a browser can open its transport from
* this lease and needs no application-authored runtime-config route. */
wsEndpoint?: string;
/** Fresh opaque follower-placement ticket minted with this follower-local lease. The optimistic
* client offers it on the WebSocket opened at {@link wsEndpoint}, pinning both legs to the same
* follower even though the first connection is created only after this response. */
affinity?: string;
/** The room-serve block (G-iv-b) — see {@link QueryLeaseRealtime}. Absent ⇒ the lease is
* byte-identical to the legacy daemon-served shape. */
realtime?: QueryLeaseRealtime;
/** The §4.2 downgrade fence (Slice I-v) — see {@link QueryLeaseRealtimeFence}. Present only on a
* labeled reply whose occupancy gate closed AND `realtime.lifecycle.drainRoom` could drain the
* room; absent otherwise (including on every room-served reply). */
realtimeFence?: QueryLeaseRealtimeFence;
/** The §4 lifecycle system-stream block (Slice I-iii) — see {@link QueryLeaseLifecycle}.
* Minted ONLY under the opt-in `realtime.lifecycle` config; absent ⇒ byte-identical to the
* pre-lifecycle response. */
lifecycle?: QueryLeaseLifecycle;
}QueryReadRequest
InterfaceDeclaration · Source: packages/api-server/src/index.ts:531 · Supporting declarations
A one-shot SSR read of a named query (SSR-DESIGN.md §6): same (name, args) surface as a lease,
but the daemon serializes the current view ONCE and registers no subscriber.
export interface QueryReadRequest<User> {
user: User;
name: string;
args: unknown;
request?: unknown;
/** The browser's stable `clientId` — the anonymous routing-key fallback (see
* {@link QueryLeaseRequest.clientId}). Lets the SSR read co-locate on the follower the booting
* client's first subscribe will hit (READ-ROUTER-DESIGN.md §2.4). */
clientId?: string;
/** The browser's opaque follower-affinity ticket — see {@link QueryLeaseRequest.affinity}.
* Forwarded on the one-shot `query` so an SSR read lands on the same pinned follower. */
affinity?: string;
}QueryReadResponse
InterfaceDeclaration · Source: packages/api-server/src/index.ts:547 · Supporting declarations
The assembled (nested-by-name) first-paint snapshot the server-side Store seeds + dehydrates
(SSR-DESIGN.md §3.3). rows hydrate without an engine; cvMin is their watermark baseline.
export interface QueryReadResponse {
rows: Array<{
cols: Record<string, unknown>;
[rel: string]: unknown;
}>;
cvMin?: number;
queryKey?: string;
}queryRealtimeLabel
FunctionDeclaration · Source: packages/api-server/src/rooms.ts:548 · Supporting declarations
The realtime label a registered query carries (stamped by registerQueries from its
defineQuery options), or undefined for an unlabeled query. The lease path uses this to
derive (room profile, args mapping) for a named query.
export declare function queryRealtimeLabel(query: ApiQuery<any, any> | undefined): RealtimeQueryLabel | undefined;queryResultToAst
FunctionDeclaration · Source: packages/api-server/src/rooms.ts:42 · Supporting declarations
export declare function queryResultToAst(result: ApiQueryResult): Ast;QueryShapesDoc
InterfaceDeclaration · Source: packages/api-server/src/index.ts:2490 · Supporting declarations
The query-shapes document rindle indices suggest consumes: the app's synced tables (name +
primary key) and one wire AST per structurally distinct shape a registered query can build.
export interface QueryShapesDoc {
tables: Array<{
name: string;
primaryKey: string[];
}>;
queries: Array<{
name: string;
ast: Ast;
}>;
}questionToDollarParams
FunctionDeclaration · Source: packages/api-server/src/index.ts:2322 · Supporting declarations
Rewrite SQLite-style ? positional placeholders to Postgres $1..$n, for mutators written
once and run against either backend (pass as {@link PostgresBackendOptions.rewriteSql}).
Skips '…' string literals (with '' escapes), "…" quoted identifiers, -- line comments,
and non-nested C-style block comments. Do not mix ? and $n styles in one statement.
export declare function questionToDollarParams(sql: string): string;RealtimeQueryLabel
InterfaceDeclaration · Source: packages/client/src/query.ts:372 · Supporting declarations
The realtime LABEL a named query may declare (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §2.1):
which api-server room PROFILE the query wants to be served from, and how the query's args map
to that profile's key args. This is the DECLARATION only — the serve decision (covering proof,
lease routing) is the server's, and the final room key is minted server-side by the profile's
own key under authoritative ctx, so a label can never place a query in a room the server
didn't derive itself. Both tiers import the SAME defineQuery value, so they always agree
which profile a query belongs to.
export interface RealtimeQueryLabel<Args = any> {
/** The room profile name — must match a `realtime.rooms` key on the api-server (validated
* loudly at `createRindleApiServer` construction). May not contain `/` (the wire room-key
* delimiter). */
readonly room: string;
/** Map the query's VALIDATED args to the profile's key args (what the server feeds the
* profile's `key(args)`). Identity when omitted. Must be pure — both tiers may run it. */
readonly args?: (queryArgs: Args) => unknown;
}registerQueries
FunctionDeclaration · Source: packages/api-server/src/index.ts:2385 · Supporting declarations
Register a list of co-located client {@link NamedQuerydefineQuery} values as the server's query
surface — the bulk, no-boilerplate counterpart to {@link defineApiQueries}. Each query already
carries its wire name and a resolve that re-runs its validator on the UNTRUSTED wire args and
builds the authoritative Query, so the server just imports every co-located query and hands the
list here. The same validated args build a byte-identical AST on both tiers.
The query's AUTHORITATIVE {@link ApiContext} is forwarded as resolve's ctx — so a context-scoped
defineQuery (e.g. "my issues") is built from the server's trusted principal, never the client's.
A context-free query simply ignores the extra argument.
Use {@link defineApiQueries} instead (or in addition) only when the server must DIVERGE from the
client — define a server-specific defineQuery with the same name and register that.
queries: registerQueries<User>([issuesPageQuery, issueDetailQuery, recentCommentsQuery, usersQuery]),
export declare function registerQueries<User>(queries: readonly NamedQuery<any, any, any>[]): ApiQueries<User>;RenderIndex
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:1249 · Supporting declarations
export type RenderIndex = Record<string, TableRenderMeta>;renderOp
FunctionDeclaration · Source: packages/api-server/src/index.ts:1302 · Supporting declarations
Render one {@link MutationOp} to a {sql, params} for the dialect, or null for a no-op (an
update whose row names only pk columns — nothing to SET, matching the client's no-op edit).
export declare function renderOp(op: MutationOp, meta: TableRenderMeta, dialect: SqlDialect): SqlStatement | null;renderPointRead
FunctionDeclaration · Source: packages/api-server/src/index.ts:1348 · Supporting declarations
Render a point read (tx.row) — SELECT <cols> FROM "T" WHERE <pk> — for read-your-writes.
export declare function renderPointRead(table: string, pk: KeyedRow, meta: TableRenderMeta, dialect: SqlDialect): SqlStatement;RindleApiError
ClassDeclaration · Source: packages/api-server/src/index.ts:1108 · Supporting declarations
export declare class RindleApiError extends Error {
readonly code: RindleApiErrorCode;
readonly status: number;
constructor(code: RindleApiErrorCode, message: string, status: number);
}RindleApiErrorCode
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:1106 · Supporting declarations
export type RindleApiErrorCode = "bad-request" | "forbidden" | "not-found" | "rejected";RindleApiRoutes
InterfaceDeclaration · Source: packages/api-server/src/index.ts:613 · Supporting declarations
export interface RindleApiRoutes {
query: string;
read: string;
mutate: string;
applyRowChangeTxn: string;
claimRoomEpoch: string;
roomLmids: string;
roomBoot: string;
stream: string;
}RindleApiServer
InterfaceDeclaration · Source: packages/api-server/src/index.ts:1029 · Supporting declarations
export interface RindleApiServer<User> {
readonly routes: RindleApiRoutes;
/** Close the SQL client created from {@link RindleApiServerOptions.database}, and drop every live
* stream's readers and timers WITHOUT a durable write (that is {@link drainStreams}). Injected
* SQL sessions and custom backends remain caller-owned. Idempotent. */
close(): void;
/** Open an LM stream (LM-STREAM-CHECKPOINT §2): commits the durable POINTER row, then hands back
* the producer handle. A resolved handle means the message already exists for every client's
* query — so a subscriber that arrives before the first token has something to attach to.
* Throws 403 unless {@link RindleApiServerOptions.streams} is configured. */
openStream(input: OpenStreamInput<User>): Promise<StreamHandle>;
/** Attach a reader at `from` — the same call serves a first-touch subscriber (`from: 0`), a late
* joiner (`from` = the seq its IVM view shows), and a reconnect (`from` = `Last-Event-ID`).
* Terminates with `end`, or with `stale`/`absent` when the client should fall back to the
* durable plane (both are ordinary answers, never errors). */
subscribeStream(input: SubscribeStreamInput<User>): Promise<StreamSubscription>;
/** Parse a default `{streamId, from?}` subscribe body and run {@link subscribeStream}. For the
* GET + `EventSource` shape, use {@link streamResponse} (or build the body with
* `streamRequestFromHttp(request)` yourself). */
handleStreamJson(body: unknown, context: ApiContext<User>): Promise<StreamSubscription>;
/** The subscribe route in ONE call: parse a GET (`?streamId=…&from=…`, with `Last-Event-ID`
* winning), authorize + subscribe, and encode the SSE response. A refusal comes back as a JSON
* error `Response` (403 for denied or unconfigured) rather than a throw, so the route body is a
* single expression after authentication. For custom transports, compose
* `streamRequestFromHttp` + {@link subscribeStream} + `streamFramesToSse` instead. */
streamResponse(request: {
url: string;
headers: {
get(name: string): string | null;
};
}, context: ApiContext<User> & {
keepAliveMs?: number;
}): Promise<Response>;
/** Checkpoint every live stream's outstanding tail, then seal it `interrupted`
* (LM-STREAM-CHECKPOINT §5). Wire it to SIGTERM: without it a rolling deploy drops each
* response's un-checkpointed tail and strands rows saying `streaming` forever. */
drainStreams(): Promise<void>;
createQueryLease(input: QueryLeaseRequest<User>): Promise<QueryLeaseResponse>;
/** (Re-)materialize every `pinnedQueries` entry with a pinned policy. Idempotent — the daemon
* dedupes by canonical query, so a re-assert reuses the existing materialization. Call it at
* startup and whenever the daemon restarts (e.g. from the daemon-client `onBootId` hook), since
* the daemon holds no durable materialization state. No-op when `pinnedQueries` is empty. */
assertPins(): Promise<void>;
pushMutation(input: PushMutationRequest<User>): Promise<PushMutationResponse>;
/** Apply an in-order batch (the client mutation queue's flush). Envelopes run strictly
* sequentially; a rejection still advances the daemon's lmid, so later envelopes in the
* batch stay contiguous and keep applying. A daemon ERROR throws for the whole batch —
* the client retries it and the daemon's mid dedup absorbs the already-applied prefix. */
pushMutations(input: PushMutationsRequest<User>): Promise<PushMutationResponse[]>;
/** One-shot SSR read (SSR-DESIGN.md §6): resolve `(name, args)` → AST (same authority path as a
* lease, `authorizeQuery` enforced), have the daemon serialize the current view once, and return
* the assembled rows for the loader to seed + dehydrate. Registers NO subscriber — a dropped
* render leaks nothing; the pipeline self-reclaims after the idle TTL ({@link
* RindleApiServerOptions.readIdleTtlMs}) unless the browser's follow-up `subscribe` lands first. */
readQuery(input: QueryReadRequest<User>): Promise<QueryReadResponse>;
handleQueryJson(body: unknown, context: ApiContext<User>): Promise<QueryLeaseResponse>;
/** Parse a default `{name, args}` read body and run {@link readQuery}. */
handleReadJson(body: unknown, context: ApiContext<User>): Promise<QueryReadResponse>;
/** Accepts `{envelope}` (one) or `{envelopes: [...]}` (an in-order batch → array reply). */
handleMutateJson(body: unknown, context: ApiContext<User>): Promise<PushMutationResponse | PushMutationResponse[]>;
/** The room's flush (§5.3.1): gate on `authorizeRoom`, forward the txn to the write
* authority, and pass the store's verdict through VERBATIM — `200 {applied, cv}`,
* `409 {error:"fenced"|"conflict", …}`, or the loud identity `500`. Write
* `status` + `body` as-is; the room's `httpAuthority` decodes them. */
handleApplyRowChangeTxnJson(body: unknown, context: ApiContext<User>): Promise<RoomHostResponse>;
/** Claim the next placement epoch for a doc (§2.5), same gate + envelope. */
handleClaimRoomEpochJson(body: unknown, context: ApiContext<User>): Promise<RoomHostResponse>;
/** The room's boot probe (§3.3), same gate + envelope. */
handleRoomLmidsJson(body: unknown, context: ApiContext<User>): Promise<RoomHostResponse>;
/** The DO shell's cold-boot callback (§10.1; enablement §3.1): authenticate the shell
* secret, resolve the doc's footprint, claim the placement epoch, mint the upstream
* lease and the flush leg. 403 until {@link RindleApiServerOptions.realtime} is set. */
handleRoomBootJson(body: unknown, context: ApiContext<User>): Promise<RoomHostResponse>;
}RindleApiServerOptions
InterfaceDeclaration · Source: packages/api-server/src/index.ts:930 · Supporting declarations
export interface RindleApiServerOptions<User> {
/** The sync/IVM control-plane client (leases, materialization, rooms). Optional once
* {@link rindle} is configured — the api-server then derives an HTTP client against the single
* ingress. Pass one explicitly for a tokened production fleet, a split/routed deployment, or a
* custom transport; an explicit client wins over the derivation. */
daemon?: RindleDaemonClient;
/** The unified connection (one URL, one key) that derives {@link daemon} and {@link database}
* from the fleet's single ingress. `rindle: {}` resolves both halves from the `rindle dev`
* environment. Any explicitly configured `daemon` / `database` / `sql` / `backend` field takes
* precedence over its derived counterpart. */
rindle?: RindleConnectionOptions;
/** Preferred managed setup. The API server constructs and owns its SQL client; authoritative
* mutators, `tx.sql`, and `scope.sql` use it, while `daemon` remains only the lease/query/
* materialization/room control plane. Mutually exclusive with {@link sql} unless `backend`
* explicitly replaces both. */
database?: RindleDatabaseOptions;
/** Advanced injection/test seam for an already-created SQL session. Most applications should
* configure {@link database} and never import `createSqlClient`. When present (and `backend` is
* absent), authoritative mutators run through {@link sqlBackend}. */
sql?: SqlSession;
/** Where mutations are applied and `lmid` is stamped ({@link MutationBackend}). Default:
* managed `sqlBackend` when `database` or `sql` is configured, otherwise the compatibility
* `daemonBackend(daemon)`. Pass `postgresBackend(...)` when Postgres is the source of truth. */
backend?: MutationBackend;
/** The typed schema (`createSchema`/`refineSchema`). Required only when a mutator uses the LOGICAL
* write vocabulary (`tx.insert`/`update`/`upsert`/`insertIgnore`/`delete`/`row`) — it drives the
* dialect SQL renderer (column order, pk, quoting). A logical op with no schema configured throws
* loudly. Pure raw-`tx.exec` mutators do not need it. */
schema?: Schema;
queries?: ApiQueries<User>;
runQuery?: RunQuery<User>;
mutators?: ApiMutators<User>;
/** Optional gate before resolving a named query, for leases and one-shot reads. If omitted,
* no gate runs. The query definition still owns row visibility predicates. */
authorizeQuery?: Authorizer<AuthorizeQueryInput<User>>;
/** Optional gate before mutation execution. If omitted, no gate runs. Mutators still own
* row-level checks and business rules inside their authoritative transaction. */
authorizeMutation?: Authorizer<AuthorizeMutationInput<User>>;
/** Rindle Realtime (RINDLE-REALTIME-ENABLEMENT-DESIGN.md §3.1): the ONE named opt-in.
* Presence activates the room flush trio AND `/room-boot`; absence keeps every room
* endpoint 403. Takes precedence over the deprecated {@link authorizeRoom}. */
realtime?: RindleRealtimeOptions<User>;
/** The LM stream plane (LM-STREAM-CHECKPOINT-DESIGN.md): the ONE named opt-in for streaming a
* model response live while the durable store only ever sees coarse checkpoints. Presence
* activates {@link RindleApiServer.openStream}/{@link RindleApiServer.subscribeStream} and
* `/stream`; absence keeps them 403. */
streams?: RindleStreamOptions<User>;
/** The room write-authority gate (§5.3.1): validates the caller is a placed room —
* the epoch-bound flush credential rides `context.request`, and what it means is
* the app's to define. The room endpoints are DISABLED (403) until this is set:
* hosting a write authority is an explicit opt-in, never a default.
* @deprecated Use {@link realtime} (its `authorize`) — this bare form gates the
* flush trio but never activates `/room-boot`. When both are set, `realtime` wins. */
authorizeRoom?: Authorizer<ApiContext<User>>;
routes?: Partial<RindleApiRoutes>;
mode?: StreamMode;
materializationPolicy?: MaterializationPolicy | ((input: QueryLeaseRequest<User>) => MaybePromise<MaterializationPolicy>);
leaseTtlMs?: number;
/** Idle TTL (ms) the warm pipeline a one-shot SSR {@link RindleApiServer.readQuery read} leaves
* behind is held at (SSR-DESIGN.md §3.4) — it must comfortably cover page-load + client-boot +
* the follow-up live `subscribe` so the browser lands on a still-warm pipeline (the warm
* handoff). The TTL is NOT part of the dedup key (max-wins), so it only ever extends a shared
* query's window. Absent ⇒ the daemon's default idle TTL. */
readIdleTtlMs?: number;
subject?: string | ((input: QueryLeaseRequest<User>) => MaybePromise<string | undefined>);
/** The ANONYMOUS routing key forwarded to the read router (READ-ROUTER-DESIGN.md §2.2) — used by
* HRW placement when there is no authenticated `subject`. The router keys on `subject ?? this`;
* the resolved value rides `metadata.routingKey` to the daemon. Default: the browser-supplied
* `clientId` (from the query POST body). NOTE: the default cannot co-locate an ANONYMOUS SSR read
* with the booting client — an SSR server can't see the browser's localStorage `clientId`, so the
* two legs compute different keys and §2.4's warm handoff misses (still correct — just an extra
* first-touch materialize). For anonymous SSR co-location, set this to read a server-set session
* cookie from `input.request` (it rides the SSR request AND every browser request). A routing
* HINT only — never authorization. Ignored by a single (unrouted) daemon, which has nothing to
* route. */
routingKey?: string | ((input: QueryLeaseRequest<User>) => MaybePromise<string | undefined>);
/** The EXPLICIT fleet pin fan-out — when set, {@link RindleApiServer.assertPins} fans each
* resolved pin across ALL live followers through it (a fleet control action over the machine
* list — FOLLOWER-AFFINITY-DESIGN.md §11) instead of materializing each pin once on the (single)
* daemon. A per-viewer `materialize` always routes ONE; a pin-assert always fans ALL — never
* inferred from `policy.kind`. Absent ⇒ single-daemon behavior (one materialize per pin). */
pinFanout?: PinFanout;
/** Named queries to keep permanently materialized via {@link RindleApiServer.assertPins}.
* Each is materialized with a `pinned` policy (survives zero subscribers) so late joiners
* attach to an already-warm result. Pins are viewer-independent — resolved with `pinUser`. */
pinnedQueries?: PinnedQuery[];
/** The user context pins resolve under (pins are shared, so they should not depend on a
* per-viewer identity). Defaults to `undefined`. */
pinUser?: User;
/** Surfaced when a SCOPED mutator ({@link scoped}) throws from code that runs AFTER `scope.transact`
* has already sealed the protocol outcome — a post-commit effect, or a compensation handler running
* after a business rejection. The outcome is fixed (this callback CANNOT change the client's
* response or the `lmid` advance), but the throw must not vanish: a failed refund is real money.
* Absent ⇒ the error is logged to `console.error`. */
onScopeError?: (err: unknown, info: {
phase: "committed" | "rejected";
envelope: MutationEnvelope;
}) => void;
}RindleConnectionOptions
InterfaceDeclaration · Source: packages/api-server/src/index.ts:917 · Supporting declarations
The unified fleet connection — one URL, one key. A Rindle is BOTH layers at once: the SQL
database below and the sync/IVM control plane above, served by a single ingress that routes
each request to the right tier. This option derives both from that one origin: the SQL layer
({@link RindleApiServerOptions.database}) at url with token, and the control-plane client
({@link RindleApiServerOptions.daemon}) at the same url — so an application configures "a
rindle", not two subsystems.
The derived control-plane client sends the same bearer. A unified ingress is an explicitly merged customer API-server trust tier: it routes SQL to the master and control requests to a follower, while the browser still receives neither credential. Deployments that keep those trust tiers separate pass an explicit {@link RindleApiServerOptions.daemon}; explicit fields always win over this derivation.
export interface RindleConnectionOptions {
/** The single ingress origin. Defaults to `$RINDLE_URL` — exported by `rindle dev` whenever
* the rendered topology collapsed read+write onto one ingress. */
url?: string;
/** Public subscription WebSocket endpoint returned on query leases. Defaults to {@link url} with
* `http:` → `ws:` / `https:` → `wss:`. Override when HTTP and WebSocket ingress differ. */
wsUrl?: string;
/** The server-side bearer for both legs of the unified ingress. Defaults to
* `$RINDLE_DATABASE_TOKEN` (also a `rindle dev` export); required through one of those channels
* unless both legs are configured explicitly. It never reaches the browser. */
token?: string;
}RindleDatabaseOptions
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:903 · Supporting declarations
Database connection used by the API server's managed SQL path.
intMode defaults to "number" because logical Rindle rows use the JSON-safe {@link WireValue}
vocabulary — "bigint" does not survive JSON.stringify, and "string" silently retypes every
integer, breaking arithmetic in mutator bodies. The cost is a HARD BOUND: a mutator read of an
integer outside ±(2^53 − 1) rejects that mutation rather than silently rounding it. Tables with
keys beyond that range (snowflake ids, and so on) must override intMode and have their mutators
handle the resulting type. Commit receipts are unaffected — they never decode row values.
export type RindleDatabaseOptions = Pick<SqlClientOptions, "url" | "authToken" | "fetch" | "intMode">;RindleRealtimeLifecycleOptions
InterfaceDeclaration · Source: packages/api-server/src/index.ts:759 · Supporting declarations
{@link RindleRealtimeOptions.lifecycle}. PRESENCE of the block is the opt-in switch (I-iii);
the fields below are the Slice I-iv occupancy knobs (§4.1, decisions D4/D6/D7). All optional —
lifecycle: {} gets the designed defaults.
export interface RindleRealtimeLifecycleOptions {
/** D6 (§4.1): the occupancy threshold for room-serving. A labeled lease whose scope counts
* FEWER than this many distinct unexpired sessions (the caller's own included) ships WITHOUT
* the realtime block — served from the daemon, indistinguishable from an uncovered query —
* but WITH the doorbell, so the 1→2 transition wakes it (that is the point: solo docs never
* cost room infrastructure). Default **2** (the design's 1→2 trigger). Set `1` to room-serve
* solo viewers (the pre-I-iv behavior under lifecycle config). */
minSessions?: number;
/** The §9.1 hysteresis window, ms (default **120_000**). Two consumers: (a) the lazy sweep
* (D4) keeps expired session rows lingering at least this long past expiry — Slice I-v's
* downgrade decision ("no other unexpired row AND the newest other row expired > graceMs
* ago") is read FROM those rows, so they must survive to be read; (b) this slice's gate
* applies the same hysteresis upward: a scope with an other-session row expired ≤ graceMs
* ago keeps room-serving through the window (see `lifecycleOccupancy` — no flap on one
* client's brief lapse). §9.1-tunable: raise it for docs where collaborators churn slowly. */
graceMs?: number;
/** TTL of an occupancy session row, ms — `expires_at = now + sessionTtlMs` on every labeled
* lease mint/renewal (D7: session identity = the request's `clientId`; two tabs are two
* sessions iff their clientIds differ). Default = the server's `leaseTtlMs`, else 5 minutes —
* matching the room-token renewal cadence (`roomTokenTtlMs`, renewed 30s early), so a
* room-attached client's renewals keep its row unexpired; a daemon-attached solo client's row
* MAY lapse (it has no renewal timer) and is refreshed by its next doorbell-triggered
* re-lease — occupancy converges through the doorbell itself. */
sessionTtlMs?: number;
/** The §4.2 downgrade drain hook (Slice I-v). When the occupancy gate CLOSES for a labeled
* lease whose scope PLAUSIBLY hosted a room (an other-session row still lingers — never a
* never-shared solo doc), the api-server calls this to drain the room's pending write-behind
* and learn its last COMMITTED `flush_seq`, then rides the value back on the lease as the
* {@link QueryLeaseRealtimeFence}. The deployment wires it to the room shell's / DO's `/drain`
* control. Absent ⇒ no fence is attached (the client hits its loud legacy downgrade path);
* a throw fails OPEN to the same (a downgrade never blocks the lease). Concurrent drains across
* api-server instances are fine — `/drain` is idempotent. */
drainRoom?: (doc: string) => Promise<{
finalFlushSeq: number;
}>;
}RindleRealtimeOptions
InterfaceDeclaration · Source: packages/api-server/src/index.ts:685 · Supporting declarations
export interface RindleRealtimeOptions<User> {
/** The boot shell secret (§10.1 — a host binding, never client-derived). Authenticates the
* room's `Authorization: Bearer` on `/room-boot` (the default {@link authorizeBoot}) and keys
* the DEFAULT epoch-bound flush credential. */
shellSecret: string;
/** NAMED room profiles (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §2.1): profile name → key
* derivation + canonical unwindowed footprint + read-only context tables. The wire room key
* for a named profile is `"<profile>/<key>"`; `/room-boot` splits it and resolves the
* profile's footprint with the bare key. Compiled + validated LOUDLY at construction (§2.3):
* a windowed footprint, a context table missing from the schema/footprint, or a registered
* query whose realtime label names a missing profile all throw from `createRindleApiServer`. */
rooms?: Record<string, RoomProfile<User>>;
/** doc → the room's approved upstream footprint (§3.1) — an `Ast` or fluent `Query`. MAY
* delegate to the named-query registry internally; throw `RindleApiError("not-found", …, 404)`
* for a doc that shouldn't exist. The §9 footprint budget belongs here — it runs once per
* placement, at lease mint.
* @deprecated Prefer named {@link rooms} profiles (RINDLE-REALTIME-QUERY-ENABLEMENT §2.1).
* This bare form remains as the single-profile LEGACY alias — the anonymous/default profile:
* a doc with no known `"<profile>/"` prefix resolves here, byte-identically to before named
* profiles existed (and with none of their construction/boot-time validation). */
resolveFootprint?: (doc: string, ctx: ApiContext<User>) => MaybePromise<ApiQueryResult>;
/** Gates the flush trio — `authorizeRoom`, relocated. Default: verify the default flush
* credential from the {@link ROOM_FLUSH_CREDENTIAL_HEADER} request header — which requires the
* transport to pass its incoming request as `context.request` (Fetch `Request` and node
* `IncomingMessage` shapes are both understood). */
authorize?: Authorizer<ApiContext<User>>;
/** Gates `/room-boot`. Default: constant-time `Authorization: Bearer` check against
* {@link shellSecret} (same `context.request` requirement as {@link authorize}). */
authorizeBoot?: Authorizer<ApiContext<User>>;
/** Mint the flush headers a placed room presents on every flush call. Default:
* {@link mintRoomFlushCredential} under {@link ROOM_FLUSH_CREDENTIAL_HEADER}. Override this
* and {@link authorize} TOGETHER — they are the two ends of one credential. */
mintFlushHeaders?: (input: {
doc: string;
epoch: number;
}) => MaybePromise<Record<string, string>>;
/** Lease TTL for the room's upstream footprint materialization (defaults to the server-wide
* `leaseTtlMs`, else the daemon's default). */
upstreamLeaseTtlMs?: number;
/** Static endpoint where rooms open their upstream subscription. In a follower fleet this is the
* fleet ws URL; `/room-boot` pairs it with the materialization's fresh placement ticket so the
* room lands on the exact follower holding its lease. Absent ⇒ no explicit upstream (the Node
* room shell may use its own default; the shipped DO shell requires this endpoint). */
upstreamWsEndpoint?: string;
/** Locate (or place) the room serving `doc` and return the ROOM ws endpoint a room-served
* lease's client should open (G-iv-b; on the DO shell this is the Worker's room URL). The
* endpoint rides the lease's dedicated `realtime.wsEndpoint` — its OWN connection, distinct from
* the daemon session's fixed ws host. Absent ⇒ room-serving is OFF: labeled queries serve from
* the daemon exactly as today (fail-open). */
locateRoom?: (doc: string) => MaybePromise<{
wsEndpoint: string;
}>;
/** The room lease token signing key (`@rindle/room/token`): `kid` + secret, matching an entry
* in the room shell's `downstream.tokenKeys` ring. Required for room-serving (without it a
* labeled query fail-opens to the daemon with a one-time warning). A separate secret from
* `shellSecret` on purpose — the shell's ring is the client-token trust domain, the shell
* secret is the boot/flush trust domain. */
roomTokenKey?: {
kid: string;
secret: string;
};
/** Room lease token TTL, ms (default 5 minutes — the §4.1 short-TTL backstop; renewal is a
* fresh lease through this server, never an extension). */
roomTokenTtlMs?: number;
/** Loud-diagnostics sink for the realtime layer (profile compilation warnings + the one-time
* per-(query, profile) "not room-served" serve-decision warnings). Defaults to
* `console.warn`; injectable for tests. */
warn?: (message: string) => void;
/** The §4 upgrade/downgrade lifecycle plane (RINDLE-REALTIME-QUERY-ENABLEMENT §4, Slice
* I-iii): PRESENCE of this block is the opt-in — every realtime-labeled lease then
* additionally mints the doorbell system lease (occupancy, §4.1) and every ROOM-SERVED lease
* the fence bundle (watermark + ledger + outcomes, §4.2/§7.1/§3.3) — see
* {@link QueryLeaseLifecycle}. Requires the daemon to have run `enable_realtime_lifecycle`
* (the four `_rindle_*` system tables must be registered or the minted materializations fail
* — which fail-opens with a one-time warning, never blocking the lease). Absent ⇒ the lease
* response is byte-identical to pre-lifecycle. */
lifecycle?: RindleRealtimeLifecycleOptions;
}RindleStreamOptions
InterfaceDeclaration · Source: packages/api-server/src/streams.ts:214 · Supporting declarations
export interface RindleStreamOptions<User> {
/** Where checkpoints land: the app's tables (the default path) or a raw `commit` callback. */
checkpoint: StreamCheckpointTarget;
/** REQUIRED. Subscribing to a stream is reading someone's chat, so there is no default-allow.
* Runs BEFORE existence is checked, so a denial cannot be used to probe for stream ids. */
authorize: Authorizer<AuthorizeStreamInput<User>>;
policy?: StreamCheckpointPolicy;
/** This process's identity — it must be UNIQUE per producer process, because the open CAS trusts
* it to distinguish rivals (§5.1). In `tables` mode, map {@link StreamColumns.host} and the open
* write persists it on the message row (so the app can route later subscribers to the hosting
* instance, §4) and uses it as the single-flight token; setting it WITHOUT a mapped `host` column
* is refused at construction. In `commit` mode it rides the `open` input. When a `host` column is
* mapped and no hostId is given, a random per-plane token is used — the CAS still holds, routing
* just has no stable name to read. */
hostId?: string;
/** Slack retained BELOW `durableSeq` so a client whose IVM view lags a checkpoint can still join
* without a `stale` round trip. Text at or above `durableSeq` is never trimmed. Default 64 KiB.
* `commit`-mode only — REFUSED (a construction-time `TypeError`) in `tables` mode, where
* compaction needs the whole produced text at close (§3.4), so the buffer is retained in full. */
retainChars?: number;
/** How long a sealed stream stays joinable before eviction. Default 30s. */
lingerMs?: number;
/** Per-subscriber frame queue cap; overflow drops that subscriber with `stale` (§4). Default 1024.
* Relayed readers reuse the same bound: a slow reader on a relayed stream costs itself the live
* leg exactly as a local one does. */
maxQueuedFrames?: number;
/** A checkpoint that exhausted its retries. The stream keeps streaming — this is a durability
* stall, not a stream stall — so the error must not vanish. Absent ⇒ `console.error`. */
onCheckpointError?: (err: unknown, info: {
streamId: string;
from: number;
seq: number;
}) => void;
/** Cross-process transport for the live plane ({@link StreamRelay}). Without one, a subscriber
* that lands on a process not hosting its stream gets `absent` and reads the durable plane at
* checkpoint granularity — correct, just chunky. */
relay?: StreamRelay;
/** Bound on `relay.attach`: a hung adapter yields `absent`, not a hung HTTP request. An
* addressing adapter MAY deliberately spend this window waiting out a subscribe that races its
* own kick. Default 2000. */
relayAttachTimeoutMs?: number;
/** A diagnostic, never a control path: relay failures (a throwing or rejecting `publish`, a failed
* or timed-out `attach`, a conform violation in the frames) land here, wrapped so a throwing hook
* cannot reach the plane. The reader-facing outcome is always the same legal `absent`/`stale`.
* Absent ⇒ `console.error`. */
onRelayError?: (err: unknown, info: StreamRelayErrorInfo) => void;
}ROOM_FLUSH_CREDENTIAL_HEADER
VariableDeclaration · Source: packages/api-server/src/index.ts:802 · Supporting declarations
export declare const ROOM_FLUSH_CREDENTIAL_HEADER = "x-rindle-room-credential";RoomBootFlush
InterfaceDeclaration · Source: packages/api-server/src/index.ts:658 · Supporting declarations
The room-boot flush leg (enablement §5): where the placed room's write-behind lands and what
credential it presents. urls are the trio's ROUTE PATHS (root-relative — the shell resolves
them against the boot call's origin), so an app that overrides routes needs no out-of-band
sync; headers ride every flush call verbatim (httpAuthority's headers).
export interface RoomBootFlush {
urls: {
apply: string;
claim: string;
lmids: string;
};
headers: Record<string, string>;
}RoomBootResponse
InterfaceDeclaration · Source: packages/api-server/src/index.ts:666 · Supporting declarations
The /room-boot response (RINDLE-REALTIME-DESIGN.md §10.1 — the DO shell's cold-boot
callback): the claimed placement epoch, the room's upstream footprint lease, and the flush
leg. A cold room boots inert and serves nothing until this returns.
export interface RoomBootResponse {
epoch: number;
upstreamLeaseToken: string;
/** Where the room opens its upstream subscription (a routed deploy's follower). Absent ⇒ the
* shell's statically configured rindled ws endpoint. */
upstreamWsEndpoint?: string;
/** Fresh opaque follower-placement ticket minted alongside `upstreamLeaseToken`. The DO offers
* it with `rindle.v1` on the separate upstream ws so a static fleet endpoint replays to the
* exact follower holding that local lease. Absent when daemon affinity is off. */
upstreamAffinity?: string;
/** Per-footprint-table scope specs (H-iv-b), compiled from the resolved footprint AST + the
* profile's context set (the legacy anonymous profile compiles with an empty context set):
* what the shell hands the wasm room's `enableWritesV2` — the §3.3 commit gate. Optional
* only for wire compatibility with pre-H-iv-b servers; a shell that doesn't receive them
* enables the v1 table-granular write plane exactly as before. */
scopes?: RoomScopeSpec[];
flush: RoomBootFlush;
}RoomFlushCredentialPayload
InterfaceDeclaration · Source: packages/api-server/src/index.ts:806 · Supporting declarations
export interface RoomFlushCredentialPayload {
v: 1;
doc: string;
epoch: number;
iat: number;
}RoomHostResponse
InterfaceDeclaration · Source: packages/api-server/src/index.ts:628 · Supporting declarations
A room-host reply the transport writes VERBATIM (status + JSON body): the
daemon's fence/conflict/identity semantics ride specific statuses and body shapes
the room's httpAuthority decodes, so this endpoint trio can't run through the
throw-on-error result shapes the viewer endpoints use.
export interface RoomHostResponse {
status: number;
body: unknown;
}RoomProfile
InterfaceDeclaration · Source: packages/api-server/src/rooms.ts:26 · Supporting declarations
One NAMED room profile (§2.1). Rooms are document-scoped, not query-scoped: a profile is what
stands up and feeds a room — the flat resolveFootprint promoted to a named, reusable unit.
Labeled queries opt into a profile; the wire room key for a named profile is minted SERVER-side
as "<profile>/<key>", and /room-boot splits it back to resolve the footprint.
export interface RoomProfile<User = unknown> {
/** Derive the profile-local room key from the (label-mapped) query args. Runs server-side under
* authoritative inputs — the client learns the key from the lease, it never computes one. */
key: (args: any) => string;
/** Build the room's canonical footprint — the replica boundary the room loads and follows —
* from the profile-local doc key (this profile's `key` output; what `/room-boot` receives
* after the prefix split). MUST be unwindowed (§2.3): no `limit`/`start`/`one` anywhere in the
* AST — enforced loudly at construction when statically resolvable, and again at every boot.
* Should be a TOTAL builder over any key (a shape constructor, not an authorizer). */
footprint: (docKey: string, ctx: ApiContext<User>) => MaybePromise<ApiQueryResult>;
/** Loaded-but-never-room-written tables (§2.2 — the "followed" set: real external writers, the
* daemon stays write-authoritative for them). Must be a subset of the footprint's tables; the
* room's writable scope is footprint minus context. Defaults to `[]`. */
context?: readonly string[];
}RoomScopeSpec
TypeAliasDeclaration · Source: packages/api-server/src/rooms.ts:249 · Supporting declarations
One footprint table's FULL scope spec (slice H-iv-b): byte-compatible with
rindle-room-core's TableScopeSpec (scope.rs), the wasm room's enableWritesV2 input. It
rides the boot wire (RoomBootResponse.scopes); since slice H-iii the LEASE wire's
{@link RoomTableSpec} carries footprintWhere too (the client-side routing proof), so the two
shapes are now structurally IDENTICAL — kept as two names because they are two wires with two
consumers (the room gate vs the client router).
footprintWhere is computed for EVERY footprint table — context (kind: "none") ones
included, because the gate proves ABSENT READS on any readable table with it (§3.1: an absent
read is covered only when the footprint predicate is decidable from the pk columns alone and
evaluates true on the read key). ABSENT (no exact membership predicate exists) ⇒ no absent
read on that table is provable — the room gate fails closed to a deopt.
footprintWhere is EXACT-only — the opposite discipline from the writable where. The
writable side ships the WIDENING extraction ({@link tablePredicate}) and that is sound (the
held-row set is the real outer bound). The absent-read proof points the other way: it uses
footprintWhere to conclude "a row with this pk would have been IN the footprint, so the
room's absence is truth's absence" — a WIDENED predicate over-claims exactly there (a root
where mixing pk-column conjuncts with a dropped EXISTS, or a correlated child whose own
where reads only its pk, would prove an absence the dropped part can contradict). So
footprintWhere is emitted ONLY when the membership predicate is exact
({@link exactMembershipPredicate}): every node reading the table is the footprint ROOT (a
child node's implicit correlation to its parent is itself a dropped, non-row-local constraint)
and the root's where extraction dropped nothing. An unconstrained exact root (the whole
table rides the footprint) emits the vacuous-true {type:"and",conditions:[]} — the gate's
combinator pins empty-AND = true — so whole-table footprints keep provable absent reads.
Child/correlated tables get NO footprintWhere and their absent reads deopt (fail closed);
propagating a parent constraint through a pk-covering correlation is a future refinement.
export type RoomScopeSpec = {
table: string;
footprintWhere?: Condition;
writable: RoomTableSpec["writable"];
};RoomTableSpec
TypeAliasDeclaration · Source: packages/api-server/src/rooms.ts:211 · Supporting declarations
One footprint table's spec on the lease wire (QueryLeaseResponse.realtime.tables): the §2.2
owned/followed split plus §3.2's per-table routing metadata, compiled from the RESOLVED
footprint AST at lease time (so key-dependent and non-static profiles compile too).
writable:
none— a context table (§2.2 "followed"): loaded by the room, never room-written; the daemon stays write-authoritative.predicate— a writable table.whereis the ROW-LOCAL part of the footprint's predicate for this table (simple column-vs-literal conditions composed with and/or); an ABSENTwheremeans no row-local constraint — every row the room holds for this table is in the writable scope.joinKeyColsare the correlation columns the footprint binds on this table (slice H's "room mutators never write join keys" enforcement input).
Dropping the non-row-local parts (correlated/EXISTS conditions, column-vs-column comparisons)
only ever WIDENS the predicate, which is sound here: the room only relays rows the footprint
materialized, so the held-row set — not this predicate — is the real outer bound; where is a
row-local refinement of it, and a wider refinement can never mark a held footprint row
non-writable that the full predicate would have allowed. (An OR with any non-row-local
disjunct is dropped WHOLE — keeping only some disjuncts would narrow, which is not sound.)
footprintWhere (H-iii — the lease-wire flip): the EXACT footprint-membership predicate,
identical to {@link RoomScopeSpec.footprintWhere} (ONE compiler feeds both wires — the lease's
table specs and the boot wire's scopes are now the SAME objects). The client's §3 router
consumes it for the pk-membership read proof; it is advisory routing metadata, never a
credential (the room gate re-proves engine-side, H-iv).
export type RoomTableSpec = {
table: string;
footprintWhere?: Condition;
writable: {
kind: "none";
} | {
kind: "predicate";
where?: Condition;
joinKeyCols: string[];
};
};RunQuery
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:200 · Supporting declarations
export type RunQuery<User> = (input: RunQueryInput<User>) => MaybePromise<ApiQueryResult>;RunQueryInput
InterfaceDeclaration · Source: packages/api-server/src/index.ts:192 · Supporting declarations
export interface RunQueryInput<User> {
user: User;
name: string;
args: unknown;
query: ApiQuery<User, any>;
context: ApiContext<User>;
}scoped
FunctionDeclaration · Source: packages/api-server/src/index.ts:390 · Supporting declarations
Mark a mutator as SCOPED so the api-server gives it a {@link MutationScope} (author-controlled tx
boundary via scope.transact) rather than running its whole body inside the transaction. Register
it alongside the tx-form mutators — it wins by key like any override:
mutators: defineApiMutators({
...sharedApiMutators(sharedMutators, sharedCtx), // tx-form (common case)
createOrder: scoped(async (scope, raw, ctx) => { // needs outside-tx work
const args = createOrder.args.parse(raw);
const chargeId = await stripe.charge(args.amount, { idempotencyKey: ctx.envelope.mid }); // outside tx
try {
await scope.transact(createOrder, args, { ...sharedCtx(ctx), chargeId }); // inside tx
} catch (e) {
await stripe.refund(chargeId); // compensate — the write rejected
throw e;
}
await sendReceipt(ctx.user); // after commit
}),
}),
export declare function scoped<User, Args>(fn: ScopedMutator<User, Args>): ScopedApiMutator<User, Args>;ScopedApiMutator
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:367 · Supporting declarations
A {@link ScopedMutator} tagged by {@link scoped} so the harness invokes it with a
{@link MutationScope}. Typed as a BRANDED tx-form {@link ApiMutator} purely so it registers in the
mutators record without widening it to a union (which would break contextual inference for every
plain tx-form entry). Its true runtime shape is (scope, args, ctx); the tag — not the type —
routes it, and it is never actually called as a tx-form mutator.
export type ScopedApiMutator<User, Args> = ApiMutator<User, Args> & {
readonly __rindleScoped: true;
};ScopedMutator
TypeAliasDeclaration · Source: packages/api-server/src/index.ts:356 · Supporting declarations
A SCOPED server mutator (WORK-OUTSIDE-TX): server-only code, ONE scope.transact, optional
post-commit code. Register it by wrapping in {@link scoped} — the tag the api-server routes on to
hand it a {@link MutationScope} instead of running its whole body inside the transaction.
export type ScopedMutator<User, Args> = (scope: MutationScope, args: Args, ctx: MutationContext<User>) => void | Promise<void>;ServerMutationTx
InterfaceDeclaration · Source: packages/api-server/src/index.ts:259 · Supporting declarations
The write handle a server mutator runs against — the ASYNC twin of the client's MutationTx. It
is both the isomorphic {@link ServerWriteTx} logical surface (insert/update/upsert/insertIgnore/
delete/row, rendered to dialect SQL) AND the legacy {@link SqlMutationTx} raw escape hatch. Both
implementations run reads through the OPEN transaction (read-your-writes): Postgres executes
everything live; the SQL-client and daemon adapters accumulate writes and lazily upgrade to an
interactive mutation session at the first read (DAEMON-INTERACTIVE-TXN-DESIGN.md §5).
export interface ServerMutationTx extends ServerWriteTx, SqlMutationTx {
/** Run a full query (a fluent `Query` or its wire `Ast`) INSIDE the open transaction —
* read-your-writes, like {@link ServerWriteTx.row} but for arbitrary shapes. Returns the
* parsed nested result tree: an array for a plural root, an object or `null` for a `.one()`
* root, with cells in their raw SQLite storage-class representations (the same vocabulary
* `row` speaks). Remote SQLite backends: compiled by `@rindle/query-compiler`'s sqlite dialect
* (bind params, NO casts — §5.4) and executed through the mutation session. Postgres: lands with
* the read-compiler catalog integration (POSTGRES-READ-COMPILER-DESIGN.md Phase B). */
query(q: Ast | Query<any, any, any>): Promise<unknown>;
}ServerSql
InterfaceDeclaration · Source: packages/api-server/src/index.ts:230 · Supporting declarations
A deliberately narrow raw-SQL facade exposed by the API server. On {@link ServerMutationTx} it is bound to the open mutation transaction; on {@link MutationScope} each call runs in its own transaction outside the mutation boundary. Column aliases should be unique: positional driver rows are keyed by column name, so a duplicate alias is represented by its last value.
export interface ServerSql {
/** Queue/execute one statement. A transaction-bound call commits with the surrounding mutation. */
execute(sql: string, params?: readonly WireValue[]): Promise<void>;
/** Queue/execute an ordered statement batch. An empty batch is a no-op. On an
* outside-transaction surface ({@link MutationScope.sql}, `backend.outsideSql`) the batch MUST
* execute as ONE atomic transaction — all statements or none. Every built-in backend does (the
* daemon's `execute-sql-txn`, the sql-client's `/v1/sql/batch`, the Postgres plugger's
* BEGIN/COMMIT); a custom backend that loops statements without a transaction silently breaks
* the stream plane's chunk+CAS and compaction invariants, which ride single `batch` calls. */
batch(statements: readonly SqlStatement[]): Promise<void>;
/** Run a read and return rows keyed by their column names. */
query<Row = Record<string, unknown>>(sql: string, params?: readonly WireValue[]): Promise<Row[]>;
}ShapeExemplar
InterfaceDeclaration · Source: packages/api-server/src/index.ts:2483 · Supporting declarations
One exemplar invocation for {@link dumpQueryShapes} — the args/user a query is built with.
Literal values never matter to the dump (shapes are deduped with literals stripped); what an
exemplar buys is BRANCH coverage, so supply one per code path a query function can take
(an optional filter present/absent, each enum axis, …).
export interface ShapeExemplar<User = unknown> {
args?: unknown;
user?: User;
}spliceStreamText
FunctionDeclaration · Source: packages/client/src/stream.ts:75 · Supporting declarations
Merge the durable plane with the live tail.
durable is what the IVM view shows; produced is what a subscription has accumulated (the prefix
it joined at, plus every chunk). Both are prefixes of the same response, so the merge is "take
the longer" — no diffing, no overlap handling, no ranges.
The length comparison is the whole algorithm, which is why a caller MUST seed its accumulator with
the text it joined at: a tail carrying only the chunks it received would read as shorter than the
durable text and be discarded. useStreamedText does that for you.
export declare function spliceStreamText(durable: string, produced: string): string;SplitDaemonClient
ClassDeclaration · Source: packages/api-server/src/index.ts:1137 · Supporting declarations
A read/write-split RindleDaemonClient (READ-ROUTER-DESIGN.md §2.1). Writes
(executeSqlTxn / rejectMutation / applyRowChangeTxn / migrate) go to the single
write-master, UNCHANGED and never through the router; reads (materialize / query /
dematerialize, and raw executeSqlRead) go to the read router. Raw reads default to a replica
(consistency:"eventual") so they scale off the write-master; pass consistency:"strong" on a
read to route it to the master for read-your-writes after a write to the same data. Hand one of
these to {@link createRindleApiServer} as daemon to point the reads leg at the router while
writes stay on the master — no placement logic enters the api-server.
const daemon = new SplitDaemonClient(
new HttpRindleDaemonClient({ baseUrl: MASTER_URL, headers: writeAuth }), // writes → master
new HttpRindleDaemonClient({ baseUrl: ROUTER_URL, headers: routerAuth }), // reads → router
);
export declare class SplitDaemonClient implements RindleDaemonClient {
private readonly writes;
private readonly reads;
constructor(writes: RindleDaemonClient, reads: RindleDaemonClient);
executeSqlTxn(input: SqlTxn): Promise<SqlTxnOutput>;
executeSqlRead(input: SqlRead): Promise<SqlReadOutput>;
rejectMutation(input: MutationRejection): Promise<MutationRejectionOutput>;
beginMutationSession(input: MutationSessionBegin): Promise<MutationSessionBeginOutput>;
execInMutationSession(input: MutationSessionExec): Promise<unknown>;
queryInMutationSession(input: MutationSessionQuery): Promise<SqlReadOutput>;
commitMutationSession(input: MutationSessionRef): Promise<SqlTxnOutput>;
rollbackMutationSession(input: MutationSessionRef): Promise<unknown>;
applyRowChangeTxn(input: RowChangeTxn): Promise<RowChangeTxnOutput>;
claimRoomEpoch(input: ClaimRoomEpochInput): Promise<ClaimRoomEpochOutput>;
roomLmids(input: RoomLmidsInput): Promise<RoomLmidsOutput>;
migrate(input: MigrateInput): Promise<MigrateOutput>;
materialize(input: MaterializeInput): Promise<MaterializeOutput>;
query(input: QueryOnceInput): Promise<QueryOnceOutput>;
dematerialize(input: DematerializeInput): Promise<DematerializeOutput>;
}sqlBackend
FunctionDeclaration · Source: packages/api-server/src/index.ts:2172 · Supporting declarations
Run API-server mutators through @rindle/sql-client's explicit mutation facade. Query leases,
SSR reads and room control continue to use daemon; only authoritative mutation execution moves
to the versioned SQL transport.
export declare function sqlBackend(sql: SqlSession): MutationBackend;SqlDialect
InterfaceDeclaration · Source: packages/api-server/src/index.ts:1224 · Supporting declarations
A SQL dialect for the logical mutation renderer.
export interface SqlDialect {
readonly name: "sqlite" | "postgres";
/** Render the i-th (1-based) bind placeholder. sqlite: `?`; postgres: `$i`. */
placeholder(oneBased: number): string;
/** Optional value coercion hook (e.g. a future SQLite `0/1` boolean). Default: identity. */
encodeValue?(v: WireValue, type: ColType): WireValue;
}sqliteDialect
VariableDeclaration · Source: packages/api-server/src/index.ts:1232 · Supporting declarations
export declare const sqliteDialect: SqlDialect;SqlMutationTx
InterfaceDeclaration · Source: packages/api-server/src/index.ts:247 · Supporting declarations
The raw-SQL escape hatch for relational/authority statements a keyed op can't express — an
owner-gated cascade, a NOT EXISTS dedup. Prefer tx.sql; exec remains the synchronous
compatibility shorthand for a queued tx.sql.execute, and statements is the raw write list.
export interface SqlMutationTx {
readonly sql: ServerSql;
exec(sql: string, params?: WireValue[]): void;
readonly statements: readonly SqlStatement[];
}STREAM_SSE_HEADERS
VariableDeclaration · Source: packages/api-server/src/streams.ts:1652 · Supporting declarations
Headers for the SSE response. x-accel-buffering is the nginx-family opt-out — without it a
buffering proxy holds the tokens and hands the user a paragraph at a time.
export declare const STREAM_SSE_HEADERS: Record<string, string>;STREAM_STATUS_STREAMING
VariableDeclaration · Source: packages/client/src/stream.ts:22 · Supporting declarations
The value in the mapped status column while a stream is live.
export declare const STREAM_STATUS_STREAMING = "streaming";StreamCheckpointPolicy
InterfaceDeclaration · Source: packages/api-server/src/streams.ts:160 · Supporting declarations
When a checkpoint fires — the FIRST of these wins (§3.1). Checkpoints are serialized, so a slow store degrades to fewer, larger checkpoints, never to a queue of them.
export interface StreamCheckpointPolicy {
/** Produced-but-uncommitted characters that force a checkpoint. Default 512. */
chars?: number;
/** Milliseconds since the last checkpoint that force one. Default 750. */
intervalMs?: number;
/** Retries for a failing commit before the slice is left for the next trigger. Default 3. */
retries?: number;
}StreamCheckpointTarget
TypeAliasDeclaration · Source: packages/api-server/src/streams.ts:154 · Supporting declarations
export type StreamCheckpointTarget = {
tables: StreamTables;
} | {
commit: StreamCommit;
};streamChunkId
FunctionDeclaration · Source: packages/api-server/src/streams.ts:397 · Supporting declarations
The chunk row's deterministic id: a replayed checkpoint collides with itself and is absorbed by
ON CONFLICT DO NOTHING — idempotency without an envelope or a dedup ledger (§3.3).
export declare function streamChunkId(streamId: string, seq: number): string;streamChunkTableDdl
FunctionDeclaration · Source: packages/api-server/src/streams.ts:406 · Supporting declarations
The chunk table's DDL, for the app's migration. The app owns the message table (this only states the three columns the plane needs on it); the chunk table is entirely protocol-shaped, so it is generated rather than hand-written.
export declare function streamChunkTableDdl(tables: StreamTables, dialect: SqlDialect): string[];StreamColumns
InterfaceDeclaration · Source: packages/api-server/src/streams.ts:92 · Supporting declarations
Which columns of the app's own tables the plane reads and writes. Every entry has a default
except cancel, error, and host, which are opt-in BY NAMING: the plane never emits SQL
against a column the app did not ask it to use.
export interface StreamColumns {
/** The message row's primary key, matched against `streamId`. Default `id`. */
key?: string;
/** The compacted response text. Default `body`. */
body?: string;
/** {@link STREAM_STATUS_STREAMING} then a {@link StreamStatus}. Default `status`. */
status?: string;
/** Total durable length — `length(body) + Σ chunk lengths`. The CAS column (§3.2). Default `seq`. */
seq?: string;
/** Opt-in (§6): a truthy value here stops the generation at the next checkpoint. No default —
* naming it is what turns cancellation on. */
cancel?: string;
/** Opt-in: where a failed generation's message is recorded. No default. */
error?: string;
/** Opt-in: where the open write records this producer's identity ({@link RindleStreamOptions.hostId}).
* Naming it upgrades the row-level single-flight guard from check-then-act to a true
* compare-and-swap — the open write turns conditional and a read-back names the winner (§5.1) —
* and gives multi-instance subscribe routing a column to read (§4). No default. */
host?: string;
/** Chunk primary key; the plane writes the deterministic `"<streamId>:<seq>"`. Default `id`. */
chunkKey?: string;
/** Chunk → message reference. Default `streamId`. */
chunkStream?: string;
/** The chunk's END offset — the ordering key. Default `seq`. */
chunkSeq?: string;
/** The chunk's slice of the response. Default `text`. */
chunkText?: string;
}StreamCommit
TypeAliasDeclaration · Source: packages/api-server/src/streams.ts:85 · Supporting declarations
The escape hatch: persist the checkpoint however the app likes. Retried on throw (§3.3), so it
must be idempotent under a repeated (from, seq) — AND under a RE-COVER: an append whose write
committed but whose ack was lost leaves the plane believing less than the store holds, so the
next append's (from, seq) can OVERLAP text already applied. Apply only the unseen suffix
(text.slice(applied - from) when applied > from), or better, return {seq: applied} — the
authoritative applied length — and the plane resynchronizes instead of re-covering at all.
Return {cancelRequested: true} to tell the producer the reader asked it to stop (§6).
export type StreamCommit = (input: StreamCommitInput) => Promise<void | {
cancelRequested?: boolean;
seq?: number;
}>;StreamCommitInput
TypeAliasDeclaration · Source: packages/api-server/src/streams.ts:62 · Supporting declarations
What a checkpoint hands the durable plane when the app supplies its own {@link StreamCommit}.
export type StreamCommitInput =
/** The pointer is marked live. The app's own mutator created the row (it owns `chatId`, `role`,
* the model name…); this only flips it to `streaming`. */
{
kind: "open";
streamId: string;
meta: unknown;
hostId?: string;
startedAt: number;
}
/** A prefix advance carrying ONLY its own slice. `text.length === seq - from`, and `from` is the
* last append the PLANE saw confirmed — so appends are contiguous in the fault-free run, but an
* append that committed while its ack was lost makes the next one RE-COVER (its `from` lags what
* the app already applied). See {@link StreamCommit} for the two ways to stay idempotent. */
| {
kind: "append";
streamId: string;
from: number;
seq: number;
text: string;
}
/** The seal. `body` is the producer's retained text and `bodyFrom` its absolute start offset:
* `bodyFrom === 0` — and `body` is the WHOLE response — unless the app opted into trimming via
* `retainChars` (`commit` mode only). A compacting app requires `bodyFrom === 0` and writes
* `body` wholesale; a non-compacting app appends the outstanding tail —
* `body.slice(from - bodyFrom)` — as a final chunk. `seq === bodyFrom + body.length`, always. */
| {
kind: "close";
streamId: string;
from: number;
seq: number;
body: string;
bodyFrom: number;
status: StreamStatus;
error?: string;
};StreamFrame
TypeAliasDeclaration · Source: packages/client/src/stream.ts:32 · Supporting declarations
One frame of a subscription. A subscription always begins with open and always ends with exactly
one terminal frame — end, stale, or absent — after which the iterator completes.
stale and absent are the two "you are on the durable plane now" answers, and both are SAFE:
the store holds everything below floorSeq and everything through durableSeq, so the reader's
IVM view converges without the stream. Neither is an error.
export type StreamFrame =
/** Join accepted. `from` is the (clamped) offset the replay starts at. */
{
type: "open";
streamId: string;
from: number;
seq: number;
durableSeq: number;
ended: boolean;
}
/** PRODUCED text — not a durability claim. `text.length === seq - from`, always. */
| {
type: "chunk";
from: number;
seq: number;
text: string;
}
/** The store now holds the prefix through `seq`. */
| {
type: "durable";
seq: number;
}
/** Sealed. No further frames. */
| {
type: "end";
seq: number;
status: StreamStatus;
error?: string;
}
/** `from` is below the producer's retained buffer floor (or the subscriber fell too far behind):
* read the store. (A raw `EventSource` rejoins automatically on its reconnect; `useStreamedText`
* deliberately stays on the durable plane instead — correct, at checkpoint granularity.) */
| {
type: "stale";
floorSeq: number;
durableSeq: number;
}
/** The process serving this subscribe is not hosting the stream (wrong instance, already evicted,
* or it never existed): the store is the whole truth. */
| {
type: "absent";
};streamFramesToSse
FunctionDeclaration · Source: packages/api-server/src/streams.ts:1683 · Supporting declarations
Encode a subscription as an SSE body. Each positional frame carries id: <seq>, so a browser
EventSource that drops the connection resumes at exactly the right offset with no application
code — its own Last-Event-ID header is the from of the next subscribe ({@link * streamRequestFromHttp}).
The reader must close the EventSource on the end frame: EventSource reconnects on ANY close,
including a clean one.
export declare function streamFramesToSse(sub: StreamSubscription, opts?: {
keepAliveMs?: number;
}): ReadableStream<Uint8Array>;StreamHandle
InterfaceDeclaration · Source: packages/api-server/src/streams.ts:271 · Supporting declarations
The producer's handle (§2). One writer per stream, by construction.
export interface StreamHandle {
readonly streamId: string;
/** Total produced code units. */
readonly seq: number;
/** Total code units the store has committed. Never exceeds {@link seq} (contract P). */
readonly durableSeq: number;
/** True once a checkpoint round-trip has seen the reader's cancel flag (§6). `pump` stops on it;
* a hand-rolled generation loop should check it. */
readonly cancelled: boolean;
/** Append a delta: fanned to subscribers synchronously, checkpointed on policy. */
push(text: string): void;
/** Force a checkpoint and resolve once the store holds every character produced so far. This is
* the ORDERING primitive: `await flush()` before writing a discrete row (a tool call, a stop
* reason) so the text precedes it in the store (§1). Rejects if the checkpoint cannot commit. */
flush(): Promise<number>;
/** Drain a delta iterable into the stream (the shape every LLM SDK's text stream already has),
* stopping early — and closing the iterator, which aborts the underlying request — once the
* reader has cancelled. */
pump(deltas: AsyncIterable<string>): Promise<void>;
/** Seal the stream: `cancelled` if the reader asked it to stop, else `complete`. In `tables` mode
* this is the compaction (§3.4) — it writes the whole body and drops the chunk rows in one
* transaction, so it also REPAIRS any checkpoint that failed along the way. */
close(): Promise<void>;
/** Seal `error` at whatever was produced. Never throws for the reason it is sealing. */
fail(error: unknown): Promise<void>;
}StreamOpenRefused
ClassDeclaration · Source: packages/api-server/src/streams.ts:1595 · Supporting declarations
The open probe's verdict: the message row is missing or already advanced. Not retried — it is a settled statement about the app's data, and retrying re-asks a question already answered.
export declare class StreamOpenRefused extends Error {
constructor(message: string);
}StreamRelay
InterfaceDeclaration · Source: packages/api-server/src/streams.ts:193 · Supporting declarations
Optional cross-process transport for the LIVE plane
(designs-implemented/LM-STREAM-RELAY-DESIGN.md). Both methods are independently optional; which
ones you implement is which topology you built — an addressing adapter (a Durable Object named by
streamId, fly-replay) implements only attach; a broadcast adapter (Redis pub/sub, NATS)
mirrors with publish and subscribes with attach; a log adapter (Redis Streams, Kafka) appends
and replays. Never consulted for the durable plane — checkpoints are unaffected by any of this.
The plane does not trust what attach yields: frames are run through the conform pass
({@link StreamRelayConform}) and any contract violation downgrades the subscription to stale,
which already means "you are on the durable plane now". A broken relay costs a reader smooth
tokens, never corrupted text — and can never reach the producer.
export interface StreamRelay {
/** Producer side: every frame this process's producer fans out (`chunk`, `durable`, and the
* terminal `end`), mirrored outward. MUST NOT block; a throw or rejected promise is caught and
* routed to {@link RindleStreamOptions.onRelayError} — a relay outage may cost the live leg,
* never the generation. Returned promises are observed but never awaited. */
publish?(streamId: string, frame: StreamFrame): void | PromiseLike<void>;
/** Subscriber side: this process is not hosting `streamId`. Return a frame source, or `undefined`
* for `absent` — exactly the no-relay answer. Consulted only AFTER `authorize` has passed, and
* only on a live-plane miss (a local stream always wins). The plane closes the source
* (`return()`) when the reader disconnects. An adapter that cannot serve `from` (pub/sub has no
* history) yields `stale` and stops — the reader converges on the durable plane (§5). */
attach?(streamId: string, from: number): Promise<AsyncIterable<StreamFrame> | undefined>;
}StreamRelayConform
ClassDeclaration · Source: packages/api-server/src/streams.ts:639 · Supporting declarations
One frame source arriving over a relay, conformed to the CP §4 contract (designs-implemented/LM-STREAM-RELAY-DESIGN.md §4).
An adapter is app code talking to Redis or a socket, and its frames feed spliceStreamText on a
browser — so the plane does not trust them. This pass enforces the frame invariants against the
prefix actually delivered and downgrades EVERY violation to a legal stale and nothing else:
stale already means "you are on the durable plane now, the store is the whole truth", so a
broken relay costs a reader smooth tokens, never corrupted text — and cannot wedge a producer.
Replayed spans (a reconnecting adapter re-delivering what it already sent) are ABSORBED rather than punished — deduping against the delivered prefix is what makes reconnect-replay safe without every adapter hand-rolling it. Spans that overlap the prefix but extend past it pass through whole: the client splices at the frame's own offset, so an exact overlap re-covers and appends.
Pure state, no I/O, no plane: feed maps one incoming frame to 0-2 outgoing frames (a missing
open is synthesized at the join offset); end/fail close out a source that finished or threw
without a terminal. After a terminal, every method returns [].
export declare class StreamRelayConform {
private readonly streamId;
/** The requested join offset — the synthesized `open`'s position, and where the prefix starts. */
private readonly from;
private readonly onViolation;
/** End of the delivered prefix. */
private pos;
private lastDurable;
private opened;
private done;
constructor(streamId: string, from: number, onViolation?: (reason: string) => void);
feed(frame: StreamFrame): StreamFrame[];
/** The source completed without a terminal (a truncated relay): the reader falls back. */
end(): StreamFrame[];
/** The source threw mid-iteration, or the plane is dropping a reader that stopped draining:
* a bare `stale` at the delivered position. */
fail(): StreamFrame[];
/** A synthesized join, for an adapter that (correctly, in broadcast mode) never mirrors the
* per-subscriber `open`: positioned at the requested offset, which the reader asked from because
* its durable view already holds it. */
private synthOpen;
private terminate;
private violate;
}StreamRelayErrorInfo
InterfaceDeclaration · Source: packages/api-server/src/streams.ts:207 · Supporting declarations
export interface StreamRelayErrorInfo {
streamId: string;
/** Where it failed: mirroring a frame out (`publish`), dialing the adapter (`attach`), or
* consuming/conforming its frames (`frames`). */
phase: "publish" | "attach" | "frames";
}streamRequestFromHttp
FunctionDeclaration · Source: packages/api-server/src/streams.ts:1662 · Supporting declarations
Pull a subscribe request out of a fetch-style GET: ?streamId=…&from=…, with Last-Event-ID
winning over an explicit from (a reconnecting EventSource knows better than its own URL —
the URL is the ORIGINAL join point, the header is where it actually got to).
export declare function streamRequestFromHttp(req: {
url: string;
headers: {
get(name: string): string | null;
};
}): {
streamId: string;
from: number;
};StreamStatus
TypeAliasDeclaration · Source: packages/client/src/stream.ts:19 · Supporting declarations
How a stream ended.
complete— the model finished.cancelled— the reader asked it to stop and the producer honoured it.error— the generation threw.interrupted— the host went away mid-generation. The one status that implies the store may be short of what was produced.
export type StreamStatus = "complete" | "cancelled" | "error" | "interrupted";StreamSubscription
InterfaceDeclaration · Source: packages/api-server/src/streams.ts:307 · Supporting declarations
export interface StreamSubscription {
readonly streamId: string;
/** Terminates after exactly one of `end` / `stale` / `absent`. */
readonly frames: AsyncIterable<StreamFrame>;
/** Detach early (a disconnected client). Idempotent. */
close(): void;
}StreamTables
InterfaceDeclaration · Source: packages/api-server/src/streams.ts:146 · Supporting declarations
The app's tables (§5). The app authors and migrates BOTH — the message row is unambiguously
app-owned (it has chatId, role, token counts) and the chunk row must be reachable from the
app's own query as a related subquery, which a Rindle system table would make awkward. The plane
only needs to be told where things live. Use {@link streamChunkTableDdl} for the chunk table's
migration.
The message table carries WHATEVER ELSE the app wants; the plane's hard requirements are only:
| mapped column | requirement | why |
|---|---|---|
key |
UNIQUE (normally the pk) | every checkpoint targets one row by it |
seq |
integer, NOT NULL DEFAULT 0 | the CAS column — nothing matches NULL (§3.2) |
body |
text, empty at open | compaction overwrites it with the whole response |
status |
text accepting streaming/complete/cancelled/error/interrupted |
a CHECK |
constraint that omits one of these turns a seal into an infra failure |
| cancel | truthy-readable, if mapped | read on the checkpoint round-trip (§6) |
| error | nullable text, if mapped | compaction writes NULL when there is no error |
| host | text, if mapped | written at open with the producer's token; the read-back decides the open race (§5.1) |
body is PLANE-OWNED and always a bare string. Rich content (an array of content blocks, tool
calls, attachments) belongs in SIBLING columns the app's own mutators write — flush() orders the
text before them. For genuinely multi-block streaming, point message at a per-BLOCK table
instead: streamId is just an app key, so one stream per block needs nothing from this plane.
export interface StreamTables {
/** The app's message table. Must already contain the row when {@link StreamPlane.open} runs. */
message: string;
/** The append-only chunk table. */
chunks: string;
columns?: StreamColumns;
}SubscribeStreamInput
InterfaceDeclaration · Source: packages/api-server/src/streams.ts:298 · Supporting declarations
export interface SubscribeStreamInput<User> {
user: User;
streamId: string;
/** "I already have this many characters" — from the client's IVM view, or a `Last-Event-ID`.
* A non-negative integer; default 0. */
from?: number;
request?: unknown;
}TableRenderMeta
InterfaceDeclaration · Source: packages/api-server/src/index.ts:1236 · Supporting declarations
Per-table metadata the renderer needs (all reachable from a TableMeta).
export interface TableRenderMeta {
/** Columns in schema (wire) order — the stable INSERT column list + completeness check. */
columns: string[];
/** Primary-key column NAMES — the WHERE / ON CONFLICT target / SET partition. */
pkNames: string[];
/** Column name → declared type (only consulted by {@link SqlDialect.encodeValue}). */
types: Record<string, ColType>;
/** Columns a full insert must name — the non-nullable ones (design 206 §6.2). */
required: string[];
/** The nullable (omittable-to-null) columns — an omitted one binds `NULL` (design 206 §6.2). */
nullable: ReadonlySet<string>;
}verifyRoomFlushCredential
FunctionDeclaration · Source: packages/api-server/src/index.ts:864 · Supporting declarations
Verify a flush credential's MAC, then its claims; returns the payload or throws. The MAC is checked FIRST — no claim is trusted before it passes.
export declare function verifyRoomFlushCredential(credential: string, shellSecret: string): Promise<RoomFlushCredentialPayload>;