@doync/client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/adapter.cjs +1 -0
  4. package/dist/adapter.d.cts +86 -0
  5. package/dist/adapter.d.cts.map +1 -0
  6. package/dist/adapter.d.ts +86 -0
  7. package/dist/adapter.d.ts.map +1 -0
  8. package/dist/adapter.js +2 -0
  9. package/dist/adapter.js.map +1 -0
  10. package/dist/client-C6jAdhbe.cjs +15 -0
  11. package/dist/client-CNyLMCw0.d.ts +812 -0
  12. package/dist/client-CNyLMCw0.d.ts.map +1 -0
  13. package/dist/client-ClV8ce6X.js +16 -0
  14. package/dist/client-ClV8ce6X.js.map +1 -0
  15. package/dist/client-DHXO0dbf.d.cts +812 -0
  16. package/dist/client-DHXO0dbf.d.cts.map +1 -0
  17. package/dist/index.cjs +0 -0
  18. package/dist/index.d.cts +2 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +0 -0
  21. package/dist/internal.cjs +1 -0
  22. package/dist/internal.d.cts +69 -0
  23. package/dist/internal.d.cts.map +1 -0
  24. package/dist/internal.d.ts +69 -0
  25. package/dist/internal.d.ts.map +1 -0
  26. package/dist/internal.js +2 -0
  27. package/dist/internal.js.map +1 -0
  28. package/package.json +79 -0
  29. package/src/adapter.ts +25 -0
  30. package/src/client-mutation-registry.ts +31 -0
  31. package/src/client.ts +100 -0
  32. package/src/engine.ts +3322 -0
  33. package/src/identity.ts +41 -0
  34. package/src/index.ts +36 -0
  35. package/src/internal.ts +37 -0
  36. package/src/migrate.ts +156 -0
  37. package/src/mutations.ts +96 -0
  38. package/src/port.ts +74 -0
  39. package/src/raw-read.ts +135 -0
  40. package/src/replica/db/0000_replica_engine_v0.sql +20 -0
  41. package/src/replica/db/0001_release_stamps.sql +6 -0
  42. package/src/replica/db/meta/0000_snapshot.json +129 -0
  43. package/src/replica/db/meta/0001_snapshot.json +167 -0
  44. package/src/replica/db/meta/_journal.json +20 -0
  45. package/src/replica/db/schema.ts +67 -0
  46. package/src/replica/index.ts +185 -0
  47. package/src/replica/meta.ts +26 -0
  48. package/src/replica/stamps.ts +102 -0
  49. package/src/replica/track.ts +190 -0
  50. package/src/socket-reconnect.ts +242 -0
  51. package/src/socket.ts +44 -0
  52. package/src/sql-raw.d.ts +4 -0
