@tanstack/db 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/SortedMap.cjs +38 -11
- package/dist/cjs/SortedMap.cjs.map +1 -1
- package/dist/cjs/SortedMap.d.cts +10 -0
- package/dist/cjs/collection.cjs +467 -95
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +81 -5
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/optimistic-action.cjs +21 -0
- package/dist/cjs/optimistic-action.cjs.map +1 -0
- package/dist/cjs/optimistic-action.d.cts +39 -0
- package/dist/cjs/query/compiled-query.cjs +21 -11
- package/dist/cjs/query/compiled-query.cjs.map +1 -1
- package/dist/cjs/query/query-builder.cjs +2 -2
- package/dist/cjs/query/query-builder.cjs.map +1 -1
- package/dist/cjs/transactions.cjs +3 -1
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/transactions.d.cts +4 -4
- package/dist/cjs/types.d.cts +45 -1
- package/dist/esm/SortedMap.d.ts +10 -0
- package/dist/esm/SortedMap.js +38 -11
- package/dist/esm/SortedMap.js.map +1 -1
- package/dist/esm/collection.d.ts +81 -5
- package/dist/esm/collection.js +467 -95
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/optimistic-action.d.ts +39 -0
- package/dist/esm/optimistic-action.js +21 -0
- package/dist/esm/optimistic-action.js.map +1 -0
- package/dist/esm/query/compiled-query.js +21 -11
- package/dist/esm/query/compiled-query.js.map +1 -1
- package/dist/esm/query/query-builder.js +2 -2
- package/dist/esm/query/query-builder.js.map +1 -1
- package/dist/esm/transactions.d.ts +4 -4
- package/dist/esm/transactions.js +3 -1
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +45 -1
- package/package.json +1 -1
- package/src/SortedMap.ts +46 -13
- package/src/collection.ts +624 -119
- package/src/index.ts +1 -0
- package/src/optimistic-action.ts +65 -0
- package/src/query/compiled-query.ts +36 -14
- package/src/query/query-builder.ts +2 -2
- package/src/transactions.ts +14 -5
- package/src/types.ts +48 -1
package/dist/esm/collection.js
CHANGED
|
@@ -30,7 +30,7 @@ class CollectionImpl {
|
|
|
30
30
|
* @throws Error if sync config is missing
|
|
31
31
|
*/
|
|
32
32
|
constructor(config) {
|
|
33
|
-
this.
|
|
33
|
+
this.pendingSyncedTransactions = [];
|
|
34
34
|
this.syncedMetadata = /* @__PURE__ */ new Map();
|
|
35
35
|
this.derivedUpserts = /* @__PURE__ */ new Map();
|
|
36
36
|
this.derivedDeletes = /* @__PURE__ */ new Set();
|
|
@@ -38,21 +38,48 @@ class CollectionImpl {
|
|
|
38
38
|
this.changeListeners = /* @__PURE__ */ new Set();
|
|
39
39
|
this.changeKeyListeners = /* @__PURE__ */ new Map();
|
|
40
40
|
this.utils = {};
|
|
41
|
-
this.pendingSyncedTransactions = [];
|
|
42
41
|
this.syncedKeys = /* @__PURE__ */ new Set();
|
|
42
|
+
this.preSyncVisibleState = /* @__PURE__ */ new Map();
|
|
43
|
+
this.recentlySyncedKeys = /* @__PURE__ */ new Set();
|
|
43
44
|
this.hasReceivedFirstCommit = false;
|
|
45
|
+
this.isCommittingSyncTransactions = false;
|
|
44
46
|
this.onFirstCommitCallbacks = [];
|
|
47
|
+
this._status = `idle`;
|
|
48
|
+
this.activeSubscribersCount = 0;
|
|
49
|
+
this.gcTimeoutId = null;
|
|
50
|
+
this.preloadPromise = null;
|
|
51
|
+
this.syncCleanupFn = null;
|
|
45
52
|
this.id = ``;
|
|
46
53
|
this.commitPendingTransactions = () => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
let hasPersistingTransaction = false;
|
|
55
|
+
for (const transaction of this.transactions.values()) {
|
|
56
|
+
if (transaction.state === `persisting`) {
|
|
57
|
+
hasPersistingTransaction = true;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!hasPersistingTransaction) {
|
|
62
|
+
this.isCommittingSyncTransactions = true;
|
|
50
63
|
const changedKeys = /* @__PURE__ */ new Set();
|
|
64
|
+
for (const transaction of this.pendingSyncedTransactions) {
|
|
65
|
+
for (const operation of transaction.operations) {
|
|
66
|
+
changedKeys.add(operation.key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
let currentVisibleState = this.preSyncVisibleState;
|
|
70
|
+
if (currentVisibleState.size === 0) {
|
|
71
|
+
currentVisibleState = /* @__PURE__ */ new Map();
|
|
72
|
+
for (const key of changedKeys) {
|
|
73
|
+
const currentValue = this.get(key);
|
|
74
|
+
if (currentValue !== void 0) {
|
|
75
|
+
currentVisibleState.set(key, currentValue);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
51
79
|
const events = [];
|
|
52
80
|
for (const transaction of this.pendingSyncedTransactions) {
|
|
53
81
|
for (const operation of transaction.operations) {
|
|
54
82
|
const key = operation.key;
|
|
55
|
-
changedKeys.add(key);
|
|
56
83
|
this.syncedKeys.add(key);
|
|
57
84
|
switch (operation.type) {
|
|
58
85
|
case `insert`:
|
|
@@ -72,17 +99,9 @@ class CollectionImpl {
|
|
|
72
99
|
this.syncedMetadata.delete(key);
|
|
73
100
|
break;
|
|
74
101
|
}
|
|
75
|
-
const previousValue = this.syncedData.get(key);
|
|
76
102
|
switch (operation.type) {
|
|
77
103
|
case `insert`:
|
|
78
104
|
this.syncedData.set(key, operation.value);
|
|
79
|
-
if (!this.derivedDeletes.has(key) && !this.derivedUpserts.has(key)) {
|
|
80
|
-
events.push({
|
|
81
|
-
type: `insert`,
|
|
82
|
-
key,
|
|
83
|
-
value: operation.value
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
105
|
break;
|
|
87
106
|
case `update`: {
|
|
88
107
|
const updatedValue = Object.assign(
|
|
@@ -91,34 +110,84 @@ class CollectionImpl {
|
|
|
91
110
|
operation.value
|
|
92
111
|
);
|
|
93
112
|
this.syncedData.set(key, updatedValue);
|
|
94
|
-
if (!this.derivedDeletes.has(key) && !this.derivedUpserts.has(key)) {
|
|
95
|
-
events.push({
|
|
96
|
-
type: `update`,
|
|
97
|
-
key,
|
|
98
|
-
value: updatedValue,
|
|
99
|
-
previousValue
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
113
|
break;
|
|
103
114
|
}
|
|
104
115
|
case `delete`:
|
|
105
116
|
this.syncedData.delete(key);
|
|
106
|
-
if (!this.derivedDeletes.has(key) && !this.derivedUpserts.has(key)) {
|
|
107
|
-
if (previousValue) {
|
|
108
|
-
events.push({
|
|
109
|
-
type: `delete`,
|
|
110
|
-
key,
|
|
111
|
-
value: previousValue
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
117
|
break;
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
}
|
|
121
|
+
this.derivedUpserts.clear();
|
|
122
|
+
this.derivedDeletes.clear();
|
|
123
|
+
this.isCommittingSyncTransactions = false;
|
|
124
|
+
for (const transaction of this.transactions.values()) {
|
|
125
|
+
if (![`completed`, `failed`].includes(transaction.state)) {
|
|
126
|
+
for (const mutation of transaction.mutations) {
|
|
127
|
+
if (mutation.collection === this) {
|
|
128
|
+
switch (mutation.type) {
|
|
129
|
+
case `insert`:
|
|
130
|
+
case `update`:
|
|
131
|
+
this.derivedUpserts.set(mutation.key, mutation.modified);
|
|
132
|
+
this.derivedDeletes.delete(mutation.key);
|
|
133
|
+
break;
|
|
134
|
+
case `delete`:
|
|
135
|
+
this.derivedUpserts.delete(mutation.key);
|
|
136
|
+
this.derivedDeletes.add(mutation.key);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const completedOptimisticOps = /* @__PURE__ */ new Map();
|
|
144
|
+
for (const transaction of this.transactions.values()) {
|
|
145
|
+
if (transaction.state === `completed`) {
|
|
146
|
+
for (const mutation of transaction.mutations) {
|
|
147
|
+
if (mutation.collection === this && changedKeys.has(mutation.key)) {
|
|
148
|
+
completedOptimisticOps.set(mutation.key, {
|
|
149
|
+
type: mutation.type,
|
|
150
|
+
value: mutation.modified
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const key of changedKeys) {
|
|
157
|
+
const previousVisibleValue = currentVisibleState.get(key);
|
|
158
|
+
const newVisibleValue = this.get(key);
|
|
159
|
+
const completedOp = completedOptimisticOps.get(key);
|
|
160
|
+
const isRedundantSync = completedOp && newVisibleValue !== void 0 && this.deepEqual(completedOp.value, newVisibleValue);
|
|
161
|
+
if (!isRedundantSync) {
|
|
162
|
+
if (previousVisibleValue === void 0 && newVisibleValue !== void 0) {
|
|
163
|
+
events.push({
|
|
164
|
+
type: `insert`,
|
|
165
|
+
key,
|
|
166
|
+
value: newVisibleValue
|
|
167
|
+
});
|
|
168
|
+
} else if (previousVisibleValue !== void 0 && newVisibleValue === void 0) {
|
|
169
|
+
events.push({
|
|
170
|
+
type: `delete`,
|
|
171
|
+
key,
|
|
172
|
+
value: previousVisibleValue
|
|
173
|
+
});
|
|
174
|
+
} else if (previousVisibleValue !== void 0 && newVisibleValue !== void 0 && !this.deepEqual(previousVisibleValue, newVisibleValue)) {
|
|
175
|
+
events.push({
|
|
176
|
+
type: `update`,
|
|
177
|
+
key,
|
|
178
|
+
value: newVisibleValue,
|
|
179
|
+
previousValue: previousVisibleValue
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
119
184
|
this._size = this.calculateSize();
|
|
120
185
|
this.emitEvents(events);
|
|
121
186
|
this.pendingSyncedTransactions = [];
|
|
187
|
+
this.preSyncVisibleState.clear();
|
|
188
|
+
Promise.resolve().then(() => {
|
|
189
|
+
this.recentlySyncedKeys.clear();
|
|
190
|
+
});
|
|
122
191
|
if (!this.hasReceivedFirstCommit) {
|
|
123
192
|
this.hasReceivedFirstCommit = true;
|
|
124
193
|
const callbacks = [...this.onFirstCommitCallbacks];
|
|
@@ -128,6 +197,7 @@ class CollectionImpl {
|
|
|
128
197
|
}
|
|
129
198
|
};
|
|
130
199
|
this.insert = (data, config2) => {
|
|
200
|
+
this.validateCollectionUsable(`insert`);
|
|
131
201
|
const ambientTransaction = getActiveTransaction();
|
|
132
202
|
if (!ambientTransaction && !this.config.onInsert) {
|
|
133
203
|
throw new Error(
|
|
@@ -179,6 +249,7 @@ class CollectionImpl {
|
|
|
179
249
|
}
|
|
180
250
|
};
|
|
181
251
|
this.delete = (keys, config2) => {
|
|
252
|
+
this.validateCollectionUsable(`delete`);
|
|
182
253
|
const ambientTransaction = getActiveTransaction();
|
|
183
254
|
if (!ambientTransaction && !this.config.onDelete) {
|
|
184
255
|
throw new Error(
|
|
@@ -246,87 +317,305 @@ class CollectionImpl {
|
|
|
246
317
|
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
247
318
|
);
|
|
248
319
|
this.config = config;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
320
|
+
collectionsStore.set(this.id, this);
|
|
321
|
+
if (this.config.compare) {
|
|
322
|
+
this.syncedData = new SortedMap(this.config.compare);
|
|
323
|
+
} else {
|
|
324
|
+
this.syncedData = /* @__PURE__ */ new Map();
|
|
325
|
+
}
|
|
326
|
+
if (config.startSync === true) {
|
|
327
|
+
this.startSync();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Register a callback to be executed on the next commit
|
|
332
|
+
* Useful for preloading collections
|
|
333
|
+
* @param callback Function to call after the next commit
|
|
334
|
+
*/
|
|
335
|
+
onFirstCommit(callback) {
|
|
336
|
+
this.onFirstCommitCallbacks.push(callback);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Gets the current status of the collection
|
|
340
|
+
*/
|
|
341
|
+
get status() {
|
|
342
|
+
return this._status;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Validates that the collection is in a usable state for data operations
|
|
346
|
+
* @private
|
|
347
|
+
*/
|
|
348
|
+
validateCollectionUsable(operation) {
|
|
349
|
+
switch (this._status) {
|
|
350
|
+
case `error`:
|
|
351
|
+
throw new Error(
|
|
352
|
+
`Cannot perform ${operation} on collection "${this.id}" - collection is in error state. Try calling cleanup() and restarting the collection.`
|
|
353
|
+
);
|
|
354
|
+
case `cleaned-up`:
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Cannot perform ${operation} on collection "${this.id}" - collection has been cleaned up. The collection will automatically restart on next access.`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Validates state transitions to prevent invalid status changes
|
|
362
|
+
* @private
|
|
363
|
+
*/
|
|
364
|
+
validateStatusTransition(from, to) {
|
|
365
|
+
if (from === to) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const validTransitions = {
|
|
369
|
+
idle: [`loading`, `error`, `cleaned-up`],
|
|
370
|
+
loading: [`ready`, `error`, `cleaned-up`],
|
|
371
|
+
ready: [`cleaned-up`, `error`],
|
|
372
|
+
error: [`cleaned-up`, `idle`],
|
|
373
|
+
"cleaned-up": [`loading`, `error`]
|
|
374
|
+
};
|
|
375
|
+
if (!validTransitions[from].includes(to)) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
`Invalid collection status transition from "${from}" to "${to}" for collection "${this.id}"`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Safely update the collection status with validation
|
|
383
|
+
* @private
|
|
384
|
+
*/
|
|
385
|
+
setStatus(newStatus) {
|
|
386
|
+
this.validateStatusTransition(this._status, newStatus);
|
|
387
|
+
this._status = newStatus;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Start sync immediately - internal method for compiled queries
|
|
391
|
+
* This bypasses lazy loading for special cases like live query results
|
|
392
|
+
*/
|
|
393
|
+
startSyncImmediate() {
|
|
394
|
+
this.startSync();
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Start the sync process for this collection
|
|
398
|
+
* This is called when the collection is first accessed or preloaded
|
|
399
|
+
*/
|
|
400
|
+
startSync() {
|
|
401
|
+
if (this._status !== `idle` && this._status !== `cleaned-up`) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
this.setStatus(`loading`);
|
|
405
|
+
try {
|
|
406
|
+
const cleanupFn = this.config.sync.sync({
|
|
407
|
+
collection: this,
|
|
408
|
+
begin: () => {
|
|
409
|
+
this.pendingSyncedTransactions.push({
|
|
410
|
+
committed: false,
|
|
411
|
+
operations: []
|
|
412
|
+
});
|
|
413
|
+
},
|
|
414
|
+
write: (messageWithoutKey) => {
|
|
415
|
+
const pendingTransaction = this.pendingSyncedTransactions[this.pendingSyncedTransactions.length - 1];
|
|
416
|
+
if (!pendingTransaction) {
|
|
417
|
+
throw new Error(`No pending sync transaction to write to`);
|
|
418
|
+
}
|
|
419
|
+
if (pendingTransaction.committed) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`The pending sync transaction is already committed, you can't still write to it.`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const key = this.getKeyFromItem(messageWithoutKey.value);
|
|
425
|
+
if (messageWithoutKey.type === `insert`) {
|
|
426
|
+
if (this.syncedData.has(key) && !pendingTransaction.operations.some(
|
|
427
|
+
(op) => op.key === key && op.type === `delete`
|
|
428
|
+
)) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`Cannot insert document with key "${key}" from sync because it already exists in the collection "${this.id}"`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const message = {
|
|
435
|
+
...messageWithoutKey,
|
|
436
|
+
key
|
|
437
|
+
};
|
|
438
|
+
pendingTransaction.operations.push(message);
|
|
439
|
+
},
|
|
440
|
+
commit: () => {
|
|
441
|
+
const pendingTransaction = this.pendingSyncedTransactions[this.pendingSyncedTransactions.length - 1];
|
|
442
|
+
if (!pendingTransaction) {
|
|
443
|
+
throw new Error(`No pending sync transaction to commit`);
|
|
444
|
+
}
|
|
445
|
+
if (pendingTransaction.committed) {
|
|
272
446
|
throw new Error(
|
|
273
|
-
`
|
|
447
|
+
`The pending sync transaction is already committed, you can't commit it again.`
|
|
274
448
|
);
|
|
275
449
|
}
|
|
450
|
+
pendingTransaction.committed = true;
|
|
451
|
+
this.commitPendingTransactions();
|
|
452
|
+
if (this._status === `loading`) {
|
|
453
|
+
this.setStatus(`ready`);
|
|
454
|
+
}
|
|
276
455
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
456
|
+
});
|
|
457
|
+
this.syncCleanupFn = typeof cleanupFn === `function` ? cleanupFn : null;
|
|
458
|
+
} catch (error) {
|
|
459
|
+
this.setStatus(`error`);
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Preload the collection data by starting sync if not already started
|
|
465
|
+
* Multiple concurrent calls will share the same promise
|
|
466
|
+
*/
|
|
467
|
+
preload() {
|
|
468
|
+
if (this.preloadPromise) {
|
|
469
|
+
return this.preloadPromise;
|
|
470
|
+
}
|
|
471
|
+
this.preloadPromise = new Promise((resolve, reject) => {
|
|
472
|
+
if (this._status === `ready`) {
|
|
473
|
+
resolve();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (this._status === `error`) {
|
|
477
|
+
reject(new Error(`Collection is in error state`));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
this.onFirstCommit(() => {
|
|
481
|
+
resolve();
|
|
482
|
+
});
|
|
483
|
+
if (this._status === `idle` || this._status === `cleaned-up`) {
|
|
484
|
+
try {
|
|
485
|
+
this.startSync();
|
|
486
|
+
} catch (error) {
|
|
487
|
+
reject(error);
|
|
488
|
+
return;
|
|
287
489
|
}
|
|
288
|
-
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
return this.preloadPromise;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Clean up the collection by stopping sync and clearing data
|
|
496
|
+
* This can be called manually or automatically by garbage collection
|
|
497
|
+
*/
|
|
498
|
+
async cleanup() {
|
|
499
|
+
if (this.gcTimeoutId) {
|
|
500
|
+
clearTimeout(this.gcTimeoutId);
|
|
501
|
+
this.gcTimeoutId = null;
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
if (this.syncCleanupFn) {
|
|
505
|
+
this.syncCleanupFn();
|
|
506
|
+
this.syncCleanupFn = null;
|
|
507
|
+
}
|
|
508
|
+
} catch (error) {
|
|
509
|
+
queueMicrotask(() => {
|
|
510
|
+
if (error instanceof Error) {
|
|
511
|
+
const wrappedError = new Error(
|
|
512
|
+
`Collection "${this.id}" sync cleanup function threw an error: ${error.message}`
|
|
513
|
+
);
|
|
514
|
+
wrappedError.cause = error;
|
|
515
|
+
wrappedError.stack = error.stack;
|
|
516
|
+
throw wrappedError;
|
|
517
|
+
} else {
|
|
289
518
|
throw new Error(
|
|
290
|
-
`
|
|
519
|
+
`Collection "${this.id}" sync cleanup function threw an error: ${String(error)}`
|
|
291
520
|
);
|
|
292
521
|
}
|
|
293
|
-
|
|
294
|
-
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
this.syncedData.clear();
|
|
525
|
+
this.syncedMetadata.clear();
|
|
526
|
+
this.derivedUpserts.clear();
|
|
527
|
+
this.derivedDeletes.clear();
|
|
528
|
+
this._size = 0;
|
|
529
|
+
this.pendingSyncedTransactions = [];
|
|
530
|
+
this.syncedKeys.clear();
|
|
531
|
+
this.hasReceivedFirstCommit = false;
|
|
532
|
+
this.onFirstCommitCallbacks = [];
|
|
533
|
+
this.preloadPromise = null;
|
|
534
|
+
this.setStatus(`cleaned-up`);
|
|
535
|
+
return Promise.resolve();
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Start the garbage collection timer
|
|
539
|
+
* Called when the collection becomes inactive (no subscribers)
|
|
540
|
+
*/
|
|
541
|
+
startGCTimer() {
|
|
542
|
+
if (this.gcTimeoutId) {
|
|
543
|
+
clearTimeout(this.gcTimeoutId);
|
|
544
|
+
}
|
|
545
|
+
const gcTime = this.config.gcTime ?? 3e5;
|
|
546
|
+
this.gcTimeoutId = setTimeout(() => {
|
|
547
|
+
if (this.activeSubscribersCount === 0) {
|
|
548
|
+
this.cleanup();
|
|
295
549
|
}
|
|
296
|
-
});
|
|
550
|
+
}, gcTime);
|
|
297
551
|
}
|
|
298
552
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* @param callback Function to call after the next commit
|
|
553
|
+
* Cancel the garbage collection timer
|
|
554
|
+
* Called when the collection becomes active again
|
|
302
555
|
*/
|
|
303
|
-
|
|
304
|
-
this.
|
|
556
|
+
cancelGCTimer() {
|
|
557
|
+
if (this.gcTimeoutId) {
|
|
558
|
+
clearTimeout(this.gcTimeoutId);
|
|
559
|
+
this.gcTimeoutId = null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Increment the active subscribers count and start sync if needed
|
|
564
|
+
*/
|
|
565
|
+
addSubscriber() {
|
|
566
|
+
this.activeSubscribersCount++;
|
|
567
|
+
this.cancelGCTimer();
|
|
568
|
+
if (this._status === `cleaned-up` || this._status === `idle`) {
|
|
569
|
+
this.startSync();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Decrement the active subscribers count and start GC timer if needed
|
|
574
|
+
*/
|
|
575
|
+
removeSubscriber() {
|
|
576
|
+
this.activeSubscribersCount--;
|
|
577
|
+
if (this.activeSubscribersCount === 0) {
|
|
578
|
+
this.activeSubscribersCount = 0;
|
|
579
|
+
this.startGCTimer();
|
|
580
|
+
} else if (this.activeSubscribersCount < 0) {
|
|
581
|
+
throw new Error(
|
|
582
|
+
`Active subscribers count is negative - this should never happen`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
305
585
|
}
|
|
306
586
|
/**
|
|
307
587
|
* Recompute optimistic state from active transactions
|
|
308
588
|
*/
|
|
309
589
|
recomputeOptimisticState() {
|
|
590
|
+
if (this.isCommittingSyncTransactions) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
310
593
|
const previousState = new Map(this.derivedUpserts);
|
|
311
594
|
const previousDeletes = new Set(this.derivedDeletes);
|
|
312
595
|
this.derivedUpserts.clear();
|
|
313
596
|
this.derivedDeletes.clear();
|
|
314
|
-
const activeTransactions =
|
|
597
|
+
const activeTransactions = [];
|
|
598
|
+
const completedTransactions = [];
|
|
599
|
+
for (const transaction of this.transactions.values()) {
|
|
600
|
+
if (transaction.state === `completed`) {
|
|
601
|
+
completedTransactions.push(transaction);
|
|
602
|
+
} else if (![`completed`, `failed`].includes(transaction.state)) {
|
|
603
|
+
activeTransactions.push(transaction);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
315
606
|
for (const transaction of activeTransactions) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
607
|
+
for (const mutation of transaction.mutations) {
|
|
608
|
+
if (mutation.collection === this) {
|
|
609
|
+
switch (mutation.type) {
|
|
610
|
+
case `insert`:
|
|
611
|
+
case `update`:
|
|
612
|
+
this.derivedUpserts.set(mutation.key, mutation.modified);
|
|
613
|
+
this.derivedDeletes.delete(mutation.key);
|
|
614
|
+
break;
|
|
615
|
+
case `delete`:
|
|
616
|
+
this.derivedUpserts.delete(mutation.key);
|
|
617
|
+
this.derivedDeletes.add(mutation.key);
|
|
618
|
+
break;
|
|
330
619
|
}
|
|
331
620
|
}
|
|
332
621
|
}
|
|
@@ -334,7 +623,41 @@ class CollectionImpl {
|
|
|
334
623
|
this._size = this.calculateSize();
|
|
335
624
|
const events = [];
|
|
336
625
|
this.collectOptimisticChanges(previousState, previousDeletes, events);
|
|
337
|
-
|
|
626
|
+
const filteredEventsBySyncStatus = events.filter(
|
|
627
|
+
(event) => !this.recentlySyncedKeys.has(event.key)
|
|
628
|
+
);
|
|
629
|
+
if (this.pendingSyncedTransactions.length > 0) {
|
|
630
|
+
const pendingSyncKeys = /* @__PURE__ */ new Set();
|
|
631
|
+
const completedTransactionMutations = /* @__PURE__ */ new Set();
|
|
632
|
+
for (const transaction of this.pendingSyncedTransactions) {
|
|
633
|
+
for (const operation of transaction.operations) {
|
|
634
|
+
pendingSyncKeys.add(operation.key);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
for (const tx of completedTransactions) {
|
|
638
|
+
for (const mutation of tx.mutations) {
|
|
639
|
+
if (mutation.collection === this) {
|
|
640
|
+
completedTransactionMutations.add(mutation.mutationId);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const filteredEvents = filteredEventsBySyncStatus.filter((event) => {
|
|
645
|
+
if (event.type === `delete` && pendingSyncKeys.has(event.key)) {
|
|
646
|
+
const hasActiveOptimisticMutation = activeTransactions.some(
|
|
647
|
+
(tx) => tx.mutations.some(
|
|
648
|
+
(m) => m.collection === this && m.key === event.key
|
|
649
|
+
)
|
|
650
|
+
);
|
|
651
|
+
if (!hasActiveOptimisticMutation) {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return true;
|
|
656
|
+
});
|
|
657
|
+
this.emitEvents(filteredEvents);
|
|
658
|
+
} else {
|
|
659
|
+
this.emitEvents(filteredEventsBySyncStatus);
|
|
660
|
+
}
|
|
338
661
|
}
|
|
339
662
|
/**
|
|
340
663
|
* Calculate the current size based on synced data and optimistic changes
|
|
@@ -471,7 +794,8 @@ class CollectionImpl {
|
|
|
471
794
|
for (const key of this.keys()) {
|
|
472
795
|
const value = this.get(key);
|
|
473
796
|
if (value !== void 0) {
|
|
474
|
-
|
|
797
|
+
const { _orderByIndex, ...copy } = value;
|
|
798
|
+
yield copy;
|
|
475
799
|
}
|
|
476
800
|
}
|
|
477
801
|
}
|
|
@@ -482,7 +806,8 @@ class CollectionImpl {
|
|
|
482
806
|
for (const key of this.keys()) {
|
|
483
807
|
const value = this.get(key);
|
|
484
808
|
if (value !== void 0) {
|
|
485
|
-
|
|
809
|
+
const { _orderByIndex, ...copy } = value;
|
|
810
|
+
yield [key, copy];
|
|
486
811
|
}
|
|
487
812
|
}
|
|
488
813
|
}
|
|
@@ -505,6 +830,24 @@ class CollectionImpl {
|
|
|
505
830
|
}
|
|
506
831
|
return `KEY::${this.id}/${key}`;
|
|
507
832
|
}
|
|
833
|
+
deepEqual(a, b) {
|
|
834
|
+
if (a === b) return true;
|
|
835
|
+
if (a == null || b == null) return false;
|
|
836
|
+
if (typeof a !== typeof b) return false;
|
|
837
|
+
if (typeof a === `object`) {
|
|
838
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
839
|
+
const keysA = Object.keys(a);
|
|
840
|
+
const keysB = Object.keys(b);
|
|
841
|
+
if (keysA.length !== keysB.length) return false;
|
|
842
|
+
const keysBSet = new Set(keysB);
|
|
843
|
+
for (const key of keysA) {
|
|
844
|
+
if (!keysBSet.has(key)) return false;
|
|
845
|
+
if (!this.deepEqual(a[key], b[key])) return false;
|
|
846
|
+
}
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
508
851
|
validateData(data, type, key) {
|
|
509
852
|
if (!this.config.schema) return data;
|
|
510
853
|
const standardSchema = this.ensureStandardSchema(this.config.schema);
|
|
@@ -549,6 +892,7 @@ class CollectionImpl {
|
|
|
549
892
|
if (typeof keys === `undefined`) {
|
|
550
893
|
throw new Error(`The first argument to update is missing`);
|
|
551
894
|
}
|
|
895
|
+
this.validateCollectionUsable(`update`);
|
|
552
896
|
const ambientTransaction = getActiveTransaction();
|
|
553
897
|
if (!ambientTransaction && !this.config.onUpdate) {
|
|
554
898
|
throw new Error(
|
|
@@ -723,18 +1067,21 @@ class CollectionImpl {
|
|
|
723
1067
|
* @returns A function that can be called to unsubscribe from the changes
|
|
724
1068
|
*/
|
|
725
1069
|
subscribeChanges(callback, { includeInitialState = false } = {}) {
|
|
1070
|
+
this.addSubscriber();
|
|
726
1071
|
if (includeInitialState) {
|
|
727
1072
|
callback(this.currentStateAsChanges());
|
|
728
1073
|
}
|
|
729
1074
|
this.changeListeners.add(callback);
|
|
730
1075
|
return () => {
|
|
731
1076
|
this.changeListeners.delete(callback);
|
|
1077
|
+
this.removeSubscriber();
|
|
732
1078
|
};
|
|
733
1079
|
}
|
|
734
1080
|
/**
|
|
735
1081
|
* Subscribe to changes for a specific key
|
|
736
1082
|
*/
|
|
737
1083
|
subscribeChangesKey(key, listener, { includeInitialState = false } = {}) {
|
|
1084
|
+
this.addSubscriber();
|
|
738
1085
|
if (!this.changeKeyListeners.has(key)) {
|
|
739
1086
|
this.changeKeyListeners.set(key, /* @__PURE__ */ new Set());
|
|
740
1087
|
}
|
|
@@ -756,13 +1103,38 @@ class CollectionImpl {
|
|
|
756
1103
|
this.changeKeyListeners.delete(key);
|
|
757
1104
|
}
|
|
758
1105
|
}
|
|
1106
|
+
this.removeSubscriber();
|
|
759
1107
|
};
|
|
760
1108
|
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Capture visible state for keys that will be affected by pending sync operations
|
|
1111
|
+
* This must be called BEFORE onTransactionStateChange clears optimistic state
|
|
1112
|
+
*/
|
|
1113
|
+
capturePreSyncVisibleState() {
|
|
1114
|
+
if (this.pendingSyncedTransactions.length === 0) return;
|
|
1115
|
+
this.preSyncVisibleState.clear();
|
|
1116
|
+
const syncedKeys = /* @__PURE__ */ new Set();
|
|
1117
|
+
for (const transaction of this.pendingSyncedTransactions) {
|
|
1118
|
+
for (const operation of transaction.operations) {
|
|
1119
|
+
syncedKeys.add(operation.key);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
for (const key of syncedKeys) {
|
|
1123
|
+
this.recentlySyncedKeys.add(key);
|
|
1124
|
+
}
|
|
1125
|
+
for (const key of syncedKeys) {
|
|
1126
|
+
const currentValue = this.get(key);
|
|
1127
|
+
if (currentValue !== void 0) {
|
|
1128
|
+
this.preSyncVisibleState.set(key, currentValue);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
761
1132
|
/**
|
|
762
1133
|
* Trigger a recomputation when transactions change
|
|
763
1134
|
* This method should be called by the Transaction class when state changes
|
|
764
1135
|
*/
|
|
765
1136
|
onTransactionStateChange() {
|
|
1137
|
+
this.capturePreSyncVisibleState();
|
|
766
1138
|
this.recomputeOptimisticState();
|
|
767
1139
|
}
|
|
768
1140
|
/**
|
|
@@ -774,7 +1146,7 @@ class CollectionImpl {
|
|
|
774
1146
|
asStoreMap() {
|
|
775
1147
|
if (!this._storeMap) {
|
|
776
1148
|
this._storeMap = new Store(new Map(this.entries()));
|
|
777
|
-
this.
|
|
1149
|
+
this.changeListeners.add(() => {
|
|
778
1150
|
this._storeMap.setState(() => new Map(this.entries()));
|
|
779
1151
|
});
|
|
780
1152
|
}
|
|
@@ -789,7 +1161,7 @@ class CollectionImpl {
|
|
|
789
1161
|
asStoreArray() {
|
|
790
1162
|
if (!this._storeArray) {
|
|
791
1163
|
this._storeArray = new Store(this.toArray);
|
|
792
|
-
this.
|
|
1164
|
+
this.changeListeners.add(() => {
|
|
793
1165
|
this._storeArray.setState(() => this.toArray);
|
|
794
1166
|
});
|
|
795
1167
|
}
|