@signaldb/core 2.0.0-beta.13 → 2.0.0-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/.vite/manifest.json +72 -46
  2. package/dist/AsyncDataAdapter.d.ts +11 -2
  3. package/dist/Collection/Cursor.d.ts +11 -1
  4. package/dist/Collection/Observer.d.ts +31 -0
  5. package/dist/DataAdapter.d.ts +15 -2
  6. package/dist/WorkerDataAdapter.d.ts +25 -2
  7. package/dist/WorkerDataAdapterHost.d.ts +2 -0
  8. package/dist/index.cjs.js +15 -15
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.mjs +15 -15
  11. package/dist/index10.cjs.js +7 -19
  12. package/dist/index10.mjs +7 -19
  13. package/dist/index11.cjs.js +19 -41
  14. package/dist/index11.mjs +19 -41
  15. package/dist/index12.cjs.js +41 -18
  16. package/dist/index12.mjs +41 -18
  17. package/dist/index13.cjs.js +18 -41
  18. package/dist/index13.mjs +18 -41
  19. package/dist/index14.cjs.js +39 -80
  20. package/dist/index14.mjs +39 -80
  21. package/dist/index15.cjs.js +82 -11
  22. package/dist/index15.mjs +82 -11
  23. package/dist/index16.cjs.js +11 -132
  24. package/dist/index16.mjs +11 -132
  25. package/dist/index17.cjs.js +127 -38
  26. package/dist/index17.mjs +127 -38
  27. package/dist/index18.cjs.js +43 -11
  28. package/dist/index18.mjs +43 -11
  29. package/dist/index19.cjs.js +10 -18
  30. package/dist/index19.mjs +11 -19
  31. package/dist/index20.cjs.js +19 -33
  32. package/dist/index20.mjs +19 -33
  33. package/dist/index21.cjs.js +33 -31
  34. package/dist/index21.mjs +33 -31
  35. package/dist/index22.cjs.js +31 -21
  36. package/dist/index22.mjs +31 -21
  37. package/dist/index23.cjs.js +16 -14
  38. package/dist/index23.mjs +16 -14
  39. package/dist/index24.cjs.js +15 -338
  40. package/dist/index24.mjs +15 -338
  41. package/dist/index25.cjs.js +122 -554
  42. package/dist/index25.mjs +121 -554
  43. package/dist/index26.cjs.js +34 -7
  44. package/dist/index26.mjs +34 -7
  45. package/dist/index27.cjs.js +339 -7
  46. package/dist/index27.mjs +339 -7
  47. package/dist/index28.cjs.js +550 -83
  48. package/dist/index28.mjs +550 -82
  49. package/dist/index29.cjs.js +8 -422
  50. package/dist/index29.mjs +8 -422
  51. package/dist/index30.cjs.js +7 -68
  52. package/dist/index30.mjs +7 -68
  53. package/dist/index31.cjs.js +86 -28
  54. package/dist/index31.mjs +85 -28
  55. package/dist/index32.cjs.js +446 -278
  56. package/dist/index32.mjs +446 -278
  57. package/dist/index33.cjs.js +68 -17
  58. package/dist/index33.mjs +68 -17
  59. package/dist/index34.cjs.js +460 -304
  60. package/dist/index34.mjs +460 -304
  61. package/dist/index35.cjs.js +14 -532
  62. package/dist/index35.mjs +14 -532
  63. package/dist/index36.cjs.js +389 -0
  64. package/dist/index36.mjs +389 -0
  65. package/dist/index37.cjs.js +539 -0
  66. package/dist/index37.mjs +539 -0
  67. package/dist/index4.cjs.js +199 -140
  68. package/dist/index4.mjs +195 -140
  69. package/dist/index5.cjs.js +152 -253
  70. package/dist/index5.mjs +152 -253
  71. package/dist/index6.cjs.js +267 -89
  72. package/dist/index6.mjs +267 -89
  73. package/dist/index7.cjs.js +121 -29
  74. package/dist/index7.mjs +121 -29
  75. package/dist/index8.cjs.js +29 -8
  76. package/dist/index8.mjs +29 -8
  77. package/dist/index9.cjs.js +8 -7
  78. package/dist/index9.mjs +8 -7
  79. package/dist/utils/incrementalQueryUpdate.d.ts +60 -0
  80. package/dist/utils/projectItems.d.ts +12 -0
  81. package/dist/utils/queryDelta.d.ts +83 -0
  82. package/package.json +1 -1