@@ -0,0 +1,812 @@
1
+ import { BoundQuery, DoyncSchema, MutationDefinition, MutationTree, SqlValue } from "@doync/core";
2
+ import { ClientMessage, ServerMessage } from "@doync/core/internal";
3
+
4
+ //#region ../../node_modules/.pnpm/@standard-schema+spec@1.1.0/node_modules/@standard-schema/spec/dist/index.d.ts
5
+ /** The Standard Typed interface. This is a base type extended by other specs. */
6
+ interface StandardTypedV1<Input = unknown, Output = Input> {
7
+ /** The Standard properties. */
8
+ readonly "~standard": StandardTypedV1.Props<Input, Output>;
9
+ }
10
+ declare namespace StandardTypedV1 {
11
+ /** The Standard Typed properties interface. */
12
+ interface Props<Input = unknown, Output = Input> {
13
+ /** The version number of the standard. */
14
+ readonly version: 1;
15
+ /** The vendor name of the schema library. */
16
+ readonly vendor: string;
17
+ /** Inferred types associated with the schema. */
18
+ readonly types?: Types<Input, Output> | undefined;
19
+ }
20
+ /** The Standard Typed types interface. */
21
+ interface Types<Input = unknown, Output = Input> {
22
+ /** The input type of the schema. */
23
+ readonly input: Input;
24
+ /** The output type of the schema. */
25
+ readonly output: Output;
26
+ }
27
+ /** Infers the input type of a Standard Typed. */
28
+ type InferInput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["input"];
29
+ /** Infers the output type of a Standard Typed. */
30
+ type InferOutput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["output"];
31
+ }
32
+ /** The Standard Schema interface. */
33
+ interface StandardSchemaV1<Input = unknown, Output = Input> {
34
+ /** The Standard Schema properties. */
35
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>;
36
+ }
37
+ declare namespace StandardSchemaV1 {
38
+ /** The Standard Schema properties interface. */
39
+ interface Props<Input = unknown, Output = Input> extends StandardTypedV1.Props<Input, Output> {
40
+ /** Validates unknown input values. */
41
+ readonly validate: (value: unknown, options?: StandardSchemaV1.Options | undefined) => Result<Output> | Promise<Result<Output>>;
42
+ }
43
+ /** The result interface of the validate function. */
44
+ type Result<Output> = SuccessResult<Output> | FailureResult;
45
+ /** The result interface if validation succeeds. */
46
+ interface SuccessResult<Output> {
47
+ /** The typed output value. */
48
+ readonly value: Output;
49
+ /** A falsy value for `issues` indicates success. */
50
+ readonly issues?: undefined;
51
+ }
52
+ interface Options {
53
+ /** Explicit support for additional vendor-specific parameters, if needed. */
54
+ readonly libraryOptions?: Record<string, unknown> | undefined;
55
+ }
56
+ /** The result interface if validation fails. */
57
+ interface FailureResult {
58
+ /** The issues of failed validation. */
59
+ readonly issues: ReadonlyArray<Issue>;
60
+ }
61
+ /** The issue interface of the failure output. */
62
+ interface Issue {
63
+ /** The error message of the issue. */
64
+ readonly message: string;
65
+ /** The path of the issue, if any. */
66
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
67
+ }
68
+ /** The path segment interface of the issue. */
69
+ interface PathSegment {
70
+ /** The key representing a path segment. */
71
+ readonly key: PropertyKey;
72
+ }
73
+ /** The Standard types interface. */
74
+ interface Types<Input = unknown, Output = Input> extends StandardTypedV1.Types<Input, Output> {}
75
+ /** Infers the input type of a Standard. */
76
+ type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
77
+ /** Infers the output type of a Standard. */
78
+ type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
79
+ }
80
+ /** The Standard JSON Schema interface. */
81
+ //#endregion
82
+ //#region src/port.d.ts
83
+ /** One local-replica row: column name → {@link SqlValue}. */
84
+ interface LocalRow {
85
+ [column: string]: SqlValue;
86
+ }
87
+ /**
88
+ * Synchronous local SQLite port the client engine drives. Platform adapters
89
+ * (wa-sqlite, node:sqlite, op-sqlite) implement this; app code does not.
90
+ * Transactions and savepoints are ordinary SQL via {@link LocalDb.exec}.
91
+ */
92
+ interface LocalDb {
93
+ /**
94
+ * Run one parameterized statement; return its rows (empty for writes/DDL/
95
+ * transaction control).
96
+ */
97
+ exec<T extends LocalRow = LocalRow>(sql: string, ...params: SqlValue[]): T[];
98
+ /**
99
+ * Run a multi-statement DDL script (no parameters). Used to replay bundled
100
+ * migrations when creating a fresh replica. Trusted input only.
101
+ */
102
+ execBatch(script: string): void;
103
+ /**
104
+ * Consumer tables written since the last call, then clear the set. Used so
105
+ * only affected subscriptions re-project after a mutation or rebase.
106
+ */
107
+ drainWrittenTables(): Set<string>;
108
+ }
109
+ /**
110
+ * Extract the target table of one WRITE statement from its SQL text — the
111
+ * adapter-side stand-in where no synchronous native change feed exists
112
+ * (ADR-0019): node:sqlite has no update hook, and op-sqlite's hook delivers
113
+ * callbacks via `invokeAsync` — a later event-loop turn, unusable for the
114
+ * engine's drain-after-apply contract (closeio/doync#202 device pass).
115
+ *
116
+ * A real parse over `@doync/sqlite-parser`, not a keyword recognizer: the AST
117
+ * attaches a `WITH …` CTE prefix to the DML node itself, so CTE-topped writes
118
+ * (`WITH src AS (…) INSERT INTO t …`) extract their target with no string
119
+ * games. Returns the unquoted table name for INSERT / REPLACE / UPDATE /
120
+ * DELETE, or `null` for a non-write (SELECT, SAVEPOINT, PRAGMA, DDL) or
121
+ * unparseable input.
122
+ *
123
+ * TRIGGER cascades remain invisible to statement text — and stay out of scope
124
+ * by design: client replicas carry no triggers (the bundled-track replay strips
125
+ * CREATE TRIGGER; ADR-0009 — clients apply state, never enforce).
126
+ */
127
+ declare function extractWriteTable(sql: string): string | null;
128
+ //#endregion
129
+ //#region src/mutations.d.ts
130
+ /**
131
+ * Write surface a client mutation body runs against: one parameterized
132
+ * statement at a time inside the mutation's savepoint, returning its rows. Same
133
+ * shape the Origin uses, so a shared body runs identically on device and
134
+ * server.
135
+ */
136
+ interface ClientMutationTx {
137
+ exec<T extends LocalRow = LocalRow>(query: string, ...params: SqlValue[]): T[];
138
+ }
139
+ /**
140
+ * Client mutation body `(args, ctx, sql)`. Must be deterministic and DB-only
141
+ * (may still be async for local work). Generate ids on the client and pass them
142
+ * in `args` — the body is replayed on rebase. External I/O belongs in a server
143
+ * override, not the client bundle.
144
+ */
145
+ type ClientMutationHandler = (args: unknown, ctx: Record<string, unknown>, sql: ClientMutationTx) => void | Promise<void>;
146
+ /**
147
+ * A registered client mutation: body plus optional standard-schema args
148
+ * validator applied at `mutate()` time. A bare handler is also accepted.
149
+ */
150
+ interface ClientMutation {
151
+ readonly args?: StandardSchemaV1;
152
+ readonly handler: ClientMutationHandler;
153
+ }
154
+ /**
155
+ * Name-keyed client mutation registry. Keys match the server's dotted names so
156
+ * a push reaches the matching authoritative body.
157
+ */
158
+ type ClientMutationRegistry = Readonly<Record<string, ClientMutation | ClientMutationHandler>>;
159
+ /**
160
+ * Normalize a registry entry (bare handler or `{args, handler}`) to the object
161
+ * form.
162
+ */
163
+ declare function asClientMutation(entry: ClientMutation | ClientMutationHandler): ClientMutation;
164
+ /**
165
+ * Validate `args` through a standard-schema synchronously (mirrors
166
+ * `@doync/core` query-args). Async validators refuse loudly — the body is
167
+ * deterministic on both ends. Returns validated value or throws.
168
+ */
169
+ declare function validateMutationArgs(schema: StandardSchemaV1, args: unknown): unknown;
170
+ //#endregion
171
+ //#region src/socket.d.ts
172
+ /**
173
+ * Minimal transport the client engine uses to talk to its Mirror. Platform
174
+ * adapters supply WebSocket / reconnect policy behind this shape.
175
+ */
176
+ interface SyncSocket {
177
+ /** Send one client→server frame (`connect` / `subscribe` / `push`). */
178
+ send(message: ClientMessage): void;
179
+ /**
180
+ * Register frame and lifecycle handlers (once at construction). `open` fires
181
+ * on every (re)connect so the engine can re-handshake; `close` pauses
182
+ * outbound traffic until the next `open`.
183
+ */
184
+ setHandlers(handlers: SyncSocketHandlers): void;
185
+ /**
186
+ * Drop and re-establish under the adapter's backoff. Used after a
187
+ * client-ahead schema skew; the next successful open triggers re-handshake.
188
+ */
189
+ reconnect(): void;
190
+ }
191
+ /**
192
+ * Transient transport states a reconnecting adapter may report: `connecting`
193
+ * (dial/backoff) and `error` (abnormal drop). Connected / disconnected ride
194
+ * `open`/`close`; auth failure is inferred from frames.
195
+ */
196
+ type SeamStatus = "connecting" | "error";
197
+ /** Handlers the engine registers on a {@link SyncSocket}. */
198
+ interface SyncSocketHandlers {
199
+ /** A server→client frame arrived. */
200
+ message(message: ServerMessage): void;
201
+ /** Socket (re)connected — engine handshakes and re-sends pending. */
202
+ open(): void;
203
+ /** Socket disconnected — engine queues writes until the next open. */
204
+ close(): void;
205
+ /**
206
+ * Optional transient status (`connecting` / `error`). Adapters without
207
+ * reconnect simply never call this.
208
+ */
209
+ status?(status: SeamStatus): void;
210
+ }
211
+ //#endregion
212
+ //#region src/replica/meta.d.ts
213
+ /**
214
+ * Read one `__doync_meta` value, or `null` if unset. Columns are `k`/`v`
215
+ * (ADR-0024).
216
+ */
217
+ declare function readMeta(db: LocalDb, key: string): string | null;
218
+ /** Upsert one `__doync_meta` value. Columns are `k`/`v` (ADR-0024). */
219
+ declare function writeMeta(db: LocalDb, key: string, value: string): void;
220
+ //#endregion
221
+ //#region src/replica/stamps.d.ts
222
+ /**
223
+ * Durable release stamp for one parked Query instance (ADR-0014 client half /
224
+ * closeio/doync#224). Shared infrastructure for `heldAt` (#225) and membership
225
+ * GC (#226) — package-internal, no public consumer surface.
226
+ *
227
+ * Lifecycle: written at last-release; deleted on re-desire and on an ADR-0028
228
+ * warmth-break; wiped with memberships on resync/forget (a stamp must never
229
+ * outlive the memberships it vouches for).
230
+ */
231
+ type ReleaseStamp = {
232
+ readonly instance: string; /** Client scalar cookie at last-release — the future `heldAt` claim. */
233
+ readonly cookie: number; /** Release moment in connected-clock units (ms). */
234
+ readonly releasedAt: number;
235
+ /**
236
+ * Resolved grace in ms — the Subscription's declared ttl, or the internal
237
+ * default (mirrors the server's DEFAULT_QUERY_TTL_MS; no public knob).
238
+ */
239
+ readonly ttlMs: number;
240
+ };
241
+ //#endregion
242
+ //#region src/replica/index.d.ts
243
+ /**
244
+ * Apply the Replica engine track via `__doync_engine` ledger (ADR-0024 / #159).
245
+ * LocalDb twin of server `runEngineTrack`. Returns whether rollback tripwire
246
+ * fired (caller finishes consumer wipe-and-resync). Named `createEngineTables`
247
+ * for stable call sites.
248
+ */
249
+ declare function createEngineTables(db: LocalDb): {
250
+ rolledBack: boolean;
251
+ };
252
+ /**
253
+ * Boot replica shape via DDL-only bundled track (ADR-0020). Fresh client is a
254
+ * partial Mirror of the same migrations (triggers skipped, FKs off ADR-0009;
255
+ * backfill skipped ADR-0006).
256
+ *
257
+ * - Fresh (no schema_version): full bundle track.
258
+ * - Persisted: leave recorded shape; new migrations arrive mid-session as
259
+ * `schema` directives ({@link applyBundledMigrations}), not at boot.
260
+ */
261
+ declare function replayMigrations(db: LocalDb, schema: DoyncSchema): void;
262
+ /**
263
+ * Apply the bundled track's non-trigger DDL for the migrations in
264
+ * `(fromVersion, toVersion]` to the replica and record `toVersion` as the
265
+ * applied `schema_version`. The single primitive behind both fresh boot replay
266
+ * ({@link replayMigrations} with `fromVersion = 0`) and a mid-session catch-up
267
+ * (`fromVersion =` the currently applied version). The DDL text is the client's
268
+ * OWN bundle (never wire-carried — ADR-0020); a `CREATE TABLE` from a fresh
269
+ * base and an `ALTER TABLE … ADD COLUMN` catch-up both keep any rows already
270
+ * present (SQLite ADD COLUMN preserves rows). Throws if a statement fails — the
271
+ * caller turns a failed local migration into wipe-and-resync (ADR-0020).
272
+ */
273
+ declare function applyBundledMigrations(db: LocalDb, schema: DoyncSchema, fromVersion: number, toVersion: number): void;
274
+ /**
275
+ * Drop every consumer (synced) table (ADR-0020's wipe fallback): the first step
276
+ * of wipe-replica-and-resync, run with foreign keys already off so no cascade
277
+ * fires and drop order is irrelevant (ADR-0009). `__doync_`-prefixed engine
278
+ * tables — the pending queue, clientId, and `__doync_engine` ledger that
279
+ * SURVIVE the wipe — are left untouched.
280
+ */
281
+ declare function dropConsumerTables(db: LocalDb, schema: DoyncSchema): void;
282
+ //#endregion
283
+ //#region src/engine.d.ts
284
+ /**
285
+ * What `mutate()` returns: `client` settles when the optimistic body applies
286
+ * locally (rejects if the body throws or args fail validation); `server`
287
+ * settles when the Origin confirms or rejects the mutation. Render off
288
+ * `client`; await `server` when you need authoritative confirmation.
289
+ */
290
+ interface MutationResult {
291
+ readonly client: Promise<void>;
292
+ readonly server: Promise<void>;
293
+ }
294
+ /**
295
+ * What happens to this identity's local store on logout: `keep` leaves it so
296
+ * unsynced writes await the next login (default); `forget` erases it
297
+ * (shared-computer / privacy). Set at construction or via
298
+ * {@link DoyncClient.setLogoutBehavior}.
299
+ */
300
+ type LogoutBehavior = "keep" | "forget";
301
+ /**
302
+ * The `__doync_meta` key the durable {@link LogoutBehavior} lives under
303
+ * (closeio/doync#135) — shared by the direct engine's
304
+ * {@link ClientEngine.setLogoutBehavior} / `createClient` write and the web DB
305
+ * worker's per-identity write + boot read, so the ends can never drift.
306
+ */
307
+ declare const __LOGOUT_BEHAVIOR_META_KEY = "logout_behavior";
308
+ /**
309
+ * The `__doync_meta` key the Client's durable connected-time counter lives
310
+ * under (ADR-0014 client-half addendum / closeio/doync#223). Blob-typed
311
+ * store-as-bound: no engine-track migration. Package-internal — release stamps
312
+ * (#224) and membership GC read it through the engine, not this key.
313
+ */
314
+ declare const __CONNECTED_CLOCK_META_KEY = "connected_clock";
315
+ /**
316
+ * Schema / recovery states the UI can show as a banner:
317
+ *
318
+ * - `reload` — client bundle cannot understand the server's shape; reload the app
319
+ * after deploying a matching client. Terminal until reload.
320
+ * - `server-behind` — client is ahead of a mid-deploy Mirror; the client backs
321
+ * off and re-handshakes automatically.
322
+ * - `resync` — local replica/memberships wiped and rebuilding; pending writes and
323
+ * clientId survive. Transient; clears when sync resumes.
324
+ * - `forget` — local store erased (including identity); boots as a fresh client.
325
+ * Transient; clears when sync resumes.
326
+ */
327
+ type SchemaEventKind = "reload" | "server-behind" | "resync" | "forget";
328
+ /**
329
+ * One schema-status transition: `kind` plus a human-readable `message` for
330
+ * diagnostics (not a control signal). `null` on the client means nominal.
331
+ */
332
+ interface SchemaEvent {
333
+ readonly kind: SchemaEventKind;
334
+ /** Human-readable reason (diagnostics; never a control signal). */
335
+ readonly message: string;
336
+ }
337
+ /** Options for one `mutate()` call. */
338
+ interface MutationOptions {
339
+ /**
340
+ * Idempotency key: a second `mutate()` with a key already in flight (or
341
+ * already settled) returns the same `{client, server}` pair and enqueues
342
+ * nothing, so a retried submit never double-writes.
343
+ */
344
+ readonly key?: string;
345
+ }
346
+ /**
347
+ * Whether a view's rows have been server-confirmed:
348
+ *
349
+ * - `unknown` — local answer only (fresh subscribe, skip, or reconnect). Rows may
350
+ * already be present; this speaks to confirmation, not emptiness.
351
+ * - `complete` — server has confirmed this subscription's rows up to the current
352
+ * sync point. Empty results can still be `complete`.
353
+ * - `error` — the Mirror could not honor the subscribe; detail on
354
+ * {@link ViewStatus.error}.
355
+ */
356
+ type QueryStatus = "unknown" | "complete" | "error";
357
+ /**
358
+ * A view's status snapshot. The object reference is stable until the visible
359
+ * status changes, so it is safe for `useSyncExternalStore`.
360
+ */
361
+ interface ViewStatus {
362
+ readonly status: QueryStatus;
363
+ /** Subscribe-failure detail; present only when `status` is `error`. */
364
+ readonly error?: Error;
365
+ }
366
+ /**
367
+ * Client ↔ Mirror connection state (backing for `useConnectionStatus`):
368
+ *
369
+ * - `connecting` — a (re)connect attempt is in flight
370
+ * - `connected` — socket open, sync live
371
+ * - `disconnected` — dropped and backing off
372
+ * - `error` — transport error
373
+ * - `needs-auth` — Mirror rejected auth; refresh credentials before sync resumes
374
+ */
375
+ type ConnectionStatus = "connecting" | "connected" | "disconnected" | "error" | "needs-auth";
376
+ /**
377
+ * A live, shared handle on a query's rows — what `subscribe()` and `local()`
378
+ * return, and what `useQuery` renders from.
379
+ *
380
+ * `current()` returns the latest rows; the array reference only changes when
381
+ * the rows do, so it is safe for `useSyncExternalStore`. `onChange` fires when
382
+ * the rows or the visible status move; re-read both `current()` and `status()`
383
+ * in the listener. A view keeps serving its last rows through disconnects and
384
+ * errors — check `status()` to tell fresh from stale.
385
+ *
386
+ * Lifecycle: creating a view is free and owns nothing. Call `retain()` when
387
+ * your component mounts, `release()` when it unmounts (both idempotent). Views
388
+ * for the same query share one subscription automatically.
389
+ */
390
+ interface View<Row extends Record<string, unknown> = Record<string, SqlValue>> {
391
+ current(): readonly Row[];
392
+ onChange(listener: () => void): () => void;
393
+ /**
394
+ * Take ownership of this handle (call from a mount effect, never during
395
+ * render). Idempotent per handle.
396
+ */
397
+ retain(): void;
398
+ /**
399
+ * Drop ownership (call from unmount cleanup). The last release unsubscribes
400
+ * upstream; this handle keeps serving its last snapshot. Idempotent.
401
+ */
402
+ release(): void;
403
+ /** Latest status snapshot. Moves ride the same `onChange` as row changes. */
404
+ status(): ViewStatus;
405
+ /**
406
+ * `true` when the query is one-row (`` sql.one`…` `` / Drizzle `findFirst`),
407
+ * so `useQuery` unwraps to `Row | undefined`. `false` for multi-row;
408
+ * `undefined` when one-ness is not yet known.
409
+ */
410
+ readonly one?: boolean;
411
+ }
412
+ /**
413
+ * A one-shot cache-and-network read — what `once()` returns and `useQueryOnce`
414
+ * renders from. `current()` is the local cache immediately; `server` resolves
415
+ * with the Mirror's answer (and updates the snapshot). Not reactive to later
416
+ * local writes. Call `dispose()` when done; a quick remount within a short
417
+ * grace keeps the same promise and in-flight request (StrictMode-safe).
418
+ */
419
+ interface OnceView<Row extends Record<string, unknown> = Record<string, SqlValue>> {
420
+ current(): readonly Row[];
421
+ onChange(listener: () => void): () => void;
422
+ dispose(): void;
423
+ /** Resolves with the server's answer (network half of cache-and-network). */
424
+ readonly server: Promise<readonly Row[]>;
425
+ }
426
+ /**
427
+ * The client call surface: `subscribe` / `once` / `local` / `mutate`, plus
428
+ * connection and schema status. Platform adapters (`@doync/web`,
429
+ * `@doync/mobile`) implement this; `@doync/react` hooks adapt over it.
430
+ *
431
+ * Query-taking methods accept a {@link BoundQuery} (from a registered query
432
+ * call) or {@link FalsyQuery} ("no query"). `options.skip` is also supported.
433
+ */
434
+ interface DoyncClient {
435
+ /**
436
+ * Live subscription for a bound query (`queries.foo(args)`). Falsy or
437
+ * `options.skip` yields an inert view (empty rows, status `unknown`).
438
+ */
439
+ subscribe<Row extends Record<string, unknown> = Record<string, SqlValue>>(query: BoundQuery<Row, boolean> | FalsyQuery, options?: SubscribeOptions): View<Row>;
440
+ /**
441
+ * One-shot cache-and-network read for a bound query. Falsy never starts a
442
+ * network half.
443
+ */
444
+ once<Row extends Record<string, unknown> = Record<string, SqlValue>>(query: BoundQuery<Row, boolean> | FalsyQuery): OnceView<Row>;
445
+ /**
446
+ * Live local-only read of raw SQL against the replica (no server
447
+ * subscription). Same retain/release lifecycle as {@link subscribe}.
448
+ */
449
+ local<Row extends Record<string, unknown> = Record<string, SqlValue>>(sql: string, ...params: SqlValue[]): View<Row>;
450
+ /**
451
+ * Apply a registered mutation optimistically and push it to the Origin. Pass
452
+ * the {@link MutationDefinition} from your mutations tree; args are
453
+ * type-checked from the definition. Returns {@link MutationResult}.
454
+ */
455
+ mutate<Args = unknown>(mutation: MutationDefinition<Args>, args: Args, options?: MutationOptions): MutationResult;
456
+ /** Current schema/recovery state, or `null` when nominal. */
457
+ readonly schemaStatus: SchemaEvent | null;
458
+ /**
459
+ * Subscribe to schema-status transitions (including a clear back to nominal).
460
+ * Returns the unsubscribe function.
461
+ */
462
+ onSchemaChange(listener: () => void): () => void;
463
+ /**
464
+ * Wipe the replica and resubscribe while keeping clientId and pending writes.
465
+ * Safe "my local data looks wrong" refresh. Optional on the interface —
466
+ * platform clients implement it.
467
+ */
468
+ resync?(): void;
469
+ /**
470
+ * Erase this identity's local data (replica, pendings, identity). Privacy /
471
+ * logout-forget path. Optional on the interface — platform clients implement
472
+ * it.
473
+ */
474
+ forget?(): void;
475
+ /**
476
+ * Durably set {@link LogoutBehavior} at runtime (e.g. a "remember me"
477
+ * checkbox). Survives restart. Optional — platform clients implement it.
478
+ */
479
+ setLogoutBehavior?(behavior: LogoutBehavior): void;
480
+ /**
481
+ * Authenticated identity, or `null` when anonymous. Updated by auth refresh;
482
+ * never derived from a bearer token by the library.
483
+ */
484
+ readonly userId: string | null;
485
+ /**
486
+ * Warm the replica for a query without creating a local view — rows flow in
487
+ * for other queries that read the same tables. Falsy yields a no-op handle.
488
+ * Call `cleanup()` to release (often never, for a session-long preload).
489
+ */
490
+ preload(query: BoundQuery | FalsyQuery, options?: PreloadOptions): PreloadHandle;
491
+ /** Current {@link ConnectionStatus} to the Mirror. */
492
+ readonly connectionStatus: ConnectionStatus;
493
+ /** Subscribe to connection-status transitions. Returns the unsubscribe. */
494
+ onConnectionChange(listener: () => void): () => void;
495
+ }
496
+ /**
497
+ * Falsy "no query" on subscribe / once / preload: `false | null | undefined`.
498
+ * Lets `cond && query(args)` and optional-prop patterns type-check.
499
+ */
500
+ type FalsyQuery = false | null | undefined;
501
+ /** Per-subscribe options. */
502
+ interface SubscribeOptions {
503
+ /**
504
+ * How long after unmount the server keeps this subscription warm, in ms of
505
+ * connected time. Absent ⇒ server default (clamped to its ceiling).
506
+ */
507
+ readonly ttl?: number;
508
+ /**
509
+ * Skip the subscribe: empty rows, status `unknown`, no network. Use for
510
+ * conditional queries under React's unconditional-hooks rule.
511
+ */
512
+ readonly skip?: boolean;
513
+ }
514
+ /** Options for {@link DoyncClient.preload}. */
515
+ interface PreloadOptions {
516
+ /**
517
+ * Connected-time grace after cleanup before the server drops the warm
518
+ * subscription, in ms. Absent ⇒ server default.
519
+ */
520
+ readonly ttl?: number;
521
+ }
522
+ /** Handle returned by {@link DoyncClient.preload}. */
523
+ interface PreloadHandle {
524
+ /**
525
+ * Release the preload. Idempotent; typically unused for a session-long
526
+ * preload.
527
+ */
528
+ cleanup(): void;
529
+ }
530
+ interface ClientEngineConfig {
531
+ /**
532
+ * The synchronous local-DB port (ADR-0019): node:sqlite in tests, wa-sqlite
533
+ * on the web.
534
+ */
535
+ readonly db: LocalDb;
536
+ /**
537
+ * The consumer's synced schema — the shape source replayed for the replica
538
+ * (ADR-0020).
539
+ */
540
+ readonly schema: DoyncSchema;
541
+ /**
542
+ * Named mutation bodies resolved on `push` — deterministic, DB-only
543
+ * (ADR-0017/0021).
544
+ */
545
+ readonly mutations: ClientMutationRegistry;
546
+ /** The Mirror socket seam (ADR-0016). */
547
+ readonly socket: SyncSocket;
548
+ /**
549
+ * The asserted projected auth context (ADR-0018 addendum / closeio/doync#167)
550
+ * queries and mutation bodies read. Consumer-owned; the library ships no
551
+ * decode. Anonymous `{}`.
552
+ */
553
+ readonly ctx?: Record<string, unknown>;
554
+ /**
555
+ * Transport credential PRESENTED as `connect{jwt}` (ADR-0016/0018) — the
556
+ * Mirror resolves token-first. Auth must ride the HANDSHAKE, not only the
557
+ * WebSocket upgrade's Cookie header: the socket lives in the SharedWorker and
558
+ * can OUTLIVE a login. The engine NEVER derives identity from this token;
559
+ * {@link userId} and {@link ctx} are the asserted siblings.
560
+ */
561
+ readonly token?: string;
562
+ /**
563
+ * The ASSERTED authenticated identity (closeio/doync#167) — drives
564
+ * {@link ClientEngine.userId}. `null` / omitted = anonymous. Never derived
565
+ * from {@link token}.
566
+ */
567
+ readonly userId?: string | null;
568
+ /**
569
+ * The durable clientId (ADR-0019's respawn-double-apply trap): reused from
570
+ * `__doync_meta` when present, else this value, else generated. Provide it to
571
+ * pin identity across engine restarts over the same DB.
572
+ */
573
+ readonly clientId?: string;
574
+ /**
575
+ * ClientId generator when none is stored/provided (default:
576
+ * `crypto.randomUUID`). Test-only pin — production paths always provide
577
+ * {@link clientId} or accept a random UUID.
578
+ */
579
+ readonly generateId?: () => string;
580
+ /**
581
+ * Consume schema-state transitions (ADR-0020): a stale-client or above-bundle
582
+ * skew (`reload`), a client-ahead skew (`server-behind`), or a
583
+ * post-failed-migration wipe (`resync`). The UI layer reloads the app on
584
+ * `reload` and can surface a retry banner on `server-behind`; the engine
585
+ * drives the recovery itself (re-handshake / wipe). The current state is also
586
+ * readable synchronously via {@link ClientEngine.schemaStatus}.
587
+ */
588
+ readonly onSchemaEvent?: (event: SchemaEvent) => void;
589
+ /**
590
+ * Wall-clock source for the connected-time counter (ADR-0014 client half /
591
+ * closeio/doync#223). Defaults to `Date.now`. Injectable so Seam A tests pin
592
+ * pong deltas without real timers; package-internal — never a public
593
+ * surface.
594
+ */
595
+ readonly now?: () => number;
596
+ }
597
+ declare class ClientEngine implements DoyncClient {
598
+ #private;
599
+ constructor(config: ClientEngineConfig);
600
+ /**
601
+ * In-memory connected-time ms (ADR-0014 / #223). Package-internal for release
602
+ * stamps (#224) and membership GC — not on {@link DoyncClient}.
603
+ */
604
+ get connectedClock(): number;
605
+ /**
606
+ * Durable release stamp for one instance, or `null` (ADR-0014 / #224).
607
+ * Package-internal for `heldAt` (#225) and GC (#226).
608
+ */
609
+ releaseStamp(instance: string): ReleaseStamp | null;
610
+ /**
611
+ * Every parked release stamp (#226 hygiene GC). Package-internal; order
612
+ * unspecified.
613
+ */
614
+ releaseStamps(): ReleaseStamp[];
615
+ /** Durable clientId (stable across restarts on the same DB). */
616
+ get clientId(): string;
617
+ /**
618
+ * Asserted `userId` from construction / {@link updateAuth}, or `null` (#104 /
619
+ * #167). Never derived from a bearer (ADR-0018) — cookie sessions assert
620
+ * `userId` with no token.
621
+ */
622
+ get userId(): string | null;
623
+ /** Scalar cookie held now (`null` on first sight). */
624
+ get cookie(): number | null;
625
+ /** Framed protocol errors (`error` frames, stray poke parts). */
626
+ get errors(): readonly string[];
627
+ /**
628
+ * Idempotency keys recovered at boot (ADR-0019). A DB_FAILOVER retry on a
629
+ * respawned engine gets the recovered pair whose orphaned `client` never
630
+ * settles for it — treat client phase as already applied.
631
+ */
632
+ get recoveredKeys(): ReadonlySet<string>;
633
+ /** Bundled schema version (ADR-0020) — declared on every handshake for skew. */
634
+ get schemaVersion(): number;
635
+ /**
636
+ * Schema-handling state, or `null` when nominal (ADR-0020). Sync counterpart
637
+ * to {@link ClientEngineConfig.onSchemaEvent} for reload/retry UI.
638
+ */
639
+ get schemaStatus(): SchemaEvent | null;
640
+ mutate<Args = unknown>(mutation: MutationDefinition<Args>, args: Args, options?: MutationOptions): MutationResult;
641
+ /**
642
+ * Adopt a refreshed SAME-USER identity triple (#167). When connected with a
643
+ * bearer, send in-band `updateAuth` (ADR-0018 / #85) so the Mirror extends
644
+ * auth without reconnect when userId matches and `issuedAt` is newer.
645
+ * Identity CHANGE (different `userId`, incl. anon↔user) is topology-level
646
+ * replica swap — SharedWorker routes only same-user refreshes here.
647
+ *
648
+ * Token is stored for the next reconnect (#92: surviving SharedWorker must
649
+ * not reuse a stale token). `ctx`/`userId` asserted, never derived. Halted
650
+ * engines never emit.
651
+ */
652
+ updateAuth(token: string | null | undefined, ctx?: Record<string, unknown>, userId?: string | null): void;
653
+ /**
654
+ * Subscribe to a registered query (ADR-0021): resolve under args+ctx,
655
+ * register upstream, return a reactive {@link View}. Local snapshots run
656
+ * resolved SQL over base+overlay (RYOW). Re-project only when a local change
657
+ * touches the instance's server Read-set.
658
+ *
659
+ * Bound form only (ADR-0027 / #200): {@link BoundQuery} or falsy →
660
+ * SkippedView. `options.skip` is also an inert View with no desire.
661
+ */
662
+ subscribe<Row extends Record<string, unknown> = Record<string, SqlValue>>(query: BoundQuery<Row, boolean> | FalsyQuery, options?: SubscribeOptions): View<Row>;
663
+ /**
664
+ * Warm the replica without materializing a View (#104, ADR-0019/0021):
665
+ * register upstream so pokes hydrate rows other queries read, at zero local
666
+ * recompute of the preload statement. Refcounts the same desired instance as
667
+ * a live subscribe (byte-identical statement). `{cleanup}` releases into TTL
668
+ * grace (ADR-0014). Bound form only (ADR-0027 / #200); falsy → no-op handle.
669
+ */
670
+ preload(query: BoundQuery | FalsyQuery, options?: PreloadOptions): PreloadHandle;
671
+ /**
672
+ * Once (ADR-0012 / ADR-0021): cache-and-network. Local replica first (real
673
+ * SQL `[]` when empty); Mirror executes once under its ctx. Local
674
+ * exec/decode/shape errors throw at call (#141). Leaves no
675
+ * Subscription/CVR/Membership. Bound form only (ADR-0027 / #200); falsy never
676
+ * starts the network half.
677
+ */
678
+ once<Row extends Record<string, unknown> = Record<string, SqlValue>>(query: BoundQuery<Row, boolean> | FalsyQuery): OnceView<Row>;
679
+ /**
680
+ * Local read (ADR-0019/0021): arbitrary SQL over the replica, never upstream;
681
+ * re-run on any local commit (unhinted).
682
+ */
683
+ local<Row extends Record<string, unknown> = Record<string, SqlValue>>(sql: string, ...params: SqlValue[]): View<Row>;
684
+ /**
685
+ * Rebuild sync state (ADR-0022 resync): wipe replica/memberships/cookie, KEEP
686
+ * clientId + pending, rebootstrap. Same core as failed-migration and
687
+ * cookie-above-head heals ({@link #wipeAndResync}). Exclusive chain; offline
688
+ * rebootstrap rides next reconnect.
689
+ */
690
+ resync(): void;
691
+ /**
692
+ * Erase this identity's local data (ADR-0022 forget): wipe replica,
693
+ * memberships, pending, and all meta (clientId), then fresh first-sight under
694
+ * a new clientId. Erases WHO, not just WHAT. Awaiters of forgotten writes are
695
+ * rejected. Topology can swap OPFS on active forget (#134); this resets the
696
+ * current store. Exclusive chain.
697
+ */
698
+ forget(): void;
699
+ /**
700
+ * Durably set {@link LogoutBehavior} at runtime ({@link
701
+ * DoyncClient.setLogoutBehavior}). Via {@link #sideWriteMeta} so it commits
702
+ * outside the optimistic overlay (#135 / ADR-0019). Direct engine stores for
703
+ * RN; web DB worker reports to hub.
704
+ */
705
+ setLogoutBehavior(behavior: LogoutBehavior): void;
706
+ /**
707
+ * Schema-state transitions including silent clear (#89) so hooks re-read
708
+ * {@link schemaStatus} and banners don't stick after recovery.
709
+ */
710
+ onSchemaChange(listener: () => void): () => void;
711
+ /**
712
+ * {@link ConnectionStatus} to the Mirror (#105/#136). Precedence: 1.
713
+ * `needs-auth` — engine inference off framed `unauthorized` (ADR-0018),
714
+ * sticky until a real frame proves the refreshed handshake; outranks seam and
715
+ * even a reopened socket. 2. `connected` — live open outranks a stale seam
716
+ * report. 3. Last {@link SeamStatus}, else `disconnected`.
717
+ *
718
+ * Full five states when the seam reports; degraded adapters (no seam) get
719
+ * connected/disconnected (+ needs-auth). Parity with shared-hub.ts.
720
+ */
721
+ get connectionStatus(): ConnectionStatus;
722
+ /** Subscribe to connection-status transitions (#105). */
723
+ onConnectionChange(listener: () => void): () => void;
724
+ }
725
+ /**
726
+ * `false | null | undefined` — the "no query" sentinel (ADR-0027 /
727
+ * closeio/doync#195/#200). Exported so adapters reuse the same predicate the
728
+ * direct engine uses rather than shadowing a three-literal check.
729
+ */
730
+ declare function isFalsyQuery(value: unknown): value is FalsyQuery;
731
+ /**
732
+ * Bound-form surface normalize (ADR-0027 / closeio/doync#200): peel a
733
+ * {@link BoundQuery} into `{leaf, args, options?}`, or reject a truthy non-bound
734
+ * impostor with a surface-named error. An uncalled RegisteredQuery (a function)
735
+ * hits "did you forget to call it?" — registration is a type-level fact, so the
736
+ * legacy definition arm is gone.
737
+ *
738
+ * Exported from `@doync/client` so the web topology and mobile wrapper share
739
+ * one surface check with the direct engine — no per-adapter copy of the
740
+ * impostor wording.
741
+ */
742
+ declare function normalizeQuerySurface<Options = never>(surface: "subscribe" | "once" | "preload", queryOrBound: unknown, options?: Options): {
743
+ query: BoundQuery["query"];
744
+ args: unknown;
745
+ options: Options | undefined;
746
+ };
747
+ /**
748
+ * A `mutate()` whose failure is known synchronously (unknown name, bad args).
749
+ * Exported for the web topology's tab-side `mutate`, which shares the
750
+ * never-throw contract (#161).
751
+ */
752
+ declare function settledRejection(error: unknown): MutationResult;
753
+ //#endregion
754
+ //#region src/client.d.ts
755
+ /**
756
+ * Options for {@link createClient}. Platform adapters (`@doync/web`,
757
+ * `@doync/mobile`) fill these; app code rarely calls `createClient` directly.
758
+ */
759
+ interface CreateClientOptions<TAuthData = unknown> {
760
+ /** Synchronous local SQLite port (wa-sqlite / node:sqlite / op-sqlite). */
761
+ readonly db: LocalDb;
762
+ /** Synced schema shared with Origin and Mirror. */
763
+ readonly schema: DoyncSchema;
764
+ /**
765
+ * Shared mutation tree from `defineMutations(shared)` — no server overrides
766
+ * in the client bundle.
767
+ */
768
+ readonly mutations: MutationTree;
769
+ /** Transport to the Mirror. */
770
+ readonly socket: SyncSocket;
771
+ /**
772
+ * Bearer credential for the handshake. Does not drive `userId` or `ctx`. Omit
773
+ * for cookie-session auth.
774
+ */
775
+ readonly token?: string;
776
+ /**
777
+ * Projected auth context for optimistic query/mutation resolution (mirror
778
+ * your server `toContext`). Defaults to `{}`.
779
+ */
780
+ readonly ctx?: TAuthData;
781
+ /**
782
+ * Authenticated identity, or `null`/omit for anonymous. Never derived from
783
+ * `token`.
784
+ */
785
+ readonly userId?: string | null;
786
+ /** Pin a durable client id across restarts. */
787
+ readonly clientId?: string;
788
+ /** Observe schema/recovery transitions. */
789
+ readonly onSchemaEvent?: (event: SchemaEvent) => void;
790
+ /**
791
+ * Initial {@link LogoutBehavior}, written durably when provided. Omit to keep
792
+ * a previously stored choice (a restart will not revert `forget` to `keep`).
793
+ * Change later via {@link DoyncClient.setLogoutBehavior}.
794
+ */
795
+ readonly logoutBehavior?: LogoutBehavior;
796
+ }
797
+ /**
798
+ * Construct a {@link DoyncClient} from local DB, schema, shared mutations, and a
799
+ * Mirror socket. App code normally goes through `@doync/web` / `@doync/mobile`
800
+ * instead.
801
+ */
802
+ declare function createClient<TAuthData = unknown>(options: CreateClientOptions<TAuthData>): DoyncClient;
803
+ /**
804
+ * Concrete-engine factory (ADR-0033 / closeio/doync#241). Same construction as
805
+ * {@link createClient}, but typed as {@link ClientEngine} so platform adapters
806
+ * (web DB-worker, mobile wrapper) can reach engine-only fields like `clientId`
807
+ * without an unchecked downcast. Exported on `@doync/client/internal` only.
808
+ */
809
+ declare function createClientEngine<TAuthData = unknown>(options: CreateClientOptions<TAuthData>): ClientEngine;
810
+ //#endregion
811
+ export { readMeta as A, validateMutationArgs as B, isFalsyQuery as C, createEngineTables as D, applyBundledMigrations as E, ClientMutation as F, LocalRow as H, ClientMutationHandler as I, ClientMutationRegistry as L, SeamStatus as M, SyncSocket as N, dropConsumerTables as O, SyncSocketHandlers as P, ClientMutationTx as R, __LOGOUT_BEHAVIOR_META_KEY as S, settledRejection as T, extractWriteTable as U, LocalDb as V, SchemaEventKind as _, ClientEngineConfig as a, ViewStatus as b, FalsyQuery as c, MutationResult as d, OnceView as f, SchemaEvent as g, QueryStatus as h, ClientEngine as i, writeMeta as j, replayMigrations as k, LogoutBehavior as l, PreloadOptions as m, createClient as n, ConnectionStatus as o, PreloadHandle as p, createClientEngine as r, DoyncClient as s, CreateClientOptions as t, MutationOptions as u, SubscribeOptions as v, normalizeQuerySurface as w, __CONNECTED_CLOCK_META_KEY as x, View as y, asClientMutation as z };
812
+ //# sourceMappingURL=client-DHXO0dbf.d.cts.map