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