@signaldb/core 2.0.0-beta.21 → 2.0.0-beta.22

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.
Files changed (46) hide show
  1. package/README.md +3 -3
  2. package/dist/.vite/manifest.json +61 -59
  3. package/dist/AsyncDataAdapter.d.ts +15 -2
  4. package/dist/AutoFetchDataAdapter.d.ts +13 -2
  5. package/dist/Collection/Observer.d.ts +1 -1
  6. package/dist/Collection/index.d.ts +11 -12
  7. package/dist/DataAdapter.d.ts +19 -3
  8. package/dist/WorkerDataAdapterHost.d.ts +19 -6
  9. package/dist/index.cjs.js +4 -4
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.mjs +4 -4
  12. package/dist/index12.cjs.js +1 -1
  13. package/dist/index12.mjs +1 -1
  14. package/dist/index15.cjs.js +6 -2
  15. package/dist/index15.mjs +6 -2
  16. package/dist/index17.cjs.js +4 -2
  17. package/dist/index17.mjs +4 -3
  18. package/dist/index18.cjs.js +1 -0
  19. package/dist/index18.mjs +1 -1
  20. package/dist/index28.cjs.js +15 -5
  21. package/dist/index28.mjs +15 -5
  22. package/dist/index29.cjs.js +29 -16
  23. package/dist/index29.mjs +29 -16
  24. package/dist/index33.cjs.js +17 -49
  25. package/dist/index33.mjs +17 -49
  26. package/dist/index34.cjs.js +45 -445
  27. package/dist/index34.mjs +45 -445
  28. package/dist/index35.cjs.js +132 -65
  29. package/dist/index35.mjs +132 -65
  30. package/dist/index36.cjs.js +385 -531
  31. package/dist/index36.mjs +386 -533
  32. package/dist/index37.cjs.js +68 -17
  33. package/dist/index37.mjs +68 -17
  34. package/dist/index38.cjs.js +536 -319
  35. package/dist/index38.mjs +538 -321
  36. package/dist/index39.cjs.js +301 -466
  37. package/dist/index39.mjs +301 -466
  38. package/dist/index40.cjs.js +484 -0
  39. package/dist/index40.mjs +484 -0
  40. package/dist/index5.cjs.js +3 -3
  41. package/dist/index5.mjs +3 -3
  42. package/dist/index6.cjs.js +1 -0
  43. package/dist/index6.mjs +1 -1
  44. package/dist/types/StorageAdapter.d.ts +80 -0
  45. package/dist/utils/executeStorageQuery.d.ts +28 -0
  46. package/package.json +1 -1
@@ -1,363 +1,580 @@
1
- const require_isEqual = require("./index2.cjs.js");
2
1
  const require_queryDelta = require("./index4.cjs.js");
3
- const require_getIndexInfo = require("./index17.cjs.js");
2
+ const require_randomId = require("./index9.cjs.js");
4
3
  const require_deepClone = require("./index18.cjs.js");
5
4
  const require_match = require("./index19.cjs.js");
6
5
  const require_modify = require("./index20.cjs.js");
7
- const require_projectItems = require("./index23.cjs.js");
8
- const require_sortItems = require("./index24.cjs.js");
9
6
  const require_incrementalQueryUpdate = require("./index25.cjs.js");
10
7
  const require_queryId = require("./index26.cjs.js");
11
- const require_idIndexQuery = require("./index27.cjs.js");
12
- const require_storageIndexQuery = require("./index33.cjs.js");
13
- const require_compact = require("./index37.cjs.js");
14
- //#region src/WorkerDataAdapterHost.ts
8
+ const require_batchOnNextTick = require("./index37.cjs.js");
9
+ //#region src/WorkerDataAdapter.ts
15
10
  /**
16
- * Turns the item states a write produced into the upsert/delete split a query update needs.
17
- *
18
- * The two lists are not symmetric: an item that is still there after the write is described by
19
- * its new state, while an item that is gone — removed, or given a new id — is described by the id
20
- * it used to have and nothing else. Mixing the states from before and after a write into one list
21
- * loses exactly that distinction.
22
- * @template T - The type of the items.
23
- * @param previousItems - The items as they were before the write.
24
- * @param modifiedItems - The items as they are after it.
25
- * @returns The changeset describing the write.
11
+ * Extracts the ids a selector names outright.
12
+ * @param selector - The selector to inspect.
13
+ * @returns The named ids, or `null` when the selector asks more than ids can answer.
26
14
  */
