@tanstack/db 0.0.11 → 0.0.13
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 +476 -144
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +107 -32
- package/dist/cjs/index.cjs +2 -1
- 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 +38 -16
- 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/types.d.cts +83 -10
- 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 +107 -32
- package/dist/esm/collection.js +477 -145
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +3 -2
- 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 +38 -16
- 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.js +3 -1
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +83 -10
- package/package.json +1 -1
- package/src/SortedMap.ts +46 -13
- package/src/collection.ts +689 -239
- package/src/index.ts +1 -0
- package/src/optimistic-action.ts +65 -0
- package/src/query/compiled-query.ts +79 -21
- package/src/query/query-builder.ts +2 -2
- package/src/transactions.ts +6 -1
- package/src/types.ts +124 -8
package/dist/esm/collection.js
CHANGED
|
@@ -3,7 +3,6 @@ import { withArrayChangeTracking, withChangeTracking } from "./proxy.js";
|
|
|
3
3
|
import { getActiveTransaction, Transaction } from "./transactions.js";
|
|
4
4
|
import { SortedMap } from "./SortedMap.js";
|
|
5
5
|
const collectionsStore = /* @__PURE__ */ new Map();
|
|
6
|
-
const loadingCollectionResolvers = /* @__PURE__ */ new Map();
|
|
7
6
|
function createCollection(options) {
|
|
8
7
|
const collection = new CollectionImpl(options);
|
|
9
8
|
if (options.utils) {
|
|
@@ -13,53 +12,10 @@ function createCollection(options) {
|
|
|
13
12
|
}
|
|
14
13
|
return collection;
|
|
15
14
|
}
|
|
16
|
-
function preloadCollection(config) {
|
|
17
|
-
if (!config.id) {
|
|
18
|
-
throw new Error(`The id property is required for preloadCollection`);
|
|
19
|
-
}
|
|
20
|
-
if (collectionsStore.has(config.id) && !loadingCollectionResolvers.has(config.id)) {
|
|
21
|
-
return Promise.resolve(
|
|
22
|
-
collectionsStore.get(config.id)
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
if (loadingCollectionResolvers.has(config.id)) {
|
|
26
|
-
return loadingCollectionResolvers.get(config.id).promise;
|
|
27
|
-
}
|
|
28
|
-
if (!collectionsStore.has(config.id)) {
|
|
29
|
-
collectionsStore.set(
|
|
30
|
-
config.id,
|
|
31
|
-
createCollection({
|
|
32
|
-
id: config.id,
|
|
33
|
-
getKey: config.getKey,
|
|
34
|
-
sync: config.sync,
|
|
35
|
-
schema: config.schema
|
|
36
|
-
})
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
const collection = collectionsStore.get(config.id);
|
|
40
|
-
let resolveFirstCommit;
|
|
41
|
-
const firstCommitPromise = new Promise((resolve) => {
|
|
42
|
-
resolveFirstCommit = resolve;
|
|
43
|
-
});
|
|
44
|
-
loadingCollectionResolvers.set(config.id, {
|
|
45
|
-
promise: firstCommitPromise,
|
|
46
|
-
resolve: resolveFirstCommit
|
|
47
|
-
});
|
|
48
|
-
collection.onFirstCommit(() => {
|
|
49
|
-
if (!config.id) {
|
|
50
|
-
throw new Error(`The id property is required for preloadCollection`);
|
|
51
|
-
}
|
|
52
|
-
if (loadingCollectionResolvers.has(config.id)) {
|
|
53
|
-
const resolver = loadingCollectionResolvers.get(config.id);
|
|
54
|
-
loadingCollectionResolvers.delete(config.id);
|
|
55
|
-
resolver.resolve(collection);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
return firstCommitPromise;
|
|
59
|
-
}
|
|
60
15
|
class SchemaValidationError extends Error {
|
|
61
16
|
constructor(type, issues, message) {
|
|
62
|
-
const defaultMessage = `${type === `insert` ? `Insert` : `Update`} validation failed: ${issues.map((issue) =>
|
|
17
|
+
const defaultMessage = `${type === `insert` ? `Insert` : `Update`} validation failed: ${issues.map((issue) => `
|
|
18
|
+
- ${issue.message} - path: ${issue.path}`).join(``)}`;
|
|
63
19
|
super(message || defaultMessage);
|
|
64
20
|
this.name = `SchemaValidationError`;
|
|
65
21
|
this.type = type;
|
|
@@ -74,7 +30,7 @@ class CollectionImpl {
|
|
|
74
30
|
* @throws Error if sync config is missing
|
|
75
31
|
*/
|
|
76
32
|
constructor(config) {
|
|
77
|
-
this.
|
|
33
|
+
this.pendingSyncedTransactions = [];
|
|
78
34
|
this.syncedMetadata = /* @__PURE__ */ new Map();
|
|
79
35
|
this.derivedUpserts = /* @__PURE__ */ new Map();
|
|
80
36
|
this.derivedDeletes = /* @__PURE__ */ new Set();
|
|
@@ -82,21 +38,48 @@ class CollectionImpl {
|
|
|
82
38
|
this.changeListeners = /* @__PURE__ */ new Set();
|
|
83
39
|
this.changeKeyListeners = /* @__PURE__ */ new Map();
|
|
84
40
|
this.utils = {};
|
|
85
|
-
this.pendingSyncedTransactions = [];
|
|
86
41
|
this.syncedKeys = /* @__PURE__ */ new Set();
|
|
42
|
+
this.preSyncVisibleState = /* @__PURE__ */ new Map();
|
|
43
|
+
this.recentlySyncedKeys = /* @__PURE__ */ new Set();
|
|
87
44
|
this.hasReceivedFirstCommit = false;
|
|
45
|
+
this.isCommittingSyncTransactions = false;
|
|
88
46
|
this.onFirstCommitCallbacks = [];
|
|
47
|
+
this._status = `idle`;
|
|
48
|
+
this.activeSubscribersCount = 0;
|
|
49
|
+
this.gcTimeoutId = null;
|
|
50
|
+
this.preloadPromise = null;
|
|
51
|
+
this.syncCleanupFn = null;
|
|
89
52
|
this.id = ``;
|
|
90
53
|
this.commitPendingTransactions = () => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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;
|
|
94
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
|
+
}
|
|
95
79
|
const events = [];
|
|
96
80
|
for (const transaction of this.pendingSyncedTransactions) {
|
|
97
81
|
for (const operation of transaction.operations) {
|
|
98
82
|
const key = operation.key;
|
|
99
|
-
changedKeys.add(key);
|
|
100
83
|
this.syncedKeys.add(key);
|
|
101
84
|
switch (operation.type) {
|
|
102
85
|
case `insert`:
|
|
@@ -116,17 +99,9 @@ class CollectionImpl {
|
|
|
116
99
|
this.syncedMetadata.delete(key);
|
|
117
100
|
break;
|
|
118
101
|
}
|
|
119
|
-
const previousValue = this.syncedData.get(key);
|
|
120
102
|
switch (operation.type) {
|
|
121
103
|
case `insert`:
|
|
122
104
|
this.syncedData.set(key, operation.value);
|
|
123
|
-
if (!this.derivedDeletes.has(key) && !this.derivedUpserts.has(key)) {
|
|
124
|
-
events.push({
|
|
125
|
-
type: `insert`,
|
|
126
|
-
key,
|
|
127
|
-
value: operation.value
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
105
|
break;
|
|
131
106
|
case `update`: {
|
|
132
107
|
const updatedValue = Object.assign(
|
|
@@ -135,34 +110,84 @@ class CollectionImpl {
|
|
|
135
110
|
operation.value
|
|
136
111
|
);
|
|
137
112
|
this.syncedData.set(key, updatedValue);
|
|
138
|
-
if (!this.derivedDeletes.has(key) && !this.derivedUpserts.has(key)) {
|
|
139
|
-
events.push({
|
|
140
|
-
type: `update`,
|
|
141
|
-
key,
|
|
142
|
-
value: updatedValue,
|
|
143
|
-
previousValue
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
113
|
break;
|
|
147
114
|
}
|
|
148
115
|
case `delete`:
|
|
149
116
|
this.syncedData.delete(key);
|
|
150
|
-
if (!this.derivedDeletes.has(key) && !this.derivedUpserts.has(key)) {
|
|
151
|
-
if (previousValue) {
|
|
152
|
-
events.push({
|
|
153
|
-
type: `delete`,
|
|
154
|
-
key,
|
|
155
|
-
value: previousValue
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
117
|
break;
|
|
160
118
|
}
|
|
161
119
|
}
|
|
162
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
|
+
}
|
|
163
184
|
this._size = this.calculateSize();
|
|
164
185
|
this.emitEvents(events);
|
|
165
186
|
this.pendingSyncedTransactions = [];
|
|
187
|
+
this.preSyncVisibleState.clear();
|
|
188
|
+
Promise.resolve().then(() => {
|
|
189
|
+
this.recentlySyncedKeys.clear();
|
|
190
|
+
});
|
|
166
191
|
if (!this.hasReceivedFirstCommit) {
|
|
167
192
|
this.hasReceivedFirstCommit = true;
|
|
168
193
|
const callbacks = [...this.onFirstCommitCallbacks];
|
|
@@ -172,6 +197,7 @@ class CollectionImpl {
|
|
|
172
197
|
}
|
|
173
198
|
};
|
|
174
199
|
this.insert = (data, config2) => {
|
|
200
|
+
this.validateCollectionUsable(`insert`);
|
|
175
201
|
const ambientTransaction = getActiveTransaction();
|
|
176
202
|
if (!ambientTransaction && !this.config.onInsert) {
|
|
177
203
|
throw new Error(
|
|
@@ -223,6 +249,7 @@ class CollectionImpl {
|
|
|
223
249
|
}
|
|
224
250
|
};
|
|
225
251
|
this.delete = (keys, config2) => {
|
|
252
|
+
this.validateCollectionUsable(`delete`);
|
|
226
253
|
const ambientTransaction = getActiveTransaction();
|
|
227
254
|
if (!ambientTransaction && !this.config.onDelete) {
|
|
228
255
|
throw new Error(
|
|
@@ -235,12 +262,17 @@ class CollectionImpl {
|
|
|
235
262
|
const keysArray = Array.isArray(keys) ? keys : [keys];
|
|
236
263
|
const mutations = [];
|
|
237
264
|
for (const key of keysArray) {
|
|
265
|
+
if (!this.has(key)) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`Collection.delete was called with key '${key}' but there is no item in the collection with this key`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
238
270
|
const globalKey = this.generateGlobalKey(key, this.get(key));
|
|
239
271
|
const mutation = {
|
|
240
272
|
mutationId: crypto.randomUUID(),
|
|
241
|
-
original: this.get(key)
|
|
273
|
+
original: this.get(key),
|
|
242
274
|
modified: this.get(key),
|
|
243
|
-
changes: this.get(key)
|
|
275
|
+
changes: this.get(key),
|
|
244
276
|
globalKey,
|
|
245
277
|
key,
|
|
246
278
|
metadata: config2 == null ? void 0 : config2.metadata,
|
|
@@ -285,87 +317,305 @@ class CollectionImpl {
|
|
|
285
317
|
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
286
318
|
);
|
|
287
319
|
this.config = config;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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) {
|
|
311
446
|
throw new Error(
|
|
312
|
-
`
|
|
447
|
+
`The pending sync transaction is already committed, you can't commit it again.`
|
|
313
448
|
);
|
|
314
449
|
}
|
|
450
|
+
pendingTransaction.committed = true;
|
|
451
|
+
this.commitPendingTransactions();
|
|
452
|
+
if (this._status === `loading`) {
|
|
453
|
+
this.setStatus(`ready`);
|
|
454
|
+
}
|
|
315
455
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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;
|
|
326
489
|
}
|
|
327
|
-
|
|
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 {
|
|
328
518
|
throw new Error(
|
|
329
|
-
`
|
|
519
|
+
`Collection "${this.id}" sync cleanup function threw an error: ${String(error)}`
|
|
330
520
|
);
|
|
331
521
|
}
|
|
332
|
-
|
|
333
|
-
|
|
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();
|
|
334
549
|
}
|
|
335
|
-
});
|
|
550
|
+
}, gcTime);
|
|
336
551
|
}
|
|
337
552
|
/**
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
* @param callback Function to call after the next commit
|
|
553
|
+
* Cancel the garbage collection timer
|
|
554
|
+
* Called when the collection becomes active again
|
|
341
555
|
*/
|
|
342
|
-
|
|
343
|
-
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
|
+
}
|
|
344
585
|
}
|
|
345
586
|
/**
|
|
346
587
|
* Recompute optimistic state from active transactions
|
|
347
588
|
*/
|
|
348
589
|
recomputeOptimisticState() {
|
|
590
|
+
if (this.isCommittingSyncTransactions) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
349
593
|
const previousState = new Map(this.derivedUpserts);
|
|
350
594
|
const previousDeletes = new Set(this.derivedDeletes);
|
|
351
595
|
this.derivedUpserts.clear();
|
|
352
596
|
this.derivedDeletes.clear();
|
|
353
|
-
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
|
+
}
|
|
354
606
|
for (const transaction of activeTransactions) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
break;
|
|
368
|
-
}
|
|
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;
|
|
369
619
|
}
|
|
370
620
|
}
|
|
371
621
|
}
|
|
@@ -373,7 +623,41 @@ class CollectionImpl {
|
|
|
373
623
|
this._size = this.calculateSize();
|
|
374
624
|
const events = [];
|
|
375
625
|
this.collectOptimisticChanges(previousState, previousDeletes, events);
|
|
376
|
-
|
|
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
|
+
}
|
|
377
661
|
}
|
|
378
662
|
/**
|
|
379
663
|
* Calculate the current size based on synced data and optimistic changes
|
|
@@ -510,7 +794,8 @@ class CollectionImpl {
|
|
|
510
794
|
for (const key of this.keys()) {
|
|
511
795
|
const value = this.get(key);
|
|
512
796
|
if (value !== void 0) {
|
|
513
|
-
|
|
797
|
+
const { _orderByIndex, ...copy } = value;
|
|
798
|
+
yield copy;
|
|
514
799
|
}
|
|
515
800
|
}
|
|
516
801
|
}
|
|
@@ -521,7 +806,8 @@ class CollectionImpl {
|
|
|
521
806
|
for (const key of this.keys()) {
|
|
522
807
|
const value = this.get(key);
|
|
523
808
|
if (value !== void 0) {
|
|
524
|
-
|
|
809
|
+
const { _orderByIndex, ...copy } = value;
|
|
810
|
+
yield [key, copy];
|
|
525
811
|
}
|
|
526
812
|
}
|
|
527
813
|
}
|
|
@@ -544,6 +830,24 @@ class CollectionImpl {
|
|
|
544
830
|
}
|
|
545
831
|
return `KEY::${this.id}/${key}`;
|
|
546
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
|
+
}
|
|
547
851
|
validateData(data, type, key) {
|
|
548
852
|
if (!this.config.schema) return data;
|
|
549
853
|
const standardSchema = this.ensureStandardSchema(this.config.schema);
|
|
@@ -588,6 +892,7 @@ class CollectionImpl {
|
|
|
588
892
|
if (typeof keys === `undefined`) {
|
|
589
893
|
throw new Error(`The first argument to update is missing`);
|
|
590
894
|
}
|
|
895
|
+
this.validateCollectionUsable(`update`);
|
|
591
896
|
const ambientTransaction = getActiveTransaction();
|
|
592
897
|
if (!ambientTransaction && !this.config.onUpdate) {
|
|
593
898
|
throw new Error(
|
|
@@ -762,18 +1067,21 @@ class CollectionImpl {
|
|
|
762
1067
|
* @returns A function that can be called to unsubscribe from the changes
|
|
763
1068
|
*/
|
|
764
1069
|
subscribeChanges(callback, { includeInitialState = false } = {}) {
|
|
1070
|
+
this.addSubscriber();
|
|
765
1071
|
if (includeInitialState) {
|
|
766
1072
|
callback(this.currentStateAsChanges());
|
|
767
1073
|
}
|
|
768
1074
|
this.changeListeners.add(callback);
|
|
769
1075
|
return () => {
|
|
770
1076
|
this.changeListeners.delete(callback);
|
|
1077
|
+
this.removeSubscriber();
|
|
771
1078
|
};
|
|
772
1079
|
}
|
|
773
1080
|
/**
|
|
774
1081
|
* Subscribe to changes for a specific key
|
|
775
1082
|
*/
|
|
776
1083
|
subscribeChangesKey(key, listener, { includeInitialState = false } = {}) {
|
|
1084
|
+
this.addSubscriber();
|
|
777
1085
|
if (!this.changeKeyListeners.has(key)) {
|
|
778
1086
|
this.changeKeyListeners.set(key, /* @__PURE__ */ new Set());
|
|
779
1087
|
}
|
|
@@ -795,13 +1103,38 @@ class CollectionImpl {
|
|
|
795
1103
|
this.changeKeyListeners.delete(key);
|
|
796
1104
|
}
|
|
797
1105
|
}
|
|
1106
|
+
this.removeSubscriber();
|
|
798
1107
|
};
|
|
799
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
|
+
}
|
|
800
1132
|
/**
|
|
801
1133
|
* Trigger a recomputation when transactions change
|
|
802
1134
|
* This method should be called by the Transaction class when state changes
|
|
803
1135
|
*/
|
|
804
1136
|
onTransactionStateChange() {
|
|
1137
|
+
this.capturePreSyncVisibleState();
|
|
805
1138
|
this.recomputeOptimisticState();
|
|
806
1139
|
}
|
|
807
1140
|
/**
|
|
@@ -813,7 +1146,7 @@ class CollectionImpl {
|
|
|
813
1146
|
asStoreMap() {
|
|
814
1147
|
if (!this._storeMap) {
|
|
815
1148
|
this._storeMap = new Store(new Map(this.entries()));
|
|
816
|
-
this.
|
|
1149
|
+
this.changeListeners.add(() => {
|
|
817
1150
|
this._storeMap.setState(() => new Map(this.entries()));
|
|
818
1151
|
});
|
|
819
1152
|
}
|
|
@@ -828,7 +1161,7 @@ class CollectionImpl {
|
|
|
828
1161
|
asStoreArray() {
|
|
829
1162
|
if (!this._storeArray) {
|
|
830
1163
|
this._storeArray = new Store(this.toArray);
|
|
831
|
-
this.
|
|
1164
|
+
this.changeListeners.add(() => {
|
|
832
1165
|
this._storeArray.setState(() => this.toArray);
|
|
833
1166
|
});
|
|
834
1167
|
}
|
|
@@ -839,7 +1172,6 @@ export {
|
|
|
839
1172
|
CollectionImpl,
|
|
840
1173
|
SchemaValidationError,
|
|
841
1174
|
collectionsStore,
|
|
842
|
-
createCollection
|
|
843
|
-
preloadCollection
|
|
1175
|
+
createCollection
|
|
844
1176
|
};
|
|
845
1177
|
//# sourceMappingURL=collection.js.map
|