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