27
- function toChangeset(previousItems, modifiedItems) {
28
- const modifiedIds = new Set(modifiedItems.map((item) => item.id));
29
- return {
30
- upserts: modifiedItems,
31
- deletes: previousItems.map((item) => item.id).filter((id) => !modifiedIds.has(id))
32
- };
15
+ function selectorIds(selector) {
16
+ if (selector == null || typeof selector !== "object") return null;
17
+ const keys = Object.keys(selector);
18
+ if (keys.length !== 1 || keys[0] !== "id") return null;
19
+ const value = selector.id;
20
+ if (value == null) return null;
21
+ if (typeof value !== "object") return [value];
22
+ const valueKeys = Object.keys(value);
23
+ if (valueKeys.length !== 1 || valueKeys[0] !== "$in") return null;
24
+ const inValues = value.$in;
25
+ return Array.isArray(inValues) ? inValues : null;
33
26
  }
34
- var WorkerDataAdapterHost = class {
35
- workerContext;
27
+ var WorkerDataAdapter = class WorkerDataAdapter {
28
+ worker;
36
29
  options;
37
30
  id;
31
+ isDisposed = false;
32
+ workerReady;
38
33
  log = () => {};
39
- storageAdapters = /* @__PURE__ */ new Map();
40
- storageAdapterReady = /* @__PURE__ */ new Map();
41
- collectionIndices = /* @__PURE__ */ new Map();
42
- queries = /* @__PURE__ */ new Map();
43
- onError = (error) => {
44
- console.error(error);
45
- };
46
- constructor(workerContext, options) {
47
- this.workerContext = workerContext;
34
+ collectionReady = /* @__PURE__ */ new Map();
35
+ batchExecutionHelpers = /* @__PURE__ */ new Map();
36
+ queries = {};
37
+ pendingRequests = /* @__PURE__ */ new Map();
38
+ pendingWrites = /* @__PURE__ */ new Map();
39
+ pendingWriteSeq = 0;
40
+ pendingWriteVersions = /* @__PURE__ */ new Map();
41
+ bumpPendingWriteVersion(collectionName) {
42
+ const current = this.pendingWriteVersions.get(collectionName) ?? 0;
43
+ this.pendingWriteVersions.set(collectionName, current + 1);
44
+ }
45
+ constructor(worker, options) {
46
+ this.worker = worker;
48
47
  this.options = options;
49
48
  this.id = this.options.id || "default-worker-data-adapter";
50
- if (this.options.onError) this.onError = this.options.onError;
51
49
  if (this.options.log) this.log = this.options.log;
52
- this.workerContext.addEventListener("message", async (event) => {
53
- try {
54
- const { workerId, id, method, args } = event.data;
55
- await this.handleMessage(workerId, id, method, args);
56
- } catch (error) {
57
- this.onError(error);
58
- }
59
- });
60
- this.respond("ready", null, null, "ready");
61
- }
62
- respond(id, data, error = null, type = "response") {
63
- this.workerContext.postMessage({
64
- id,
65
- workerId: this.id,
66
- type,
67
- data,
68
- error
50
+ this.workerReady = new Promise((resolve, reject) => {
51
+ const timeoutId = setTimeout(() => {
52
+ reject(/* @__PURE__ */ new Error("WorkerDataAdapter initialization timed out"));
53
+ }, 5e3);
54
+ this.resolveWorkerReady = () => {
55
+ clearTimeout(timeoutId);
56
+ resolve();
57
+ };
69
58
  });
59
+ this.worker.addEventListener("message", this.handleWorkerMessage);
70
60
  }
71
- async handleMessage(workerId, id, method, args) {
72
- if (workerId !== this.id) return;
73
- const fn = this[method];
74
- if (typeof fn !== "function") {
75
- this.respond(id, null, /* @__PURE__ */ new Error(`Method ${method} not found`));
61
+ resolveWorkerReady = () => {};
62
+ handleWorkerMessage = (event) => {
63
+ const message = event.data;
64
+ if (message == null) return;
65
+ if (message.workerId !== this.id) return;
66
+ if (message.type === "ready") {
67
+ this.resolveWorkerReady();
76
68
  return;
77
69
  }
78
- this.log(method, ...args);
79
- await this.isReady(args[0]);
80
- try {
81
- const result = await fn.apply(this, args);
82
- this.respond(id, result);
83
- } catch (error) {
84
- this.respond(id, null, error);
70
+ if (message.type === "response") {
71
+ if (message.id == null) return;
72
+ const pending = this.pendingRequests.get(message.id);
73
+ if (!pending) return;
74
+ this.pendingRequests.delete(message.id);
75
+ this.log("response", message.data ?? message.error);
76
+ if (message.error) pending.reject(message.error);
77
+ else pending.resolve(message.data);
78
+ return;
85
79
  }
86
- }
87
- async getIndexInfo(collectionName, selector) {
88
- const storageAdapter = this.storageAdapters.get(collectionName);
89
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
90
- if (selector != null && Object.keys(selector).length === 1 && "id" in selector) {
91
- const idResult = require_idIndexQuery.default(selector);
92
- if (idResult.matched) return {
93
- matched: true,
94
- ids: require_compact.default(idResult.ids),
95
- optimizedSelector: {}
96
- };
80
+ if (message.type === "queryUpdate") this.handleQueryUpdate(message.data, message.error ?? null);
81
+ };
82
+ handleQueryUpdate(data, error) {
83
+ if (data == null) return;
84
+ const { collectionName, qid, selector, options, state, items, delta } = data;
85
+ if (collectionName == null) return;
86
+ const collectionQueries = this.queries[collectionName];
87
+ if (!collectionQueries) return;
88
+ const id = qid ?? (selector === void 0 ? void 0 : require_queryId.default(selector, options));
89
+ if (id == null) return;
90
+ const query = collectionQueries.get(id);
91
+ if (!query) return;
92
+ this.log("queryUpdate", query.selector, query.options, state, data ?? error);
93
+ let nextItems = items;
94
+ let deltaToPublish;
95
+ if (delta != null) {
96
+ const canApply = require_queryDelta.canApplyQueryDelta(query.items, delta);
97
+ if (!canApply || require_queryDelta.isEmptyQueryDelta(delta)) {
98
+ if (state === query.state) return;
99
+ this.updateQuery(collectionName, {
100
+ selector: query.selector,
101
+ options: query.options
102
+ }, {
103
+ state,
104
+ error
105
+ });
106
+ const settled = collectionQueries.get(id);
107
+ if (!settled) return;
108
+ settled.stateChangeCallbacks.forEach((callback) => require_queryDelta.callWithDelta(callback, state, canApply ? delta : void 0));
109
+ return;
110
+ }
111
+ const servedBefore = this.flattenPendingWrites(collectionName) == null ? null : this.servedResult(collectionName, query);
112
+ nextItems = require_queryDelta.applyQueryDelta(query.items, delta);
113
+ if (servedBefore == null) deltaToPublish = delta;
114
+ else {
115
+ this.updateQuery(collectionName, {
116
+ selector: query.selector,
117
+ options: query.options
118
+ }, {
119
+ state,
120
+ error,
121
+ items: nextItems
122
+ });
123
+ const stored = collectionQueries.get(id);
124
+ if (!stored) return;
125
+ const servedDelta = require_queryDelta.diffQueryResults(servedBefore, this.servedResult(collectionName, stored));
126
+ if (require_queryDelta.isEmptyQueryDelta(servedDelta) && state === query.state) return;
127
+ stored.stateChangeCallbacks.forEach((callback) => require_queryDelta.callWithDelta(callback, state, servedDelta));
128
+ return;
129
+ }
97
130
  }
98
- if (selector == null) return {
99
- matched: false,
100
- ids: [],
101
- optimizedSelector: {}
102
- };
103
- return require_getIndexInfo.default((this.collectionIndices.get(collectionName) ?? []).map((field) => require_storageIndexQuery.default(storageAdapter, field)), selector);
131
+ this.updateQuery(collectionName, {
132
+ selector: query.selector,
133
+ options: query.options
134
+ }, {
135
+ state,
136
+ error,
137
+ items: nextItems
138
+ });
139
+ const updated = collectionQueries.get(id);
140
+ if (!updated) return;
141
+ updated.stateChangeCallbacks.forEach((callback) => require_queryDelta.callWithDelta(callback, state, deltaToPublish));
104
142
  }
105
- async queryItems(collectionName, selector) {
106
- const storageAdapter = this.storageAdapters.get(collectionName);
107
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
108
- const indexInfo = await this.getIndexInfo(collectionName, selector);
109
- const matchItems = (item) => {
110
- if (indexInfo.optimizedSelector == null) return true;
111
- if (Object.keys(indexInfo.optimizedSelector).length <= 0) return true;
112
- return require_match.default(item, indexInfo.optimizedSelector);
113
- };
114
- if (indexInfo.matched) {
115
- const items = await storageAdapter.readIds(indexInfo.ids);
116
- if (require_isEqual.default(indexInfo.optimizedSelector, {})) return items;
117
- return items.filter(matchItems);
118
- } else {
119
- const allItems = await storageAdapter.readAll();
120
- if (require_isEqual.default(selector, {})) return allItems;
121
- return allItems.filter(matchItems);
143
+ async exec(method, collectionName, ...args) {
144
+ await this.workerReady;
145
+ if (method !== "isReady") {
146
+ const collectionReady = this.collectionReady.get(collectionName);
147
+ if (!collectionReady) throw new Error(`Collection "${collectionName}" is not registered in WorkerDataAdapter`);
148
+ await collectionReady;
122
149
  }
150
+ if (this.isDisposed) throw new Error("WorkerDataAdapter is disposed");
151
+ return new Promise((resolve, reject) => {
152
+ const messageId = require_randomId.default();
153
+ this.pendingRequests.set(messageId, {
154
+ resolve,
155
+ reject
156
+ });
157
+ this.worker.postMessage({
158
+ id: messageId,
159
+ workerId: this.id,
160
+ method,
161
+ args: [collectionName, ...args]
162
+ });
163
+ });
123
164
  }
124
- async executeQuery(collectionName, selector, options) {
125
- if (selector === null) return [];
126
- const items = await this.queryItems(collectionName, selector || {});
127
- const { sort, skip, limit, fields } = options || {};
128
- const sorted = sort ? require_sortItems.default(items, sort) : items;
129
- const skipped = skip ? sorted.slice(skip) : sorted;
130
- return require_projectItems.default(limit ? skipped.slice(0, limit) : skipped, fields);
165
+ /**
166
+ * Issues a call whose result nobody is waiting for, and makes sure a failure has somewhere to
167
+ * go. A bare rejection here would surface as an uncaught error — which is what a disposed
168
+ * collection produced every time a cursor was cleaned up after it.
169
+ * @param method - The method to call on the worker.
170
+ * @param collectionName - The collection it applies to.
171
+ * @param args - The remaining arguments.
172
+ * @param onError - Called when the call fails, in place of merely logging it.
173
+ */
174
+ execInBackground(method, collectionName, args, onError) {
175
+ this.exec(method, collectionName, ...args).catch((error) => {
176
+ if (onError) {
177
+ onError(error);
178
+ return;
179
+ }
180
+ this.log(method, "failed", error);
181
+ });
131
182
  }
132
- ensureQuery(collectionName, selector, options) {
133
- const id = require_queryId.default(selector, options);
134
- if (!this.queries.get(collectionName)) throw new Error(`Collection ${collectionName} not initialized!`);
135
- let query = this.queries.get(collectionName)?.get(id);
136
- if (!query) {
137
- query = {
138
- selector,
139
- options,
140
- items: null
141
- };
142
- this.queries.get(collectionName)?.set(id, query);
183
+ queryItemsById(query) {
184
+ if (!query.itemsById) query.itemsById = new Map(query.items.map((item) => [item.id, item]));
185
+ return query.itemsById;
186
+ }
187
+ flattenPendingWrites(collectionName) {
188
+ const state = this.pendingWrites.get(collectionName);
189
+ if (!state || state.writes.size === 0) return null;
190
+ return state.flat;
191
+ }
192
+ static pushPendingEntry(state, id, item, seq) {
193
+ const stack = state.byId.get(id);
194
+ if (stack) stack.push({
195
+ seq,
196
+ item
197
+ });
198
+ else state.byId.set(id, [{
199
+ seq,
200
+ item
201
+ }]);
202
+ WorkerDataAdapter.writeFlatEntry(state, id, item);
203
+ }
204
+ static dropPendingEntry(state, id, seq) {
205
+ const stack = state.byId.get(id);
206
+ if (!stack) return;
207
+ const index = stack.findIndex((entry) => entry.seq === seq);
208
+ if (index !== -1) stack.splice(index, 1);
209
+ const top = stack.at(-1);
210
+ if (!top) {
211
+ state.byId.delete(id);
212
+ state.flat.upserts.delete(id);
213
+ state.flat.deletes.delete(id);
214
+ return;
143
215
  }
144
- return query;
216
+ WorkerDataAdapter.writeFlatEntry(state, id, top.item);
145
217
  }
146
- setQueryItems(query, items) {
147
- query.items = items;
148
- query.itemIds = void 0;
218
+ static writeFlatEntry(state, id, item) {
219
+ if (item === null) {
220
+ state.flat.deletes.add(id);
221
+ state.flat.upserts.delete(id);
222
+ return;
223
+ }
224
+ state.flat.upserts.set(id, item);
225
+ state.flat.deletes.delete(id);
149
226
  }
150
- queryItemIds(query) {
151
- if (!query.itemIds) query.itemIds = new Set((query.items ?? []).map((item) => item.id));
152
- return query.itemIds;
227
+ static providesFullItems(query) {
228
+ return query.options?.fields == null;
153
229
  }
154
- emitQueryUpdate(collectionName, selector, options, state, error, items, delta) {
155
- const id = require_queryId.default(selector, options);
156
- if (!this.queries.get(collectionName)) throw new Error(`Collection ${collectionName} not initialized!`);
157
- this.respond(id, {
158
- collectionName,
159
- qid: id,
160
- selector,
161
- options,
162
- state,
163
- error,
230
+ observableItems(collectionName) {
231
+ const byId = /* @__PURE__ */ new Map();
232
+ this.queries[collectionName]?.forEach((query) => {
233
+ if (!WorkerDataAdapter.providesFullItems(query)) return;
234
+ query.items.forEach((item) => byId.set(item.id, item));
235
+ });
236
+ const pending = this.flattenPendingWrites(collectionName);
237
+ if (pending) {
238
+ pending.upserts.forEach((item, id) => byId.set(id, item));
239
+ pending.deletes.forEach((id) => byId.delete(id));
240
+ }
241
+ return [...byId.values()];
242
+ }
243
+ observableItemsByIds(collectionName, ids) {
244
+ const pending = this.flattenPendingWrites(collectionName);
245
+ const found = /* @__PURE__ */ new Map();
246
+ ids.forEach((id) => {
247
+ if (pending?.deletes.has(id)) return;
248
+ const pendingItem = pending?.upserts.get(id);
249
+ if (pendingItem) {
250
+ found.set(id, pendingItem);
251
+ return;
252
+ }
253
+ const queries = this.queries[collectionName];
254
+ if (!queries) return;
255
+ for (const query of queries.values()) {
256
+ if (!WorkerDataAdapter.providesFullItems(query)) continue;
257
+ const item = this.queryItemsById(query).get(id);
258
+ if (item) {
259
+ found.set(id, item);
260
+ return;
261
+ }
262
+ }
263
+ });
264
+ return [...found.values()];
265
+ }
266
+ servedResult(collectionName, query) {
267
+ const pendingVersion = this.pendingWriteVersions.get(collectionName) ?? 0;
268
+ if (query.served && query.served.fromItems === query.items && query.served.pendingVersion === pendingVersion) return query.served.items;
269
+ const items = this.advanceServedResult(collectionName, query, pendingVersion) ?? this.computeServedResult(collectionName, query);
270
+ query.served = {
164
271
  items,
165
- delta
166
- }, null, "queryUpdate");
272
+ fromItems: query.items,
273
+ pendingVersion
274
+ };
275
+ return items;
167
276
  }
168
- ensureStorageAdapter(name) {
169
- if (this.storageAdapters.has(name)) return;
170
- const adapter = this.options.storage(name);
171
- if (!adapter) return;
172
- this.storageAdapters.set(name, adapter);
277
+ advanceServedResult(collectionName, query, pendingVersion) {
278
+ const served = query.served;
279
+ if (!served || served.fromItems !== query.items) return null;
280
+ if (served.pendingVersion !== pendingVersion - 1) return null;
281
+ if (!WorkerDataAdapter.providesFullItems(query) || query.options?.limit != null) return null;
282
+ const state = this.pendingWrites.get(collectionName);
283
+ if (state?.lastChange == null || state.lastChange.version !== pendingVersion) return null;
284
+ return require_incrementalQueryUpdate.mergeChangesetIntoResult(served.items, query.selector, query.options, this.changesetForIds(collectionName, query, state.lastChange.ids));
173
285
  }
174
- async checkQueryUpdates(collectionName, changes) {
175
- const queries = this.queries.get(collectionName);
176
- if (!queries) throw new Error(`Collection ${collectionName} not initialized!`);
177
- if (changes.upserts.length === 0 && changes.deletes.length === 0) return;
178
- const affectedQueries = [...queries.values()].filter((query) => {
179
- const ids = this.queryItemIds(query);
180
- if (changes.deletes.some((id) => ids.has(id))) return true;
181
- return changes.upserts.some((item) => ids.has(item.id) || require_match.default(item, query.selector));
182
- });
183
- if (affectedQueries.length === 0) return;
184
- await Promise.all(affectedQueries.map(async (query) => {
185
- const { selector, options } = query;
186
- const previous = query.items;
187
- const incremental = previous == null ? null : require_incrementalQueryUpdate.default(previous, selector, options, changes);
188
- if (incremental != null) {
189
- const delta = require_queryDelta.diffQueryResults(previous, incremental);
190
- if (require_queryDelta.isEmptyQueryDelta(delta)) return;
191
- this.setQueryItems(query, incremental);
192
- this.emitQueryUpdate(collectionName, selector, options, "complete", null, void 0, delta);
286
+ changesetForIds(collectionName, query, ids) {
287
+ const pending = this.flattenPendingWrites(collectionName);
288
+ const stored = this.queryItemsById(query);
289
+ const upserts = [];
290
+ const deletes = [];
291
+ ids.forEach((id) => {
292
+ const pendingItem = pending?.upserts.get(id);
293
+ if (pendingItem) {
294
+ upserts.push(pendingItem);
193
295
  return;
194
296
  }
195
- this.emitQueryUpdate(collectionName, selector, options, "active", null);
196
- const queryItems = await this.executeQuery(collectionName, selector, options);
197
- if (previous == null) {
198
- this.setQueryItems(query, queryItems);
199
- this.emitQueryUpdate(collectionName, selector, options, "complete", null, queryItems);
297
+ if (pending?.deletes.has(id)) {
298
+ deletes.push(id);
200
299
  return;
201
300
  }
202
- const delta = require_queryDelta.diffQueryResults(previous, queryItems);
203
- this.setQueryItems(query, queryItems);
204
- this.emitQueryUpdate(collectionName, selector, options, "complete", null, void 0, delta);
205
- }));
301
+ const storedItem = stored.get(id);
302
+ if (storedItem) upserts.push(storedItem);
303
+ else deletes.push(id);
304
+ });
305
+ return {
306
+ upserts,
307
+ deletes
308
+ };
206
309
  }
207
- registerCollection = async (collectionName, indices) => {
208
- this.collectionIndices.set(collectionName, indices);
209
- this.queries.set(collectionName, /* @__PURE__ */ new Map());
210
- this.ensureStorageAdapter(collectionName);
211
- const storageAdapter = this.storageAdapters.get(collectionName);
212
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
213
- const setupPromise = (async () => {
214
- await storageAdapter.setup();
215
- await Promise.all(indices.map((index) => storageAdapter.createIndex(index)));
216
- })();
217
- this.storageAdapterReady.set(collectionName, setupPromise);
218
- await setupPromise;
219
- };
220
- unregisterCollection = async (collectionName) => {
221
- this.storageAdapters.delete(collectionName);
222
- this.queries.delete(collectionName);
223
- };
224
- registerQuery = async (collectionName, selector, options) => {
225
- const query = this.ensureQuery(collectionName, selector, options);
226
- const queryItems = await this.executeQuery(collectionName, selector, options);
227
- this.setQueryItems(query, queryItems);
228
- this.emitQueryUpdate(collectionName, selector, options, "complete", null, queryItems);
229
- };
230
- unregisterQuery = async (collectionName, selector, options) => {
231
- const id = require_queryId.default(selector, options);
232
- if (!this.queries.get(collectionName)) throw new Error(`Collection ${collectionName} not initialized!`);
233
- this.queries.get(collectionName)?.delete(id);
234
- };
235
- insert = async (collectionName, input) => {
236
- const storageAdapter = this.storageAdapters.get(collectionName);
237
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
238
- const existingItems = await this.executeQuery(collectionName, { id: { $in: input.map((i) => i[0].id) } });
239
- const result = input.map(([item]) => {
240
- if (item.id == null) return /* @__PURE__ */ new Error("Item must have an id");
241
- if (existingItems.some((existing) => existing.id === item.id)) return /* @__PURE__ */ new Error(`Item with id ${item.id} already exists`);
242
- return item;
310
+ computeServedResult(collectionName, query) {
311
+ const pending = this.flattenPendingWrites(collectionName);
312
+ if (!pending) return query.items;
313
+ const byId = this.queryItemsById(query);
314
+ let affected = false;
315
+ pending.deletes.forEach((id) => {
316
+ if (byId.has(id)) affected = true;
243
317
  });
244
- const newItems = result.filter((item) => !(item instanceof Error));
245
- await storageAdapter.insert(newItems);
246
- await this.checkQueryUpdates(collectionName, {
247
- upserts: newItems,
248
- deletes: []
318
+ if (!affected) pending.upserts.forEach((item, id) => {
319
+ if (affected) return;
320
+ if (byId.has(id)) affected = true;
321
+ else if (query.selector != null && require_match.default(item, query.selector)) affected = true;
249
322
  });
250
- return result;
251
- };
252
- updateOne = async (collectionName, parameters) => {
253
- const storageAdapter = this.storageAdapters.get(collectionName);
254
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
255
- const previousItems = [];
256
- const result = await Promise.all(parameters.map(async ([selector, modifier]) => {
257
- const item = await this.executeQuery(collectionName, selector, { limit: 1 }).then((items) => items[0] ?? null);
258
- const { $setOnInsert, ...restModifier } = modifier;
259
- if (item == null) return [];
260
- previousItems.push(item);
261
- const modifiedItem = require_modify.default(require_deepClone.default(item), restModifier);
262
- if (item.id !== modifiedItem.id) {
263
- if ((await this.executeQuery(collectionName, { id: modifiedItem.id }, { limit: 1 })).length > 0) return /* @__PURE__ */ new Error(`Item with id ${modifiedItem.id} already exists`);
264
- }
265
- return [modifiedItem];
266
- }));
267
- const modifiedItems = require_compact.default(result.filter((item) => !(item instanceof Error)).flat());
268
- if (modifiedItems.length > 0) {
269
- await storageAdapter.replace(modifiedItems);
270
- await this.checkQueryUpdates(collectionName, toChangeset(previousItems, modifiedItems));
271
- }
272
- return result;
273
- };
274
- updateMany = async (collectionName, parameters) => {
275
- const storageAdapter = this.storageAdapters.get(collectionName);
276
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
277
- const previousItems = [];
278
- const result = await Promise.all(parameters.map(async ([selector, modifier]) => {
279
- const items = await this.executeQuery(collectionName, selector);
280
- if (items.length === 0) return [];
281
- const { $setOnInsert, ...restModifier } = modifier;
282
- try {
283
- return await Promise.all(items.map(async (item) => {
284
- const modifiedItem = require_modify.default(require_deepClone.default(item), restModifier);
285
- if (item.id !== modifiedItem.id) {
286
- if ((await this.executeQuery(collectionName, { id: modifiedItem.id }, { limit: 1 })).length > 0) throw new Error(`Item with id ${modifiedItem.id} already exists`);
287
- }
288
- previousItems.push(item);
289
- return modifiedItem;
290
- }));
291
- } catch (error) {
292
- return error;
293
- }
294
- }));
295
- const modifiedItems = require_compact.default(result.filter((item) => !(item instanceof Error)).flat());
296
- if (modifiedItems.length > 0) {
297
- await storageAdapter.replace(modifiedItems);
298
- await this.checkQueryUpdates(collectionName, toChangeset(previousItems, modifiedItems));
299
- }
300
- return result;
301
- };
302
- replaceOne = async (collectionName, parameters) => {
303
- const storageAdapter = this.storageAdapters.get(collectionName);
304
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
305
- const previousItems = [];
306
- const result = await Promise.all(parameters.map(async ([selector, replacement]) => {
307
- const item = await this.executeQuery(collectionName, selector, { limit: 1 }).then((items) => items[0] ?? null);
308
- if (item == null) return [];
309
- previousItems.push(item);
310
- const modifiedItem = {
311
- ...replacement,
312
- id: replacement.id ?? item.id
323
+ if (!affected) return query.items;
324
+ return require_incrementalQueryUpdate.mergeChangesetIntoResult(query.items, query.selector, query.options, {
325
+ upserts: [...pending.upserts.values()],
326
+ deletes: [...pending.deletes]
327
+ });
328
+ }
329
+ /**
330
+ * Registers a write's effect locally and notifies every active query it
331
+ * touches, then returns a function that drops it again once the write
332
+ * settles.
333
+ * @param collectionName - The collection the write targets.
334
+ * @param upserts - Items inserted or updated by the write.
335
+ * @param deletes - Ids removed by the write.
336
+ * @returns A function that drops the pending write and re-notifies.
337
+ */
338
+ applyPendingWrite(collectionName, upserts, deletes) {
339
+ if (upserts.length === 0 && deletes.length === 0) return () => {};
340
+ const affectedIds = /* @__PURE__ */ new Set([...upserts.map((item) => item.id), ...deletes]);
341
+ const affected = this.affectedQueries(collectionName, upserts, affectedIds);
342
+ const servedBefore = this.servedResults(collectionName, affected);
343
+ const seq = this.pendingWriteSeq += 1;
344
+ const state = this.pendingWrites.get(collectionName) ?? {
345
+ writes: /* @__PURE__ */ new Map(),
346
+ byId: /* @__PURE__ */ new Map(),
347
+ flat: {
348
+ upserts: /* @__PURE__ */ new Map(),
349
+ deletes: /* @__PURE__ */ new Set()
350
+ },
351
+ lastChange: null
352
+ };
353
+ const write = {
354
+ upserts: new Map(upserts.map((item) => [item.id, item])),
355
+ deletes: new Set(deletes)
356
+ };
357
+ state.writes.set(seq, write);
358
+ write.upserts.forEach((item, id) => WorkerDataAdapter.pushPendingEntry(state, id, item, seq));
359
+ write.deletes.forEach((id) => WorkerDataAdapter.pushPendingEntry(state, id, null, seq));
360
+ this.pendingWrites.set(collectionName, state);
361
+ this.bumpPendingWriteVersion(collectionName);
362
+ state.lastChange = {
363
+ version: this.pendingWriteVersions.get(collectionName) ?? 0,
364
+ ids: [...affectedIds]
365
+ };
366
+ this.notifyWithDeltas(collectionName, affected, servedBefore);
367
+ return () => {
368
+ const current = this.pendingWrites.get(collectionName);
369
+ if (!current) return;
370
+ const settled = current.writes.get(seq);
371
+ if (!settled) return;
372
+ const affectedOnDrop = this.affectedQueries(collectionName, upserts, affectedIds);
373
+ const beforeDrop = this.servedResults(collectionName, affectedOnDrop);
374
+ current.writes.delete(seq);
375
+ settled.upserts.forEach((item, id) => WorkerDataAdapter.dropPendingEntry(current, id, seq));
376
+ settled.deletes.forEach((id) => WorkerDataAdapter.dropPendingEntry(current, id, seq));
377
+ if (current.writes.size === 0) this.pendingWrites.delete(collectionName);
378
+ this.bumpPendingWriteVersion(collectionName);
379
+ if (current.writes.size > 0) current.lastChange = {
380
+ version: this.pendingWriteVersions.get(collectionName) ?? 0,
381
+ ids: [...affectedIds]
313
382
  };
314
- if (item.id !== modifiedItem.id) {
315
- if ((await this.executeQuery(collectionName, { id: modifiedItem.id }, { limit: 1 })).length > 0) return /* @__PURE__ */ new Error(`Item with id ${modifiedItem.id} already exists`);
316
- }
317
- return [modifiedItem];
318
- }));
319
- const modifiedItems = require_compact.default(result.filter((item) => !(item instanceof Error)).flat());
320
- if (modifiedItems.length > 0) {
321
- await storageAdapter.replace(modifiedItems);
322
- await this.checkQueryUpdates(collectionName, toChangeset(previousItems, modifiedItems));
323
- }
324
- return result;
325
- };
326
- removeOne = async (collectionName, selectors) => {
327
- const storageAdapter = this.storageAdapters.get(collectionName);
328
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
329
- const result = await Promise.all(selectors.map(async ([selector]) => {
330
- const item = await this.executeQuery(collectionName, selector, { limit: 1 }).then((items) => items[0] ?? null);
331
- if (item == null) return [];
332
- return [item];
333
- }));
334
- const items = result.flat();
335
- if (items.length > 0) {
336
- await storageAdapter.remove(items);
337
- await this.checkQueryUpdates(collectionName, {
338
- upserts: [],
339
- deletes: items.map((item) => item.id)
340
- });
341
- }
342
- return result;
343
- };
344
- removeMany = async (collectionName, selectors) => {
345
- const storageAdapter = this.storageAdapters.get(collectionName);
346
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
347
- const result = await Promise.all(selectors.map(async ([selector]) => this.executeQuery(collectionName, selector)));
348
- const items = result.flat();
349
- if (items.length > 0) {
350
- await storageAdapter.remove(items);
351
- await this.checkQueryUpdates(collectionName, {
352
- upserts: [],
353
- deletes: items.map((item) => item.id)
383
+ this.notifyWithDeltas(collectionName, affectedOnDrop, beforeDrop);
384
+ };
385
+ }
386
+ affectedQueries(collectionName, upserts, affectedIds) {
387
+ const affected = [];
388
+ this.queries[collectionName]?.forEach((query) => {
389
+ const byId = this.queryItemsById(query);
390
+ let wasHolding = false;
391
+ affectedIds.forEach((id) => {
392
+ if (byId.has(id)) wasHolding = true;
354
393
  });
394
+ const nowMatches = upserts.some((item) => query.selector != null && require_match.default(item, query.selector));
395
+ if (!wasHolding && !nowMatches) return;
396
+ affected.push(query);
397
+ });
398
+ return affected;
399
+ }
400
+ servedResults(collectionName, queries) {
401
+ return new Map(queries.map((query) => [query, this.servedResult(collectionName, query)]));
402
+ }
403
+ notifyWithDeltas(collectionName, queries, servedBefore) {
404
+ queries.forEach((query) => {
405
+ const before = servedBefore.get(query);
406
+ if (before == null) return;
407
+ const delta = require_queryDelta.diffQueryResults(before, this.servedResult(collectionName, query));
408
+ if (require_queryDelta.isEmptyQueryDelta(delta)) return;
409
+ query.stateChangeCallbacks.forEach((callback) => require_queryDelta.callWithDelta(callback, query.state, delta));
410
+ });
411
+ }
412
+ matchObservableItems(collectionName, selector, onlyFirst) {
413
+ if (selector == null) return [];
414
+ const ids = selectorIds(selector);
415
+ const matches = ids == null ? this.observableItems(collectionName).filter((item) => require_match.default(item, selector)) : this.observableItemsByIds(collectionName, ids);
416
+ return onlyFirst ? matches.slice(0, 1) : matches;
417
+ }
418
+ resolveUpdate(collectionName, selector, modifier, onlyFirst) {
419
+ const { $setOnInsert, ...restModifier } = modifier;
420
+ const upserts = [];
421
+ const deletes = [];
422
+ this.matchObservableItems(collectionName, selector, onlyFirst).forEach((item) => {
423
+ const modifiedItem = require_modify.default(require_deepClone.default(item), restModifier);
424
+ upserts.push(modifiedItem);
425
+ if (modifiedItem.id !== item.id) deletes.push(item.id);
426
+ });
427
+ return {
428
+ upserts,
429
+ deletes
430
+ };
431
+ }
432
+ resolveRemoval(collectionName, selector, onlyFirst) {
433
+ return this.matchObservableItems(collectionName, selector, onlyFirst).map((item) => item.id);
434
+ }
435
+ resolveReplacement(item, replacement) {
436
+ const modifiedItem = {
437
+ ...replacement,
438
+ id: replacement.id ?? item.id
439
+ };
440
+ return {
441
+ upserts: [modifiedItem],
442
+ deletes: modifiedItem.id === item.id ? [] : [item.id]
443
+ };
444
+ }
445
+ async withPendingWrite(collectionName, delta, run) {
446
+ const dropPendingWrite = this.applyPendingWrite(collectionName, delta.upserts, delta.deletes);
447
+ try {
448
+ return await run();
449
+ } finally {
450
+ dropPendingWrite();
355
451
  }
356
- return result;
357
- };
358
- isReady = async (collectionName) => {
359
- return this.storageAdapterReady.get(collectionName);
360
- };
452
+ }
453
+ enqueueBatched(collectionName, method, args) {
454
+ const helper = this.batchExecutionHelpers.get(collectionName);
455
+ if (!helper) throw new Error(`Collection "${collectionName}" is not registered in WorkerDataAdapter`);
456
+ return helper.enqueue(method, args);
457
+ }
458
+ updateQuery(collectionName, query, update) {
459
+ const id = require_queryId.default(query.selector, query.options);
460
+ const collectionQueries = this.queries[collectionName];
461
+ if (!collectionQueries) return;
462
+ const existing = collectionQueries.get(id);
463
+ const newState = {
464
+ selector: query.selector,
465
+ options: query.options,
466
+ state: "active",
467
+ error: null,
468
+ stateChangeCallbacks: [],
469
+ ...existing,
470
+ ...update,
471
+ ...update.items ? {
472
+ items: update.items,
473
+ itemsById: void 0
474
+ } : { items: existing?.items ?? [] }
475
+ };
476
+ collectionQueries.set(id, newState);
477
+ this.queries[collectionName] = collectionQueries;
478
+ }
479
+ createCollectionBackend(collection, indices) {
480
+ this.queries[collection.name] = /* @__PURE__ */ new Map();
481
+ this.execInBackground("registerCollection", collection.name, [indices]);
482
+ this.collectionReady.set(collection.name, this.exec("isReady", collection.name));
483
+ this.batchExecutionHelpers.set(collection.name, require_batchOnNextTick.default(async (method, args) => this.exec(method, collection.name, args)));
484
+ return {
485
+ insert: async (item) => {
486
+ return this.withPendingWrite(collection.name, {
487
+ upserts: [item],
488
+ deletes: []
489
+ }, () => this.enqueueBatched(collection.name, "insert", [item]));
490
+ },
491
+ updateOne: async (selector, modifier) => {
492
+ return this.withPendingWrite(collection.name, this.resolveUpdate(collection.name, selector, modifier, true), () => this.enqueueBatched(collection.name, "updateOne", [selector, modifier]));
493
+ },
494
+ updateMany: async (selector, modifier) => {
495
+ return this.withPendingWrite(collection.name, this.resolveUpdate(collection.name, selector, modifier, false), () => this.enqueueBatched(collection.name, "updateMany", [selector, modifier]));
496
+ },
497
+ replaceOne: async (selector, replacement) => {
498
+ const [item] = this.matchObservableItems(collection.name, selector, true);
499
+ return this.withPendingWrite(collection.name, item == null ? {
500
+ upserts: [],
501
+ deletes: []
502
+ } : this.resolveReplacement(item, replacement), () => this.enqueueBatched(collection.name, "replaceOne", [selector, replacement]));
503
+ },
504
+ removeOne: async (selector) => {
505
+ return this.withPendingWrite(collection.name, {
506
+ upserts: [],
507
+ deletes: this.resolveRemoval(collection.name, selector, true)
508
+ }, () => this.enqueueBatched(collection.name, "removeOne", [selector]));
509
+ },
510
+ removeMany: async (selector) => {
511
+ return this.withPendingWrite(collection.name, {
512
+ upserts: [],
513
+ deletes: this.resolveRemoval(collection.name, selector, false)
514
+ }, () => this.enqueueBatched(collection.name, "removeMany", [selector]));
515
+ },
516
+ registerQuery: (selector, options) => {
517
+ this.updateQuery(collection.name, {
518
+ selector,
519
+ options
520
+ }, {
521
+ state: "active",
522
+ error: null,
523
+ items: []
524
+ });
525
+ this.execInBackground("registerQuery", collection.name, [selector, options], (error) => {
526
+ const query = this.queries[collection.name]?.get(require_queryId.default(selector, options));
527
+ if (!query) return;
528
+ this.updateQuery(collection.name, {
529
+ selector,
530
+ options
531
+ }, {
532
+ state: "error",
533
+ error
534
+ });
535
+ query.stateChangeCallbacks.forEach((callback) => callback("error"));
536
+ });
537
+ },
538
+ unregisterQuery: (selector, options) => {
539
+ this.queries[collection.name]?.delete(require_queryId.default(selector, options));
540
+ this.execInBackground("unregisterQuery", collection.name, [selector, options]);
541
+ },
542
+ getQueryState: (selector, options) => {
543
+ return (this.queries[collection.name]?.get(require_queryId.default(selector, options)))?.state || "active";
544
+ },
545
+ getQueryError: (selector, options) => {
546
+ return (this.queries[collection.name]?.get(require_queryId.default(selector, options)))?.error || null;
547
+ },
548
+ getQueryResult: (selector, options) => {
549
+ const query = this.queries[collection.name]?.get(require_queryId.default(selector, options));
550
+ if (!query) return [];
551
+ return this.servedResult(collection.name, query);
552
+ },
553
+ onQueryStateChange: (selector, options, callback) => {
554
+ this.updateQuery(collection.name, {
555
+ selector,
556
+ options
557
+ }, { stateChangeCallbacks: [...this.queries[collection.name]?.get(require_queryId.default(selector, options))?.stateChangeCallbacks || [], callback] });
558
+ return () => {
559
+ const currentCallbacks = this.queries[collection.name]?.get(require_queryId.default(selector, options))?.stateChangeCallbacks;
560
+ if (!currentCallbacks) return;
561
+ this.updateQuery(collection.name, {
562
+ selector,
563
+ options
564
+ }, { stateChangeCallbacks: currentCallbacks.filter((existingCallback) => existingCallback !== callback) });
565
+ };
566
+ },
567
+ executeQuery: (selector, options) => this.exec("executeQuery", collection.name, selector, options),
568
+ dispose: async () => {
569
+ await this.exec("unregisterCollection", collection.name);
570
+ this.isDisposed = true;
571
+ this.worker.terminate?.();
572
+ },
573
+ isReady: async () => {
574
+ await this.collectionReady.get(collection.name);
575
+ }
576
+ };
577
+ }
361
578
  };
362
579
  //#endregion
363
- exports.default = WorkerDataAdapterHost;
580
+ exports.default = WorkerDataAdapter;