API index and search · Build metadata
@rindle/daemon-client
0.0.0 · Public export map; development manifest version (0.0.0).
ClaimRoomEpochInput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:139 · Supporting declarations
/claim-room-epoch (RINDLE-REALTIME §2.5): bump + return the placement epoch.
export interface ClaimRoomEpochInput {
doc: string;
}ClaimRoomEpochOutput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:142 · Supporting declarations
export interface ClaimRoomEpochOutput {
epoch: number;
}DaemonHttpError
ClassDeclaration · Source: packages/daemon-client/src/index.ts:434 · Supporting declarations
export declare class DaemonHttpError extends Error {
readonly status: number;
readonly statusText: string;
readonly body: string;
constructor(status: number, statusText: string, body: string);
}DematerializeInput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:37 · Supporting declarations
export interface DematerializeInput {
materializationId?: string;
queryKey?: string;
name?: string;
}DematerializeOutput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:43 · Supporting declarations
export interface DematerializeOutput {
removed: boolean;
}FetchLike
TypeAliasDeclaration · Source: packages/daemon-client/src/index.ts:391 · Supporting declarations
export type FetchLike = (input: string, init: {
method: string;
headers: Record<string, string>;
body: string;
}) => Promise<FetchResponseLike>;FetchResponseLike
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:381 · Supporting declarations
export interface FetchResponseLike {
ok: boolean;
status: number;
statusText: string;
text(): Promise<string>;
/** Optional response headers — the real `fetch` Response exposes these. Used to read the
* daemon's `Rindle-Boot-Id` (see {@link HttpRindleDaemonClientOptions.onBootId}). */
headers?: {
get(name: string): string | null;
};
}HeadersFactory
TypeAliasDeclaration · Source: packages/daemon-client/src/index.ts:379 · Supporting declarations
export type HeadersFactory = () => HeadersInput | PromiseLike<HeadersInput>;HeadersInput
TypeAliasDeclaration · Source: packages/daemon-client/src/index.ts:378 · Supporting declarations
export type HeadersInput = Record<string, HeaderValue>;HeaderValue
TypeAliasDeclaration · Source: packages/daemon-client/src/index.ts:377 · Supporting declarations
export type HeaderValue = string | undefined;HttpRindleDaemonClient
ClassDeclaration · Source: packages/daemon-client/src/index.ts:448 · Supporting declarations
export declare class HttpRindleDaemonClient implements RindleDaemonClient {
private readonly baseUrl;
private readonly fetchImpl;
private readonly headers?;
private readonly paths;
private readonly onBootId?;
private lastBootId?;
constructor(opts: HttpRindleDaemonClientOptions);
materialize(input: MaterializeInput): Promise<MaterializeOutput>;
dematerialize(input: DematerializeInput): Promise<DematerializeOutput>;
executeSqlTxn(input: SqlTxn): Promise<SqlTxnOutput>;
executeSqlRead(input: SqlRead): Promise<SqlReadOutput>;
applyRowChangeTxn(input: RowChangeTxn): Promise<RowChangeTxnOutput>;
claimRoomEpoch(input: ClaimRoomEpochInput): Promise<ClaimRoomEpochOutput>;
roomLmids(input: RoomLmidsInput): Promise<RoomLmidsOutput>;
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>;
query(input: QueryOnceInput): Promise<QueryOnceOutput>;
migrate(input: MigrateInput): Promise<MigrateOutput>;
/** Apply an ordered SET of migrations in one round-trip — the array form of {@link migrate}. The
* daemon applies them in order (stopping at the first failure) and, when supervised, self-restarts
* ONCE for the whole set. Drive it from a migration runner (`rindle migrate apply` does). See
* {@link MigrateBatchOutput} — inspect its `error` field rather than relying on the HTTP status. */
migrateBatch(inputs: MigrateInput[]): Promise<MigrateBatchOutput>;
/** The daemon's introspected base-table schema for client-schema codegen
* (`DRIZZLE-MIGRATIONS-DESIGN.md` §6.2). Read-only `GET /schema`, bearer-auth'd when the daemon
* has a token configured. Deliberately NOT on {@link RindleDaemonClient}: schema codegen is a
* dev/CI concern, not part of the runtime control-plane contract a router/splitter satisfies. */
schema(): Promise<SchemaOutput>;
private post;
/** A bearer-auth'd GET (the read-only `/schema`, `/version`-style routes). Carries no request
* body — `defaultFetch` strips the placeholder before the real fetch (WHATWG fetch rejects a
* GET body). */
private get;
/** Notice the daemon's boot id on any response; fire `onBootId` on the first one and on every
* change (a restart). Updates `lastBootId` BEFORE the hook so a re-assert it triggers (which
* POSTs again, same boot id) does not re-fire — no recursion. */
private observeBootId;
private resolveHeaders;
}HttpRindleDaemonClientOptions
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:396 · Supporting declarations
export interface HttpRindleDaemonClientOptions {
baseUrl: string;
fetch?: FetchLike;
headers?: HeadersInput | HeadersFactory;
paths?: Partial<HttpRindleDaemonClientPaths>;
/** Fired with the daemon's boot id on the first control-plane response and again whenever it
* CHANGES — i.e. the daemon restarted (it keeps no durable lease/materialization state). Use
* it to re-assert pins / re-materialize. Treat every call as "(re)assert now"; it rides
* responses you already make (e.g. an ingester's writes), so no polling is needed. Must not
* throw or reject — handle your own errors (e.g. `onBootId: () => server.assertPins().catch(log)`). */
onBootId?: (bootId: string) => void;
}HttpRindleDaemonClientPaths
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:358 · Supporting declarations
export interface HttpRindleDaemonClientPaths {
materialize: string;
dematerialize: string;
executeSqlTxn: string;
executeSqlRead: string;
applyRowChangeTxn: string;
rejectMutation: string;
query: string;
migrate: string;
schema: string;
claimRoomEpoch: string;
roomLmids: string;
mutateSessionBegin: string;
mutateSessionExec: string;
mutateSessionQuery: string;
mutateSessionCommit: string;
mutateSessionRollback: string;
}MaterializationPolicy
TypeAliasDeclaration · Source: packages/daemon-client/src/index.ts:5 · Supporting declarations
export type MaterializationPolicy = {
kind: "pinned";
name?: string;
} | {
kind: "whileSubscribed";
idleTtlMs?: number;
};MaterializeInput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:9 · Supporting declarations
export interface MaterializeInput {
ast: unknown;
mode?: StreamMode;
policy?: MaterializationPolicy;
subject?: string;
leaseTtlMs?: number;
maxSubscribers?: number;
metadata?: Record<string, unknown>;
/** The browser's opaque follower-affinity ticket (FOLLOWER-AFFINITY-DESIGN.md §3), forwarded by
* the api-server. {@link HttpRindleDaemonClient} lifts it into the `Rindle-Affinity` request
* HEADER (never the body) so the fleet edge routes this `/materialize` to the follower the
* browser's ws is pinned to (§4) — both legs co-locate on one follower. Placement, not
* authorization: the follower verifies it, the api-server forwards it opaquely. Absent ⇒ no
* affinity (single daemon / affinity off), byte-identical to today. */
affinity?: string;
}MaterializeOutput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:26 · Supporting declarations
export interface MaterializeOutput {
materializationId: string;
queryKey?: string;
leaseToken: string;
reused?: boolean;
/** Fresh follower-placement ticket returned by an affinity-enabled daemon. A machine client
* opening a separate ws for this follower-local lease (for example a room upstream) offers it
* as a subprotocol so the fleet edge routes that socket back to the minting follower. */
affinity?: string;
}MigrateBatchOutput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:260 · Supporting declarations
The response to the ARRAY form of /migrate ({@link HttpRindleDaemonClient.migrateBatch}): a
whole ordered set applied in one round-trip so a supervised daemon self-restarts ONCE for the
set instead of once per migration. results are the migrations processed before any failure, in
order; applied counts the newly-applied ones; schemaVersion is the latest applied id;
restarting is true when the daemon is self-restarting to re-introspect (wait for it before the
next call). On a failed migration error/failedId name it — the batch stops there, earlier
migrations are committed (forward-only), and the daemon does NOT restart: fix forward and
re-send. Unlike the single-migration form, this always arrives as HTTP 200; inspect error.
export interface MigrateBatchOutput {
results: MigrateResultItem[];
applied: number;
schemaVersion?: string;
restarting?: boolean;
error?: string;
errorCode?: string;
failedId?: string;
}MigrateInput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:222 · Supporting declarations
One pure migration file: either ordered DDL statements or ordered DML statements. The opaque
id is the journaled idempotency key. checksum is the canonical SHA-256 content identity
sent by current deploy tooling; it is required for data migrations and optional only for
compatibility with legacy private DDL callers. overrideHash is an operator-reviewed checksum
for the content this migration originally applied with. DDL may be additive or destructive,
while RENAME and raw blob remain unsupported.
export interface MigrateInput {
id: string;
checksum?: string;
overrideHash?: string;
statements: string[];
}MigrateOutput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:235 · Supporting declarations
The /migrate response: applied is false when id was already journaled (an idempotent
replay ran nothing); hashOverridden reports that the supplied override matched the journaled
content identity; schemaVersion is the version the daemon will report AFTER it serves the new
shape (the latest applied migration id, DRIZZLE-MIGRATIONS-DESIGN.md §5.3). restarting is
true when the daemon is self-restarting to re-introspect (a supervised daemon with
RINDLE_RESTART_ON_MIGRATE) — the caller should wait for it to come back before its next call.
export interface MigrateOutput {
applied: boolean;
hashOverridden?: boolean;
schemaVersion?: string;
restarting?: boolean;
}MigrateResultItem
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:245 · Supporting declarations
One entry in a {@link MigrateBatchOutput}: the migration id, whether this apply ran it
(false = the id was already journaled, an idempotent no-op), and whether its override matched
the journaled content identity.
export interface MigrateResultItem {
id: string;
applied: boolean;
hashOverridden?: boolean;
schemaVersion?: string;
}MutationRejection
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:205 · Supporting declarations
export interface MutationRejection {
clientID: string;
mid: number;
reason?: string;
}MutationRejectionOutput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:211 · Supporting declarations
export interface MutationRejectionOutput {
cv?: number;
lmid?: number;
}MutationSessionBegin
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:164 · Supporting declarations
/mutate-session/begin (DAEMON-INTERACTIVE-TXN-DESIGN.md §4.1): open an interactive write
transaction on the master, held open across round trips under the daemon-owned deadline.
Client-attributed sessions (clientID + mid) get mid dedup/gap resolution UP FRONT — an
absorbed replay opens no session and the caller must skip the mutator. statements is the
accumulated write prefix to replay into the fresh transaction (sound: nothing before the
first read observed DB state); query rides the begin so a one-read mutator pays exactly
one extra round trip over the batch path.
export interface MutationSessionBegin {
clientID?: string;
mid?: number;
/** A foreign session's dedup watermark; see {@link WriteProducer}. Mutually exclusive with
* `clientID`/`mid`. */
producer?: WriteProducer;
statements?: SqlStatement[];
query?: SqlStatement;
}MutationSessionBeginOutput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:174 · Supporting declarations
export interface MutationSessionBeginOutput {
/** Present iff a session opened. */
sessionId?: string;
/** The `query` read's result, when one rode the begin (`{cols, rows}`, bare cell arrays). */
read?: SqlReadOutput;
/** True when mid/producer-watermark dedup absorbed the envelope at begin: NO session opened, the
* mutator must not run — the remaining fields are the authoritative replay output, the same
* shape {@link RindleDaemonClient.executeSqlTxn} answers for a deduped mid. */
absorbed?: boolean;
applied?: boolean;
lmid?: number;
lmidAdvances?: Array<{
clientID: string;
lmid: number;
}>;
}MutationSessionExec
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:188 · Supporting declarations
export interface MutationSessionExec {
sessionId: string;
statements: SqlStatement[];
}MutationSessionQuery
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:195 · Supporting declarations
A read through the open transaction (read-your-writes) — flat {sessionId, sql, params},
answered in the /execute-sql-read shape.
export interface MutationSessionQuery {
sessionId: string;
sql: string;
params?: WireValue[];
}MutationSessionRef
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:201 · Supporting declarations
export interface MutationSessionRef {
sessionId: string;
}QueryOnceInput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:272 · Supporting declarations
The SSR one-shot read (SSR-DESIGN.md §3). Materialize-or-reuse the query, read its current
view ONCE, return it assembled; registers no subscriber. visibilityKey/ttlMs are optional.
export interface QueryOnceInput {
ast: unknown;
visibilityKey?: string;
ttlMs?: number;
/** The browser's opaque follower-affinity ticket — see {@link MaterializeInput.affinity}. Lifted
* into the `Rindle-Affinity` header so an SSR / one-shot read co-locates on the same pinned
* follower the browser's ws is (or will be) on. */
affinity?: string;
}QueryOnceOutput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:284 · Supporting declarations
The POST /query response: assembled rows (nested by name, ready to hydrate without an
engine), the cvMin baseline they reflect, the per-query schema hello, and queryKey.
export interface QueryOnceOutput {
queryKey?: string;
cvMin?: number;
bootId?: string;
schema?: unknown;
rows: Array<{
cols: Record<string, WireValue>;
[rel: string]: unknown;
}>;
}RindleDaemonClient
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:320 · Supporting declarations
export interface RindleDaemonClient {
materialize(input: MaterializeInput): Promise<MaterializeOutput>;
dematerialize(input: DematerializeInput): Promise<DematerializeOutput>;
executeSqlTxn(input: SqlTxn): Promise<SqlTxnOutput>;
/** Raw SQL read (a single `SELECT` + params) against the latest committed snapshot, returning
* `{ cols, rows }` (bare row arrays). The read counterpart to {@link executeSqlTxn}. Under
* {@link SplitDaemonClient} this defaults to a replica (`consistency:"eventual"`); pass
* `consistency:"strong"` for read-your-writes on the master. */
executeSqlRead(input: SqlRead): Promise<SqlReadOutput>;
applyRowChangeTxn(input: RowChangeTxn): Promise<RowChangeTxnOutput>;
rejectMutation(input: MutationRejection): Promise<MutationRejectionOutput>;
/** SSR one-shot read (`SSR-DESIGN.md` §3): the current assembled view, no subscription. */
query(input: QueryOnceInput): Promise<QueryOnceOutput>;
/** Apply one schema migration through the daemon's controlled DDL channel + journal it
* (`DRIZZLE-MIGRATIONS-DESIGN.md` §5.1). Idempotent by `id`. The new shape is served only after
* the daemon is bounced (migrate-at-bounce, §5.2) — this applies DDL to the file but does not
* re-introspect the running engine. Drive it from a migration runner, not the request path. */
migrate(input: MigrateInput): Promise<MigrateOutput>;
/** Open an interactive mutation session (DAEMON-INTERACTIVE-TXN-DESIGN.md §4) — the write
* transaction a read-bearing mutator lazily upgrades onto. Optional as a group with the four
* ops below: only the daemon's control plane serves sessions (master-only — a follower's
* write-fence rejects begin); a client that lacks them forces the legacy committed-state
* read path. Session ops against a finished/expired session reject with a
* {@link DaemonHttpError} whose `status` is 410 — treat it as infra (retry the envelope;
* begin-time dedup absorbs a committed outcome). */
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>;
/** Claim the next placement epoch for a room's doc (RINDLE-REALTIME §2.5). Optional:
* only daemons serving as a room write authority implement it; the API server's
* room host refuses loudly when its configured client lacks it. */
claimRoomEpoch?(input: ClaimRoomEpochInput): Promise<ClaimRoomEpochOutput>;
/** The room's boot probe (§3.3). Optional, same contract as {@link claimRoomEpoch}. */
roomLmids?(input: RoomLmidsInput): Promise<RoomLmidsOutput>;
}RoomLmidsInput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:149 · Supporting declarations
/room-lmids (§3.3): the domain-scoped ledger's lmid per client under doc — the
room's boot probe. doc scopes the read to _rindle_room_client_mutations (§7.1), so a
client's room stream never aliases its slow-path daemon stream.
export interface RoomLmidsInput {
doc: string;
clients: string[];
}RoomLmidsOutput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:153 · Supporting declarations
export interface RoomLmidsOutput {
lmids: Record<string, number>;
}RowChangeTxn
TypeAliasDeclaration · Source: packages/daemon-client/src/index.ts:112 · Supporting declarations
export type RowChangeTxn = {
source: string;
offset: string;
changes: Array<{
table: string;
op: "add";
row: WireValue[];
} | {
table: string;
op: "remove";
old: WireValue[];
} | {
table: string;
op: "edit";
old: WireValue[];
row: WireValue[];
}>;
/** The room write-behind extensions (RINDLE-REALTIME §5.3, each opt-in by
* presence): the placement fence (`doc` + `epoch` — a stale epoch gets a
* `409 {error:"fenced"}`), the §8.3 batch identity (`batchHash` — same id +
* different body is a loud 500), and the CAS preconditions (`cas: true` — a miss
* gets a `409 {error:"conflict", conflicts}` with authoritative images, nothing
* applied). Plain change sources send none and are byte-compatible unchanged. */
doc?: string;
epoch?: number;
batchHash?: string;
cas?: boolean;
};RowChangeTxnOutput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:132 · Supporting declarations
export interface RowChangeTxnOutput {
/** `false` = the `(source, offset)` keyset absorbed a replay. */
applied?: boolean;
cv?: number;
}SchemaColumn
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:293 · Supporting declarations
One column in a {@link SchemaTable}: its name and the daemon's ColType wire name.
export interface SchemaColumn {
name: string;
/** The daemon's `ColType` wire name. Because the daemon only ever *introspects* the SQLite file
* and affinity is lossy, this is `"string"` or `"number"` in practice — a column's app-level
* `"boolean"`/`"json"` intent is not recoverable from the file (`DRIZZLE-MIGRATIONS-DESIGN.md`
* §6.2). The full vocabulary is `"string" | "number" | "boolean" | "json"`. */
type: string;
/** `true` when the column is nullable (introspected `pragma_table_info.notnull == 0`); the
* generated column becomes `.nullable()`, typing it `T | null`. PK columns are always `false`
* (row identity). Absent on older daemons ⇒ treat as non-nullable. See design 206. */
nullable?: boolean;
}SchemaOutput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:316 · Supporting declarations
The daemon's introspected base-table schema (GET /schema) — the input to client-schema
codegen (DRIZZLE-MIGRATIONS-DESIGN.md §6.2). Tables are sorted by name and the daemon's own
bookkeeping tables (_rindle_*) are excluded.
export interface SchemaOutput {
tables: SchemaTable[];
}SchemaTable
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:307 · Supporting declarations
One base table in the daemon's introspected schema: its name, ordered columns, and PK columns.
export interface SchemaTable {
name: string;
columns: SchemaColumn[];
primaryKey: string[];
}SqlRead
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:97 · Supporting declarations
A raw read: one SELECT and its positional parameters. The read counterpart to {@link SqlTxn}
— no idempotency/mutation fields (reads are idempotent).
consistency / routingKey are client-tier routing directives: consistency is consumed
by {@link SplitDaemonClient }, routingKey by the read router. They ride along the wire (the
router is reached through an HttpRindleDaemonClient, so routingKey must) but the daemon
itself ignores them and reads only sql + params. consistency defaults to "eventual"
(route to a replica, scaling reads off the write-master); set "strong" to route to the master
for read-your-writes after a write to the same data. routingKey co-locates a replica read on a
chosen follower (e.g. by tenant); absent, the router places it by the SQL text.
export interface SqlRead {
sql: string;
params?: WireValue[];
consistency?: "eventual" | "strong";
routingKey?: string;
}SqlReadOutput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:107 · Supporting declarations
The /execute-sql-read response: result columns in order and each row as a bare cell array
(NOT keyed objects — the daemon keeps per-cell object construction off the write-master; zip
cols with each row client-side if you want objects). Lossless for duplicate/aliased columns.
export interface SqlReadOutput {
cols: string[];
rows: WireValue[][];
}SqlStatement
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:47 · Supporting declarations
export interface SqlStatement {
sql: string;
params?: WireValue[];
}SqlTxn
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:66 · Supporting declarations
export interface SqlTxn {
/** A foreign write's dedup watermark. Mutually exclusive with `clientID`/`mid` — supplying
* both is a `400`, because a silent precedence rule is what let the two write authorities
* disagree about which receipt they stored. */
producer?: WriteProducer;
clientID?: string;
mid?: number;
statements: SqlStatement[];
}SqlTxnOutput
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:76 · Supporting declarations
export interface SqlTxnOutput {
applied?: boolean;
cv?: number;
txId?: number | string;
/** Replicator-master commit identity (the public mutation SQL facade calls this `cursor`). */
cursor?: string;
/** Flat convenience mirror of this client's entry in `lmidAdvances`. */
lmid?: number;
lmidAdvances?: Array<{
clientID: string;
lmid: number;
}>;
}StreamMode
TypeAliasDeclaration · Source: packages/daemon-client/src/index.ts:3 · Supporting declarations
export type StreamMode = "flat" | "normalized";WireValue
TypeAliasDeclaration · Source: packages/daemon-client/src/index.ts:1 · Supporting declarations
export type WireValue = number | string | boolean | null;WriteProducer
InterfaceDeclaration · Source: packages/daemon-client/src/index.ts:61 · Supporting declarations
A foreign (non-client) writer's durable identity: a stable, low-cardinality id plus that
producer's own gapless sequence — the same (producer, sequence) model Kafka uses.
The daemon stores ONE row per producer and overwrites it in place, so the dedup state is
bounded by how many producers you have, not by how much you write. The contract that buys:
number a producer's writes 1, 2, 3, … and send them in order. seq <= last is absorbed
(applied: false, nothing runs), seq == last + 1 applies, and anything beyond is a 409
gap. Express concurrency with several producer ids, not by fanning out under one — and keep
ids stable and few, because a fresh id per process re-introduces unbounded growth.
export interface WriteProducer {
id: string;
seq: number;
}