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