package/dist/index34.mjs CHANGED
@@ -1,335 +1,491 @@
1
- import isEqual from "./index2.mjs";
2
- import getMatchingKeys from "./index13.mjs";
3
- import getIndexInfo from "./index16.mjs";
4
- import deepClone from "./index17.mjs";
5
- import match from "./index18.mjs";
6
- import modify from "./index19.mjs";
7
- import project from "./index21.mjs";
8
- import queryId from "./index22.mjs";
9
- import sortItems from "./index23.mjs";
10
- import compact from "./index33.mjs";
11
- //#region src/WorkerDataAdapterHost.ts
12
- var WorkerDataAdapterHost = class {
13
- workerContext;
1
+ import { applyQueryDelta, callWithDelta, canApplyQueryDelta, diffQueryResults, isEmptyQueryDelta } from "./index4.mjs";
2
+ import randomId from "./index9.mjs";
3
+ import deepClone from "./index18.mjs";
4
+ import match from "./index19.mjs";
5
+ import modify from "./index20.mjs";
6
+ import { mergeChangesetIntoResult } from "./index25.mjs";
7
+ import queryId from "./index26.mjs";
8
+ import batchOnNextTick from "./index33.mjs";
9
+ //#region src/WorkerDataAdapter.ts
10
+ /**
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.
14
+ */
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;
26
+ }
27
+ var WorkerDataAdapter = class WorkerDataAdapter {
28
+ worker;
14
29
  options;
15
30
  id;
31
+ isDisposed = false;
32
+ workerReady;
16
33
  log = () => {};
17
- storageAdapters = /* @__PURE__ */ new Map();
18
- storageAdapterReady = /* @__PURE__ */ new Map();
19
- collectionIndices = /* @__PURE__ */ new Map();
20
- queries = /* @__PURE__ */ new Map();
21
- onError = (error) => {
22
- console.error(error);
23
- };
24
- constructor(workerContext, options) {
25
- 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;
26
47
  this.options = options;
27
48
  this.id = this.options.id || "default-worker-data-adapter";
28
- if (this.options.onError) this.onError = this.options.onError;
29
49
  if (this.options.log) this.log = this.options.log;
30
- this.workerContext.addEventListener("message", async (event) => {
31
- try {
32
- const { workerId, id, method, args } = event.data;
33
- await this.handleMessage(workerId, id, method, args);
34
- } catch (error) {
35
- this.onError(error);
36
- }
37
- });
38
- this.respond("ready", null, null, "ready");
39
- }
40
- respond(id, data, error = null, type = "response") {
41
- this.workerContext.postMessage({
42
- id,
43
- workerId: this.id,
44
- type,
45
- data,
46
- 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
+ };
47
58
  });
59
+ this.worker.addEventListener("message", this.handleWorkerMessage);
48
60
  }
49
- async handleMessage(workerId, id, method, args) {
50
- if (workerId !== this.id) return;
51
- const fn = this[method];
52
- if (typeof fn !== "function") {
53
- 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();
54
68
  return;
55
69
  }
56
- this.log(method, ...args);
57
- await this.isReady(args[0]);
58
- try {
59
- const result = await fn.apply(this, args);
60
- this.respond(id, result);
61
- } catch (error) {
62
- 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;
63
79
  }
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 : queryId(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
+ if (!canApplyQueryDelta(query.items, delta)) return;
97
+ if (isEmptyQueryDelta(delta)) return;
98
+ const servedBefore = this.flattenPendingWrites(collectionName) == null ? null : this.servedResult(collectionName, query);
99
+ nextItems = applyQueryDelta(query.items, delta);
100
+ if (servedBefore == null) deltaToPublish = delta;
101
+ else {
102
+ this.updateQuery(collectionName, {
103
+ selector: query.selector,
104
+ options: query.options
105
+ }, {
106
+ state,
107
+ error,
108
+ items: nextItems
109
+ });
110
+ const stored = collectionQueries.get(id);
111
+ if (!stored) return;
112
+ const servedDelta = diffQueryResults(servedBefore, this.servedResult(collectionName, stored));
113
+ if (isEmptyQueryDelta(servedDelta) && state === query.state) return;
114
+ stored.stateChangeCallbacks.forEach((callback) => callWithDelta(callback, state, servedDelta));
115
+ return;
116
+ }
117
+ }
118
+ this.updateQuery(collectionName, {
119
+ selector: query.selector,
120
+ options: query.options
121
+ }, {
122
+ state,
123
+ error,
124
+ items: nextItems
125
+ });
126
+ const updated = collectionQueries.get(id);
127
+ if (!updated) return;
128
+ updated.stateChangeCallbacks.forEach((callback) => callWithDelta(callback, state, deltaToPublish));
64
129
  }
