@tanstack/db-sqlite-persistence-core 0.1.2

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,1705 @@
1
+ import { compileSingleRowExpression, toBooleanPredicate } from "@tanstack/db";
2
+ import { InvalidPersistedCollectionConfigError, InvalidPersistedStorageKeyEncodingError, InvalidPersistedStorageKeyError, InvalidSyncConfigError, InvalidPersistedCollectionCoordinatorError, InvalidPersistenceAdapterError } from "./errors.js";
3
+ const REQUIRED_COORDINATOR_METHODS = [
4
+ `getNodeId`,
5
+ `subscribe`,
6
+ `publish`,
7
+ `isLeader`,
8
+ `ensureLeadership`,
9
+ `requestEnsurePersistedIndex`
10
+ ];
11
+ const REQUIRED_ADAPTER_METHODS = [`loadSubset`, `applyCommittedTx`, `ensureIndex`];
12
+ const TARGETED_INVALIDATION_KEY_LIMIT = 128;
13
+ const DEFAULT_DB_NAME = `tanstack-db`;
14
+ const REMOTE_ENSURE_RETRY_DELAY_MS = 50;
15
+ class SingleProcessCoordinator {
16
+ constructor(nodeId = crypto.randomUUID()) {
17
+ this.nodeId = nodeId;
18
+ }
19
+ getNodeId() {
20
+ return this.nodeId;
21
+ }
22
+ subscribe() {
23
+ return () => {
24
+ };
25
+ }
26
+ publish() {
27
+ }
28
+ isLeader() {
29
+ return true;
30
+ }
31
+ async ensureLeadership() {
32
+ }
33
+ async requestEnsureRemoteSubset() {
34
+ }
35
+ async requestEnsurePersistedIndex() {
36
+ }
37
+ pullSince() {
38
+ return Promise.resolve({
39
+ type: `rpc:pullSince:res`,
40
+ rpcId: crypto.randomUUID(),
41
+ ok: true,
42
+ latestTerm: 1,
43
+ latestSeq: 0,
44
+ latestRowVersion: 0,
45
+ requiresFullReload: false,
46
+ changedKeys: [],
47
+ deletedKeys: [],
48
+ deltas: []
49
+ });
50
+ }
51
+ }
52
+ function validatePersistedCollectionCoordinator(coordinator) {
53
+ for (const method of REQUIRED_COORDINATOR_METHODS) {
54
+ if (typeof coordinator[method] !== `function`) {
55
+ throw new InvalidPersistedCollectionCoordinatorError(method);
56
+ }
57
+ }
58
+ }
59
+ function validatePersistenceAdapter(adapter) {
60
+ for (const method of REQUIRED_ADAPTER_METHODS) {
61
+ if (typeof adapter[method] !== `function`) {
62
+ throw new InvalidPersistenceAdapterError(method);
63
+ }
64
+ }
65
+ }
66
+ function resolvePersistence(persistence) {
67
+ validatePersistenceAdapter(persistence.adapter);
68
+ const coordinator = persistence.coordinator ?? new SingleProcessCoordinator();
69
+ validatePersistedCollectionCoordinator(coordinator);
70
+ return {
71
+ ...persistence,
72
+ coordinator
73
+ };
74
+ }
75
+ function resolvePersistenceForMode(persistence, mode) {
76
+ const modeSpecificPersistence = persistence.resolvePersistenceForMode?.(mode);
77
+ return resolvePersistence(modeSpecificPersistence ?? persistence);
78
+ }
79
+ function resolvePersistenceForCollection(persistence, options) {
80
+ const collectionSpecificPersistence = persistence.resolvePersistenceForCollection?.(options);
81
+ if (collectionSpecificPersistence) {
82
+ return resolvePersistence(collectionSpecificPersistence);
83
+ }
84
+ return resolvePersistenceForMode(persistence, options.mode);
85
+ }
86
+ function hasOwnSyncKey(options) {
87
+ return Object.prototype.hasOwnProperty.call(options, `sync`);
88
+ }
89
+ function isRecord(value) {
90
+ return typeof value === `object` && value !== null;
91
+ }
92
+ function isValidSyncConfig(value) {
93
+ if (!isRecord(value)) {
94
+ return false;
95
+ }
96
+ return typeof value.sync === `function`;
97
+ }
98
+ class ApplyMutex {
99
+ constructor() {
100
+ this.queue = Promise.resolve();
101
+ }
102
+ async run(task) {
103
+ const taskPromise = this.queue.then(() => task());
104
+ this.queue = taskPromise.then(
105
+ () => void 0,
106
+ () => void 0
107
+ );
108
+ return taskPromise;
109
+ }
110
+ }
111
+ function toStableSerializable(value) {
112
+ if (value == null) {
113
+ return value;
114
+ }
115
+ switch (typeof value) {
116
+ case `string`:
117
+ case `number`:
118
+ case `boolean`:
119
+ return value;
120
+ case `bigint`:
121
+ return value.toString();
122
+ case `function`:
123
+ case `symbol`:
124
+ case `undefined`:
125
+ return void 0;
126
+ }
127
+ if (value instanceof Date) {
128
+ return value.toISOString();
129
+ }
130
+ if (Array.isArray(value)) {
131
+ return value.map((entry) => toStableSerializable(entry)).filter((entry) => entry !== void 0);
132
+ }
133
+ if (value instanceof Set) {
134
+ return Array.from(value).map((entry) => toStableSerializable(entry)).filter((entry) => entry !== void 0).sort((left, right) => {
135
+ const leftSerialized = JSON.stringify(left);
136
+ const rightSerialized = JSON.stringify(right);
137
+ return leftSerialized < rightSerialized ? -1 : leftSerialized > rightSerialized ? 1 : 0;
138
+ });
139
+ }
140
+ if (value instanceof Map) {
141
+ return Array.from(value.entries()).map(([key, mapValue]) => ({
142
+ key: toStableSerializable(key),
143
+ value: toStableSerializable(mapValue)
144
+ })).filter((entry) => entry.key !== void 0 && entry.value !== void 0).sort((left, right) => {
145
+ const leftSerialized = JSON.stringify(left.key);
146
+ const rightSerialized = JSON.stringify(right.key);
147
+ return leftSerialized < rightSerialized ? -1 : leftSerialized > rightSerialized ? 1 : 0;
148
+ });
149
+ }
150
+ const record = value;
151
+ const orderedKeys = Object.keys(record).sort(
152
+ (left, right) => left < right ? -1 : left > right ? 1 : 0
153
+ );
154
+ const serializableRecord = {};
155
+ for (const key of orderedKeys) {
156
+ const serializableValue = toStableSerializable(record[key]);
157
+ if (serializableValue !== void 0) {
158
+ serializableRecord[key] = serializableValue;
159
+ }
160
+ }
161
+ return serializableRecord;
162
+ }
163
+ function stableSerialize(value) {
164
+ return JSON.stringify(toStableSerializable(value) ?? null);
165
+ }
166
+ function normalizeSubsetOptionsForKey(options) {
167
+ return {
168
+ where: toStableSerializable(options.where),
169
+ orderBy: toStableSerializable(options.orderBy),
170
+ limit: options.limit,
171
+ cursor: toStableSerializable(options.cursor),
172
+ offset: options.offset
173
+ };
174
+ }
175
+ function normalizeSyncFnResult(result) {
176
+ if (typeof result === `function`) {
177
+ return { cleanup: result };
178
+ }
179
+ if (result === void 0) {
180
+ return {};
181
+ }
182
+ return result;
183
+ }
184
+ function isTxCommittedPayload(payload) {
185
+ if (!isRecord(payload) || payload.type !== `tx:committed`) {
186
+ return false;
187
+ }
188
+ if (typeof payload.term !== `number` || typeof payload.seq !== `number` || typeof payload.txId !== `string` || typeof payload.latestRowVersion !== `number` || typeof payload.requiresFullReload !== `boolean`) {
189
+ return false;
190
+ }
191
+ if (payload.requiresFullReload) {
192
+ return true;
193
+ }
194
+ return Array.isArray(payload.changedRows) && Array.isArray(payload.deletedKeys) && (payload.rowMetadataMutations === void 0 || Array.isArray(payload.rowMetadataMutations)) && (payload.collectionMetadataMutations === void 0 || Array.isArray(payload.collectionMetadataMutations));
195
+ }
196
+ function isCollectionResetPayload(payload) {
197
+ return isRecord(payload) && payload.type === `collection:reset` && typeof payload.schemaVersion === `number` && typeof payload.resetEpoch === `number`;
198
+ }
199
+ function toPersistedMutationEnvelope(mutation) {
200
+ const key = mutation.key;
201
+ const value = mutation.type === `delete` ? mutation.original : mutation.modified;
202
+ return {
203
+ mutationId: mutation.mutationId,
204
+ type: mutation.type,
205
+ key,
206
+ value
207
+ };
208
+ }
209
+ class PersistedCollectionRuntime {
210
+ constructor(mode, collectionId, persistence, syncMode, dbName) {
211
+ this.mode = mode;
212
+ this.collectionId = collectionId;
213
+ this.persistence = persistence;
214
+ this.syncMode = syncMode;
215
+ this.dbName = dbName;
216
+ this.applyMutex = new ApplyMutex();
217
+ this.activeSubsets = /* @__PURE__ */ new Map();
218
+ this.pendingRemoteSubsetEnsures = /* @__PURE__ */ new Map();
219
+ this.queuedHydrationTransactions = [];
220
+ this.queuedTxCommitted = [];
221
+ this.subscriptionIds = /* @__PURE__ */ new WeakMap();
222
+ this.collection = null;
223
+ this.syncControls = {
224
+ begin: null,
225
+ write: null,
226
+ commit: null,
227
+ truncate: null,
228
+ metadata: null
229
+ };
230
+ this.started = false;
231
+ this.startupMetadataPromise = null;
232
+ this.startPromise = null;
233
+ this.internalApplyDepth = 0;
234
+ this.isHydrating = false;
235
+ this.coordinatorUnsubscribe = null;
236
+ this.indexAddedUnsubscribe = null;
237
+ this.indexRemovedUnsubscribe = null;
238
+ this.remoteEnsureRetryTimer = null;
239
+ this.nextSubscriptionId = 0;
240
+ this.latestTerm = 0;
241
+ this.latestSeq = 0;
242
+ this.latestRowVersion = 0;
243
+ this.localTerm = 1;
244
+ this.localSeq = 0;
245
+ this.localRowVersion = 0;
246
+ }
247
+ setSyncControls(syncControls) {
248
+ this.syncControls = syncControls;
249
+ }
250
+ clearSyncControls() {
251
+ this.syncControls = {
252
+ begin: null,
253
+ write: null,
254
+ commit: null,
255
+ truncate: null,
256
+ metadata: null
257
+ };
258
+ }
259
+ isHydratingNow() {
260
+ return this.isHydrating;
261
+ }
262
+ isApplyingInternally() {
263
+ return this.internalApplyDepth > 0;
264
+ }
265
+ setCollection(collection) {
266
+ if (this.collection === collection) {
267
+ return;
268
+ }
269
+ this.collection = collection;
270
+ this.attachCoordinatorSubscription();
271
+ }
272
+ getLeadershipState() {
273
+ return {
274
+ nodeId: this.persistence.coordinator.getNodeId(),
275
+ isLeader: this.persistence.coordinator.isLeader(this.collectionId)
276
+ };
277
+ }
278
+ async ensureStarted() {
279
+ if (this.startPromise) {
280
+ return this.startPromise;
281
+ }
282
+ this.startPromise = this.startInternal();
283
+ return this.startPromise;
284
+ }
285
+ async ensureStartupMetadataLoaded() {
286
+ if (this.startupMetadataPromise) {
287
+ return this.startupMetadataPromise;
288
+ }
289
+ this.startupMetadataPromise = this.loadStartupMetadataInternal();
290
+ return this.startupMetadataPromise;
291
+ }
292
+ async startInternal() {
293
+ if (this.started) {
294
+ return;
295
+ }
296
+ this.started = true;
297
+ await this.ensureStartupMetadataLoaded();
298
+ const indexBootstrapSnapshot = this.collection?.getIndexMetadata() ?? [];
299
+ this.attachIndexLifecycleListeners();
300
+ await this.bootstrapPersistedIndexes(indexBootstrapSnapshot);
301
+ if (this.syncMode !== `on-demand`) {
302
+ this.activeSubsets.set(this.getSubsetKey({}), {});
303
+ await this.applyMutex.run(
304
+ () => this.hydrateSubsetUnsafe({}, { requestRemoteEnsure: false })
305
+ );
306
+ }
307
+ }
308
+ async loadStartupMetadataInternal() {
309
+ if (this.persistence.adapter.getStreamPosition) {
310
+ const position = await this.persistence.adapter.getStreamPosition(
311
+ this.collectionId
312
+ );
313
+ this.observeStreamPosition(
314
+ position.latestTerm,
315
+ position.latestSeq,
316
+ position.latestRowVersion
317
+ );
318
+ }
319
+ await this.loadCollectionMetadataIntoCollection();
320
+ }
321
+ async loadCollectionMetadataIntoCollection() {
322
+ const collectionMetadata = await this.loadCollectionMetadataSnapshot();
323
+ this.replaceCollectionMetadataSnapshot(collectionMetadata);
324
+ }
325
+ async loadCollectionMetadataSnapshot() {
326
+ if (!this.persistence.adapter.loadCollectionMetadata) {
327
+ return [];
328
+ }
329
+ return this.persistence.adapter.loadCollectionMetadata(this.collectionId);
330
+ }
331
+ replaceCollectionMetadataSnapshot(collectionMetadata) {
332
+ if (!this.syncControls.begin || !this.syncControls.commit || !this.syncControls.metadata) {
333
+ return;
334
+ }
335
+ const nextMetadata = new Map(
336
+ collectionMetadata.map(({ key, value }) => [key, value])
337
+ );
338
+ const currentKeys = this.syncControls.metadata.collection.list().map(({ key }) => key);
339
+ this.withInternalApply(() => {
340
+ this.syncControls.begin?.({ immediate: true });
341
+ currentKeys.forEach((key) => {
342
+ if (!nextMetadata.has(key)) {
343
+ this.syncControls.metadata?.collection.delete(key);
344
+ }
345
+ });
346
+ nextMetadata.forEach((value, key) => {
347
+ this.syncControls.metadata?.collection.set(key, value);
348
+ });
349
+ this.syncControls.commit?.();
350
+ });
351
+ }
352
+ async loadSubset(options, upstreamLoadSubset) {
353
+ this.activeSubsets.set(this.getSubsetKey(options), options);
354
+ await this.applyMutex.run(
355
+ () => this.hydrateSubsetUnsafe(options, {
356
+ requestRemoteEnsure: this.mode === `sync-present`
357
+ })
358
+ );
359
+ if (upstreamLoadSubset) {
360
+ try {
361
+ const maybePromise = upstreamLoadSubset(options);
362
+ if (maybePromise instanceof Promise) {
363
+ maybePromise.catch((error) => {
364
+ console.warn(
365
+ `Failed to load remote subset in persisted wrapper:`,
366
+ error
367
+ );
368
+ this.queueRemoteSubsetEnsure(options);
369
+ });
370
+ }
371
+ } catch (error) {
372
+ console.warn(`Failed to trigger remote subset load:`, error);
373
+ this.queueRemoteSubsetEnsure(options);
374
+ }
375
+ }
376
+ }
377
+ unloadSubset(options, upstreamUnloadSubset) {
378
+ this.activeSubsets.delete(this.getSubsetKey(options));
379
+ upstreamUnloadSubset?.(options);
380
+ }
381
+ async forceReloadSubset(options) {
382
+ this.activeSubsets.set(this.getSubsetKey(options), options);
383
+ await this.applyMutex.run(
384
+ () => this.hydrateSubsetUnsafe(options, { requestRemoteEnsure: false })
385
+ );
386
+ }
387
+ queueHydrationBufferedTransaction(transaction) {
388
+ this.queuedHydrationTransactions.push(transaction);
389
+ }
390
+ async persistAndBroadcastExternalSyncTransaction(transaction) {
391
+ await this.applyMutex.run(
392
+ () => this.persistAndBroadcastExternalSyncTransactionUnsafe(transaction)
393
+ );
394
+ }
395
+ normalizeSyncWriteMessage(message) {
396
+ if (!this.collection) {
397
+ throw new InvalidPersistedCollectionConfigError(
398
+ `collection must be attached before sync writes are processed`
399
+ );
400
+ }
401
+ if (`key` in message) {
402
+ const key2 = message.key;
403
+ const previousValue = this.collection.get(key2) ?? {};
404
+ return {
405
+ forwardMessage: {
406
+ type: `delete`,
407
+ key: key2
408
+ },
409
+ operation: {
410
+ type: `delete`,
411
+ key: key2,
412
+ value: previousValue
413
+ }
414
+ };
415
+ }
416
+ if (message.type === `delete`) {
417
+ const key2 = this.collection.getKeyFromItem(message.value);
418
+ const previousValue = this.collection.get(key2) ?? message.value;
419
+ return {
420
+ forwardMessage: {
421
+ type: `delete`,
422
+ key: key2
423
+ },
424
+ operation: {
425
+ type: `delete`,
426
+ key: key2,
427
+ value: previousValue
428
+ }
429
+ };
430
+ }
431
+ const key = this.collection.getKeyFromItem(message.value);
432
+ return {
433
+ forwardMessage: {
434
+ type: `update`,
435
+ value: message.value,
436
+ metadata: message.metadata
437
+ },
438
+ operation: {
439
+ type: `update`,
440
+ key,
441
+ value: message.value,
442
+ metadata: message.metadata
443
+ }
444
+ };
445
+ }
446
+ async persistAndConfirmCollectionMutations(mutations) {
447
+ if (mutations.length === 0) {
448
+ return;
449
+ }
450
+ await this.applyMutex.run(async () => {
451
+ const acceptedMutationIds = await this.persistCollectionMutationsUnsafe(mutations);
452
+ const acceptedMutationIdSet = new Set(acceptedMutationIds);
453
+ const acceptedMutations = mutations.filter(
454
+ (mutation) => acceptedMutationIdSet.has(mutation.mutationId)
455
+ );
456
+ if (acceptedMutations.length !== mutations.length) {
457
+ throw new Error(
458
+ `persistence coordinator accepted ${acceptedMutations.length} of ${mutations.length} mutations; partial acceptance is not supported`
459
+ );
460
+ }
461
+ this.confirmMutationsSyncUnsafe(acceptedMutations);
462
+ });
463
+ }
464
+ async acceptTransactionMutations(transaction) {
465
+ const collectionMutations = this.filterMutationsForCollection(
466
+ transaction.mutations
467
+ );
468
+ if (collectionMutations.length === 0) {
469
+ return;
470
+ }
471
+ await this.persistAndConfirmCollectionMutations(collectionMutations);
472
+ }
473
+ cleanup() {
474
+ this.coordinatorUnsubscribe?.();
475
+ this.coordinatorUnsubscribe = null;
476
+ this.indexAddedUnsubscribe?.();
477
+ this.indexAddedUnsubscribe = null;
478
+ this.indexRemovedUnsubscribe?.();
479
+ this.indexRemovedUnsubscribe = null;
480
+ if (this.remoteEnsureRetryTimer !== null) {
481
+ clearTimeout(this.remoteEnsureRetryTimer);
482
+ this.remoteEnsureRetryTimer = null;
483
+ }
484
+ this.pendingRemoteSubsetEnsures.clear();
485
+ this.activeSubsets.clear();
486
+ this.queuedHydrationTransactions.length = 0;
487
+ this.queuedTxCommitted.length = 0;
488
+ this.clearSyncControls();
489
+ }
490
+ withInternalApply(task) {
491
+ this.internalApplyDepth++;
492
+ try {
493
+ task();
494
+ } finally {
495
+ this.internalApplyDepth--;
496
+ }
497
+ }
498
+ getRequiredIndexSignatures() {
499
+ if (!this.collection) {
500
+ return [];
501
+ }
502
+ return this.collection.getIndexMetadata().map((metadata) => metadata.signature);
503
+ }
504
+ loadSubsetRowsUnsafe(options) {
505
+ return this.persistence.adapter.loadSubset(this.collectionId, options, {
506
+ requiredIndexSignatures: this.getRequiredIndexSignatures()
507
+ });
508
+ }
509
+ async scanPersistedRowsUnsafe(options) {
510
+ if (!this.persistence.adapter.scanRows) {
511
+ return [];
512
+ }
513
+ return this.persistence.adapter.scanRows(this.collectionId, options);
514
+ }
515
+ async scanPersistedRows(options) {
516
+ return this.applyMutex.run(() => this.scanPersistedRowsUnsafe(options));
517
+ }
518
+ async hydrateSubsetUnsafe(options, config) {
519
+ this.isHydrating = true;
520
+ try {
521
+ const rows = await this.loadSubsetRowsUnsafe(options);
522
+ this.applyRowsToCollection(rows);
523
+ } finally {
524
+ this.isHydrating = false;
525
+ }
526
+ await this.flushQueuedHydrationTransactionsUnsafe();
527
+ await this.flushQueuedTxCommittedUnsafe();
528
+ if (config.requestRemoteEnsure) {
529
+ this.queueRemoteSubsetEnsure(options);
530
+ }
531
+ }
532
+ applyRowsToCollection(rows) {
533
+ if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit) {
534
+ return;
535
+ }
536
+ this.withInternalApply(() => {
537
+ this.syncControls.begin?.({ immediate: true });
538
+ for (const row of rows) {
539
+ this.syncControls.write?.({
540
+ type: `update`,
541
+ value: row.value,
542
+ metadata: row.metadata
543
+ });
544
+ }
545
+ this.syncControls.commit?.();
546
+ });
547
+ }
548
+ replaceCollectionSnapshot(rows, collectionMetadata) {
549
+ if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit || !this.syncControls.metadata) {
550
+ return;
551
+ }
552
+ const nextMetadata = new Map(
553
+ collectionMetadata.map(({ key, value }) => [key, value])
554
+ );
555
+ const currentKeys = this.syncControls.metadata.collection.list().map(({ key }) => key);
556
+ this.withInternalApply(() => {
557
+ this.syncControls.begin?.({ immediate: true });
558
+ this.syncControls.truncate?.();
559
+ for (const row of rows) {
560
+ this.syncControls.write?.({
561
+ type: `update`,
562
+ value: row.value,
563
+ metadata: row.metadata
564
+ });
565
+ }
566
+ currentKeys.forEach((key) => {
567
+ if (!nextMetadata.has(key)) {
568
+ this.syncControls.metadata?.collection.delete(key);
569
+ }
570
+ });
571
+ nextMetadata.forEach((value, key) => {
572
+ this.syncControls.metadata?.collection.set(key, value);
573
+ });
574
+ this.syncControls.commit?.();
575
+ });
576
+ }
577
+ async flushQueuedHydrationTransactionsUnsafe() {
578
+ while (this.queuedHydrationTransactions.length > 0) {
579
+ const transaction = this.queuedHydrationTransactions.shift();
580
+ if (!transaction) {
581
+ continue;
582
+ }
583
+ await this.applyBufferedSyncTransactionUnsafe(transaction);
584
+ }
585
+ }
586
+ async applyBufferedSyncTransactionUnsafe(transaction) {
587
+ if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit) {
588
+ return;
589
+ }
590
+ const applyToCollection = () => {
591
+ this.syncControls.begin?.();
592
+ if (transaction.truncate) {
593
+ this.syncControls.truncate?.();
594
+ }
595
+ for (const operation of transaction.operations) {
596
+ if (operation.type === `delete`) {
597
+ this.syncControls.write?.({
598
+ type: `delete`,
599
+ key: operation.key
600
+ });
601
+ } else {
602
+ this.syncControls.write?.({
603
+ type: `update`,
604
+ value: operation.value,
605
+ metadata: operation.metadata
606
+ });
607
+ }
608
+ }
609
+ for (const [key, metadataWrite] of transaction.rowMetadataWrites) {
610
+ if (metadataWrite.type === `delete`) {
611
+ this.syncControls.metadata?.row.delete(key);
612
+ } else {
613
+ this.syncControls.metadata?.row.set(key, metadataWrite.value);
614
+ }
615
+ }
616
+ for (const [key, metadataWrite] of transaction.collectionMetadataWrites) {
617
+ if (metadataWrite.type === `delete`) {
618
+ this.syncControls.metadata?.collection.delete(key);
619
+ } else {
620
+ this.syncControls.metadata?.collection.set(key, metadataWrite.value);
621
+ }
622
+ }
623
+ this.syncControls.commit?.();
624
+ };
625
+ if (transaction.internal) {
626
+ this.withInternalApply(applyToCollection);
627
+ return;
628
+ }
629
+ applyToCollection();
630
+ await this.persistAndBroadcastExternalSyncTransactionUnsafe(transaction);
631
+ }
632
+ async persistAndBroadcastExternalSyncTransactionUnsafe(transaction) {
633
+ if (transaction.internal) {
634
+ return;
635
+ }
636
+ const streamPosition = this.nextLocalStreamPosition();
637
+ if (!transaction.truncate && transaction.operations.length === 0 && transaction.rowMetadataWrites.size === 0 && transaction.collectionMetadataWrites.size === 0) {
638
+ this.publishTxCommittedEvent(
639
+ this.createTxCommittedPayload({
640
+ term: streamPosition.term,
641
+ seq: streamPosition.seq,
642
+ txId: crypto.randomUUID(),
643
+ latestRowVersion: streamPosition.rowVersion,
644
+ changedRows: [],
645
+ deletedKeys: [],
646
+ requiresFullReload: true
647
+ })
648
+ );
649
+ return;
650
+ }
651
+ const tx = this.createPersistedTxFromOperations(transaction, streamPosition);
652
+ await this.persistence.adapter.applyCommittedTx(this.collectionId, tx);
653
+ this.publishTxCommittedEvent(
654
+ this.createTxCommittedPayload({
655
+ term: tx.term,
656
+ seq: tx.seq,
657
+ txId: tx.txId,
658
+ latestRowVersion: tx.rowVersion,
659
+ requiresFullReload: transaction.truncate,
660
+ changedRows: transaction.operations.filter((operation) => operation.type === `update`).map((operation) => ({ key: operation.key, value: operation.value })),
661
+ deletedKeys: transaction.operations.filter((operation) => operation.type === `delete`).map((operation) => operation.key),
662
+ rowMetadataMutations: tx.rowMetadataMutations,
663
+ collectionMetadataMutations: tx.collectionMetadataMutations
664
+ })
665
+ );
666
+ }
667
+ createPersistedTxFromOperations(transaction, streamPosition) {
668
+ return {
669
+ txId: crypto.randomUUID(),
670
+ term: streamPosition.term,
671
+ seq: streamPosition.seq,
672
+ rowVersion: streamPosition.rowVersion,
673
+ truncate: transaction.truncate,
674
+ mutations: transaction.operations.map(
675
+ (operation) => operation.type === `update` ? {
676
+ type: `update`,
677
+ key: operation.key,
678
+ value: operation.value
679
+ } : {
680
+ type: `delete`,
681
+ key: operation.key,
682
+ value: operation.value
683
+ }
684
+ ),
685
+ rowMetadataMutations: Array.from(
686
+ transaction.rowMetadataWrites.entries()
687
+ ).map(
688
+ ([key, metadataWrite]) => metadataWrite.type === `delete` ? { type: `delete`, key } : { type: `set`, key, value: metadataWrite.value }
689
+ ),
690
+ collectionMetadataMutations: Array.from(
691
+ transaction.collectionMetadataWrites.entries()
692
+ ).map(
693
+ ([key, metadataWrite]) => metadataWrite.type === `delete` ? { type: `delete`, key } : { type: `set`, key, value: metadataWrite.value }
694
+ )
695
+ };
696
+ }
697
+ createPersistedTxFromMutations(mutations, streamPosition) {
698
+ return {
699
+ txId: crypto.randomUUID(),
700
+ term: streamPosition.term,
701
+ seq: streamPosition.seq,
702
+ rowVersion: streamPosition.rowVersion,
703
+ mutations: mutations.map((mutation) => {
704
+ if (mutation.type === `delete`) {
705
+ return {
706
+ type: `delete`,
707
+ key: mutation.key,
708
+ value: mutation.original
709
+ };
710
+ }
711
+ if (mutation.type === `insert`) {
712
+ return {
713
+ type: `insert`,
714
+ key: mutation.key,
715
+ value: mutation.modified
716
+ };
717
+ }
718
+ return {
719
+ type: `update`,
720
+ key: mutation.key,
721
+ value: mutation.modified
722
+ };
723
+ })
724
+ };
725
+ }
726
+ confirmMutationsSyncUnsafe(mutations) {
727
+ if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit) {
728
+ return;
729
+ }
730
+ this.withInternalApply(() => {
731
+ this.syncControls.begin?.({ immediate: true });
732
+ for (const mutation of mutations) {
733
+ if (mutation.type === `delete`) {
734
+ this.syncControls.write?.({
735
+ type: `delete`,
736
+ key: mutation.key
737
+ });
738
+ } else {
739
+ this.syncControls.write?.({
740
+ type: `update`,
741
+ value: mutation.modified
742
+ });
743
+ }
744
+ }
745
+ this.syncControls.commit?.();
746
+ });
747
+ }
748
+ filterMutationsForCollection(mutations) {
749
+ const collection = this.collection;
750
+ return mutations.filter((mutation) => {
751
+ if (collection) {
752
+ return mutation.collection === collection;
753
+ }
754
+ return mutation.collection.id === this.collectionId;
755
+ });
756
+ }
757
+ async persistCollectionMutationsUnsafe(mutations) {
758
+ if (this.persistence.coordinator.requestApplyLocalMutations) {
759
+ const envelopeMutations = mutations.map(
760
+ (mutation) => toPersistedMutationEnvelope(
761
+ mutation
762
+ )
763
+ );
764
+ const response = await this.persistence.coordinator.requestApplyLocalMutations(
765
+ this.collectionId,
766
+ envelopeMutations
767
+ );
768
+ if (!response.ok) {
769
+ throw new Error(
770
+ `failed to apply local mutations through coordinator: ${response.error}`
771
+ );
772
+ }
773
+ this.observeStreamPosition(
774
+ response.term,
775
+ response.seq,
776
+ response.latestRowVersion
777
+ );
778
+ const uniqueAcceptedMutationIds = Array.from(
779
+ new Set(response.acceptedMutationIds)
780
+ );
781
+ const submittedMutationIds = new Set(
782
+ mutations.map((mutation) => mutation.mutationId)
783
+ );
784
+ const hasUnknownAcceptedMutationId = uniqueAcceptedMutationIds.some(
785
+ (mutationId) => !submittedMutationIds.has(mutationId)
786
+ );
787
+ if (hasUnknownAcceptedMutationId) {
788
+ throw new Error(
789
+ `persistence coordinator returned unknown mutation ids in applyLocalMutations response`
790
+ );
791
+ }
792
+ return uniqueAcceptedMutationIds;
793
+ }
794
+ const streamPosition = this.nextLocalStreamPosition();
795
+ const tx = this.createPersistedTxFromMutations(mutations, streamPosition);
796
+ await this.persistence.adapter.applyCommittedTx(this.collectionId, tx);
797
+ this.publishTxCommittedEvent(
798
+ this.createTxCommittedPayload({
799
+ term: tx.term,
800
+ seq: tx.seq,
801
+ txId: tx.txId,
802
+ latestRowVersion: tx.rowVersion,
803
+ changedRows: mutations.filter((mutation) => mutation.type !== `delete`).map((mutation) => ({
804
+ key: mutation.key,
805
+ value: mutation.modified
806
+ })),
807
+ deletedKeys: mutations.filter((mutation) => mutation.type === `delete`).map((mutation) => mutation.key),
808
+ rowMetadataMutations: tx.rowMetadataMutations,
809
+ collectionMetadataMutations: tx.collectionMetadataMutations
810
+ })
811
+ );
812
+ return mutations.map((mutation) => mutation.mutationId);
813
+ }
814
+ createTxCommittedPayload(args) {
815
+ const rowMetadataMutations = args.rowMetadataMutations ?? [];
816
+ const collectionMetadataMutations = args.collectionMetadataMutations ?? [];
817
+ const requiresFullReload = args.requiresFullReload === true || args.changedRows.length + args.deletedKeys.length + rowMetadataMutations.length + collectionMetadataMutations.length > TARGETED_INVALIDATION_KEY_LIMIT;
818
+ if (requiresFullReload) {
819
+ return {
820
+ type: `tx:committed`,
821
+ term: args.term,
822
+ seq: args.seq,
823
+ txId: args.txId,
824
+ latestRowVersion: args.latestRowVersion,
825
+ requiresFullReload: true
826
+ };
827
+ }
828
+ return {
829
+ type: `tx:committed`,
830
+ term: args.term,
831
+ seq: args.seq,
832
+ txId: args.txId,
833
+ latestRowVersion: args.latestRowVersion,
834
+ requiresFullReload: false,
835
+ changedRows: args.changedRows,
836
+ deletedKeys: args.deletedKeys,
837
+ rowMetadataMutations,
838
+ collectionMetadataMutations
839
+ };
840
+ }
841
+ publishTxCommittedEvent(txCommitted) {
842
+ this.observeStreamPosition(
843
+ txCommitted.term,
844
+ txCommitted.seq,
845
+ txCommitted.latestRowVersion
846
+ );
847
+ const envelope = {
848
+ v: 1,
849
+ dbName: this.dbName,
850
+ collectionId: this.collectionId,
851
+ senderId: this.persistence.coordinator.getNodeId(),
852
+ ts: Date.now(),
853
+ payload: txCommitted
854
+ };
855
+ this.persistence.coordinator.publish(this.collectionId, envelope);
856
+ }
857
+ observeStreamPosition(term, seq, rowVersion) {
858
+ if (term > this.latestTerm || term === this.latestTerm && seq > this.latestSeq) {
859
+ this.latestTerm = term;
860
+ this.latestSeq = seq;
861
+ }
862
+ if (rowVersion > this.latestRowVersion) {
863
+ this.latestRowVersion = rowVersion;
864
+ }
865
+ if (term > this.localTerm) {
866
+ this.localTerm = term;
867
+ this.localSeq = seq;
868
+ } else if (term === this.localTerm && seq > this.localSeq) {
869
+ this.localSeq = seq;
870
+ }
871
+ if (rowVersion > this.localRowVersion) {
872
+ this.localRowVersion = rowVersion;
873
+ }
874
+ }
875
+ nextLocalStreamPosition() {
876
+ this.localTerm = Math.max(this.localTerm, this.latestTerm || 1);
877
+ this.localSeq = Math.max(this.localSeq, this.latestSeq) + 1;
878
+ this.localRowVersion = Math.max(this.localRowVersion, this.latestRowVersion) + 1;
879
+ return {
880
+ term: this.localTerm,
881
+ seq: this.localSeq,
882
+ rowVersion: this.localRowVersion
883
+ };
884
+ }
885
+ getSubsetKey(options) {
886
+ const subscription = options.subscription;
887
+ if (subscription && typeof subscription === `object`) {
888
+ const existingId = this.subscriptionIds.get(subscription);
889
+ if (existingId) {
890
+ return existingId;
891
+ }
892
+ this.nextSubscriptionId++;
893
+ const id = `sub:${this.nextSubscriptionId}`;
894
+ this.subscriptionIds.set(subscription, id);
895
+ return id;
896
+ }
897
+ return `opts:${stableSerialize(normalizeSubsetOptionsForKey(options))}`;
898
+ }
899
+ queueRemoteSubsetEnsure(options) {
900
+ if (this.mode !== `sync-present` || !this.persistence.coordinator.requestEnsureRemoteSubset) {
901
+ return;
902
+ }
903
+ this.pendingRemoteSubsetEnsures.set(this.getSubsetKey(options), options);
904
+ void this.flushPendingRemoteSubsetEnsures();
905
+ }
906
+ scheduleRemoteEnsureRetry() {
907
+ if (this.mode !== `sync-present` || !this.persistence.coordinator.requestEnsureRemoteSubset) {
908
+ return;
909
+ }
910
+ if (this.pendingRemoteSubsetEnsures.size === 0 || this.remoteEnsureRetryTimer !== null) {
911
+ return;
912
+ }
913
+ this.remoteEnsureRetryTimer = setTimeout(() => {
914
+ this.remoteEnsureRetryTimer = null;
915
+ void this.flushPendingRemoteSubsetEnsures();
916
+ }, REMOTE_ENSURE_RETRY_DELAY_MS);
917
+ }
918
+ async flushPendingRemoteSubsetEnsures() {
919
+ if (this.mode !== `sync-present` || !this.persistence.coordinator.requestEnsureRemoteSubset) {
920
+ return;
921
+ }
922
+ if (this.remoteEnsureRetryTimer !== null) {
923
+ clearTimeout(this.remoteEnsureRetryTimer);
924
+ this.remoteEnsureRetryTimer = null;
925
+ }
926
+ for (const [subsetKey, options] of this.pendingRemoteSubsetEnsures) {
927
+ try {
928
+ await this.persistence.coordinator.requestEnsureRemoteSubset(
929
+ this.collectionId,
930
+ options
931
+ );
932
+ this.pendingRemoteSubsetEnsures.delete(subsetKey);
933
+ } catch (error) {
934
+ console.warn(`Failed to ensure remote subset:`, error);
935
+ }
936
+ }
937
+ this.scheduleRemoteEnsureRetry();
938
+ }
939
+ attachCoordinatorSubscription() {
940
+ if (this.coordinatorUnsubscribe) {
941
+ return;
942
+ }
943
+ this.coordinatorUnsubscribe = this.persistence.coordinator.subscribe(
944
+ this.collectionId,
945
+ (message) => {
946
+ this.onCoordinatorMessage(message);
947
+ }
948
+ );
949
+ }
950
+ onCoordinatorMessage(message) {
951
+ if (message.collectionId !== this.collectionId) {
952
+ return;
953
+ }
954
+ const { payload } = message;
955
+ const isSelf = message.senderId === this.persistence.coordinator.getNodeId();
956
+ if (isTxCommittedPayload(payload)) {
957
+ if (this.isHydrating) {
958
+ this.queuedTxCommitted.push(payload);
959
+ return;
960
+ }
961
+ void this.applyMutex.run(() => this.processCommittedTxUnsafe(payload)).catch((error) => {
962
+ console.warn(`Failed to process tx:committed message:`, error);
963
+ });
964
+ return;
965
+ }
966
+ if (isSelf) {
967
+ return;
968
+ }
969
+ if (isCollectionResetPayload(payload)) {
970
+ void this.applyMutex.run(() => this.truncateAndReloadUnsafe()).catch((error) => {
971
+ console.warn(`Failed to process collection reset message:`, error);
972
+ });
973
+ }
974
+ }
975
+ async flushQueuedTxCommittedUnsafe() {
976
+ while (this.queuedTxCommitted.length > 0) {
977
+ const queued = this.queuedTxCommitted.shift();
978
+ if (!queued) {
979
+ continue;
980
+ }
981
+ await this.processCommittedTxUnsafe(queued);
982
+ }
983
+ }
984
+ async processCommittedTxUnsafe(txCommitted) {
985
+ if (txCommitted.term < this.latestTerm) {
986
+ return;
987
+ }
988
+ if (txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq) {
989
+ return;
990
+ }
991
+ const hasGapInCurrentTerm = txCommitted.term === this.latestTerm && txCommitted.seq > this.latestSeq + 1;
992
+ const hasGapAcrossTerms = txCommitted.term > this.latestTerm && txCommitted.seq > 1;
993
+ const hasGap = hasGapInCurrentTerm || hasGapAcrossTerms;
994
+ if (hasGap) {
995
+ await this.recoverFromSeqGapUnsafe();
996
+ if (txCommitted.term < this.latestTerm || txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq) {
997
+ return;
998
+ }
999
+ }
1000
+ this.observeStreamPosition(
1001
+ txCommitted.term,
1002
+ txCommitted.seq,
1003
+ txCommitted.latestRowVersion
1004
+ );
1005
+ await this.invalidateFromCommittedTxUnsafe(txCommitted);
1006
+ }
1007
+ async recoverFromSeqGapUnsafe() {
1008
+ if (this.persistence.coordinator.pullSince && this.latestRowVersion >= 0) {
1009
+ try {
1010
+ const pullResponse = await this.persistence.coordinator.pullSince(
1011
+ this.collectionId,
1012
+ this.latestRowVersion
1013
+ );
1014
+ if (pullResponse.ok) {
1015
+ this.observeStreamPosition(
1016
+ pullResponse.latestTerm,
1017
+ pullResponse.latestSeq,
1018
+ pullResponse.latestRowVersion
1019
+ );
1020
+ if (pullResponse.requiresFullReload || !pullResponse.deltas) {
1021
+ await this.reloadActiveSubsetsUnsafe();
1022
+ return;
1023
+ }
1024
+ for (const delta of pullResponse.deltas) {
1025
+ await this.invalidateFromCommittedTxUnsafe({
1026
+ type: `tx:committed`,
1027
+ term: pullResponse.latestTerm,
1028
+ seq: pullResponse.latestSeq,
1029
+ txId: delta.txId,
1030
+ latestRowVersion: delta.latestRowVersion,
1031
+ requiresFullReload: false,
1032
+ changedRows: delta.changedRows,
1033
+ deletedKeys: delta.deletedKeys,
1034
+ rowMetadataMutations: delta.rowMetadataMutations,
1035
+ collectionMetadataMutations: delta.collectionMetadataMutations
1036
+ });
1037
+ }
1038
+ return;
1039
+ }
1040
+ } catch (error) {
1041
+ console.warn(`Failed pullSince recovery attempt:`, error);
1042
+ }
1043
+ }
1044
+ await this.truncateAndReloadUnsafe();
1045
+ if (this.mode === `sync-present`) {
1046
+ for (const options of this.activeSubsets.values()) {
1047
+ this.queueRemoteSubsetEnsure(options);
1048
+ }
1049
+ }
1050
+ }
1051
+ async truncateAndReloadUnsafe() {
1052
+ if (this.syncControls.begin && this.syncControls.commit) {
1053
+ this.withInternalApply(() => {
1054
+ this.syncControls.begin?.({ immediate: true });
1055
+ this.syncControls.truncate?.();
1056
+ this.syncControls.commit?.();
1057
+ });
1058
+ }
1059
+ await this.reloadActiveSubsetsUnsafe();
1060
+ }
1061
+ async invalidateFromCommittedTxUnsafe(txCommitted) {
1062
+ if (txCommitted.requiresFullReload) {
1063
+ await this.reloadActiveSubsetsUnsafe();
1064
+ return;
1065
+ }
1066
+ const changedKeyCount = txCommitted.changedRows.length + txCommitted.deletedKeys.length;
1067
+ if (changedKeyCount > TARGETED_INVALIDATION_KEY_LIMIT) {
1068
+ await this.reloadActiveSubsetsUnsafe();
1069
+ return;
1070
+ }
1071
+ const hasPaginatedSubset = Array.from(this.activeSubsets.values()).some(
1072
+ (opt) => opt.limit != null || opt.offset != null || opt.cursor != null
1073
+ );
1074
+ if (!hasPaginatedSubset || changedKeyCount === 0) {
1075
+ await this.applyTargetedInvalidationUnsafe(txCommitted);
1076
+ return;
1077
+ }
1078
+ await this.reloadActiveSubsetsUnsafe();
1079
+ }
1080
+ async applyTargetedInvalidationUnsafe(txCommitted) {
1081
+ const subsetEvaluators = Array.from(this.activeSubsets.values()).map(
1082
+ (opt) => opt.where ? compileSingleRowExpression(opt.where) : null
1083
+ );
1084
+ this.withInternalApply(() => {
1085
+ this.syncControls.begin?.({ immediate: true });
1086
+ for (const {
1087
+ key: changedKey,
1088
+ value: newValue
1089
+ } of txCommitted.changedRows) {
1090
+ const matchesAnySubset = subsetEvaluators.some((evaluator) => {
1091
+ if (!evaluator) return true;
1092
+ return toBooleanPredicate(evaluator(newValue));
1093
+ });
1094
+ if (matchesAnySubset) {
1095
+ this.syncControls.write?.({ type: `update`, value: newValue });
1096
+ } else if (this.collection?.get(changedKey) !== void 0) {
1097
+ this.syncControls.write?.({ type: `delete`, key: changedKey });
1098
+ }
1099
+ }
1100
+ for (const deletedKey of txCommitted.deletedKeys) {
1101
+ this.syncControls.write?.({ type: `delete`, key: deletedKey });
1102
+ }
1103
+ txCommitted.rowMetadataMutations?.forEach((mutation) => {
1104
+ if (mutation.type === `delete`) {
1105
+ this.syncControls.metadata?.row.delete(mutation.key);
1106
+ } else {
1107
+ this.syncControls.metadata?.row.set(
1108
+ mutation.key,
1109
+ mutation.value
1110
+ );
1111
+ }
1112
+ });
1113
+ txCommitted.collectionMetadataMutations?.forEach((mutation) => {
1114
+ if (mutation.type === `delete`) {
1115
+ this.syncControls.metadata?.collection.delete(mutation.key);
1116
+ } else {
1117
+ this.syncControls.metadata?.collection.set(
1118
+ mutation.key,
1119
+ mutation.value
1120
+ );
1121
+ }
1122
+ });
1123
+ this.syncControls.commit?.();
1124
+ });
1125
+ }
1126
+ async reloadActiveSubsetsUnsafe() {
1127
+ const activeSubsetOptions = this.activeSubsets.size > 0 ? Array.from(this.activeSubsets.values()) : [{}];
1128
+ this.isHydrating = true;
1129
+ try {
1130
+ const mergedRows = /* @__PURE__ */ new Map();
1131
+ const collectionMetadata = await this.loadCollectionMetadataSnapshot();
1132
+ for (const options of activeSubsetOptions) {
1133
+ const subsetRows = await this.loadSubsetRowsUnsafe(options);
1134
+ for (const row of subsetRows) {
1135
+ mergedRows.set(row.key, {
1136
+ value: row.value,
1137
+ metadata: row.metadata
1138
+ });
1139
+ }
1140
+ }
1141
+ this.replaceCollectionSnapshot(
1142
+ Array.from(mergedRows.entries()).map(([key, row]) => ({
1143
+ key,
1144
+ value: row.value,
1145
+ metadata: row.metadata
1146
+ })),
1147
+ collectionMetadata
1148
+ );
1149
+ } finally {
1150
+ this.isHydrating = false;
1151
+ }
1152
+ await this.flushQueuedHydrationTransactionsUnsafe();
1153
+ await this.flushQueuedTxCommittedUnsafe();
1154
+ }
1155
+ attachIndexLifecycleListeners() {
1156
+ if (!this.collection || this.indexAddedUnsubscribe || this.indexRemovedUnsubscribe) {
1157
+ return;
1158
+ }
1159
+ this.indexAddedUnsubscribe = this.collection.on(`index:added`, (event) => {
1160
+ void this.ensurePersistedIndex(event.index);
1161
+ });
1162
+ this.indexRemovedUnsubscribe = this.collection.on(
1163
+ `index:removed`,
1164
+ (event) => {
1165
+ void this.markIndexRemoved(event.index);
1166
+ }
1167
+ );
1168
+ }
1169
+ async bootstrapPersistedIndexes(indexMetadataSnapshot) {
1170
+ const collection = this.collection;
1171
+ if (!collection && !indexMetadataSnapshot) {
1172
+ return;
1173
+ }
1174
+ const indexMetadata = indexMetadataSnapshot ?? collection?.getIndexMetadata() ?? [];
1175
+ for (const metadata of indexMetadata) {
1176
+ await this.ensurePersistedIndex(metadata);
1177
+ }
1178
+ }
1179
+ buildPersistedIndexSpec(index) {
1180
+ return {
1181
+ expressionSql: [stableSerialize(index.expression)],
1182
+ metadata: {
1183
+ name: index.name ?? null,
1184
+ resolver: toStableSerializable(index.resolver),
1185
+ options: toStableSerializable(index.options)
1186
+ }
1187
+ };
1188
+ }
1189
+ async ensurePersistedIndex(indexMetadata) {
1190
+ const spec = this.buildPersistedIndexSpec(indexMetadata);
1191
+ try {
1192
+ await this.persistence.adapter.ensureIndex(
1193
+ this.collectionId,
1194
+ indexMetadata.signature,
1195
+ spec
1196
+ );
1197
+ } catch (error) {
1198
+ console.warn(`Failed to ensure persisted index in adapter:`, error);
1199
+ }
1200
+ try {
1201
+ await this.persistence.coordinator.requestEnsurePersistedIndex(
1202
+ this.collectionId,
1203
+ indexMetadata.signature,
1204
+ spec
1205
+ );
1206
+ } catch (error) {
1207
+ console.warn(
1208
+ `Failed to ensure persisted index through coordinator:`,
1209
+ error
1210
+ );
1211
+ }
1212
+ }
1213
+ async markIndexRemoved(indexMetadata) {
1214
+ if (!this.persistence.adapter.markIndexRemoved) {
1215
+ return;
1216
+ }
1217
+ try {
1218
+ await this.persistence.adapter.markIndexRemoved(
1219
+ this.collectionId,
1220
+ indexMetadata.signature
1221
+ );
1222
+ } catch (error) {
1223
+ console.warn(`Failed to mark persisted index removed:`, error);
1224
+ }
1225
+ }
1226
+ }
1227
+ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1228
+ return {
1229
+ ...sourceSyncConfig,
1230
+ sync: (params) => {
1231
+ const transactionStack = [];
1232
+ const getOpenTransaction = () => transactionStack[transactionStack.length - 1];
1233
+ let fullStartPromise = null;
1234
+ const cancelledLoadKeys = /* @__PURE__ */ new Set();
1235
+ const loadSubscriptionIds = /* @__PURE__ */ new WeakMap();
1236
+ let nextLoadSubscriptionId = 0;
1237
+ const getLoadKey = (options) => {
1238
+ const subscription = options.subscription;
1239
+ if (subscription && typeof subscription === `object`) {
1240
+ const existingId = loadSubscriptionIds.get(subscription);
1241
+ if (existingId) {
1242
+ return `sub:${existingId}`;
1243
+ }
1244
+ nextLoadSubscriptionId++;
1245
+ const nextId = String(nextLoadSubscriptionId);
1246
+ loadSubscriptionIds.set(subscription, nextId);
1247
+ return `sub:${nextId}`;
1248
+ }
1249
+ return `opts:${stableSerialize(normalizeSubsetOptionsForKey(options))}`;
1250
+ };
1251
+ runtime.setSyncControls({
1252
+ begin: params.begin,
1253
+ write: params.write,
1254
+ commit: params.commit,
1255
+ truncate: params.truncate,
1256
+ metadata: params.metadata ?? null
1257
+ });
1258
+ runtime.setCollection(
1259
+ params.collection
1260
+ );
1261
+ const wrappedParams = {
1262
+ ...params,
1263
+ markReady: () => {
1264
+ void (fullStartPromise ?? runtime.ensureStarted()).then(() => {
1265
+ params.markReady();
1266
+ }).catch((error) => {
1267
+ console.warn(
1268
+ `Failed persisted sync startup before markReady:`,
1269
+ error
1270
+ );
1271
+ params.markReady();
1272
+ });
1273
+ },
1274
+ begin: (options) => {
1275
+ const transaction = {
1276
+ operations: [],
1277
+ rowMetadataWrites: /* @__PURE__ */ new Map(),
1278
+ collectionMetadataWrites: /* @__PURE__ */ new Map(),
1279
+ truncate: false,
1280
+ internal: runtime.isApplyingInternally(),
1281
+ queuedBecauseHydrating: !runtime.isApplyingInternally() && runtime.isHydratingNow()
1282
+ };
1283
+ transactionStack.push(transaction);
1284
+ if (!transaction.queuedBecauseHydrating) {
1285
+ params.begin(options);
1286
+ }
1287
+ },
1288
+ write: (message) => {
1289
+ const normalization = runtime.normalizeSyncWriteMessage(message);
1290
+ const openTransaction = getOpenTransaction();
1291
+ if (!openTransaction) {
1292
+ params.write(normalization.forwardMessage);
1293
+ return;
1294
+ }
1295
+ openTransaction.operations.push(normalization.operation);
1296
+ if (normalization.operation.type === `delete`) {
1297
+ openTransaction.rowMetadataWrites.set(normalization.operation.key, {
1298
+ type: `delete`
1299
+ });
1300
+ } else if (message.type === `insert` && normalization.operation.metadata === void 0) {
1301
+ openTransaction.rowMetadataWrites.set(normalization.operation.key, {
1302
+ type: `delete`
1303
+ });
1304
+ } else if (normalization.operation.metadata !== void 0) {
1305
+ openTransaction.rowMetadataWrites.set(normalization.operation.key, {
1306
+ type: `set`,
1307
+ value: normalization.operation.metadata
1308
+ });
1309
+ }
1310
+ if (!openTransaction.queuedBecauseHydrating) {
1311
+ params.write(normalization.forwardMessage);
1312
+ }
1313
+ },
1314
+ metadata: params.metadata ? {
1315
+ row: {
1316
+ get: (key) => {
1317
+ const openTransaction = getOpenTransaction();
1318
+ const pendingWrite = openTransaction?.rowMetadataWrites.get(key);
1319
+ if (pendingWrite) {
1320
+ return pendingWrite.type === `delete` ? void 0 : pendingWrite.value;
1321
+ }
1322
+ if (openTransaction?.truncate) {
1323
+ return void 0;
1324
+ }
1325
+ return params.metadata.row.get(key);
1326
+ },
1327
+ scanPersisted: (options) => runtime.scanPersistedRows(options),
1328
+ set: (key, value) => {
1329
+ const openTransaction = getOpenTransaction();
1330
+ if (!openTransaction) {
1331
+ throw new InvalidPersistedCollectionConfigError(
1332
+ `metadata.row.set must be called within an open sync transaction`
1333
+ );
1334
+ }
1335
+ openTransaction.rowMetadataWrites.set(key, {
1336
+ type: `set`,
1337
+ value
1338
+ });
1339
+ if (!openTransaction.queuedBecauseHydrating) {
1340
+ params.metadata.row.set(key, value);
1341
+ }
1342
+ },
1343
+ delete: (key) => {
1344
+ const openTransaction = getOpenTransaction();
1345
+ if (!openTransaction) {
1346
+ throw new InvalidPersistedCollectionConfigError(
1347
+ `metadata.row.delete must be called within an open sync transaction`
1348
+ );
1349
+ }
1350
+ openTransaction.rowMetadataWrites.set(key, {
1351
+ type: `delete`
1352
+ });
1353
+ if (!openTransaction.queuedBecauseHydrating) {
1354
+ params.metadata.row.delete(key);
1355
+ }
1356
+ }
1357
+ },
1358
+ collection: {
1359
+ get: (key) => {
1360
+ const openTransaction = getOpenTransaction();
1361
+ const pendingWrite = openTransaction?.collectionMetadataWrites.get(key);
1362
+ if (pendingWrite) {
1363
+ return pendingWrite.type === `delete` ? void 0 : pendingWrite.value;
1364
+ }
1365
+ return params.metadata.collection.get(key);
1366
+ },
1367
+ set: (key, value) => {
1368
+ const openTransaction = getOpenTransaction();
1369
+ if (!openTransaction) {
1370
+ throw new InvalidPersistedCollectionConfigError(
1371
+ `metadata.collection.set must be called within an open sync transaction`
1372
+ );
1373
+ }
1374
+ openTransaction.collectionMetadataWrites.set(key, {
1375
+ type: `set`,
1376
+ value
1377
+ });
1378
+ if (!openTransaction.queuedBecauseHydrating) {
1379
+ params.metadata.collection.set(key, value);
1380
+ }
1381
+ },
1382
+ delete: (key) => {
1383
+ const openTransaction = getOpenTransaction();
1384
+ if (!openTransaction) {
1385
+ throw new InvalidPersistedCollectionConfigError(
1386
+ `metadata.collection.delete must be called within an open sync transaction`
1387
+ );
1388
+ }
1389
+ openTransaction.collectionMetadataWrites.set(key, {
1390
+ type: `delete`
1391
+ });
1392
+ if (!openTransaction.queuedBecauseHydrating) {
1393
+ params.metadata.collection.delete(key);
1394
+ }
1395
+ },
1396
+ list: (prefix) => {
1397
+ const merged = new Map(
1398
+ params.metadata.collection.list().map(({ key, value }) => [key, value])
1399
+ );
1400
+ const openTransaction = getOpenTransaction();
1401
+ if (openTransaction) {
1402
+ for (const [
1403
+ key,
1404
+ metadataWrite
1405
+ ] of openTransaction.collectionMetadataWrites) {
1406
+ if (metadataWrite.type === `delete`) {
1407
+ merged.delete(key);
1408
+ } else {
1409
+ merged.set(key, metadataWrite.value);
1410
+ }
1411
+ }
1412
+ }
1413
+ return Array.from(merged.entries()).filter(([key]) => prefix ? key.startsWith(prefix) : true).map(([key, value]) => ({
1414
+ key,
1415
+ value
1416
+ }));
1417
+ }
1418
+ }
1419
+ } : void 0,
1420
+ truncate: () => {
1421
+ const openTransaction = getOpenTransaction();
1422
+ if (!openTransaction) {
1423
+ params.truncate();
1424
+ return;
1425
+ }
1426
+ openTransaction.operations = [];
1427
+ openTransaction.rowMetadataWrites.clear();
1428
+ openTransaction.truncate = true;
1429
+ if (!openTransaction.queuedBecauseHydrating) {
1430
+ params.truncate();
1431
+ }
1432
+ },
1433
+ commit: () => {
1434
+ const openTransaction = transactionStack.pop();
1435
+ if (!openTransaction) {
1436
+ params.commit();
1437
+ return;
1438
+ }
1439
+ if (openTransaction.queuedBecauseHydrating) {
1440
+ runtime.queueHydrationBufferedTransaction({
1441
+ operations: openTransaction.operations,
1442
+ rowMetadataWrites: openTransaction.rowMetadataWrites,
1443
+ collectionMetadataWrites: openTransaction.collectionMetadataWrites,
1444
+ truncate: openTransaction.truncate,
1445
+ internal: openTransaction.internal
1446
+ });
1447
+ return;
1448
+ }
1449
+ params.commit();
1450
+ if (!openTransaction.internal) {
1451
+ void runtime.persistAndBroadcastExternalSyncTransaction({
1452
+ operations: openTransaction.operations,
1453
+ rowMetadataWrites: openTransaction.rowMetadataWrites,
1454
+ collectionMetadataWrites: openTransaction.collectionMetadataWrites,
1455
+ truncate: openTransaction.truncate,
1456
+ internal: false
1457
+ }).catch((error) => {
1458
+ console.warn(
1459
+ `Failed to persist wrapped sync transaction:`,
1460
+ error
1461
+ );
1462
+ });
1463
+ }
1464
+ }
1465
+ };
1466
+ let sourceResult = {};
1467
+ const startupState = { cleanedUp: false };
1468
+ fullStartPromise = runtime.ensureStarted();
1469
+ const sourceResultPromise = (async () => {
1470
+ await runtime.ensureStartupMetadataLoaded();
1471
+ if (startupState.cleanedUp) {
1472
+ return sourceResult;
1473
+ }
1474
+ sourceResult = normalizeSyncFnResult(
1475
+ sourceSyncConfig.sync(wrappedParams)
1476
+ );
1477
+ return sourceResult;
1478
+ })();
1479
+ return {
1480
+ cleanup: () => {
1481
+ startupState.cleanedUp = true;
1482
+ sourceResult.cleanup?.();
1483
+ runtime.cleanup();
1484
+ runtime.clearSyncControls();
1485
+ },
1486
+ loadSubset: async (options) => {
1487
+ const loadKey = getLoadKey(options);
1488
+ cancelledLoadKeys.delete(loadKey);
1489
+ await fullStartPromise;
1490
+ const resolvedSourceResult = await sourceResultPromise;
1491
+ if (startupState.cleanedUp || cancelledLoadKeys.has(loadKey)) {
1492
+ return;
1493
+ }
1494
+ await runtime.loadSubset(options, resolvedSourceResult.loadSubset);
1495
+ },
1496
+ unloadSubset: (options) => {
1497
+ cancelledLoadKeys.add(getLoadKey(options));
1498
+ runtime.unloadSubset(options, sourceResult.unloadSubset);
1499
+ }
1500
+ };
1501
+ }
1502
+ };
1503
+ }
1504
+ function createLoopbackSyncConfig(runtime) {
1505
+ return {
1506
+ sync: (params) => {
1507
+ runtime.setSyncControls({
1508
+ begin: params.begin,
1509
+ write: params.write,
1510
+ commit: params.commit,
1511
+ truncate: params.truncate,
1512
+ metadata: params.metadata ?? null
1513
+ });
1514
+ runtime.setCollection(
1515
+ params.collection
1516
+ );
1517
+ void runtime.ensureStarted().then(() => {
1518
+ params.markReady();
1519
+ }).catch((error) => {
1520
+ console.warn(`Failed persisted loopback startup:`, error);
1521
+ params.markReady();
1522
+ });
1523
+ return {
1524
+ cleanup: () => {
1525
+ runtime.cleanup();
1526
+ runtime.clearSyncControls();
1527
+ },
1528
+ loadSubset: (options) => runtime.loadSubset(options),
1529
+ unloadSubset: (options) => runtime.unloadSubset(options)
1530
+ };
1531
+ },
1532
+ getSyncMetadata: () => ({
1533
+ source: `persisted-phase-2-loopback`
1534
+ })
1535
+ };
1536
+ }
1537
+ function persistedCollectionOptions(options) {
1538
+ if (!isRecord(options.persistence)) {
1539
+ throw new InvalidPersistedCollectionConfigError(
1540
+ `persistedCollectionOptions requires a persistence adapter`
1541
+ );
1542
+ }
1543
+ if (hasOwnSyncKey(options)) {
1544
+ if (!isValidSyncConfig(options.sync)) {
1545
+ throw new InvalidSyncConfigError(
1546
+ `when the "sync" key is present it must provide a callable sync function`
1547
+ );
1548
+ }
1549
+ const { schemaVersion: schemaVersion2, ...syncOptions } = options;
1550
+ const collectionId2 = syncOptions.id ?? `persisted-collection:${crypto.randomUUID()}`;
1551
+ const persistence2 = resolvePersistenceForCollection(
1552
+ syncOptions.persistence,
1553
+ {
1554
+ collectionId: collectionId2,
1555
+ mode: `sync-present`,
1556
+ schemaVersion: schemaVersion2
1557
+ }
1558
+ );
1559
+ const runtime2 = new PersistedCollectionRuntime(
1560
+ `sync-present`,
1561
+ collectionId2,
1562
+ persistence2,
1563
+ syncOptions.syncMode ?? `eager`,
1564
+ collectionId2
1565
+ );
1566
+ return {
1567
+ ...syncOptions,
1568
+ id: collectionId2,
1569
+ sync: createWrappedSyncConfig(syncOptions.sync, runtime2),
1570
+ persistence: persistence2
1571
+ };
1572
+ }
1573
+ const { schemaVersion, ...localOnlyOptions } = options;
1574
+ const collectionId = localOnlyOptions.id ?? `persisted-collection:${crypto.randomUUID()}`;
1575
+ const persistence = resolvePersistenceForCollection(
1576
+ localOnlyOptions.persistence,
1577
+ {
1578
+ collectionId,
1579
+ mode: `sync-absent`,
1580
+ schemaVersion
1581
+ }
1582
+ );
1583
+ const runtime = new PersistedCollectionRuntime(
1584
+ `sync-absent`,
1585
+ collectionId,
1586
+ persistence,
1587
+ localOnlyOptions.syncMode ?? `eager`,
1588
+ localOnlyOptions.id ?? DEFAULT_DB_NAME
1589
+ );
1590
+ const wrappedOnInsert = async (params) => {
1591
+ const handlerResult = localOnlyOptions.onInsert ? await localOnlyOptions.onInsert(
1592
+ params
1593
+ ) : void 0;
1594
+ await runtime.persistAndConfirmCollectionMutations(
1595
+ params.transaction.mutations
1596
+ );
1597
+ return handlerResult ?? {};
1598
+ };
1599
+ const wrappedOnUpdate = async (params) => {
1600
+ const handlerResult = localOnlyOptions.onUpdate ? await localOnlyOptions.onUpdate(
1601
+ params
1602
+ ) : void 0;
1603
+ await runtime.persistAndConfirmCollectionMutations(
1604
+ params.transaction.mutations
1605
+ );
1606
+ return handlerResult ?? {};
1607
+ };
1608
+ const wrappedOnDelete = async (params) => {
1609
+ const handlerResult = localOnlyOptions.onDelete ? await localOnlyOptions.onDelete(
1610
+ params
1611
+ ) : void 0;
1612
+ await runtime.persistAndConfirmCollectionMutations(
1613
+ params.transaction.mutations
1614
+ );
1615
+ return handlerResult ?? {};
1616
+ };
1617
+ const acceptMutations = async (transaction) => {
1618
+ await runtime.acceptTransactionMutations(transaction);
1619
+ };
1620
+ const persistedUtils = {
1621
+ acceptMutations,
1622
+ getLeadershipState: () => runtime.getLeadershipState(),
1623
+ forceReloadSubset: (subsetOptions) => runtime.forceReloadSubset(subsetOptions)
1624
+ };
1625
+ const mergedUtils = {
1626
+ ...localOnlyOptions.utils ?? {},
1627
+ ...persistedUtils
1628
+ };
1629
+ return {
1630
+ ...localOnlyOptions,
1631
+ id: collectionId,
1632
+ persistence,
1633
+ sync: createLoopbackSyncConfig(runtime),
1634
+ onInsert: wrappedOnInsert,
1635
+ onUpdate: wrappedOnUpdate,
1636
+ onDelete: wrappedOnDelete,
1637
+ utils: mergedUtils,
1638
+ startSync: true,
1639
+ gcTime: localOnlyOptions.gcTime ?? 0
1640
+ };
1641
+ }
1642
+ function encodePersistedStorageKey(key) {
1643
+ if (typeof key === `number`) {
1644
+ if (!Number.isFinite(key)) {
1645
+ throw new InvalidPersistedStorageKeyError(key);
1646
+ }
1647
+ if (Object.is(key, -0)) {
1648
+ return `n:-0`;
1649
+ }
1650
+ return `n:${key}`;
1651
+ }
1652
+ return `s:${key}`;
1653
+ }
1654
+ function decodePersistedStorageKey(encoded) {
1655
+ if (encoded === `n:-0`) {
1656
+ return -0;
1657
+ }
1658
+ if (encoded.startsWith(`n:`)) {
1659
+ return Number(encoded.slice(2));
1660
+ }
1661
+ if (encoded.startsWith(`s:`)) {
1662
+ return encoded.slice(2);
1663
+ }
1664
+ throw new InvalidPersistedStorageKeyEncodingError(encoded);
1665
+ }
1666
+ const PERSISTED_TABLE_NAME_ALPHABET = `abcdefghijklmnopqrstuvwxyz234567`;
1667
+ function hashCollectionId(collectionId) {
1668
+ let hash = 2166136261;
1669
+ for (let index = 0; index < collectionId.length; index++) {
1670
+ hash ^= collectionId.charCodeAt(index);
1671
+ hash = Math.imul(hash, 16777619);
1672
+ }
1673
+ return hash >>> 0;
1674
+ }
1675
+ function toBase32(input) {
1676
+ if (input === 0) {
1677
+ return PERSISTED_TABLE_NAME_ALPHABET.charAt(0);
1678
+ }
1679
+ let value = input >>> 0;
1680
+ let output = ``;
1681
+ while (value > 0) {
1682
+ output = `${PERSISTED_TABLE_NAME_ALPHABET.charAt(value % 32)}${output}`;
1683
+ value = Math.floor(value / 32);
1684
+ }
1685
+ return output;
1686
+ }
1687
+ function createPersistedTableName(collectionId, prefix = `c`) {
1688
+ if (!collectionId) {
1689
+ throw new InvalidPersistedCollectionConfigError(
1690
+ `collectionId is required to derive a persisted table name`
1691
+ );
1692
+ }
1693
+ const hashPart = toBase32(hashCollectionId(collectionId));
1694
+ const lengthPart = collectionId.length.toString(36);
1695
+ return `${prefix}_${hashPart}_${lengthPart}`;
1696
+ }
1697
+ export {
1698
+ SingleProcessCoordinator,
1699
+ createPersistedTableName,
1700
+ decodePersistedStorageKey,
1701
+ encodePersistedStorageKey,
1702
+ persistedCollectionOptions,
1703
+ validatePersistedCollectionCoordinator
1704
+ };
1705
+ //# sourceMappingURL=persisted.js.map