@voltro/local-first 0.52.0 → 0.54.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.
- package/CHANGELOG.md +424 -0
- package/README.md +1 -1
- package/THIRD-PARTY-NOTICES.md +29 -1
- package/dist/editor.d.ts +125 -0
- package/dist/editor.js +53 -0
- package/dist/index.d.ts +368 -27
- package/dist/index.js +192 -31
- package/dist/react.d.ts +174 -10
- package/dist/react.js +143 -71
- package/dist/{room-wFNUwEgi.js → room-24dGMM80.js} +119 -57
- package/package.json +26 -4
package/dist/index.d.ts
CHANGED
|
@@ -62,18 +62,29 @@ export declare interface ConnectionState {
|
|
|
62
62
|
readonly attempt: number;
|
|
63
63
|
}
|
|
64
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
65
|
export declare interface CrdtBackend {
|
|
73
66
|
/** Identifies the library, e.g. `"yjs"`. Surfaced for diagnostics/telemetry. */
|
|
74
67
|
readonly name: string;
|
|
75
68
|
/** Create a fresh text buffer, optionally seeded with `initial`. */
|
|
76
69
|
createText: (initial?: string) => CrdtTextHandle;
|
|
70
|
+
/** Create a whole-document handle, optionally seeded from encoded state. */
|
|
71
|
+
createDoc: (initial?: CrdtState) => CrdtDocHandle;
|
|
72
|
+
/**
|
|
73
|
+
* Re-encode a state through a live document so the backend garbage-collects
|
|
74
|
+
* what it can (Yjs: `mergeUpdates` never GCs tombstones, so a hot document's
|
|
75
|
+
* blob grows monotonically without this). Loses UPDATE HISTORY by design —
|
|
76
|
+
* a version-history feature must snapshot BEFORE compacting.
|
|
77
|
+
*/
|
|
78
|
+
compact: (state: CrdtState) => CrdtState;
|
|
79
|
+
/**
|
|
80
|
+
* HARD compaction for a text state: re-create the document from its
|
|
81
|
+
* MATERIALISED text, dropping all history, tombstones and client ids. The
|
|
82
|
+
* result is minimal — and a NEW EPOCH: it does not merge with updates from
|
|
83
|
+
* the old lineage, so the server treats a rebase as a fresh snapshot for
|
|
84
|
+
* every subscriber (which the subscription lane already handles as a
|
|
85
|
+
* reset). Use when `compact` no longer bounds a hot document.
|
|
86
|
+
*/
|
|
87
|
+
rebaseText: (state: CrdtState) => CrdtState;
|
|
77
88
|
/**
|
|
78
89
|
* Merge two encoded states into one converged encoded state.
|
|
79
90
|
*
|
|
@@ -89,6 +100,70 @@ export declare interface CrdtBackend {
|
|
|
89
100
|
emptyState: () => CrdtState;
|
|
90
101
|
}
|
|
91
102
|
|
|
103
|
+
/**
|
|
104
|
+
* A CRDT implementation. One per library. The default is {@link yjsBackend}.
|
|
105
|
+
*
|
|
106
|
+
* `merge` and `decodeText` are STATELESS with respect to any live handle: they
|
|
107
|
+
* operate purely on encoded bytes, which is what lets the sync layer and tests
|
|
108
|
+
* converge two states without holding a document open.
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* A live, mutable CRDT DOCUMENT — the whole-doc generalisation `crdtDoc()`
|
|
112
|
+
* stores (rich text, maps, arrays; whatever the backend's document model
|
|
113
|
+
* holds). `crdtText()` remains the single-field specialisation over the same
|
|
114
|
+
* backend.
|
|
115
|
+
*/
|
|
116
|
+
export declare interface CrdtDocHandle {
|
|
117
|
+
/**
|
|
118
|
+
* The underlying library document (a `Y.Doc` for the yjs backend), typed
|
|
119
|
+
* opaquely: an EDITOR BINDING needs the real object (Tiptap's Collaboration
|
|
120
|
+
* extension takes a Y.Doc), while every sync-layer consumer only moves
|
|
121
|
+
* encoded bytes and must never reach in. Cast at the binding, nowhere else.
|
|
122
|
+
*/
|
|
123
|
+
readonly raw: unknown;
|
|
124
|
+
/** Full state, encoded for transport or merge. */
|
|
125
|
+
encodeState: () => CrdtState;
|
|
126
|
+
/** This doc's state VECTOR — the compact "what I have" summary a peer
|
|
127
|
+
* diffs against. */
|
|
128
|
+
stateVector: () => Uint8Array;
|
|
129
|
+
/**
|
|
130
|
+
* Encode only what a peer holding `sinceVector` is missing — the
|
|
131
|
+
* INCREMENTAL update lane. A 1-character edit against a 100 KB doc encodes
|
|
132
|
+
* to a few dozen bytes, not the full state.
|
|
133
|
+
*/
|
|
134
|
+
encodeUpdateSince: (sinceVector: Uint8Array) => CrdtState;
|
|
135
|
+
/** Fold an encoded state or incremental update into this doc, in place. */
|
|
136
|
+
applyState: (state: CrdtState) => void;
|
|
137
|
+
/**
|
|
138
|
+
* Subscribe to update blobs this doc produces. Returns the unsubscribe.
|
|
139
|
+
*
|
|
140
|
+
* `local` distinguishes an edit made HERE from one produced by folding a
|
|
141
|
+
* remote state through `applyState`, and it is the difference between a
|
|
142
|
+
* working push loop and an echo: without it, applying a peer's update fires
|
|
143
|
+
* this handler, the app pushes it back, and every client re-broadcasts what
|
|
144
|
+
* it just received. Measured with three tabs open: one keystroke produced
|
|
145
|
+
* three server writes instead of one. It converges — the merge is
|
|
146
|
+
* idempotent — but the amplification scales with the session.
|
|
147
|
+
*
|
|
148
|
+
* It had to come from the BACKEND rather than an `applying` flag around
|
|
149
|
+
* `applyState` in app code: that flag is only correct while the backend emits
|
|
150
|
+
* synchronously, which `CrdtBackend` deliberately does not promise ("the
|
|
151
|
+
* backend decision lives behind our abstraction so it can change").
|
|
152
|
+
*/
|
|
153
|
+
onUpdate: (handler: (update: CrdtState, meta: {
|
|
154
|
+
readonly local: boolean;
|
|
155
|
+
}) => void) => () => void;
|
|
156
|
+
/**
|
|
157
|
+
* Encode a stable ANCHOR at `index` of the named text field — a position
|
|
158
|
+
* that survives concurrent edits (the primitive an inline-comment UI pins
|
|
159
|
+
* threads with; the UI itself is the comments plugin's business).
|
|
160
|
+
*/
|
|
161
|
+
encodeAnchor: (field: string, index: number) => Uint8Array;
|
|
162
|
+
/** Resolve an anchor back to its current index, or `undefined` when the
|
|
163
|
+
* anchored region was deleted. */
|
|
164
|
+
resolveAnchor: (encoded: Uint8Array) => number | undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
92
167
|
/** Identifies one CRDT-managed cell: a `crdtText()` COLUMN of one ROW of one TABLE. */
|
|
93
168
|
export declare interface CrdtDocKey {
|
|
94
169
|
readonly table: string;
|
|
@@ -96,6 +171,14 @@ export declare interface CrdtDocKey {
|
|
|
96
171
|
readonly column: string;
|
|
97
172
|
}
|
|
98
173
|
|
|
174
|
+
/**
|
|
175
|
+
* The client half of the CRDT downstream lane (plan 18): fold a `mergeCells`
|
|
176
|
+
* delta op's INCREMENTAL update into the held cell state. Pass as
|
|
177
|
+
* `@voltro/client`'s `SubscriptionCacheOptions.mergeCell`. A held cell that
|
|
178
|
+
* is not bytes (never delivered yet) takes the update as the new state.
|
|
179
|
+
*/
|
|
180
|
+
export declare const crdtMergeCell: (_column: string, prevValue: unknown, update: unknown) => unknown;
|
|
181
|
+
|
|
99
182
|
/**
|
|
100
183
|
* An encoded CRDT state or delta, as opaque bytes. Produced by
|
|
101
184
|
* {@link CrdtBackend.encodeState} / {@link CrdtTextHandle.encodeState},
|
|
@@ -168,6 +251,26 @@ export declare interface CrdtWritePayload extends CrdtDocKey {
|
|
|
168
251
|
readonly update: CrdtState;
|
|
169
252
|
}
|
|
170
253
|
|
|
254
|
+
/**
|
|
255
|
+
* The fallback-aware entry: durable when the environment allows it, in-memory
|
|
256
|
+
* + online-only otherwise — the plan's storage-fallback rule. The caller gets
|
|
257
|
+
* told which one it got; silence here would make a lockdown browser look like
|
|
258
|
+
* a working offline app until the first reload lost everything.
|
|
259
|
+
*/
|
|
260
|
+
export declare const createDurableKv: (options?: {
|
|
261
|
+
readonly factory?: KvIndexedDbFactory;
|
|
262
|
+
readonly databaseName?: string;
|
|
263
|
+
}) => Promise<DurableKvResult>;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* A durable KvStore over IndexedDB. Throws when no factory is available —
|
|
267
|
+
* `createDurableKv` below is the fallback-aware entry callers should use.
|
|
268
|
+
*/
|
|
269
|
+
export declare const createIndexedDbKv: (options?: {
|
|
270
|
+
readonly factory?: KvIndexedDbFactory;
|
|
271
|
+
readonly databaseName?: string;
|
|
272
|
+
}) => Promise<KvStore>;
|
|
273
|
+
|
|
171
274
|
/**
|
|
172
275
|
* Create a durable {@link PersistenceAdapter} over IndexedDB.
|
|
173
276
|
*
|
|
@@ -181,6 +284,9 @@ export declare const createIndexedDbPersistence: (options?: {
|
|
|
181
284
|
readonly databaseName?: string;
|
|
182
285
|
}) => Promise<PersistenceAdapter>;
|
|
183
286
|
|
|
287
|
+
/** In-memory KvStore — real (copies nothing; values are caller-owned JSON). */
|
|
288
|
+
export declare const createInMemoryKv: () => KvStore;
|
|
289
|
+
|
|
184
290
|
/**
|
|
185
291
|
* An in-memory {@link PersistenceAdapter} backed by Maps. Real, not a stub: it
|
|
186
292
|
* fully satisfies the contract, and copies bytes on the way in and out so a
|
|
@@ -214,6 +320,18 @@ export declare const createInMemoryPresenceChannel: (bus?: InMemoryPresenceBus)
|
|
|
214
320
|
*/
|
|
215
321
|
export declare const createPresenceRoom: <TPresence>(options: PresenceRoomOptions<TPresence>) => PresenceRoom<TPresence>;
|
|
216
322
|
|
|
323
|
+
export declare const createQueryMirror: (kv: KvStore, partition: MirrorPartition, options?: QueryMirrorOptions, now?: () => number) => QueryMirror;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Bind a partitioned {@link QueryMirror} to the subscription cache's mirror
|
|
327
|
+
* seam. Only declared tags persist; the mirror's own encrypted-column strip
|
|
328
|
+
* runs through `metadata` via the tag→table map.
|
|
329
|
+
*
|
|
330
|
+
* NOTE the mirror instance must already carry the CURRENT subject's
|
|
331
|
+
* partition — see the partition-switch note in the file header.
|
|
332
|
+
*/
|
|
333
|
+
export declare const createSubscriptionMirrorBinding: (mirror: QueryMirror, options: MirrorBindingOptions) => SubscriptionMirrorShape;
|
|
334
|
+
|
|
217
335
|
/**
|
|
218
336
|
* Create a sync client over a {@link SyncTransport}.
|
|
219
337
|
*
|
|
@@ -245,6 +363,26 @@ export declare const defaultCrdtBackend: CrdtBackend;
|
|
|
245
363
|
*/
|
|
246
364
|
export declare const deriveSyncStatus: (phase: ConnectionPhase, outstanding: number) => SyncStatus;
|
|
247
365
|
|
|
366
|
+
export declare interface DrainLockOptions {
|
|
367
|
+
/** Injectable locks implementation. Defaults to `navigator.locks` (which
|
|
368
|
+
* node 22+ and every modern browser provide). Pass `null` to force the
|
|
369
|
+
* no-locks path — a host genuinely without the API, or a test. */
|
|
370
|
+
readonly locks?: WebLocksLike | null;
|
|
371
|
+
/** Partition discriminator — one drainer per (subject, tenant) queue. */
|
|
372
|
+
readonly name: string;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export declare type DrainOutcome = 'drained' | 'held-elsewhere' | 'no-locks';
|
|
376
|
+
|
|
377
|
+
export declare interface DurableKvResult {
|
|
378
|
+
readonly kv: KvStore;
|
|
379
|
+
/** `'durable'` when IndexedDB opened; `'memory'` when the environment has
|
|
380
|
+
* none (non-browser host, lockdown mode) or the open failed (quota, a
|
|
381
|
+
* private tab that refuses at open time). VISIBLE degradation — render it
|
|
382
|
+
* in a sync status — never a crash. */
|
|
383
|
+
readonly durability: 'durable' | 'memory';
|
|
384
|
+
}
|
|
385
|
+
|
|
248
386
|
/** The encoded empty state — the identity element for {@link mergeCrdtStates}. */
|
|
249
387
|
export declare const emptyCrdtState: (backend?: CrdtBackend) => CrdtState;
|
|
250
388
|
|
|
@@ -268,16 +406,35 @@ declare interface IDBDatabaseLike {
|
|
|
268
406
|
createObjectStore: (name: string) => IDBObjectStoreLike;
|
|
269
407
|
}
|
|
270
408
|
|
|
409
|
+
declare interface IDBDatabaseLike_2 {
|
|
410
|
+
transaction(name: string, mode: 'readonly' | 'readwrite'): IDBTransactionLike_2;
|
|
411
|
+
objectStoreNames: {
|
|
412
|
+
contains(name: string): boolean;
|
|
413
|
+
};
|
|
414
|
+
createObjectStore(name: string): unknown;
|
|
415
|
+
}
|
|
416
|
+
|
|
271
417
|
declare interface IDBObjectStoreLike {
|
|
272
418
|
get: (key: string) => IDBRequestLike<unknown>;
|
|
273
419
|
put: (value: unknown, key: string) => IDBRequestLike<unknown>;
|
|
274
420
|
delete: (key: string) => IDBRequestLike<unknown>;
|
|
275
421
|
}
|
|
276
422
|
|
|
423
|
+
declare interface IDBObjectStoreLike_2 {
|
|
424
|
+
get(key: string): IDBRequestLike_2<unknown>;
|
|
425
|
+
put(value: unknown, key: string): IDBRequestLike_2<unknown>;
|
|
426
|
+
delete(key: string): IDBRequestLike_2<unknown>;
|
|
427
|
+
getAllKeys(range?: unknown): IDBRequestLike_2<ReadonlyArray<string>>;
|
|
428
|
+
}
|
|
429
|
+
|
|
277
430
|
declare interface IDBOpenRequestLike extends IDBRequestLike<IDBDatabaseLike> {
|
|
278
431
|
onupgradeneeded: ((this: unknown, ev: unknown) => void) | null;
|
|
279
432
|
}
|
|
280
433
|
|
|
434
|
+
declare interface IDBOpenRequestLike_2 extends IDBRequestLike_2<IDBDatabaseLike_2> {
|
|
435
|
+
onupgradeneeded: ((this: unknown, ev: unknown) => void) | null;
|
|
436
|
+
}
|
|
437
|
+
|
|
281
438
|
declare interface IDBRequestLike<T> {
|
|
282
439
|
result: T;
|
|
283
440
|
error: unknown;
|
|
@@ -285,18 +442,36 @@ declare interface IDBRequestLike<T> {
|
|
|
285
442
|
onerror: ((this: unknown, ev: unknown) => void) | null;
|
|
286
443
|
}
|
|
287
444
|
|
|
445
|
+
declare interface IDBRequestLike_2<T> {
|
|
446
|
+
result: T;
|
|
447
|
+
error: unknown;
|
|
448
|
+
onsuccess: ((this: unknown, ev: unknown) => void) | null;
|
|
449
|
+
onerror: ((this: unknown, ev: unknown) => void) | null;
|
|
450
|
+
}
|
|
451
|
+
|
|
288
452
|
declare interface IDBTransactionLike {
|
|
289
453
|
objectStore: (name: string) => IDBObjectStoreLike;
|
|
290
454
|
}
|
|
291
455
|
|
|
456
|
+
declare interface IDBTransactionLike_2 {
|
|
457
|
+
objectStore(name: string): IDBObjectStoreLike_2;
|
|
458
|
+
}
|
|
459
|
+
|
|
292
460
|
/**
|
|
293
461
|
* DONE — Durable persistence.
|
|
294
462
|
* `createIndexedDbPersistence` (./persistence/indexedDb) is a durable
|
|
295
463
|
* `PersistenceAdapter` over IndexedDB — no WASM, no added dependency, tested
|
|
296
464
|
* against a fake IDB backend that survives a reopen. `createInMemoryPersistence`
|
|
297
|
-
* remains the ephemeral default.
|
|
298
|
-
*
|
|
299
|
-
*
|
|
465
|
+
* remains the ephemeral default. The engine's query mirror adds `KvStore`
|
|
466
|
+
* (in-memory + IndexedDB + `createDurableKv` with a VISIBLE memory
|
|
467
|
+
* fallback) beneath `createQueryMirror` — budget eviction included
|
|
468
|
+
* (`enforceBudget`, oldest-saved first). wa-sqlite was considered and
|
|
469
|
+
* REJECTED with the reasoning recorded in `mirror/queryMirror.ts`: the
|
|
470
|
+
* client's query surface is `(tag, input)`, predicates never exist
|
|
471
|
+
* client-side, so a browser SQL engine would evaluate a language the
|
|
472
|
+
* client never sees. A SQLite BACKING behind `KvStore` remains possible
|
|
473
|
+
* without touching a consumer (mobile's expo-sqlite adapter takes exactly
|
|
474
|
+
* that seam).
|
|
300
475
|
*
|
|
301
476
|
* DONE — Bi-directional sync wire.
|
|
302
477
|
* `createSyncClient` (./sync/syncClient) maps the pure sync-queue reducer onto
|
|
@@ -323,10 +498,14 @@ declare interface IDBTransactionLike {
|
|
|
323
498
|
* ephemeral awareness over a `PresenceChannel` — join/leave, announce-back
|
|
324
499
|
* discovery, cursor propagation, TTL expiry. Tested over the in-memory channel
|
|
325
500
|
* (`createInMemoryPresenceChannel`), which is the SAME dumb string-payload
|
|
326
|
-
* shape as the framework's `BroadcastProvider`.
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
501
|
+
* shape as the framework's `BroadcastProvider`. The runtime binding is DONE
|
|
502
|
+
* (plan 02 phase 1): `usePresenceChannel` in `@voltro/plugin-presence/web`
|
|
503
|
+
* rides the framework's EXISTING presence lane — `presence.heartbeat`
|
|
504
|
+
* carries the payload as member meta, the push-driven `presence.list`
|
|
505
|
+
* roster delivers it — so cross-replica fan-out is plugin-broadcast's and
|
|
506
|
+
* there is ONE presence wire, not two. The shape parity is pinned by
|
|
507
|
+
* `presence/channelParity.test-d.ts` (structural restatement, no
|
|
508
|
+
* production dependency edge).
|
|
330
509
|
*
|
|
331
510
|
* DONE — localFirst mixin + discovery.
|
|
332
511
|
* `localFirst()` (`@voltro/database`) marks a table; the runtime SchemaRegistry
|
|
@@ -355,6 +534,20 @@ export declare interface InMemoryPresenceBus {
|
|
|
355
534
|
readonly rooms: Map<string, Set<(payload: string) => void>>;
|
|
356
535
|
}
|
|
357
536
|
|
|
537
|
+
export declare interface KvIndexedDbFactory {
|
|
538
|
+
open(name: string, version: number): IDBOpenRequestLike_2;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export declare interface KvStore {
|
|
542
|
+
/** Identifies the backing (`"in-memory"` / `"indexeddb"`). Diagnostics. */
|
|
543
|
+
readonly kind: string;
|
|
544
|
+
readonly get: (key: string) => Promise<unknown>;
|
|
545
|
+
readonly put: (key: string, value: unknown) => Promise<void>;
|
|
546
|
+
readonly del: (key: string) => Promise<void>;
|
|
547
|
+
/** All stored keys starting with `prefix`, unordered. */
|
|
548
|
+
readonly keysWithPrefix: (prefix: string) => Promise<ReadonlyArray<string>>;
|
|
549
|
+
}
|
|
550
|
+
|
|
358
551
|
/**
|
|
359
552
|
* Last-write-wins: newer `updatedAt` wins; exact ties break on the (stable,
|
|
360
553
|
* symmetric) tiebreak key so both peers converge on the same value.
|
|
@@ -372,15 +565,82 @@ export declare const lastWriteWins: ConflictResolver;
|
|
|
372
565
|
*/
|
|
373
566
|
export declare const loadPersistedSyncQueue: <T = unknown>(adapter: PersistenceAdapter, online?: boolean) => Promise<PersistedSyncQueue<T>>;
|
|
374
567
|
|
|
568
|
+
export declare const mergeCrdtStates: (a: CrdtState, b: CrdtState, backend?: CrdtBackend) => CrdtState;
|
|
569
|
+
|
|
570
|
+
export declare interface MirrorBindingOptions {
|
|
571
|
+
/** Which rpc tags are backed by which `localFirst()` table — the app's
|
|
572
|
+
* declaration of its sync set. A tag absent here is NOT mirrored. */
|
|
573
|
+
readonly tags: Readonly<Record<string, string>>;
|
|
574
|
+
/** The codegen-emitted metadata (`localFirstTables` from
|
|
575
|
+
* `.framework/localFirst.generated.ts`): table → columns to strip. */
|
|
576
|
+
readonly metadata?: Readonly<Record<string, {
|
|
577
|
+
readonly encryptedColumns: ReadonlyArray<string>;
|
|
578
|
+
}>>;
|
|
579
|
+
/**
|
|
580
|
+
* Schema fingerprint of the CLIENT build (any stable string — a build hash,
|
|
581
|
+
* an app version, `JSON.stringify(localFirstTables)`). The local-DB
|
|
582
|
+
* migration path: entries persist under the fingerprint they were written
|
|
583
|
+
* with, and a load under a DIFFERENT fingerprint returns nothing — a
|
|
584
|
+
* v1-mirrored row set is never rendered into a v2 UI whose columns moved.
|
|
585
|
+
* The DISCARD is visible as a plain cold start (loading, then the server's
|
|
586
|
+
* fresh snapshot), never a crash or a mixed-shape render. The offline
|
|
587
|
+
* QUEUE deliberately does not gate on this: a queued v1 write replays
|
|
588
|
+
* against the v2 server, whose input schema is the authority — a write it
|
|
589
|
+
* rejects surfaces as a failed/conflicted outbox entry the user can see
|
|
590
|
+
* and resolve, which beats silently dropping their work.
|
|
591
|
+
*/
|
|
592
|
+
readonly schemaFingerprint?: string;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export declare interface MirroredQuery {
|
|
596
|
+
/** The rpc tag — kept for diagnostics and schema-migration checks. */
|
|
597
|
+
readonly tag: string;
|
|
598
|
+
/** The materialised rows (or single value) as the client would render them. */
|
|
599
|
+
readonly data: unknown;
|
|
600
|
+
/** The subscription revision `data` corresponds to — presented on
|
|
601
|
+
* re-subscribe as `voltro-resume-from`, so a reload inside the resume
|
|
602
|
+
* window continues with deltas instead of a snapshot. */
|
|
603
|
+
readonly revision: number;
|
|
604
|
+
/** When this entry was last written (ms epoch) — budget eviction input. */
|
|
605
|
+
readonly savedAt: number;
|
|
606
|
+
/** Schema fingerprint of the build that wrote the entry (see the binding's
|
|
607
|
+
* `schemaFingerprint` — the local-DB migration gate). Absent when the app
|
|
608
|
+
* does not stamp builds. */
|
|
609
|
+
readonly fingerprint?: string;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export declare interface MirrorPartition {
|
|
613
|
+
readonly subjectId: string;
|
|
614
|
+
readonly tenantId: string;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Structural mirror of `@voltro/client`'s `OutboxEntry` — kept structural so
|
|
618
|
+
* this package does not depend on the client package (the client consumes
|
|
619
|
+
* US through its `OutboxPersistence` seam, not the other way around). */
|
|
620
|
+
export declare interface OutboxEntryShape {
|
|
621
|
+
readonly id: string;
|
|
622
|
+
readonly tag: string;
|
|
623
|
+
readonly input: unknown;
|
|
624
|
+
readonly status: 'pending' | 'sent' | 'failed' | 'conflict';
|
|
625
|
+
readonly attempts: number;
|
|
626
|
+
readonly error?: unknown;
|
|
627
|
+
}
|
|
628
|
+
|
|
375
629
|
/**
|
|
376
|
-
*
|
|
630
|
+
* Bind an outbox to a {@link PersistenceAdapter}. Pass the result as
|
|
631
|
+
* `useOutbox({ persistence })`.
|
|
377
632
|
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
633
|
+
* `now` is injectable for determinism (the adapter's `QueuedWrite` carries an
|
|
634
|
+
* `enqueuedAt`); entries that do not decode as outbox entries are DROPPED on
|
|
635
|
+
* load rather than crashing the hydration — a queue poisoned by another
|
|
636
|
+
* writer's shape degrades to the entries it can vouch for.
|
|
382
637
|
*/
|
|
383
|
-
export declare const
|
|
638
|
+
export declare const outboxPersistence: (adapter: PersistenceAdapter, now?: () => number) => OutboxPersistenceBinding;
|
|
639
|
+
|
|
640
|
+
export declare interface OutboxPersistenceBinding {
|
|
641
|
+
readonly load: () => Promise<ReadonlyArray<OutboxEntryShape>>;
|
|
642
|
+
readonly save: (queue: ReadonlyArray<OutboxEntryShape>) => Promise<void>;
|
|
643
|
+
}
|
|
384
644
|
|
|
385
645
|
/** Total outstanding writes (pending + the one in flight). 0 means fully synced. */
|
|
386
646
|
export declare const outstandingCount: (state: SyncQueueState) => number;
|
|
@@ -472,6 +732,25 @@ export declare interface PresenceRoomOptions<TPresence> {
|
|
|
472
732
|
readonly now?: () => number;
|
|
473
733
|
}
|
|
474
734
|
|
|
735
|
+
export declare interface QueryMirror {
|
|
736
|
+
readonly partition: MirrorPartition;
|
|
737
|
+
readonly save: (queryKey: string, entry: Omit<MirroredQuery, 'savedAt'>) => Promise<void>;
|
|
738
|
+
readonly load: (queryKey: string) => Promise<MirroredQuery | undefined>;
|
|
739
|
+
/** Drop every mirrored query of THIS partition (logout, revocation). */
|
|
740
|
+
readonly purge: () => Promise<void>;
|
|
741
|
+
/** Enforce the storage budget: evict least-recently-saved entries until the
|
|
742
|
+
* serialised size estimate fits `maxBytes`. Returns evicted keys. */
|
|
743
|
+
readonly enforceBudget: (maxBytes: number) => Promise<ReadonlyArray<string>>;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export declare interface QueryMirrorOptions {
|
|
747
|
+
/** Column-exposure metadata for `localFirst()` tables, as emitted by the
|
|
748
|
+
* codegen: table → columns that must NEVER be persisted locally
|
|
749
|
+
* (`.encrypted()`). The mirror strips them from every row of a saved
|
|
750
|
+
* entry when `tableOf` resolves the entry's tag to a table. */
|
|
751
|
+
readonly encryptedColumnsOf?: (tag: string) => ReadonlyArray<string> | undefined;
|
|
752
|
+
}
|
|
753
|
+
|
|
475
754
|
/** A single queued write. `payload` is opaque to the queue — the wire seam owns its shape. */
|
|
476
755
|
export declare interface QueuedWrite<T = unknown> {
|
|
477
756
|
readonly id: string;
|
|
@@ -488,17 +767,61 @@ export declare interface RemoteCrdtState extends CrdtDocKey {
|
|
|
488
767
|
}
|
|
489
768
|
|
|
490
769
|
/**
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
* around them ships; see the DONE entry above)
|
|
495
|
-
* - a `PresenceChannel` bound to a provisioned Redis/NATS broker at scale
|
|
496
|
-
* - (optional) a wa-sqlite/Turso durable adapter for cross-tab SQL
|
|
770
|
+
* Resolve a conflicted row write: CRDT columns merge, the rest go through the
|
|
771
|
+
* policy. `local`/`remote` carry per-field `{ value, updatedAt, writer }`;
|
|
772
|
+
* CRDT columns' values are their encoded states.
|
|
497
773
|
*/
|
|
498
|
-
export declare const
|
|
774
|
+
export declare const resolveWithPolicy: (policy: ConflictPolicy, local: VersionedRecord, remote: VersionedRecord, options?: ResolveWithPolicyOptions) => Record<string, unknown>;
|
|
775
|
+
|
|
776
|
+
export declare interface ResolveWithPolicyOptions {
|
|
777
|
+
/** Column names that are `crdtText()` cells — merged, never policy-resolved.
|
|
778
|
+
* Comes from the codegen's localFirst column metadata. */
|
|
779
|
+
readonly crdtColumns?: ReadonlyArray<string>;
|
|
780
|
+
readonly backend?: CrdtBackend;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* The genuinely REMAINING runtime seam — the two app-level NAMES nothing can
|
|
785
|
+
* derive, not un-built framework code: the mutation tag that writes the
|
|
786
|
+
* `crdtText()` column and the reactive query tag that streams the row, which
|
|
787
|
+
* `useCrdtText` is pointed at. The lifecycle around them ships; see the DONE
|
|
788
|
+
* entry above.
|
|
789
|
+
*
|
|
790
|
+
* **Two entries left this list, in opposite directions, and the distinction is
|
|
791
|
+
* the reason this comment is longer than the array.** A seam is work somebody
|
|
792
|
+
* still has to do. Neither of these was:
|
|
793
|
+
*
|
|
794
|
+
* - `presence-broker-binding` is BUILT (`usePresenceChannel` in
|
|
795
|
+
* `@voltro/plugin-presence/web`, riding the framework's own presence lane;
|
|
796
|
+
* shape pinned by `presence/channelParity.test-d.ts`). The DONE entry above
|
|
797
|
+
* has said so since plan 02 phase 1 while this array said the opposite —
|
|
798
|
+
* one file claiming both, which is worse than either claim alone.
|
|
799
|
+
* - `wasm-sqlite-durable-adapter` was REJECTED, with the reasoning recorded
|
|
800
|
+
* in `mirror/queryMirror.ts`: the client's query surface is `(tag, input)`
|
|
801
|
+
* and predicates never exist client-side, so a browser SQL engine would
|
|
802
|
+
* evaluate a language the client never sees. A decision we made is not a
|
|
803
|
+
* gap in the framework, and listing it as one invites somebody to close it.
|
|
804
|
+
* The `KvStore` seam still admits a SQLite BACKING (mobile's expo-sqlite
|
|
805
|
+
* adapter takes exactly that) — that is a different, already-open door.
|
|
806
|
+
*/
|
|
807
|
+
export declare const RUNTIME_SEAMS: readonly ["sync-transport-app-tags"];
|
|
499
808
|
|
|
500
809
|
export declare type RuntimeSeam = (typeof RUNTIME_SEAMS)[number];
|
|
501
810
|
|
|
811
|
+
/** Structural mirror of `@voltro/client`'s SubscriptionMirror — restated so
|
|
812
|
+
* this package carries no client dependency; the client consumes US. */
|
|
813
|
+
export declare interface SubscriptionMirrorShape {
|
|
814
|
+
readonly load: (key: string) => Promise<{
|
|
815
|
+
readonly data: unknown;
|
|
816
|
+
readonly revision: number;
|
|
817
|
+
} | undefined>;
|
|
818
|
+
readonly save: (key: string, snap: {
|
|
819
|
+
readonly tag: string | undefined;
|
|
820
|
+
readonly data: unknown;
|
|
821
|
+
readonly revision: number;
|
|
822
|
+
}) => Promise<void>;
|
|
823
|
+
}
|
|
824
|
+
|
|
502
825
|
export declare interface SyncClient {
|
|
503
826
|
/** Apply a local edit: merge it into local state immediately (optimistic),
|
|
504
827
|
* persist, queue it, and start draining if online. */
|
|
@@ -609,6 +932,24 @@ export declare interface SyncTransport {
|
|
|
609
932
|
/** A record's fields, each carrying its version metadata. */
|
|
610
933
|
export declare type VersionedRecord = Record<string, FieldValue>;
|
|
611
934
|
|
|
935
|
+
export declare interface WebLocksLike {
|
|
936
|
+
readonly request: (name: string, options: {
|
|
937
|
+
readonly mode: 'exclusive';
|
|
938
|
+
readonly ifAvailable: boolean;
|
|
939
|
+
}, callback: (lock: unknown | null) => Promise<void>) => Promise<void>;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Run `drain` under the partition's exclusive cross-tab lock.
|
|
944
|
+
*
|
|
945
|
+
* `ifAvailable: true` — a tab that finds the lock held does NOT queue behind
|
|
946
|
+
* it (the holder is already draining the same shared queue; a second run the
|
|
947
|
+
* moment it finishes would replay an already-compacted queue). It reports
|
|
948
|
+
* `held-elsewhere` and relies on the holder's drain + its own next trigger
|
|
949
|
+
* (reconnect, enqueue) to try again.
|
|
950
|
+
*/
|
|
951
|
+
export declare const withDrainLock: (options: DrainLockOptions, drain: () => Promise<void>) => Promise<DrainOutcome>;
|
|
952
|
+
|
|
612
953
|
export declare const yjsBackend: CrdtBackend;
|
|
613
954
|
|
|
614
955
|
export { }
|