Testing
import * as mod from "jsr:@nzip/lofi/testing";
The @nzip/lofi/testing public surface: Playwright-backed helpers for testing
local-first behavior, including two-client fixtures, concurrent offline
convergence, app-owned readiness waits, value-free failure artifacts, and a
CDP virtual authenticator for headless WebAuthn flows.
The supported Playwright release is 1.61 — the playwright alias in the
workspace import map. Install a matching Chromium with
deno run -A npm:[email protected] install chromium.
The scenario surface (scenario, converge, and the row assertions) turns
sync simulation testing into plain test declarations: named peers make
concurrent and offline edits through the app's own schema, and the test
asserts they converge. Headless scenarios drive real synced clients against
a local sync server and need FFI permissions; browser scenarios drive the
two-client Playwright fixture against a served app; scenario.fuzz runs
seeded, replayable operation sequences for property-style coverage.
Alongside the browser fixtures, this entry re-exports the deterministic seams of the schema and runtime registries — key installation, registry clears, and fake runtime installation — so a test can isolate schema declarations, encrypted and shared-field key state, write handles, and verb dispatch without booting a runtime. Production code never calls these: the runtime owns each seam at boot, and calling one in an application corrupts the state the runtime maintains.
assertNoRow
function
async function assertNoRow(peer: ScenarioPeer<unknown>, table: ScenarioTableRef<unknown>, id: string): Promise<void>
Assert that a peer's local view does not contain the row.
assertRow
function
async function assertRow<Row>(peer: ScenarioPeer<unknown>, table: ScenarioTableRef<Row>, id: string, partial?: Partial<Row>): Promise<void>
Assert that a peer's local view contains the row, optionally matching a subset of its columns.
assertRowCount
function
async function assertRowCount(peer: ScenarioPeer<unknown>, table: ScenarioTableRef<unknown>, expected: number): Promise<void>
Assert the number of rows a peer's local view of a table contains.
assertValueFreeState
function
function assertValueFreeState(value: unknown, path: string): asserts value is ValueFreeState
Assert that value satisfies the value-free rule: strings and non-finite
numbers are rejected, so state artifacts record shape, counts, and booleans
but never user values or credentials. Snapshot callbacks passed to the
fixtures are validated with this at capture time; call it directly to
pre-validate a snapshot callback's output in a fast test.
| Parameter | Description |
|---|---|
value | The candidate snapshot value. |
path | The label used for the offending location in error messages. |
BrowserTestClient
class
class BrowserTestClient {
constructor(name: ClientName, context: BrowserContext, page: Page, baseURL: string, createContext: (state?: MemoryStorageState) => Promise<BrowserContext>, redact: (value: string) => string, traceOnFailure: boolean);
get context(): BrowserContext;
get page(): Page;
get offline(): boolean;
get diagnostics(): readonly BrowserDiagnostic[];
startRecording(): Promise<void>;
goOffline(): Promise<void>;
goOnline(): Promise<void>;
reloadPage(): Promise<Page>;
restartPage(): Promise<Page>;
restartClient(options: { preserveIdentity?: boolean }): Promise<Page>;
captureTrace(path: string): Promise<boolean>;
close(): Promise<void>;
}
One browser client (context + page) whose identity and IndexedDB stay in memory. Records redacted diagnostics and can go offline/online, restart, and capture a sanitized trace.
BrowserUnavailableError
class
class BrowserUnavailableError extends Error {
constructor(options?: ErrorOptions);
readonly name: string;
}
Thrown when Playwright's Chromium browser is not installed, with install guidance.
clearEffectDeclarations
function
function clearEffectDeclarations(): void
Clears every declaration; deterministic-test seam.
clearEncryptedColumnKey
function
function clearEncryptedColumnKey(): void
Forgets the installed key; encrypted columns fail closed afterwards.
clearEncryptedColumnRegistry
function
function clearEncryptedColumnRegistry(): void
Empties the encrypted-column registry; tests call this between schemas.
clearFingerprintPins
function
function clearFingerprintPins(appId: string): void
Forgets every pin for an app; tests call this between scenarios.
clearSharedColumnRegistry
function
function clearSharedColumnRegistry(): void
Empties the registry; tests call this between schemas.
clearSharedFieldKeys
function
function clearSharedFieldKeys(): void
Empties every installed field key; tests and logout call this.
converge
function
async function converge<A>(...args: readonly ((ScenarioPeer<A> | ScenarioPeerControls | ConvergeOptions<A>))[]): Promise<void>
Assert that all peers converge to identical views. Settles every peer's
issued writes, then polls until every peer reads the same state, or fails
at the deadline with a per-peer diff. All peers must be online. Headless
peers compare their tables row by row; browser peers compare their
app-supplied value-free snapshots. Accepts an options object after the
peers: converge(alice, bob, { timeoutMs: 30_000 }).
ConvergenceScenarioError
class
class ConvergenceScenarioError extends Error {
constructor(stage: ConvergenceStage, options?: ConvergenceScenarioErrorOptions);
readonly name: string;
readonly captureError?: unknown;
}
Thrown when a convergence scenario fails, naming the stage that failed via stage.
createTwoClientFixture
function
async function createTwoClientFixture(options: TwoClientFixtureOptions): Promise<TwoClientFixture>
Launch (or reuse) a browser and build a TwoClientFixture: opens both
clients at baseURL, applies the chosen identity mode with state kept in
memory, and starts recording. Cleans up on any setup failure.
| Parameter | Description |
|---|---|
options | The base URL, identity mode, and optional browser, context, and artifact settings. |
Returns: A ready fixture whose two clients are open at baseURL with identity applied.
Example
const fixture = await createTwoClientFixture({
baseURL,
identity: { mode: "shared", preparePrimary: (client) => ready(client) },
artifacts: { directory: "test-results" },
});
try {
await fixture.goOffline();
// ...apply edits on fixture.first and fixture.second, then converge...
} finally {
await fixture.close();
}
createWriteHandle
function
function createWriteHandle<T>(writeId: string): { handle: WriteHandle<T>; controller: WriteHandleController<T> }
Creates a handle in saving together with its controller — the only path
to a handle's lifecycle mutators. The ledger keeps the controller and hands
the observe-only handle to application code.
generateFuzzPlan
function
function generateFuzzPlan(input: FuzzPlanInput): FuzzPlan
Generate a fuzz plan: a deterministic, seed-replayable sequence of inserts, updates, removes, offline windows, and sync barriers across the peers. Updates and removes only target rows their peer has locally created or has observed through a sync barrier; offline windows are bounded; the plan ends with every peer back online.
getSharedFieldKey
function
function getSharedFieldKey(scope: string, generation: number): Uint8Array | null
The installed key for a scope and generation, or null while pending.
installSharedFieldKey
function
function installSharedFieldKey(scope: string, generation: number, key: Uint8Array): void
Installs an unwrapped field key and notifies keyring subscribers.
latestSharedFieldGeneration
function
function latestSharedFieldGeneration(scope: string): number | null
The newest generation installed for a scope, or null when none is.
memoryPopKeyStore
function
function memoryPopKeyStore(): PopKeyStore
A process-lifetime store for tests and non-browser environments.
ReadinessError
class
class ReadinessError extends Error {
constructor(description: string, options?: ErrorOptions);
readonly name: string;
}
Thrown when the readiness predicate does not become true before the timeout.
redactDiagnosticText
function
function redactDiagnosticText(value: string, secretValues: readonly string[]): string
Redact common credential forms from diagnostic text: URL userinfo, query,
and fragment components; JSON and key=value secret assignments; bearer
tokens; and every literal in secretValues. The fixtures apply this to all
retained diagnostics; apply it likewise to any custom capture path.
| Parameter | Description |
|---|---|
value | The diagnostic text to redact. |
secretValues | Literal credential values to remove wherever they appear. |
Returns: The text with credential forms replaced by [redacted] markers.
runConcurrentOfflineConvergence
function
async function runConcurrentOfflineConvergence<Edit, Client extends OfflineTestClient>(fixture: OfflineTestFixture<Client>, scenario: ConcurrentOfflineScenario<Edit, Client>): Promise<void>
Coordinates the transport lifecycle while the app owns edits, assertions and conflict semantics. Both offline edits are started in the same microtask.
scenario
const
const scenario: ScenarioApi;
Declare simulation-test scenarios: named peers make concurrent and offline edits through the app's own schema, and the test asserts they converge.
scenario("offline rename vs remote delete", { app, permissions }, async ({ alice, bob }) => {
const doc = await alice.db.documents.insert({ title: "Untitled" });
await converge(alice, bob);
await alice.offline();
await alice.db.documents.update(doc.id, { title: "Draft" });
await bob.db.documents.remove(doc.id);
await alice.online();
await alice.settle();
await alice.restart(); // the live writer keeps its doomed rename until it restarts
await converge(alice, bob);
await assertNoRow(alice, app.documents, doc.id);
});
ScenarioError
class
class ScenarioError extends Error {
constructor(stage: ScenarioStage, message: string, options?: ScenarioErrorOptions);
readonly name: string;
readonly peer?: string;
readonly details?: string;
}
Thrown when a scenario fails, naming the lifecycle stage that failed via stage.
setEncryptedColumnKey
function
function setEncryptedColumnKey(key: Uint8Array): void
Installs the account-derived 32-byte master key for encrypted columns. The runtime calls this at boot (before creating the database client) and again whenever the account secret changes; tests inject a fixed key.
setMutationRuntime
function
function setMutationRuntime(runtime: MutationRuntime): void
Installs the runtime half verbs dispatch through. The package runtime installs it at boot; application code never calls this. Tests install a deterministic fake to unit-test verbs without a booted runtime.
sharedColumnConfigs
function
function sharedColumnConfigs(): SharedColumnConfig[]
Every registered shared-column configuration.
sharedKeyScope
function
function sharedKeyScope(groupTable: string, groupId: string): string
The keyring scope of a group resource: "groupTable/groupId".
subscribeSharedKeyring
function
function subscribeSharedKeyring(listener: () => void): () => void
Subscribes to keyring changes. Live-query stores resubscribe on change so rows previously surfaced as pending re-materialize into plaintext.
TwoClientFixture
class
class TwoClientFixture {
constructor(browser: Browser, ownsBrowser: boolean, first: BrowserTestClient, second: BrowserTestClient, artifacts: FailureArtifactOptions | undefined);
readonly clients: readonly [BrowserTestClient, BrowserTestClient];
get first(): BrowserTestClient;
get second(): BrowserTestClient;
goOffline(): Promise<void>;
goOnline(): Promise<void>;
captureFailure(label: string, snapshot?: (client: BrowserTestClient) => Promise<ValueFreeState>): Promise<FailureArtifacts | undefined>;
close(): Promise<void>;
}
Coordinates a pair of BrowserTestClients sharing one browser, with
helpers to take both clients offline/online and to capture redacted,
value-free failure artifacts.
waitForReady
function
function waitForReady(page: Page, predicate: () => boolean | Promise<boolean>, options?: ReadinessOptions): Promise<void>
function waitForReady<Argument>(page: Page, predicate: (argument: Argument) => boolean | Promise<boolean>, argument: Argument, options?: ReadinessOptions): Promise<void>
async function waitForReady<Argument>(page: Page, predicate: (argument: Argument) => boolean | Promise<boolean>, argumentOrOptions?: Argument | ReadinessOptions, maybeOptions?: ReadinessOptions): Promise<void>
Wait for an app-owned browser predicate that takes no argument. The predicate runs in the page and Playwright owns the timeout/polling, so tests do not need arbitrary sleeps.
| Parameter | Description |
|---|---|
page | The Playwright page to poll. |
predicate | An app-owned readiness check evaluated inside the page. |
options | Optional description, timeout, and polling configuration. |
Returns: Resolves when the predicate becomes true; rejects with ReadinessError on timeout.
Example
import { waitForReady } from "@nzip/lofi/testing";
await waitForReady(client.page, () => document.querySelector(".task-list") !== null, {
description: "task list rendered",
});
withVirtualAuthenticator
function
async function withVirtualAuthenticator(page: Page, options: VirtualAuthenticatorOptions): Promise<VirtualAuthenticatorHandle>
Install a CDP virtual authenticator on page and return a handle that removes
it on VirtualAuthenticatorHandle.dispose. Defaults model an internal,
resident, user-verifying platform authenticator that auto-satisfies presence.
WriteHandle
class
class WriteHandle<T> implements PromiseLike<T> {
constructor(writeId: string);
get writeId(): string;
get batchId(): string | null;
get stage(): WriteStage;
get reason(): WriteRejection | null;
get saved(): Promise<T>;
get synced(): Promise<T>;
then<Fulfilled = T, Rejected = never>(onfulfilled?: ((value: T) => Fulfilled | PromiseLike<Fulfilled>) | null, onrejected?: ((reason: unknown) => Rejected | PromiseLike<Rejected>) | null): Promise<Fulfilled | Rejected>;
subscribe(listener: () => void): () => void;
}
A single write observed through the author-facing lifecycle.
await write (the thenable) resolves at saved with the write's value —
for inserts, the created row. write.synced resolves when the store
confirms the write and rejects with WriteRejectedError when the
store denies it. stage and reason are current-state properties;
subscribe notifies immediately and on every later change.
On a device without managed sync there is no store to confirm anything:
local durability is settlement, and the handle reaches synced as soon as
it is saved. In Preact components, render a handle with useWrite and
the app-wide pending set with usePendingWrites.
Handles are issued by the runtime and are observe-only: the lifecycle mutators live on a controller the ledger keeps at construction, so no consumer of a handle can advance or settle it.
Example
const write = placeOrder({ sku, quantity }); // a verb returns a WriteHandle
const order = await write; // resolves at saved — safe to navigate
write.synced.catch((error) => {
if (error instanceof WriteRejectedError) showDenied(error.message);
});
BrowserDiagnostic
interface
interface BrowserDiagnostic {
readonly client: ClientName;
readonly kind: "console" | "page-error" | "request-failed";
readonly level?: string;
readonly message: string;
readonly url?: string;
}
A redacted console, page-error, or failed-request record captured from a client.
BrowserScenarioContext
interface
interface BrowserScenarioContext {
readonly alice: BrowserScenarioPeer;
readonly bob: BrowserScenarioPeer;
}
The peers a browser scenario body receives.
BrowserScenarioOptions
interface
interface BrowserScenarioOptions {
baseURL: string | undefined;
identity: IdentityOptions;
ready: (peer: BrowserScenarioPeer) => Promise<void>;
snapshot: (peer: BrowserScenarioPeer) => Promise<ValueFreeState>;
artifacts?: FailureArtifactOptions;
timeoutMs?: number;
}
Configuration for a browser scenario.
BrowserScenarioPeer
interface
interface BrowserScenarioPeer extends ScenarioPeerControls {
readonly client: BrowserTestClient;
readonly page: Page;
}
A browser scenario peer: the scenario controls over a real browser client.
There is no typed db here — the app's data lives inside the page, so
edits go through the page and state is observed through the scenario's
value-free snapshot hook.
ClientName
type
type ClientName = "first" | "second";
Identifies which of the fixture's two clients a value belongs to.
ConcurrentOfflineScenario
interface
interface ConcurrentOfflineScenario<Edit, Client extends OfflineTestClient = BrowserTestClient> {
readonly edits: readonly [Edit, Edit];
readonly timeoutMs?: number;
readonly ready: (client: Client, signal: AbortSignal) => Promise<void>;
readonly apply: (client: Client, edit: Edit, signal: AbortSignal) => Promise<void>;
readonly locallyApplied: (client: Client, edit: Edit, signal: AbortSignal) => Promise<void>;
readonly whilePending?: (fixture: OfflineTestFixture<Client>, signal: AbortSignal) => Promise<void>;
readonly converged: (fixture: OfflineTestFixture<Client>, signal: AbortSignal) => Promise<void>;
readonly snapshot?: (client: Client) => Promise<ValueFreeState>;
readonly failureLabel?: string;
}
App-owned definition of a concurrent-offline convergence test: the two edits plus the hooks that assert readiness, apply and locally verify each edit, and confirm convergence after reconnection.
ConvergenceScenarioErrorOptions
interface
interface ConvergenceScenarioErrorOptions extends ErrorOptions {
captureError?: unknown;
}
Options for ConvergenceScenarioError, extending ErrorOptions with the capture outcome.
ConvergenceStage
type
type ConvergenceStage =
| "readiness"
| "going offline"
| "concurrent edits"
| "local persistence"
| "pending-work hook"
| "reconnection"
| "convergence";
The lifecycle stages the convergence runner moves through, in order.
ConvergeOptions
interface
interface ConvergeOptions<A> {
tables?: readonly ScenarioTableKey<A>[];
timeoutMs?: number;
pollMs?: number;
counterColumns?: "presence" | "value";
}
Options accepted by converge alongside the peers.
DevicePublicKey
type
type DevicePublicKey = {
alg: "ES256";
spki: string;
};
The device public key offered at the scope-down exchange.
EffectContext
type
type EffectContext = {
journalId: string;
writeId: string;
verb: string | null;
table: string;
op: "insert" | "update" | "remove";
rowId: string;
writeCreatedAt: number;
fate: "synced" | "rejected";
cause: "denied" | "expired" | null;
code: string | null;
reason: string | null;
};
Delivery metadata passed to every effect handler. Delivery is
at-least-once: a crash between handler start and journal completion re-runs
the handler at the next boot, so handlers calling external services should
pass EffectContext.journalId as an idempotency key.
FailureArtifactOptions
interface
interface FailureArtifactOptions {
readonly directory: string;
readonly secretValues?: readonly string[];
readonly mask?: (client: BrowserTestClient) => readonly Locator[];
}
Configures where and how redacted failure artifacts are written on capture.
FailureArtifacts
interface
interface FailureArtifacts {
readonly directory: string;
readonly files: readonly string[];
}
The directory and file paths produced by a failure capture.
FuzzColumn
interface
interface FuzzColumn {
name: string;
type: string;
nullable: boolean;
hasDefault: boolean;
mergeStrategy?: string;
references?: string;
}
One column of a table, as the fuzz generator sees it.
FuzzOp
type
type FuzzOp =
| { kind: "insert"; peer: string; table: string; values: Record<string, unknown>; ref: number }
| { kind: "update"; peer: string; table: string; ref: number; patch: Record<string, unknown> }
| { kind: "remove"; peer: string; table: string; ref: number }
| { kind: "offline"; peer: string }
| { kind: "online"; peer: string }
| { kind: "sync" };
One step of a fuzz plan. Row-targeting steps reference the insert that
created the row by its ref — the index of that insert in the plan — since
row ids are only assigned when the plan runs.
FuzzOpKind
type
type FuzzOpKind =
| "insert"
| "update"
| "remove"
| "offline"
| "online"
| "sync";
The kinds of operations a fuzz plan is built from.
FuzzPlan
interface
interface FuzzPlan {
seed: number;
ops: readonly FuzzOp[];
skippedTables: readonly string[];
}
A generated fuzz plan: the operations plus what the generator left out.
FuzzPlanInput
interface
interface FuzzPlanInput {
seed: number;
steps: number;
peers: readonly string[];
tables: { [table: string]: readonly FuzzColumn[] };
weights?: Partial<Record<FuzzOpKind, number>>;
}
Input to the pure fuzz-plan generator.
FuzzScenarioOptions
interface
interface FuzzScenarioOptions<A extends ScenarioApp> extends ScenarioConfig<A> {
seed?: number;
steps?: number;
tables?: readonly string[];
weights?: Partial<Record<FuzzOpKind, number>>;
}
Options for a fuzz scenario, extending the scenario config with fuzz knobs.
IdentityOptions
type
type IdentityOptions = { readonly mode: "shared"; readonly preparePrimary: (client: BrowserTestClient) => Promise<void> } | { readonly mode: "isolated"; readonly prepare?: (client: BrowserTestClient) => Promise<void> };
How the two clients obtain identity: shared clones the primary's prepared
state (in memory) into the second client, isolated prepares each client on
its own.
MemoryStorageState
type
type MemoryStorageState = Awaited<ReturnType<BrowserContext["storageState"]>>;
Playwright storage state held only in memory — cookies plus per-origin
localStorage and IndexedDB — used to clone a prepared identity into a
fresh browser context without an on-disk state file.
MutationDescriptor
type
type MutationDescriptor = {
readonly verbName: string;
readonly op: MutationOp<unknown, unknown>;
readonly units: readonly EffectUnit<{ id: string }>[];
readonly expiresAfterMs: number | null;
};
The registered declaration the runtime dispatches for one verb.
MutationRuntime
type
type MutationRuntime = {
dispatch(descriptor: MutationDescriptor, args: readonly unknown[]): WriteHandle<unknown>;
dispatchChained(descriptor: MutationDescriptor, args: readonly unknown[], parentJournalId: string): Promise<void>;
recordLog(label: string, context: EffectContext): void;
recordTrace?(label: string | null, context: EffectContext): void;
recordDebug?(event: string, context: EffectContext): void;
enqueueNotice?(input: NoticeInput, context: EffectContext): Promise<void>;
applyMark?(table: TableProxy<unknown, unknown>, rowId: string, patch: Record<string, unknown>): Promise<void>;
unitRegistered?(name: string): void;
};
The runtime half installed by the package runtime before verbs are called.
NoticeInput
type
type NoticeInput = {
message: string;
tone: "info" | "success" | "warning" | "error";
ttlMs: number | null;
};
One durable notice a notice unit enqueues. The queue is persistent
and UI-agnostic: entries may be created at a boot re-arm with nothing
mounted, and a component renders them later. tone classifies the message
for the render; ttlMs bounds its life when the author sets no explicit
dismissal.
OfflineTestClient
interface
interface OfflineTestClient {
readonly offline: boolean;
goOffline(): Promise<void>;
goOnline(): Promise<void>;
}
Minimal client contract the convergence runner needs: offline state and toggles.
OfflineTestFixture
interface
interface OfflineTestFixture<Client extends OfflineTestClient> {
readonly clients: readonly [Client, Client];
readonly first: Client;
readonly second: Client;
goOffline(): Promise<void>;
goOnline(): Promise<void>;
captureFailure(label: string, snapshot?: (client: Client) => Promise<ValueFreeState>): Promise<unknown>;
}
Minimal two-client fixture contract required to drive an offline scenario.
PopKeyStore
type
type PopKeyStore = {
get(keyId: string): Promise<CryptoKeyPair | null>;
put(keyId: string, pair: CryptoKeyPair): Promise<void>;
};
Storage for the device keypair; IndexedDB in browsers.
ReadinessOptions
interface
interface ReadinessOptions {
description?: string;
timeoutMs?: number;
polling?: "raf" | number;
}
Options controlling waitForReady's description, timeout, and polling.
SafeContextOptions
type
type SafeContextOptions = Omit<BrowserContextOptions, "storageState"> & { storageState?: never };
Playwright context options with storageState forbidden, so identity never
leaves memory via an on-disk state file.
ScenarioApi
interface
interface ScenarioApi {
fuzz<A extends ScenarioApp>(name: string, options: FuzzScenarioOptions<A>): void;
browser(name: string, options: BrowserScenarioOptions, body: (context: BrowserScenarioContext) => Promise<void>): void;
}
The scenario entry points. The call signature registers a headless
scenario; ScenarioApi.fuzz and ScenarioApi.browser register
the fuzz and browser flavors. Each call registers one Deno.test, so
scenarios are declared at the top level of a test module.
ScenarioApp
type
type ScenarioApp = {
readonly wasmSchema: { [table: string]: { readonly columns: readonly { readonly name: string; readonly nullable: boolean; readonly default?: unknown; readonly references?: string; readonly column_type: { readonly type: string }; readonly merge_strategy?: string }[] } };
};
The app-object shape the headless adapter needs: table handles plus the compiled schema.
ScenarioConfig
interface
interface ScenarioConfig<A extends ScenarioApp> {
app: A;
permissions: CompiledPermissions;
timeoutMs?: number;
}
The app under test plus the scenario's own settings.
ScenarioContext
interface
interface ScenarioContext<A> {
readonly alice: ScenarioPeer<A>;
readonly bob: ScenarioPeer<A>;
addPeer(name: string): Promise<ScenarioPeer<A>>;
}
The peers a scenario body receives, plus a factory for additional ones.
ScenarioDb
type
type ScenarioDb<A> = { [mapped type] };
The app's tables as scenario facades, keyed by the app's own table names.
ScenarioErrorOptions
interface
interface ScenarioErrorOptions extends ErrorOptions {
peer?: string;
details?: string;
}
Options for ScenarioError, extending ErrorOptions with scenario context.
ScenarioPeer
interface
interface ScenarioPeer<A> extends ScenarioPeerControls {
readonly db: ScenarioDb<A>;
restart(): Promise<void>;
}
A headless scenario peer: a real synced client with the scenario controls
plus the app's typed table API. Writes issued through db apply to
this peer's local view immediately — pair them with
ScenarioPeerControls.settle or a convergence assertion when the
scenario needs them visible elsewhere.
ScenarioPeerControls
interface
interface ScenarioPeerControls {
readonly name: string;
readonly isOffline: boolean;
offline(): Promise<void>;
online(): Promise<void>;
settle(options?: ScenarioSettleOptions): Promise<void>;
}
The controls every scenario peer implements, headless or browser: a stable name, an offline window toggle, and a durability barrier for issued writes.
ScenarioReadOptions
interface
interface ScenarioReadOptions {
tier?: "local" | "global";
}
Options for reads through a ScenarioTable.
ScenarioSettleOptions
interface
interface ScenarioSettleOptions {
timeoutMs?: number;
}
Options for ScenarioPeerControls.settle.
ScenarioStage
type
type ScenarioStage =
| "boot"
| "deploy"
| "peers"
| "body"
| "convergence"
| "assertion"
| "teardown";
The lifecycle stages a scenario moves through, in order.
ScenarioTable
interface
interface ScenarioTable<Row, Init, Where> {
insert(values: Init): Promise<Row>;
update(id: string, patch: Partial<Init>): Promise<void>;
remove(id: string): Promise<void>;
all(where?: Where, options?: ScenarioReadOptions): Promise<readonly Row[]>;
get(id: string, options?: ScenarioReadOptions): Promise<Row | undefined>;
}
One table of a headless peer's typed facade: the app's insert/update/remove
writes, applied to the peer's local view immediately (online or offline),
plus local-view reads. Writes issued here are tracked by the owning peer so
ScenarioPeerControls.settle can wait for their global durability.
ScenarioTableKey
type
type ScenarioTableKey<A> = { [mapped type] }[keyof A] & string;
The table names of an app: every key whose value is a table handle. The
app object's non-table members (union, wasmSchema) are excluded by
shape, not by name, so future non-table members stay excluded too.
ScenarioTableRef
type
type ScenarioTableRef<Row> = {
readonly _table: string;
readonly _rowType: Row;
};
A reference to one of the app's tables, as the row assertions accept it:
any of the app object's own table handles (e.g. app.documents) matches.
SharedColumnConfig
type
type SharedColumnConfig = {
label: string;
kind: "text" | "json";
group: string;
groupIdColumn: string;
keys: string;
directory: string;
};
The wiring one shared column declares: where its group and keys live.
TwoClientFixtureOptions
interface
interface TwoClientFixtureOptions {
readonly baseURL: string;
readonly identity: IdentityOptions;
readonly browser?: Browser;
readonly context?: SafeContextOptions;
readonly artifacts?: FailureArtifactOptions;
readonly traceOnFailure?: boolean;
}
Options for constructing a TwoClientFixture via createTwoClientFixture.
ValueFreeState
type
type ValueFreeState =
| null
| boolean
| number
| { [key: string]: ValueFreeState }
| readonly ValueFreeState[];
A JSON-like value restricted to booleans, finite numbers, arrays, and plain objects. Strings are excluded so state snapshots record shape and counts but never user values or credentials.
VirtualAuthenticatorCredential
interface
interface VirtualAuthenticatorCredential {
readonly credentialId: string;
readonly isResidentCredential: boolean;
readonly rpId?: string;
readonly privateKey: string;
readonly userHandle?: string;
readonly signCount: number;
readonly largeBlob?: string;
}
Serializable CDP credential used to copy one virtual passkey between browser profiles.
VirtualAuthenticatorHandle
interface
interface VirtualAuthenticatorHandle {
readonly authenticatorId: string;
credentials(): Promise<VirtualAuthenticatorCredential[]>;
addCredential(credential: VirtualAuthenticatorCredential): Promise<void>;
clearCredentials(): Promise<void>;
dispose(): Promise<void>;
}
A handle to an installed virtual authenticator; call dispose to remove it.
VirtualAuthenticatorOptions
interface
interface VirtualAuthenticatorOptions {
readonly protocol?: "ctap2" | "u2f";
readonly transport?: "usb" | "nfc" | "ble" | "cable" | "internal";
readonly hasResidentKey?: boolean;
readonly hasUserVerification?: boolean;
readonly isUserVerified?: boolean;
readonly automaticPresenceSimulation?: boolean;
}
Options for withVirtualAuthenticator.
WriteHandleController
type
type WriteHandleController<T> = {
setBatchId(batchId: string | null): void;
advance(stage: Exclude<WriteStage, "rejected">, value?: T): void;
reject(rejection: WriteRejection): void;
fail(error: unknown): void;
};
The runtime-only mutator half of a write handle. Only the write ledger holds a handle's controller — the handle itself exposes no way to advance or settle, so application code can observe a write's fate but never forge it.
WriteRejection
type
type WriteRejection = {
cause: "denied" | "expired";
code: string | null;
reason: string;
};
Why a write settled as rejected: the structured cause, code, and reason.
WriteStage
type
type WriteStage =
| "saving"
| "saved"
| "syncing"
| "synced"
| "rejected";
The closed, framework-owned write lifecycle. Stages are monotonic:
saving → saved → syncing → synced | rejected. syncing is reserved for a
runtime that can observe the transport; the current storage engine exposes
no such signal, so today writes move from saved directly to synced or
rejected and no handle ever reports syncing. Branch on saved vs
settled, not on seeing syncing.