@voltro/local-first 0.29.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.
@@ -0,0 +1,603 @@
1
+ /**
2
+ * `true` when the queue is ready to hand its head to the transport: online,
3
+ * nothing already in flight, and something waiting. A `drain-start` dispatched
4
+ * when this is `false` is a no-op, so callers can dispatch it optimistically.
5
+ */
6
+ export declare const canDrain: (state: SyncQueueState) => boolean;
7
+
8
+ export declare interface ConflictPolicy {
9
+ /** Resolve a single named field. Fields with no declared policy fall back to LWW. */
10
+ readonly resolveField: (field: string, local: FieldValue, remote: FieldValue) => unknown;
11
+ /**
12
+ * Resolve two versions of a whole record, field by field. The result covers
13
+ * the union of both sides' keys; a key present on only one side is taken as-is.
14
+ */
15
+ readonly resolveRecord: (local: VersionedRecord, remote: VersionedRecord) => Record<string, unknown>;
16
+ }
17
+
18
+ /**
19
+ * Declare per-field conflict policy for a table's non-CRDT fields.
20
+ *
21
+ * ```ts
22
+ * const policy = conflictPolicy({
23
+ * title: 'lastWriteWins',
24
+ * permissions: customResolver,
25
+ * })
26
+ * policy.resolveRecord(localRow, remoteRow) // → merged plain values
27
+ * ```
28
+ *
29
+ * Fields not named here resolve with {@link lastWriteWins}, so a policy never
30
+ * has to enumerate every column to be safe.
31
+ */
32
+ export declare const conflictPolicy: (policies: Record<string, FieldPolicy>) => ConflictPolicy;
33
+
34
+ /** Resolves two competing versions of one field to a single winning value. */
35
+ export declare type ConflictResolver<T = unknown> = (local: FieldValue<T>, remote: FieldValue<T>) => T;
36
+
37
+ export declare type ConnectionAction =
38
+ /** Browser/OS reports the network interface is up. */
39
+ {
40
+ readonly type: 'network-up';
41
+ }
42
+ /** Browser/OS reports the network is down. */
43
+ | {
44
+ readonly type: 'network-down';
45
+ }
46
+ /** A reachability confirmation (a real round-trip) succeeded. */
47
+ | {
48
+ readonly type: 'confirmed';
49
+ }
50
+ /** A reachability confirmation failed; stay reconnecting, count the try. */
51
+ | {
52
+ readonly type: 'confirm-failed';
53
+ };
54
+
55
+ export declare type ConnectionPhase = 'online' | 'offline' | 'reconnecting';
56
+
57
+ export declare const connectionReducer: (state: ConnectionState, action: ConnectionAction) => ConnectionState;
58
+
59
+ export declare interface ConnectionState {
60
+ readonly phase: ConnectionPhase;
61
+ /** Confirmation attempts made since the network last came back. 0 when online/offline. */
62
+ readonly attempt: number;
63
+ }
64
+
65
+ /**
66
+ * A CRDT implementation. One per library. The default is {@link yjsBackend}.
67
+ *
68
+ * `merge` and `decodeText` are STATELESS with respect to any live handle: they
69
+ * operate purely on encoded bytes, which is what lets the sync layer and tests
70
+ * converge two states without holding a document open.
71
+ */
72
+ export declare interface CrdtBackend {
73
+ /** Identifies the library, e.g. `"yjs"`. Surfaced for diagnostics/telemetry. */
74
+ readonly name: string;
75
+ /** Create a fresh text buffer, optionally seeded with `initial`. */
76
+ createText: (initial?: string) => CrdtTextHandle;
77
+ /**
78
+ * Merge two encoded states into one converged encoded state.
79
+ *
80
+ * The core primitive. MUST be:
81
+ * - commutative up to convergence: `decodeText(merge(a,b)) === decodeText(merge(b,a))`
82
+ * - idempotent: `decodeText(merge(m,a)) === decodeText(m)` when `m` already contains `a`
83
+ * - an identity over the empty state: `merge(empty, a)` decodes to `a`'s text
84
+ */
85
+ merge: (a: CrdtState, b: CrdtState) => CrdtState;
86
+ /** The plain-string view of an encoded state. */
87
+ decodeText: (state: CrdtState) => string;
88
+ /** The encoded state of an empty document — the merge identity element. */
89
+ emptyState: () => CrdtState;
90
+ }
91
+
92
+ /** Identifies one CRDT-managed cell: a `crdtText()` COLUMN of one ROW of one TABLE. */
93
+ export declare interface CrdtDocKey {
94
+ readonly table: string;
95
+ readonly id: string;
96
+ readonly column: string;
97
+ }
98
+
99
+ /**
100
+ * An encoded CRDT state or delta, as opaque bytes. Produced by
101
+ * {@link CrdtBackend.encodeState} / {@link CrdtTextHandle.encodeState},
102
+ * consumed by {@link CrdtBackend.merge} and {@link CrdtBackend.decodeText}.
103
+ *
104
+ * It is opaque ON PURPOSE: the shape is the backend's business. Yjs emits a
105
+ * v1 update blob; Loro would emit its own. Consumers only ever move these
106
+ * bytes around and hand them back.
107
+ */
108
+ export declare type CrdtState = Uint8Array;
109
+
110
+ /**
111
+ * A CRDT-managed text field. A thin, fluent wrapper over a live
112
+ * {@link CrdtTextHandle}: mutations return `this` so edits chain, and the
113
+ * string view + encoded state are always one call away.
114
+ *
115
+ * ```ts
116
+ * const body = crdtText('hello')
117
+ * body.insert(5, ' world')
118
+ * body.toString() // "hello world"
119
+ * const state = body.encode() // bytes to sync
120
+ * ```
121
+ */
122
+ export declare interface CrdtText {
123
+ /** The backing library's name (e.g. `"yjs"`). */
124
+ readonly backend: string;
125
+ /** Current text as a plain string. */
126
+ toString: () => string;
127
+ /** Insert `text` at `index` (UTF-16 code units). Chainable. */
128
+ insert: (index: number, text: string) => CrdtText;
129
+ /** Delete `length` code units at `index`. Chainable. */
130
+ delete: (index: number, length: number) => CrdtText;
131
+ /** Fold an incoming encoded state into this field; concurrent edits converge. Chainable. */
132
+ merge: (state: CrdtState) => CrdtText;
133
+ /** This field's full state, encoded for transport. */
134
+ encode: () => CrdtState;
135
+ }
136
+
137
+ /**
138
+ * Create a CRDT text field, optionally seeded with `initial`.
139
+ *
140
+ * @param backend defaults to {@link defaultCrdtBackend} (Yjs). Passing one is
141
+ * how tests and a later Loro swap avoid touching every call site.
142
+ */
143
+ export declare const crdtText: (initial?: string, backend?: CrdtBackend) => CrdtText;
144
+
145
+ /**
146
+ * A live, mutable CRDT text buffer. Edits are local until you
147
+ * {@link CrdtTextHandle.encodeState | encode} them for transport/merge.
148
+ */
149
+ export declare interface CrdtTextHandle {
150
+ /** The current text as a plain string — the view a UI renders. */
151
+ toString: () => string;
152
+ /** Insert `text` at UTF-16 code-unit `index`. */
153
+ insert: (index: number, text: string) => void;
154
+ /** Delete `length` code units starting at `index`. */
155
+ delete: (index: number, length: number) => void;
156
+ /** This buffer's full state, encoded for transport or merge. */
157
+ encodeState: () => CrdtState;
158
+ /**
159
+ * Fold another buffer's encoded state into this one. Concurrent edits
160
+ * converge — this is the CRDT property, applied in place.
161
+ */
162
+ applyState: (state: CrdtState) => void;
163
+ }
164
+
165
+ /** A local edit to sync: the encoded CRDT update for one cell. `update` is a
166
+ * full-state-or-delta blob (a Yjs update is both), folded server-side. */
167
+ export declare interface CrdtWritePayload extends CrdtDocKey {
168
+ readonly update: CrdtState;
169
+ }
170
+
171
+ /**
172
+ * Create a durable {@link PersistenceAdapter} over IndexedDB.
173
+ *
174
+ * @param options.factory the IndexedDB implementation. Defaults to the ambient
175
+ * `globalThis.indexedDB`. Tests pass a fake; a worker/host can pass its own.
176
+ * @param options.databaseName the IDB database name. Default `"voltro-local-first"`.
177
+ * Distinct apps on one origin should pass distinct names.
178
+ */
179
+ export declare const createIndexedDbPersistence: (options?: {
180
+ readonly factory?: IndexedDbFactory;
181
+ readonly databaseName?: string;
182
+ }) => Promise<PersistenceAdapter>;
183
+
184
+ /**
185
+ * An in-memory {@link PersistenceAdapter} backed by Maps. Real, not a stub: it
186
+ * fully satisfies the contract, and copies bytes on the way in and out so a
187
+ * caller mutating a `Uint8Array` after saving cannot corrupt what was stored
188
+ * (a durable backing serialises, so this keeps the in-memory impl honest to the
189
+ * same isolation). Loses everything when the process ends — that is the ONLY
190
+ * thing the WASM/Turso seam adds over it.
191
+ */
192
+ export declare const createInMemoryPersistence: () => PersistenceAdapter;
193
+
194
+ /** Create a fresh shared bus. Pass it to two `createInMemoryPresenceChannel`
195
+ * calls to model two peers over one broker. */
196
+ export declare const createInMemoryPresenceBus: () => InMemoryPresenceBus;
197
+
198
+ /**
199
+ * An in-memory {@link PresenceChannel} over a shared {@link InMemoryPresenceBus}.
200
+ * Real, not a stub: it fans a publish out to every OTHER handler on the room
201
+ * synchronously — the same delivery a broker gives, minus the network.
202
+ *
203
+ * A handler NEVER receives its own channel's publish (a broker's own-origin
204
+ * skip): otherwise a peer would see itself as an "other". Isolation is per
205
+ * channel instance, so one peer = one channel.
206
+ */
207
+ export declare const createInMemoryPresenceChannel: (bus?: InMemoryPresenceBus) => PresenceChannel;
208
+
209
+ /**
210
+ * Create a presence room over a {@link PresenceChannel}. Publishes an initial
211
+ * "join" immediately, so a peer already listening sees this one at once; on
212
+ * hearing from a peer it has not seen, it re-announces itself (announce-back) so
213
+ * mutual discovery holds without any central registry.
214
+ */
215
+ export declare const createPresenceRoom: <TPresence>(options: PresenceRoomOptions<TPresence>) => PresenceRoom<TPresence>;
216
+
217
+ /**
218
+ * Create a sync client over a {@link SyncTransport}.
219
+ *
220
+ * ```ts
221
+ * const client = createSyncClient({
222
+ * transport: {
223
+ * kind: 'sync-transport',
224
+ * push: (w) => runMutation('documents.setBody', w.payload), // useMutation
225
+ * onRemoteState: (h) => subscribeRow('documents', (row) => // useSubscription
226
+ * h({ table: 'documents', id: row.id, column: 'body', state: row.body })),
227
+ * },
228
+ * adapter: await someDurablePersistence(), // optional
229
+ * })
230
+ * client.enqueue({ table: 'documents', id: 'd1', column: 'body', update })
231
+ * ```
232
+ */
233
+ export declare const createSyncClient: (options: SyncClientOptions) => SyncClient;
234
+
235
+ /** The plain-string view of an encoded CRDT state. */
236
+ export declare const decodeCrdtText: (state: CrdtState, backend?: CrdtBackend) => string;
237
+
238
+ /** The backend used when a call site does not pass one. Swap point for Loro. */
239
+ export declare const defaultCrdtBackend: CrdtBackend;
240
+
241
+ /**
242
+ * Derive the user-visible status. Offline wins (nothing can sync); otherwise
243
+ * `syncing` while writes are outstanding OR we are still confirming the server,
244
+ * else `synced`.
245
+ */
246
+ export declare const deriveSyncStatus: (phase: ConnectionPhase, outstanding: number) => SyncStatus;
247
+
248
+ /** The encoded empty state — the identity element for {@link mergeCrdtStates}. */
249
+ export declare const emptyCrdtState: (backend?: CrdtBackend) => CrdtState;
250
+
251
+ /** Either the built-in name or a custom resolver. */
252
+ export declare type FieldPolicy<T = unknown> = 'lastWriteWins' | ConflictResolver<T>;
253
+
254
+ /** A value tagged with when and by whom it was written — the input to a resolver. */
255
+ export declare interface FieldValue<T = unknown> {
256
+ readonly value: T;
257
+ /** Wall-clock of the write (ms epoch). The primary ordering key. */
258
+ readonly updatedAt: number;
259
+ /** Stable writer identity (device/user id). Used ONLY to break exact ties, deterministically. */
260
+ readonly writer?: string;
261
+ }
262
+
263
+ declare interface IDBDatabaseLike {
264
+ transaction: (store: string, mode: 'readonly' | 'readwrite') => IDBTransactionLike;
265
+ objectStoreNames: {
266
+ contains: (name: string) => boolean;
267
+ };
268
+ createObjectStore: (name: string) => IDBObjectStoreLike;
269
+ }
270
+
271
+ declare interface IDBObjectStoreLike {
272
+ get: (key: string) => IDBRequestLike<unknown>;
273
+ put: (value: unknown, key: string) => IDBRequestLike<unknown>;
274
+ delete: (key: string) => IDBRequestLike<unknown>;
275
+ }
276
+
277
+ declare interface IDBOpenRequestLike extends IDBRequestLike<IDBDatabaseLike> {
278
+ onupgradeneeded: ((this: unknown, ev: unknown) => void) | null;
279
+ }
280
+
281
+ declare interface IDBRequestLike<T> {
282
+ result: T;
283
+ error: unknown;
284
+ onsuccess: ((this: unknown, ev: unknown) => void) | null;
285
+ onerror: ((this: unknown, ev: unknown) => void) | null;
286
+ }
287
+
288
+ declare interface IDBTransactionLike {
289
+ objectStore: (name: string) => IDBObjectStoreLike;
290
+ }
291
+
292
+ /**
293
+ * DONE — Durable persistence.
294
+ * `createIndexedDbPersistence` (./persistence/indexedDb) is a durable
295
+ * `PersistenceAdapter` over IndexedDB — no WASM, no added dependency, tested
296
+ * against a fake IDB backend that survives a reopen. `createInMemoryPersistence`
297
+ * remains the ephemeral default. REMAINING (optional): a wa-sqlite / Turso
298
+ * adapter for cross-tab SQL, a sibling file behind the SAME interface + the
299
+ * plan's LRU eviction. Nothing above the adapter changes when it lands.
300
+ *
301
+ * DONE — Bi-directional sync wire.
302
+ * `createSyncClient` (./sync/syncClient) maps the pure sync-queue reducer onto
303
+ * a `SyncTransport`: a local edit → optimistic merge + queued write; reconnect
304
+ * → drain to `push`; incoming merged state → local CRDT merge; convergence.
305
+ * Tested against an in-memory dispatcher that mirrors the server's
306
+ * authoritative merge. REMAINING (runtime binding, NOT this package): supply
307
+ * the two transport functions from a running app — `push` from the client's
308
+ * mutation caller (`useMutation` / `resolveByTag` writing the `crdtText()`
309
+ * column), `onRemoteState` from the reactive subscription that streams the row.
310
+ *
311
+ * DONE — Presence / awareness.
312
+ * `createPresenceRoom` (./presence/room) + the `usePresence` hook implement
313
+ * ephemeral awareness over a `PresenceChannel` — join/leave, announce-back
314
+ * discovery, cursor propagation, TTL expiry. Tested over the in-memory channel
315
+ * (`createInMemoryPresenceChannel`), which is the SAME dumb string-payload
316
+ * shape as the framework's `BroadcastProvider`. REMAINING (runtime binding):
317
+ * a `PresenceChannel` that forwards onto the app's provisioned broker
318
+ * (in-memory locally; Redis / NATS at scale — both already shipped in
319
+ * `@voltro/plugin-broadcast`). That is a network hop, not new logic.
320
+ *
321
+ * DONE — localFirst mixin + discovery.
322
+ * `localFirst()` (`@voltro/database`) marks a table; the runtime SchemaRegistry
323
+ * reflects it as `hasLocalFirst(table)` (alongside `crdtColumns(table)`) — the
324
+ * discovery surface, with NO codegen change (a marker mixin rides `.with()`
325
+ * like any column type). `crdtText()` was already a real column type with an
326
+ * authoritative server merge on the write path.
327
+ */
328
+ export declare const IMPLEMENTED_SINCE_SEAMS: readonly ["durable-persistence-indexeddb", "sync-transport-syncclient", "presence-room", "localFirst-mixin-discovery"];
329
+
330
+ export declare type ImplementedSeam = (typeof IMPLEMENTED_SINCE_SEAMS)[number];
331
+
332
+ export declare interface IndexedDbFactory {
333
+ open: (name: string, version?: number) => IDBOpenRequestLike;
334
+ }
335
+
336
+ export declare const initialConnection: (phase?: ConnectionPhase) => ConnectionState;
337
+
338
+ /** A fresh, empty queue. `online` defaults to `true`. */
339
+ export declare const initialSyncQueue: <T = unknown>(online?: boolean) => SyncQueueState<T>;
340
+
341
+ /** A shared in-process fan-out bus. Two channels built over the SAME bus see
342
+ * each other's publishes — the presence analogue of `memoryProvider`'s named
343
+ * bus, which is what lets a test model two independent peers on one broker. */
344
+ export declare interface InMemoryPresenceBus {
345
+ readonly rooms: Map<string, Set<(payload: string) => void>>;
346
+ }
347
+
348
+ /**
349
+ * Last-write-wins: newer `updatedAt` wins; exact ties break on the (stable,
350
+ * symmetric) tiebreak key so both peers converge on the same value.
351
+ */
352
+ export declare const lastWriteWins: ConflictResolver;
353
+
354
+ /**
355
+ * Rehydrate a {@link PersistedSyncQueue} from an adapter. Any writes the adapter
356
+ * has (from a prior session) come back as `pending`, oldest first, so a client
357
+ * that went offline, queued writes, and closed picks them up on next launch.
358
+ *
359
+ * @param online seeds the connection belief (default `true`). A client that
360
+ * knows it starts offline passes `false` so `canDrain` stays shut until an
361
+ * `online` action arrives.
362
+ */
363
+ export declare const loadPersistedSyncQueue: <T = unknown>(adapter: PersistenceAdapter, online?: boolean) => Promise<PersistedSyncQueue<T>>;
364
+
365
+ /**
366
+ * The heart of the package: converge two encoded CRDT states into one.
367
+ *
368
+ * Deterministic and order-independent up to the resulting text
369
+ * (`decodeCrdtText(mergeCrdtStates(a, b))` equals the same for `(b, a)`),
370
+ * idempotent (re-merging a state already contained is a no-op on the text), and
371
+ * treats {@link CrdtBackend.emptyState} as identity.
372
+ */
373
+ export declare const mergeCrdtStates: (a: CrdtState, b: CrdtState, backend?: CrdtBackend) => CrdtState;
374
+
375
+ /** Total outstanding writes (pending + the one in flight). 0 means fully synced. */
376
+ export declare const outstandingCount: (state: SyncQueueState) => number;
377
+
378
+ /** A sync queue whose every transition is persisted through an adapter. */
379
+ export declare interface PersistedSyncQueue<T = unknown> {
380
+ /** The current in-memory queue state. */
381
+ getState: () => SyncQueueState<T>;
382
+ /**
383
+ * Apply an action with the pure reducer, persist the new outstanding set, and
384
+ * resolve with the new state. Persistence happens before the promise resolves,
385
+ * so an `await`ed dispatch guarantees the write survived.
386
+ */
387
+ dispatch: (action: SyncQueueAction<T>) => Promise<SyncQueueState<T>>;
388
+ }
389
+
390
+ /**
391
+ * Durable local storage for a local-first client. Every method is async because
392
+ * the real backings (WASM SQLite, Turso, IndexedDB) are — the in-memory impl
393
+ * resolves immediately but keeps the same shape so tests exercise the real
394
+ * contract, not a synchronous shortcut.
395
+ */
396
+ export declare interface PersistenceAdapter {
397
+ /** Identifies the backing, e.g. `"in-memory"` / `"wa-sqlite"`. Diagnostics. */
398
+ readonly kind: string;
399
+ /** The stored CRDT state for a record, or `undefined` if none is persisted. */
400
+ loadCrdtState: (table: string, id: string) => Promise<CrdtState | undefined>;
401
+ /** Persist a record's CRDT state, replacing any prior value. */
402
+ saveCrdtState: (table: string, id: string, state: CrdtState) => Promise<void>;
403
+ /** The persisted offline write queue (oldest first). Empty when none saved. */
404
+ loadQueue: () => Promise<readonly QueuedWrite[]>;
405
+ /** Persist the entire offline write queue, replacing the prior snapshot. */
406
+ saveQueue: (writes: readonly QueuedWrite[]) => Promise<void>;
407
+ /** Mark a record as never-evict (the plan's "pinned" records). */
408
+ pin: (table: string, id: string) => Promise<void>;
409
+ /** Whether a record is pinned. */
410
+ isPinned: (table: string, id: string) => Promise<boolean>;
411
+ }
412
+
413
+ /**
414
+ * A dumb ephemeral pub/sub channel. Publish a string payload to a room;
415
+ * subscribe to a room and get every payload (from other peers) back.
416
+ *
417
+ * Mirrors `BroadcastProvider` so a runtime adapter can forward straight onto the
418
+ * app's broker. `publish` is fire-and-forget (presence loss is tolerable — the
419
+ * next heartbeat corrects it); `subscribe` returns its unsubscribe.
420
+ */
421
+ export declare interface PresenceChannel {
422
+ readonly kind: 'presence-channel';
423
+ readonly publish: (room: string, payload: string) => void;
424
+ readonly subscribe: (room: string, handler: (payload: string) => void) => () => void;
425
+ }
426
+
427
+ /** One other peer's last-known presence. */
428
+ export declare interface PresencePeer<TPresence> {
429
+ readonly peerId: string;
430
+ readonly presence: TPresence;
431
+ /** Clock value of the last message seen from this peer (for TTL expiry). */
432
+ readonly lastSeen: number;
433
+ }
434
+
435
+ export declare interface PresenceRoom<TPresence> {
436
+ /** This peer's current presence. */
437
+ readonly self: () => TPresence;
438
+ /** Every OTHER live peer, newest-message-first is NOT guaranteed — order is
439
+ * insertion. Expired peers are excluded once `sweep()` has run. */
440
+ readonly others: () => readonly PresencePeer<TPresence>[];
441
+ /** Replace this peer's presence and publish it. */
442
+ readonly update: (presence: TPresence) => void;
443
+ /** Re-publish current presence (drives keep-alive; call on an interval). */
444
+ readonly heartbeat: () => void;
445
+ /** Drop peers whose last message is older than `ttlMs`. Call on an interval. */
446
+ readonly sweep: () => void;
447
+ /** Subscribe to any change (a peer joined/updated/left, or self changed). */
448
+ readonly onChange: (listener: () => void) => () => void;
449
+ /** Announce departure and stop listening. Idempotent. */
450
+ readonly leave: () => void;
451
+ }
452
+
453
+ export declare interface PresenceRoomOptions<TPresence> {
454
+ readonly channel: PresenceChannel;
455
+ readonly roomId: string;
456
+ /** Unique id for THIS peer/connection. Two tabs of one user are two peers. */
457
+ readonly peerId: string;
458
+ readonly initial: TPresence;
459
+ /** A peer with no message in this many ms is considered gone. Default 30_000. */
460
+ readonly ttlMs?: number;
461
+ /** Injectable clock. Defaults to `Date.now`. */
462
+ readonly now?: () => number;
463
+ }
464
+
465
+ /** A single queued write. `payload` is opaque to the queue — the wire seam owns its shape. */
466
+ export declare interface QueuedWrite<T = unknown> {
467
+ readonly id: string;
468
+ readonly payload: T;
469
+ /** When the write was first enqueued (ms epoch). Set by the caller for determinism. */
470
+ readonly enqueuedAt: number;
471
+ /** How many times delivery has been attempted and failed. 0 until the first `fail`. */
472
+ readonly attempts: number;
473
+ }
474
+
475
+ /** Merged authoritative state for one cell, delivered back from the server. */
476
+ export declare interface RemoteCrdtState extends CrdtDocKey {
477
+ readonly state: CrdtState;
478
+ }
479
+
480
+ /**
481
+ * The genuinely REMAINING runtime seams — provisioned infra + the thin app-level
482
+ * binding to it, not un-built framework code:
483
+ * - a `SyncTransport` bound to a specific running app's mutation/subscription
484
+ * - a `PresenceChannel` bound to a provisioned Redis/NATS broker at scale
485
+ * - (optional) a wa-sqlite/Turso durable adapter for cross-tab SQL
486
+ */
487
+ export declare const RUNTIME_SEAMS: readonly ["sync-transport-app-binding", "presence-broker-binding", "wasm-sqlite-durable-adapter"];
488
+
489
+ export declare type RuntimeSeam = (typeof RUNTIME_SEAMS)[number];
490
+
491
+ export declare interface SyncClient {
492
+ /** Apply a local edit: merge it into local state immediately (optimistic),
493
+ * persist, queue it, and start draining if online. */
494
+ readonly enqueue: (payload: CrdtWritePayload) => void;
495
+ /** Report reachability. Going online kicks the drain loop. */
496
+ readonly setOnline: (online: boolean) => void;
497
+ /** Current queue state (pending + in-flight). */
498
+ readonly queueState: () => SyncQueueState<CrdtWritePayload>;
499
+ /** Outstanding writes; 0 means fully synced. */
500
+ readonly outstanding: () => number;
501
+ /** The current merged local state for a cell, or undefined if none. */
502
+ readonly getState: (key: CrdtDocKey) => CrdtState | undefined;
503
+ /** The plain-text view of a cell's merged state (empty string if none). */
504
+ readonly getText: (key: CrdtDocKey) => string;
505
+ /** Subscribe to any local change (a local edit, an acked write, or incoming
506
+ * remote state). Returns an unsubscribe. */
507
+ readonly subscribe: (listener: () => void) => () => void;
508
+ /** Drain the queue to quiescence and resolve — the test/await seam. Resolves
509
+ * when nothing more can be drained (synced, offline, or all writes gave up). */
510
+ readonly flush: () => Promise<void>;
511
+ /** Tear down the transport subscription. */
512
+ readonly close: () => void;
513
+ }
514
+
515
+ export declare interface SyncClientOptions {
516
+ readonly transport: SyncTransport;
517
+ /** Durable local storage. When present, local CRDT state + the queue survive
518
+ * a reload. Omit for an ephemeral (in-memory) client. */
519
+ readonly adapter?: PersistenceAdapter;
520
+ /** CRDT backend for the local optimistic merge. Defaults to Yjs. */
521
+ readonly backend?: CrdtBackend;
522
+ /** Seed the connection belief (default `true`). `false` holds the queue shut
523
+ * until `setOnline(true)`. */
524
+ readonly online?: boolean;
525
+ /** Injectable clock for deterministic `enqueuedAt`. Defaults to `Date.now`. */
526
+ readonly now?: () => number;
527
+ /** Stop re-driving a write after this many failed attempts (it stays queued,
528
+ * head-of-line, for the next `setOnline`/`enqueue` to retry). Default 5. */
529
+ readonly maxAttempts?: number;
530
+ }
531
+
532
+ export declare type SyncQueueAction<T = unknown> =
533
+ /** Queue a new write at the tail. */
534
+ {
535
+ readonly type: 'enqueue';
536
+ readonly write: QueuedWrite<T>;
537
+ }
538
+ /** Network/reachability came back. */
539
+ | {
540
+ readonly type: 'online';
541
+ }
542
+ /** Network/reachability went away. An in-flight write returns to the front. */
543
+ | {
544
+ readonly type: 'offline';
545
+ }
546
+ /** Move the head into `inFlight` so the transport can deliver it. */
547
+ | {
548
+ readonly type: 'drain-start';
549
+ }
550
+ /** The in-flight write (matched by id) was delivered. */
551
+ | {
552
+ readonly type: 'ack';
553
+ readonly id: string;
554
+ }
555
+ /** The in-flight write (matched by id) failed; it returns to the front, attempts+1. */
556
+ | {
557
+ readonly type: 'fail';
558
+ readonly id: string;
559
+ readonly error: string;
560
+ };
561
+
562
+ export declare const syncQueueReducer: <T>(state: SyncQueueState<T>, action: SyncQueueAction<T>) => SyncQueueState<T>;
563
+
564
+ export declare interface SyncQueueState<T = unknown> {
565
+ /** Whether the queue believes it can reach the server. Drives draining. */
566
+ readonly online: boolean;
567
+ /** Writes waiting to be sent, oldest first. */
568
+ readonly pending: readonly QueuedWrite<T>[];
569
+ /** The write currently being delivered, if any. Only one at a time (in-order delivery). */
570
+ readonly inFlight: QueuedWrite<T> | undefined;
571
+ /** Last delivery error message, if the most recent attempt failed. */
572
+ readonly lastError: string | undefined;
573
+ }
574
+
575
+ /** What an offline indicator shows: a join of network phase and queue depth. */
576
+ export declare type SyncStatus = 'offline' | 'syncing' | 'synced';
577
+
578
+ /**
579
+ * The transport the {@link SyncClient} rides. Two functions the framework binds
580
+ * to its EXISTING wire:
581
+ *
582
+ * - `push` — deliver a queued CRDT write to the server. Bound to a mutation
583
+ * that writes the `crdtText()` column (the runtime's MutationStore folds it
584
+ * with `mergeCrdtColumnsIntoPatch`). Rejects to trigger the queue's retry.
585
+ * - `onRemoteState` — receive merged state pushed from the server. Bound to
586
+ * the reactive subscription that already streams the row. Returns an
587
+ * unsubscribe.
588
+ *
589
+ * In tests it is an in-memory dispatcher that merges + broadcasts, exercising
590
+ * the exact convergence contract without a network.
591
+ */
592
+ export declare interface SyncTransport {
593
+ readonly kind: 'sync-transport';
594
+ readonly push: (write: QueuedWrite<CrdtWritePayload>) => Promise<void>;
595
+ readonly onRemoteState: (handler: (remote: RemoteCrdtState) => void) => () => void;
596
+ }
597
+
598
+ /** A record's fields, each carrying its version metadata. */
599
+ export declare type VersionedRecord = Record<string, FieldValue>;
600
+
601
+ export declare const yjsBackend: CrdtBackend;
602
+
603
+ export { }