65
- async getIndexInfo(collectionName, selector) {
66
- const storageAdapter = this.storageAdapters.get(collectionName);
67
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
68
- if (selector != null && Object.keys(selector).length === 1 && "id" in selector && typeof selector.id !== "object") return {
69
- matched: true,
70
- ids: compact([selector.id]),
71
- optimizedSelector: {}
72
- };
73
- if (selector == null) return {
74
- matched: false,
75
- ids: [],
76
- optimizedSelector: {}
130
+ async exec(method, collectionName, ...args) {
131
+ await this.workerReady;
132
+ if (method !== "isReady") {
133
+ const collectionReady = this.collectionReady.get(collectionName);
134
+ if (!collectionReady) throw new Error(`Collection "${collectionName}" is not registered in WorkerDataAdapter`);
135
+ await collectionReady;
136
+ }
137
+ if (this.isDisposed) throw new Error("WorkerDataAdapter is disposed");
138
+ return new Promise((resolve, reject) => {
139
+ const messageId = randomId();
140
+ this.pendingRequests.set(messageId, {
141
+ resolve,
142
+ reject
143
+ });
144
+ this.worker.postMessage({
145
+ id: messageId,
146
+ workerId: this.id,
147
+ method,
148
+ args: [collectionName, ...args]
149
+ });
150
+ });
151
+ }
152
+ /**
153
+ * Issues a call whose result nobody is waiting for, and makes sure a failure has somewhere to
154
+ * go. A bare rejection here would surface as an uncaught error — which is what a disposed
155
+ * collection produced every time a cursor was cleaned up after it.
156
+ * @param method - The method to call on the worker.
157
+ * @param collectionName - The collection it applies to.
158
+ * @param args - The remaining arguments.
159
+ * @param onError - Called when the call fails, in place of merely logging it.
160
+ */
161
+ execInBackground(method, collectionName, args = [], onError) {
162
+ this.exec(method, collectionName, ...args).catch((error) => {
163
+ if (onError) {
164
+ onError(error);
165
+ return;
166
+ }
167
+ this.log(method, "failed", error);
168
+ });
169
+ }
170
+ queryItemsById(query) {
171
+ if (!query.itemsById) query.itemsById = new Map(query.items.map((item) => [item.id, item]));
172
+ return query.itemsById;
173
+ }
174
+ flattenPendingWrites(collectionName) {
175
+ const pending = this.pendingWrites.get(collectionName);
176
+ if (!pending || pending.size === 0) return null;
177
+ const upserts = /* @__PURE__ */ new Map();
178
+ const deletes = /* @__PURE__ */ new Set();
179
+ [...pending.entries()].sort(([a], [b]) => a - b).forEach(([, write]) => {
180
+ write.upserts.forEach((item, id) => {
181
+ upserts.set(id, item);
182
+ deletes.delete(id);
183
+ });
184
+ write.deletes.forEach((id) => {
185
+ deletes.add(id);
186
+ upserts.delete(id);
187
+ });
188
+ });
189
+ return {
190
+ upserts,
191
+ deletes
77
192
  };
78
- return getIndexInfo((this.collectionIndices.get(collectionName) ?? []).map((field) => async (flatSelector) => {
79
- if (!Object.hasOwnProperty.call(flatSelector, field)) return { matched: false };
80
- const index = await storageAdapter.readIndex(field);
81
- const fieldSelector = flatSelector[field];
82
- const filteresForNull = fieldSelector == null || fieldSelector.$exists === false;
83
- const keys = filteresForNull ? {
84
- include: null,
85
- exclude: [...index.keys()].filter((key) => key != null)
86
- } : getMatchingKeys(field, flatSelector);
87
- if (keys.include == null && keys.exclude == null) return { matched: false };
88
- let includedIds = [];
89
- if (keys.include == null) for (const set of index.values()) for (const pos of set) includedIds.push(pos);
90
- else for (const key of keys.include) {
91
- const idSet = index.get(key);
92
- if (idSet) for (const id of idSet) includedIds.push(id);
193
+ }
194
+ static providesFullItems(query) {
195
+ return query.options?.fields == null;
196
+ }
197
+ observableItems(collectionName) {
198
+ const byId = /* @__PURE__ */ new Map();
199
+ this.queries[collectionName]?.forEach((query) => {
200
+ if (!WorkerDataAdapter.providesFullItems(query)) return;
201
+ query.items.forEach((item) => byId.set(item.id, item));
202
+ });
203
+ const pending = this.flattenPendingWrites(collectionName);
204
+ if (pending) {
205
+ pending.upserts.forEach((item, id) => byId.set(id, item));
206
+ pending.deletes.forEach((id) => byId.delete(id));
207
+ }
208
+ return [...byId.values()];
209
+ }
210
+ observableItemsByIds(collectionName, ids) {
211
+ const pending = this.flattenPendingWrites(collectionName);
212
+ const found = /* @__PURE__ */ new Map();
213
+ ids.forEach((id) => {
214
+ if (pending?.deletes.has(id)) return;
215
+ const pendingItem = pending?.upserts.get(id);
216
+ if (pendingItem) {
217
+ found.set(id, pendingItem);
218
+ return;
93
219
  }
94
- if (keys.exclude != null) {
95
- const excludeIds = /* @__PURE__ */ new Set();
96
- for (const key of keys.exclude) {
97
- const idSet = index.get(key);
98
- if (idSet) for (const id of idSet) excludeIds.add(id);
220
+ const queries = this.queries[collectionName];
221
+ if (!queries) return;
222
+ for (const query of queries.values()) {
223
+ if (!WorkerDataAdapter.providesFullItems(query)) continue;
224
+ const item = this.queryItemsById(query).get(id);
225
+ if (item) {
226
+ found.set(id, item);
227
+ return;
99
228
  }
100
- includedIds = includedIds.filter((pos) => !excludeIds.has(pos));
101
229
  }
102
- return {
103
- matched: true,
104
- ids: includedIds,
105
- fields: [field],
106
- keepSelector: filteresForNull
107
- };
108
- }), selector);
230
+ });
231
+ return [...found.values()];
109
232
  }
110
- async queryItems(collectionName, selector) {
111
- const storageAdapter = this.storageAdapters.get(collectionName);
112
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
113
- const indexInfo = await this.getIndexInfo(collectionName, selector);
114
- const matchItems = (item) => {
115
- if (indexInfo.optimizedSelector == null) return true;
116
- if (Object.keys(indexInfo.optimizedSelector).length <= 0) return true;
117
- return match(item, indexInfo.optimizedSelector);
233
+ servedResult(collectionName, query) {
234
+ const pendingVersion = this.pendingWriteVersions.get(collectionName) ?? 0;
235
+ if (query.served && query.served.fromItems === query.items && query.served.pendingVersion === pendingVersion) return query.served.items;
236
+ const items = this.computeServedResult(collectionName, query);
237
+ query.served = {
238
+ items,
239
+ fromItems: query.items,
240
+ pendingVersion
118
241
  };
119
- if (indexInfo.matched) {
120
- const items = await storageAdapter.readIds(indexInfo.ids);
121
- if (isEqual(indexInfo.optimizedSelector, {})) return items;
122
- return items.filter(matchItems);
123
- } else {
124
- const allItems = await storageAdapter.readAll();
125
- if (isEqual(selector, {})) return allItems;
126
- return allItems.filter(matchItems);
127
- }
242
+ return items;
128
243
  }
129
- async executeQuery(collectionName, selector, options) {
130
- if (selector === null) return [];
131
- const items = await this.queryItems(collectionName, selector || {});
132
- const { sort, skip, limit, fields } = options || {};
133
- const sorted = sort ? sortItems(items, sort) : items;
134
- const skipped = skip ? sorted.slice(skip) : sorted;
135
- const limited = limit ? skipped.slice(0, limit) : skipped;
136
- const idExcluded = fields && fields.id === 0;
137
- return limited.map((item) => {
138
- if (!fields) return item;
139
- return {
140
- ...idExcluded ? {} : { id: item.id },
141
- ...project(item, fields)
142
- };
244
+ computeServedResult(collectionName, query) {
245
+ const pending = this.flattenPendingWrites(collectionName);
246
+ if (!pending) return query.items;
247
+ const byId = this.queryItemsById(query);
248
+ let affected = false;
249
+ pending.deletes.forEach((id) => {
250
+ if (byId.has(id)) affected = true;
251
+ });
252
+ if (!affected) pending.upserts.forEach((item, id) => {
253
+ if (affected) return;
254
+ if (byId.has(id)) affected = true;
255
+ else if (query.selector != null && match(item, query.selector)) affected = true;
256
+ });
257
+ if (!affected) return query.items;
258
+ return mergeChangesetIntoResult(query.items, query.selector, query.options, {
259
+ upserts: [...pending.upserts.values()],
260
+ deletes: [...pending.deletes]
143
261
  });
144
262
  }
145
- ensureQuery(collectionName, selector, options) {
146
- const id = queryId(selector, options);
147
- if (!this.queries.get(collectionName)) throw new Error(`Collection ${collectionName} not initialized!`);
148
- let query = this.queries.get(collectionName)?.get(id);
149
- if (!query) {
150
- query = {
151
- selector,
152
- options
153
- };
154
- this.queries.get(collectionName)?.set(id, query);
155
- }
156
- return query;
263
+ /**
264
+ * Registers a write's effect locally and notifies every active query it
265
+ * touches, then returns a function that drops it again once the write
266
+ * settles.
267
+ * @param collectionName - The collection the write targets.
268
+ * @param upserts - Items inserted or updated by the write.
269
+ * @param deletes - Ids removed by the write.
270
+ * @returns A function that drops the pending write and re-notifies.
271
+ */
272
+ applyPendingWrite(collectionName, upserts, deletes) {
273
+ if (upserts.length === 0 && deletes.length === 0) return () => {};
274
+ const affectedIds = new Set([...upserts.map((item) => item.id), ...deletes]);
275
+ const affected = this.affectedQueries(collectionName, upserts, affectedIds);
276
+ const servedBefore = this.servedResults(collectionName, affected);
277
+ const seq = this.pendingWriteSeq += 1;
278
+ const pending = this.pendingWrites.get(collectionName) ?? /* @__PURE__ */ new Map();
279
+ pending.set(seq, {
280
+ upserts: new Map(upserts.map((item) => [item.id, item])),
281
+ deletes: new Set(deletes)
282
+ });
283
+ this.pendingWrites.set(collectionName, pending);
284
+ this.bumpPendingWriteVersion(collectionName);
285
+ this.notifyWithDeltas(collectionName, affected, servedBefore);
286
+ return () => {
287
+ const current = this.pendingWrites.get(collectionName);
288
+ if (!current) return;
289
+ const affectedOnDrop = this.affectedQueries(collectionName, upserts, affectedIds);
290
+ const beforeDrop = this.servedResults(collectionName, affectedOnDrop);
291
+ current.delete(seq);
292
+ if (current.size === 0) this.pendingWrites.delete(collectionName);
293
+ this.bumpPendingWriteVersion(collectionName);
294
+ this.notifyWithDeltas(collectionName, affectedOnDrop, beforeDrop);
295
+ };
157
296
  }
158
- emitQueryUpdate(collectionName, selector, options, state, error, items) {
159
- const id = queryId(selector, options);
160
- if (!this.queries.get(collectionName)) throw new Error(`Collection ${collectionName} not initialized!`);
161
- this.respond(id, {
162
- collectionName,
163
- selector,
164
- options,
165
- state,
166
- error,
167
- items
168
- }, null, "queryUpdate");
297
+ affectedQueries(collectionName, upserts, affectedIds) {
298
+ const affected = [];
299
+ this.queries[collectionName]?.forEach((query) => {
300
+ const byId = this.queryItemsById(query);
301
+ let wasHolding = false;
302
+ affectedIds.forEach((id) => {
303
+ if (byId.has(id)) wasHolding = true;
304
+ });
305
+ const nowMatches = upserts.some((item) => query.selector != null && match(item, query.selector));
306
+ if (!wasHolding && !nowMatches) return;
307
+ affected.push(query);
308
+ });
309
+ return affected;
169
310
  }
170
- ensureStorageAdapter(name) {
171
- if (this.storageAdapters.has(name)) return;
172
- const adapter = this.options.storage(name);
173
- if (!adapter) return;
174
- this.storageAdapters.set(name, adapter);
311
+ servedResults(collectionName, queries) {
312
+ return new Map(queries.map((query) => [query, this.servedResult(collectionName, query)]));
175
313
  }
176
- async checkQueryUpdates(collectionName, items) {
177
- const queries = this.queries.get(collectionName);
178
- if (!queries) throw new Error(`Collection ${collectionName} not initialized!`);
179
- const affectedQueries = [...queries.values()].filter(({ selector }) => items.some((item) => match(item, selector))) ?? [];
180
- if (affectedQueries.length === 0) return;
181
- affectedQueries.forEach(({ selector, options }) => {
182
- this.emitQueryUpdate(collectionName, selector, options, "active", null);
314
+ notifyWithDeltas(collectionName, queries, servedBefore) {
315
+ queries.forEach((query) => {
316
+ const before = servedBefore.get(query);
317
+ if (before == null) return;
318
+ const delta = diffQueryResults(before, this.servedResult(collectionName, query));
319
+ if (isEmptyQueryDelta(delta)) return;
320
+ query.stateChangeCallbacks.forEach((callback) => callWithDelta(callback, query.state, delta));
183
321
  });
184
- await Promise.all(affectedQueries.map(async ({ selector, options }) => {
185
- const queryItems = await this.executeQuery(collectionName, selector, options);
186
- this.emitQueryUpdate(collectionName, selector, options, "complete", null, queryItems);
187
- }));
188
322
  }
189
- registerCollection = async (collectionName, indices) => {
190
- this.collectionIndices.set(collectionName, indices);
191
- this.queries.set(collectionName, /* @__PURE__ */ new Map());
192
- this.ensureStorageAdapter(collectionName);
193
- const storageAdapter = this.storageAdapters.get(collectionName);
194
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
195
- const setupPromise = (async () => {
196
- await storageAdapter.setup();
197
- await Promise.all(indices.map((index) => storageAdapter.createIndex(index)));
198
- })();
199
- this.storageAdapterReady.set(collectionName, setupPromise);
200
- await setupPromise;
201
- };
202
- unregisterCollection = async (collectionName) => {
203
- this.storageAdapters.delete(collectionName);
204
- this.queries.delete(collectionName);
205
- };
206
- registerQuery = async (collectionName, selector, options) => {
207
- this.ensureQuery(collectionName, selector, options);
208
- const queryItems = await this.executeQuery(collectionName, selector, options);
209
- this.emitQueryUpdate(collectionName, selector, options, "complete", null, queryItems);
210
- };
211
- unregisterQuery = async (collectionName, selector, options) => {
212
- const id = queryId(selector, options);
213
- if (!this.queries.get(collectionName)) throw new Error(`Collection ${collectionName} not initialized!`);
214
- this.queries.get(collectionName)?.delete(id);
215
- };
216
- insert = async (collectionName, input) => {
217
- const storageAdapter = this.storageAdapters.get(collectionName);
218
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
219
- const existingItems = await this.executeQuery(collectionName, { id: { $in: input.map((i) => i[0].id) } });
220
- const result = input.map(([item]) => {
221
- if (item.id == null) return /* @__PURE__ */ new Error("Item must have an id");
222
- if (existingItems.some((existing) => existing.id === item.id)) return /* @__PURE__ */ new Error(`Item with id ${item.id} already exists`);
223
- return item;
224
- });
225
- const newItems = result.filter((item) => !(item instanceof Error));
226
- await storageAdapter.insert(newItems);
227
- await this.checkQueryUpdates(collectionName, newItems);
228
- return result;
229
- };
230
- updateOne = async (collectionName, parameters) => {
231
- const storageAdapter = this.storageAdapters.get(collectionName);
232
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
233
- const previousItems = [];
234
- const result = await Promise.all(parameters.map(async ([selector, modifier]) => {
235
- const item = await this.executeQuery(collectionName, selector, { limit: 1 }).then((items) => items[0] ?? null);
236
- const { $setOnInsert, ...restModifier } = modifier;
237
- if (item == null) return [];
238
- previousItems.push(item);
323
+ matchObservableItems(collectionName, selector, onlyFirst) {
324
+ if (selector == null) return [];
325
+ const ids = selectorIds(selector);
326
+ const matches = ids == null ? this.observableItems(collectionName).filter((item) => match(item, selector)) : this.observableItemsByIds(collectionName, ids);
327
+ return onlyFirst ? matches.slice(0, 1) : matches;
328
+ }
329
+ resolveUpdate(collectionName, selector, modifier, onlyFirst) {
330
+ const { $setOnInsert, ...restModifier } = modifier;
331
+ const upserts = [];
332
+ const deletes = [];
333
+ this.matchObservableItems(collectionName, selector, onlyFirst).forEach((item) => {
239
334
  const modifiedItem = modify(deepClone(item), restModifier);
240
- if (item.id !== modifiedItem.id) {
241
- if ((await this.executeQuery(collectionName, { id: modifiedItem.id }, { limit: 1 })).length > 0) return /* @__PURE__ */ new Error(`Item with id ${modifiedItem.id} already exists`);
242
- }
243
- return [modifiedItem];
244
- }));
245
- const modifiedItems = compact(result.filter((item) => !(item instanceof Error)).flat());
246
- if (modifiedItems.length > 0) {
247
- await storageAdapter.replace(modifiedItems);
248
- await this.checkQueryUpdates(collectionName, [...modifiedItems, ...previousItems]);
249
- }
250
- return result;
251
- };
252
- updateMany = 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 items = await this.executeQuery(collectionName, selector);
258
- if (items.length === 0) return [];
259
- const { $setOnInsert, ...restModifier } = modifier;
260
- try {
261
- return await Promise.all(items.map(async (item) => {
262
- const modifiedItem = modify(deepClone(item), restModifier);
263
- if (item.id !== modifiedItem.id) {
264
- if ((await this.executeQuery(collectionName, { id: modifiedItem.id }, { limit: 1 })).length > 0) throw new Error(`Item with id ${modifiedItem.id} already exists`);
265
- }
266
- previousItems.push(item);
267
- return modifiedItem;
268
- }));
269
- } catch (error) {
270
- return error;
271
- }
272
- }));
273
- const modifiedItems = compact(result.filter((item) => !(item instanceof Error)).flat());
274
- if (modifiedItems.length > 0) {
275
- await storageAdapter.replace(modifiedItems);
276
- await this.checkQueryUpdates(collectionName, [...modifiedItems, ...previousItems]);
335
+ upserts.push(modifiedItem);
336
+ if (modifiedItem.id !== item.id) deletes.push(item.id);
337
+ });
338
+ return {
339
+ upserts,
340
+ deletes
341
+ };
342
+ }
343
+ resolveRemoval(collectionName, selector, onlyFirst) {
344
+ return this.matchObservableItems(collectionName, selector, onlyFirst).map((item) => item.id);
345
+ }
346
+ resolveReplacement(item, replacement) {
347
+ const modifiedItem = {
348
+ ...replacement,
349
+ id: replacement.id ?? item.id
350
+ };
351
+ return {
352
+ upserts: [modifiedItem],
353
+ deletes: modifiedItem.id === item.id ? [] : [item.id]
354
+ };
355
+ }
356
+ async withPendingWrite(collectionName, delta, run) {
357
+ const dropPendingWrite = this.applyPendingWrite(collectionName, delta.upserts, delta.deletes);
358
+ try {
359
+ return await run();
360
+ } finally {
361
+ dropPendingWrite();
277
362
  }
278
- return result;
279
- };
280
- replaceOne = async (collectionName, parameters) => {
281
- const storageAdapter = this.storageAdapters.get(collectionName);
282
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
283
- const previousItems = [];
284
- const result = await Promise.all(parameters.map(async ([selector, replacement]) => {
285
- const item = await this.executeQuery(collectionName, selector, { limit: 1 }).then((items) => items[0] ?? null);
286
- if (item == null) return [];
287
- previousItems.push(item);
288
- const modifiedItem = {
289
- ...replacement,
290
- id: replacement.id ?? item.id
291
- };
292
- if (item.id !== modifiedItem.id) {
293
- if ((await this.executeQuery(collectionName, { id: modifiedItem.id }, { limit: 1 })).length > 0) return /* @__PURE__ */ new Error(`Item with id ${modifiedItem.id} already exists`);
363
+ }
364
+ enqueueBatched(collectionName, method, args) {
365
+ const helper = this.batchExecutionHelpers.get(collectionName);
366
+ if (!helper) throw new Error(`Collection "${collectionName}" is not registered in WorkerDataAdapter`);
367
+ return helper.enqueue(method, args);
368
+ }
369
+ updateQuery(collectionName, query, update) {
370
+ const id = queryId(query.selector, query.options);
371
+ const collectionQueries = this.queries[collectionName];
372
+ if (!collectionQueries) return;
373
+ const existing = collectionQueries.get(id);
374
+ const newState = {
375
+ selector: query.selector,
376
+ options: query.options,
377
+ state: "active",
378
+ error: null,
379
+ stateChangeCallbacks: [],
380
+ ...existing,
381
+ ...update,
382
+ ...update.items ? {
383
+ items: update.items,
384
+ itemsById: void 0
385
+ } : { items: existing?.items ?? [] }
386
+ };
387
+ collectionQueries.set(id, newState);
388
+ this.queries[collectionName] = collectionQueries;
389
+ }
390
+ createCollectionBackend(collection, indices) {
391
+ this.queries[collection.name] = /* @__PURE__ */ new Map();
392
+ this.execInBackground("registerCollection", collection.name, [indices]);
393
+ this.collectionReady.set(collection.name, this.exec("isReady", collection.name));
394
+ this.batchExecutionHelpers.set(collection.name, batchOnNextTick(async (method, args) => this.exec(method, collection.name, args)));
395
+ return {
396
+ insert: async (item) => {
397
+ return this.withPendingWrite(collection.name, {
398
+ upserts: [item],
399
+ deletes: []
400
+ }, () => this.enqueueBatched(collection.name, "insert", [item]));
401
+ },
402
+ updateOne: async (selector, modifier) => {
403
+ return this.withPendingWrite(collection.name, this.resolveUpdate(collection.name, selector, modifier, true), () => this.enqueueBatched(collection.name, "updateOne", [selector, modifier]));
404
+ },
405
+ updateMany: async (selector, modifier) => {
406
+ return this.withPendingWrite(collection.name, this.resolveUpdate(collection.name, selector, modifier, false), () => this.enqueueBatched(collection.name, "updateMany", [selector, modifier]));
407
+ },
408
+ replaceOne: async (selector, replacement) => {
409
+ const [item] = this.matchObservableItems(collection.name, selector, true);
410
+ return this.withPendingWrite(collection.name, item == null ? {
411
+ upserts: [],
412
+ deletes: []
413
+ } : this.resolveReplacement(item, replacement), () => this.enqueueBatched(collection.name, "replaceOne", [selector, replacement]));
414
+ },
415
+ removeOne: async (selector) => {
416
+ return this.withPendingWrite(collection.name, {
417
+ upserts: [],
418
+ deletes: this.resolveRemoval(collection.name, selector, true)
419
+ }, () => this.enqueueBatched(collection.name, "removeOne", [selector]));
420
+ },
421
+ removeMany: async (selector) => {
422
+ return this.withPendingWrite(collection.name, {
423
+ upserts: [],
424
+ deletes: this.resolveRemoval(collection.name, selector, false)
425
+ }, () => this.enqueueBatched(collection.name, "removeMany", [selector]));
426
+ },
427
+ registerQuery: (selector, options) => {
428
+ this.updateQuery(collection.name, {
429
+ selector,
430
+ options
431
+ }, {
432
+ state: "active",
433
+ error: null,
434
+ items: []
435
+ });
436
+ this.execInBackground("registerQuery", collection.name, [selector, options], (error) => {
437
+ const query = this.queries[collection.name]?.get(queryId(selector, options));
438
+ if (!query) return;
439
+ this.updateQuery(collection.name, {
440
+ selector,
441
+ options
442
+ }, {
443
+ state: "error",
444
+ error
445
+ });
446
+ query.stateChangeCallbacks.forEach((callback) => callback("error"));
447
+ });
448
+ },
449
+ unregisterQuery: (selector, options) => {
450
+ this.queries[collection.name]?.delete(queryId(selector, options));
451
+ this.execInBackground("unregisterQuery", collection.name, [selector, options]);
452
+ },
453
+ getQueryState: (selector, options) => {
454
+ return (this.queries[collection.name]?.get(queryId(selector, options)))?.state || "active";
455
+ },
456
+ getQueryError: (selector, options) => {
457
+ return (this.queries[collection.name]?.get(queryId(selector, options)))?.error || null;
458
+ },
459
+ getQueryResult: (selector, options) => {
460
+ const query = this.queries[collection.name]?.get(queryId(selector, options));
461
+ if (!query) return [];
462
+ return this.servedResult(collection.name, query);
463
+ },
464
+ onQueryStateChange: (selector, options, callback) => {
465
+ this.updateQuery(collection.name, {
466
+ selector,
467
+ options
468
+ }, { stateChangeCallbacks: [...this.queries[collection.name]?.get(queryId(selector, options))?.stateChangeCallbacks || [], callback] });
469
+ return () => {
470
+ const currentCallbacks = this.queries[collection.name]?.get(queryId(selector, options))?.stateChangeCallbacks;
471
+ if (!currentCallbacks) return;
472
+ this.updateQuery(collection.name, {
473
+ selector,
474
+ options
475
+ }, { stateChangeCallbacks: currentCallbacks.filter((existingCallback) => existingCallback !== callback) });
476
+ };
477
+ },
478
+ executeQuery: (selector, options) => this.exec("executeQuery", collection.name, selector, options),
479
+ dispose: async () => {
480
+ await this.exec("unregisterCollection", collection.name);
481
+ this.isDisposed = true;
482
+ this.worker.terminate?.();
483
+ },
484
+ isReady: async () => {
485
+ await this.exec("isReady", collection.name);
294
486
  }
295
- return [modifiedItem];
296
- }));
297
- const modifiedItems = compact(result.filter((item) => !(item instanceof Error)).flat());
298
- if (modifiedItems.length > 0) {
299
- await storageAdapter.replace(modifiedItems);
300
- await this.checkQueryUpdates(collectionName, [...modifiedItems, ...previousItems]);
301
- }
302
- return result;
303
- };
304
- removeOne = async (collectionName, selectors) => {
305
- const storageAdapter = this.storageAdapters.get(collectionName);
306
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
307
- const result = await Promise.all(selectors.map(async ([selector]) => {
308
- const item = await this.executeQuery(collectionName, selector, { limit: 1 }).then((items) => items[0] ?? null);
309
- if (item == null) return [];
310
- return [item];
311
- }));
312
- const items = result.flat();
313
- if (items.length > 0) {
314
- await storageAdapter.remove(items);
315
- await this.checkQueryUpdates(collectionName, items);
316
- }
317
- return result;
318
- };
319
- removeMany = async (collectionName, selectors) => {
320
- const storageAdapter = this.storageAdapters.get(collectionName);
321
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
322
- const result = await Promise.all(selectors.map(async ([selector]) => this.executeQuery(collectionName, selector)));
323
- const items = result.flat();
324
- if (items.length > 0) {
325
- await storageAdapter.remove(items);
326
- await this.checkQueryUpdates(collectionName, items);
327
- }
328
- return result;
329
- };
330
- isReady = async (collectionName) => {
331
- return this.storageAdapterReady.get(collectionName);
332
- };
487
+ };
488
+ }
333
489
  };
334
490
  //#endregion
335
- export { WorkerDataAdapterHost as default };
491
+ export { WorkerDataAdapter as default };