@signaldb/core 2.0.0-beta.17 → 2.0.0-beta.19

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/index36.mjs CHANGED
@@ -1,389 +1,580 @@
1
- import isEqual from "./index2.mjs";
2
- import { diffQueryResults, isEmptyQueryDelta } from "./index4.mjs";
3
- import getMatchingKeys from "./index14.mjs";
4
- import getIndexInfo from "./index17.mjs";
1
+ import { applyQueryDelta, callWithDelta, canApplyQueryDelta, diffQueryResults, isEmptyQueryDelta } from "./index4.mjs";
2
+ import randomId from "./index9.mjs";
5
3
  import deepClone from "./index18.mjs";
6
4
  import match from "./index19.mjs";
7
5
  import modify from "./index20.mjs";
8
- import projectItems from "./index23.mjs";
9
- import sortItems from "./index24.mjs";
10
- import incrementalQueryUpdate from "./index25.mjs";
6
+ import { mergeChangesetIntoResult } from "./index25.mjs";
11
7
  import queryId from "./index26.mjs";
12
- import compact from "./index35.mjs";
13
- //#region src/WorkerDataAdapterHost.ts
8
+ import batchOnNextTick from "./index35.mjs";
9
+ //#region src/WorkerDataAdapter.ts
14
10
  /**
15
- * Turns the item states a write produced into the upsert/delete split a query update needs.
16
- *
17
- * The two lists are not symmetric: an item that is still there after the write is described by
18
- * its new state, while an item that is gone — removed, or given a new id — is described by the id
19
- * it used to have and nothing else. Mixing the states from before and after a write into one list
20
- * loses exactly that distinction.
21
- * @template T - The type of the items.
22
- * @param previousItems - The items as they were before the write.
23
- * @param modifiedItems - The items as they are after it.
24
- * @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.
25
14
  */
26
- function toChangeset(previousItems, modifiedItems) {
27
- const modifiedIds = new Set(modifiedItems.map((item) => item.id));
28
- return {
29
- upserts: modifiedItems,
30
- deletes: previousItems.map((item) => item.id).filter((id) => !modifiedIds.has(id))
31
- };
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;
32
26
  }
33
- var WorkerDataAdapterHost = class {
34
- workerContext;
27
+ var WorkerDataAdapter = class WorkerDataAdapter {
28
+ worker;
35
29
  options;
36
30
  id;
31
+ isDisposed = false;
32
+ workerReady;
37
33
  log = () => {};
38
- storageAdapters = /* @__PURE__ */ new Map();
39
- storageAdapterReady = /* @__PURE__ */ new Map();
40
- collectionIndices = /* @__PURE__ */ new Map();
41
- queries = /* @__PURE__ */ new Map();
42
- onError = (error) => {
43
- console.error(error);
44
- };
45
- constructor(workerContext, options) {
46
- 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;
47
47
  this.options = options;
48
48
  this.id = this.options.id || "default-worker-data-adapter";
49
- if (this.options.onError) this.onError = this.options.onError;
50
49
  if (this.options.log) this.log = this.options.log;
51
- this.workerContext.addEventListener("message", async (event) => {
52
- try {
53
- const { workerId, id, method, args } = event.data;
54
- await this.handleMessage(workerId, id, method, args);
55
- } catch (error) {
56
- this.onError(error);
57
- }
58
- });
59
- this.respond("ready", null, null, "ready");
60
- }
61
- respond(id, data, error = null, type = "response") {
62
- this.workerContext.postMessage({
63
- id,
64
- workerId: this.id,
65
- type,
66
- data,
67
- 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
+ };
68
58
  });
59
+ this.worker.addEventListener("message", this.handleWorkerMessage);
69
60
  }
70
- async handleMessage(workerId, id, method, args) {
71
- if (workerId !== this.id) return;
72
- const fn = this[method];
73
- if (typeof fn !== "function") {
74
- 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();
75
68
  return;
76
69
  }
77
- this.log(method, ...args);
78
- await this.isReady(args[0]);
79
- try {
80
- const result = await fn.apply(this, args);
81
- this.respond(id, result);
82
- } catch (error) {
83
- 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;
84
79
  }
85
- }
86
- async getIndexInfo(collectionName, selector) {
87
- const storageAdapter = this.storageAdapters.get(collectionName);
88
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
89
- if (selector != null && Object.keys(selector).length === 1 && "id" in selector && typeof selector.id !== "object") return {
90
- matched: true,
91
- ids: compact([selector.id]),
92
- optimizedSelector: {}
93
- };
94
- if (selector == null) return {
95
- matched: false,
96
- ids: [],
97
- optimizedSelector: {}
98
- };
99
- return getIndexInfo((this.collectionIndices.get(collectionName) ?? []).map((field) => async (flatSelector) => {
100
- if (!Object.hasOwnProperty.call(flatSelector, field)) return { matched: false };
101
- const index = await storageAdapter.readIndex(field);
102
- const fieldSelector = flatSelector[field];
103
- const filteresForNull = fieldSelector == null || fieldSelector.$exists === false;
104
- const keys = filteresForNull ? {
105
- include: null,
106
- exclude: [...index.keys()].filter((key) => key != null)
107
- } : getMatchingKeys(field, flatSelector);
108
- if (keys.include == null && keys.exclude == null) return { matched: false };
109
- let includedIds = [];
110
- if (keys.include == null) for (const set of index.values()) for (const pos of set) includedIds.push(pos);
111
- else for (const key of keys.include) {
112
- const idSet = index.get(key);
113
- if (idSet) for (const id of idSet) includedIds.push(id);
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
+ const canApply = canApplyQueryDelta(query.items, delta);
97
+ if (!canApply || 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) => callWithDelta(callback, state, canApply ? delta : void 0));
109
+ return;
114
110
  }
115
- if (keys.exclude != null) {
116
- const excludeIds = /* @__PURE__ */ new Set();
117
- for (const key of keys.exclude) {
118
- const idSet = index.get(key);
119
- if (idSet) for (const id of idSet) excludeIds.add(id);
120
- }
121
- includedIds = includedIds.filter((pos) => !excludeIds.has(pos));
111
+ const servedBefore = this.flattenPendingWrites(collectionName) == null ? null : this.servedResult(collectionName, query);
112
+ nextItems = 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 = diffQueryResults(servedBefore, this.servedResult(collectionName, stored));
126
+ if (isEmptyQueryDelta(servedDelta) && state === query.state) return;
127
+ stored.stateChangeCallbacks.forEach((callback) => callWithDelta(callback, state, servedDelta));
128
+ return;
122
129
  }
123
- return {
124
- matched: true,
125
- ids: includedIds,
126
- fields: [field],
127
- keepSelector: filteresForNull
128
- };
129
- }), selector);
130
+ }
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) => callWithDelta(callback, state, deltaToPublish));
130
142
  }
131
- async queryItems(collectionName, selector) {
132
- const storageAdapter = this.storageAdapters.get(collectionName);
133
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
134
- const indexInfo = await this.getIndexInfo(collectionName, selector);
135
- const matchItems = (item) => {
136
- if (indexInfo.optimizedSelector == null) return true;
137
- if (Object.keys(indexInfo.optimizedSelector).length <= 0) return true;
138
- return match(item, indexInfo.optimizedSelector);
139
- };
140
- if (indexInfo.matched) {
141
- const items = await storageAdapter.readIds(indexInfo.ids);
142
- if (isEqual(indexInfo.optimizedSelector, {})) return items;
143
- return items.filter(matchItems);
144
- } else {
145
- const allItems = await storageAdapter.readAll();
146
- if (isEqual(selector, {})) return allItems;
147
- 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;
148
149
  }
150
+ if (this.isDisposed) throw new Error("WorkerDataAdapter is disposed");
151
+ return new Promise((resolve, reject) => {
152
+ const messageId = randomId();
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
+ });
149
164
  }
150
- async executeQuery(collectionName, selector, options) {
151
- if (selector === null) return [];
152
- const items = await this.queryItems(collectionName, selector || {});
153
- const { sort, skip, limit, fields } = options || {};
154
- const sorted = sort ? sortItems(items, sort) : items;
155
- const skipped = skip ? sorted.slice(skip) : sorted;
156
- return projectItems(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
+ });
157
182
  }
158
- ensureQuery(collectionName, selector, options) {
159
- const id = queryId(selector, options);
160
- if (!this.queries.get(collectionName)) throw new Error(`Collection ${collectionName} not initialized!`);
161
- let query = this.queries.get(collectionName)?.get(id);
162
- if (!query) {
163
- query = {
164
- selector,
165
- options,
166
- items: null
167
- };
168
- 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;
169
215
  }
170
- return query;
216
+ WorkerDataAdapter.writeFlatEntry(state, id, top.item);
171
217
  }
172
- setQueryItems(query, items) {
173
- query.items = items;
174
- 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);
175
226
  }
176
- queryItemIds(query) {
177
- if (!query.itemIds) query.itemIds = new Set((query.items ?? []).map((item) => item.id));
178
- return query.itemIds;
227
+ static providesFullItems(query) {
228
+ return query.options?.fields == null;
179
229
  }
180
- emitQueryUpdate(collectionName, selector, options, state, error, items, delta) {
181
- const id = queryId(selector, options);
182
- if (!this.queries.get(collectionName)) throw new Error(`Collection ${collectionName} not initialized!`);
183
- this.respond(id, {
184
- collectionName,
185
- qid: id,
186
- selector,
187
- options,
188
- state,
189
- 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 = {
190
271
  items,
191
- delta
192
- }, null, "queryUpdate");
272
+ fromItems: query.items,
273
+ pendingVersion
274
+ };
275
+ return items;
193
276
  }
194
- ensureStorageAdapter(name) {
195
- if (this.storageAdapters.has(name)) return;
196
- const adapter = this.options.storage(name);
197
- if (!adapter) return;
198
- 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 mergeChangesetIntoResult(served.items, query.selector, query.options, this.changesetForIds(collectionName, query, state.lastChange.ids));
199
285
  }
200
- async checkQueryUpdates(collectionName, changes) {
201
- const queries = this.queries.get(collectionName);
202
- if (!queries) throw new Error(`Collection ${collectionName} not initialized!`);
203
- if (changes.upserts.length === 0 && changes.deletes.length === 0) return;
204
- const affectedQueries = [...queries.values()].filter((query) => {
205
- const ids = this.queryItemIds(query);
206
- if (changes.deletes.some((id) => ids.has(id))) return true;
207
- return changes.upserts.some((item) => ids.has(item.id) || match(item, query.selector));
208
- });
209
- if (affectedQueries.length === 0) return;
210
- await Promise.all(affectedQueries.map(async (query) => {
211
- const { selector, options } = query;
212
- const previous = query.items;
213
- const incremental = previous == null ? null : incrementalQueryUpdate(previous, selector, options, changes);
214
- if (incremental != null) {
215
- const delta = diffQueryResults(previous, incremental);
216
- if (isEmptyQueryDelta(delta)) return;
217
- this.setQueryItems(query, incremental);
218
- 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);
219
295
  return;
220
296
  }
221
- this.emitQueryUpdate(collectionName, selector, options, "active", null);
222
- const queryItems = await this.executeQuery(collectionName, selector, options);
223
- if (previous == null) {
224
- this.setQueryItems(query, queryItems);
225
- this.emitQueryUpdate(collectionName, selector, options, "complete", null, queryItems);
297
+ if (pending?.deletes.has(id)) {
298
+ deletes.push(id);
226
299
  return;
227
300
  }
228
- const delta = diffQueryResults(previous, queryItems);
229
- this.setQueryItems(query, queryItems);
230
- this.emitQueryUpdate(collectionName, selector, options, "complete", null, void 0, delta);
231
- }));
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
+ };
232
309
  }
233
- registerCollection = async (collectionName, indices) => {
234
- this.collectionIndices.set(collectionName, indices);
235
- this.queries.set(collectionName, /* @__PURE__ */ new Map());
236
- this.ensureStorageAdapter(collectionName);
237
- const storageAdapter = this.storageAdapters.get(collectionName);
238
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
239
- const setupPromise = (async () => {
240
- await storageAdapter.setup();
241
- await Promise.all(indices.map((index) => storageAdapter.createIndex(index)));
242
- })();
243
- this.storageAdapterReady.set(collectionName, setupPromise);
244
- await setupPromise;
245
- };
246
- unregisterCollection = async (collectionName) => {
247
- this.storageAdapters.delete(collectionName);
248
- this.queries.delete(collectionName);
249
- };
250
- registerQuery = async (collectionName, selector, options) => {
251
- const query = this.ensureQuery(collectionName, selector, options);
252
- const queryItems = await this.executeQuery(collectionName, selector, options);
253
- this.setQueryItems(query, queryItems);
254
- this.emitQueryUpdate(collectionName, selector, options, "complete", null, queryItems);
255
- };
256
- unregisterQuery = async (collectionName, selector, options) => {
257
- const id = queryId(selector, options);
258
- if (!this.queries.get(collectionName)) throw new Error(`Collection ${collectionName} not initialized!`);
259
- this.queries.get(collectionName)?.delete(id);
260
- };
261
- insert = async (collectionName, input) => {
262
- const storageAdapter = this.storageAdapters.get(collectionName);
263
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
264
- const existingItems = await this.executeQuery(collectionName, { id: { $in: input.map((i) => i[0].id) } });
265
- const result = input.map(([item]) => {
266
- if (item.id == null) return /* @__PURE__ */ new Error("Item must have an id");
267
- if (existingItems.some((existing) => existing.id === item.id)) return /* @__PURE__ */ new Error(`Item with id ${item.id} already exists`);
268
- 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;
269
317
  });
270
- const newItems = result.filter((item) => !(item instanceof Error));
271
- await storageAdapter.insert(newItems);
272
- await this.checkQueryUpdates(collectionName, {
273
- upserts: newItems,
274
- 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 && match(item, query.selector)) affected = true;
275
322
  });
276
- return result;
277
- };
278
- updateOne = async (collectionName, parameters) => {
279
- const storageAdapter = this.storageAdapters.get(collectionName);
280
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
281
- const previousItems = [];
282
- const result = await Promise.all(parameters.map(async ([selector, modifier]) => {
283
- const item = await this.executeQuery(collectionName, selector, { limit: 1 }).then((items) => items[0] ?? null);
284
- const { $setOnInsert, ...restModifier } = modifier;
285
- if (item == null) return [];
286
- previousItems.push(item);
287
- const modifiedItem = modify(deepClone(item), restModifier);
288
- if (item.id !== modifiedItem.id) {
289
- if ((await this.executeQuery(collectionName, { id: modifiedItem.id }, { limit: 1 })).length > 0) return /* @__PURE__ */ new Error(`Item with id ${modifiedItem.id} already exists`);
290
- }
291
- return [modifiedItem];
292
- }));
293
- const modifiedItems = compact(result.filter((item) => !(item instanceof Error)).flat());
294
- if (modifiedItems.length > 0) {
295
- await storageAdapter.replace(modifiedItems);
296
- await this.checkQueryUpdates(collectionName, toChangeset(previousItems, modifiedItems));
297
- }
298
- return result;
299
- };
300
- updateMany = async (collectionName, parameters) => {
301
- const storageAdapter = this.storageAdapters.get(collectionName);
302
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
303
- const previousItems = [];
304
- const result = await Promise.all(parameters.map(async ([selector, modifier]) => {
305
- const items = await this.executeQuery(collectionName, selector);
306
- if (items.length === 0) return [];
307
- const { $setOnInsert, ...restModifier } = modifier;
308
- try {
309
- return await Promise.all(items.map(async (item) => {
310
- const modifiedItem = modify(deepClone(item), restModifier);
311
- if (item.id !== modifiedItem.id) {
312
- if ((await this.executeQuery(collectionName, { id: modifiedItem.id }, { limit: 1 })).length > 0) throw new Error(`Item with id ${modifiedItem.id} already exists`);
313
- }
314
- previousItems.push(item);
315
- return modifiedItem;
316
- }));
317
- } catch (error) {
318
- return error;
319
- }
320
- }));
321
- const modifiedItems = compact(result.filter((item) => !(item instanceof Error)).flat());
322
- if (modifiedItems.length > 0) {
323
- await storageAdapter.replace(modifiedItems);
324
- await this.checkQueryUpdates(collectionName, toChangeset(previousItems, modifiedItems));
325
- }
326
- return result;
327
- };
328
- replaceOne = async (collectionName, parameters) => {
329
- const storageAdapter = this.storageAdapters.get(collectionName);
330
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
331
- const previousItems = [];
332
- const result = await Promise.all(parameters.map(async ([selector, replacement]) => {
333
- const item = await this.executeQuery(collectionName, selector, { limit: 1 }).then((items) => items[0] ?? null);
334
- if (item == null) return [];
335
- previousItems.push(item);
336
- const modifiedItem = {
337
- ...replacement,
338
- id: replacement.id ?? item.id
323
+ if (!affected) return query.items;
324
+ return 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 = 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]
339
382
  };
340
- if (item.id !== modifiedItem.id) {
341
- if ((await this.executeQuery(collectionName, { id: modifiedItem.id }, { limit: 1 })).length > 0) return /* @__PURE__ */ new Error(`Item with id ${modifiedItem.id} already exists`);
342
- }
343
- return [modifiedItem];
344
- }));
345
- const modifiedItems = compact(result.filter((item) => !(item instanceof Error)).flat());
346
- if (modifiedItems.length > 0) {
347
- await storageAdapter.replace(modifiedItems);
348
- await this.checkQueryUpdates(collectionName, toChangeset(previousItems, modifiedItems));
349
- }
350
- return result;
351
- };
352
- removeOne = async (collectionName, selectors) => {
353
- const storageAdapter = this.storageAdapters.get(collectionName);
354
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
355
- const result = await Promise.all(selectors.map(async ([selector]) => {
356
- const item = await this.executeQuery(collectionName, selector, { limit: 1 }).then((items) => items[0] ?? null);
357
- if (item == null) return [];
358
- return [item];
359
- }));
360
- const items = result.flat();
361
- if (items.length > 0) {
362
- await storageAdapter.remove(items);
363
- await this.checkQueryUpdates(collectionName, {
364
- upserts: [],
365
- deletes: items.map((item) => item.id)
366
- });
367
- }
368
- return result;
369
- };
370
- removeMany = async (collectionName, selectors) => {
371
- const storageAdapter = this.storageAdapters.get(collectionName);
372
- if (!storageAdapter) throw new Error(`No persistence adapter for collection ${collectionName}`);
373
- const result = await Promise.all(selectors.map(async ([selector]) => this.executeQuery(collectionName, selector)));
374
- const items = result.flat();
375
- if (items.length > 0) {
376
- await storageAdapter.remove(items);
377
- await this.checkQueryUpdates(collectionName, {
378
- upserts: [],
379
- 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;
380
393
  });
394
+ const nowMatches = upserts.some((item) => query.selector != null && match(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 = diffQueryResults(before, this.servedResult(collectionName, query));
408
+ if (isEmptyQueryDelta(delta)) return;
409
+ query.stateChangeCallbacks.forEach((callback) => 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) => match(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 = modify(deepClone(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();
381
451
  }
382
- return result;
383
- };
384
- isReady = async (collectionName) => {
385
- return this.storageAdapterReady.get(collectionName);
386
- };
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 = queryId(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, batchOnNextTick(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(queryId(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(queryId(selector, options));
540
+ this.execInBackground("unregisterQuery", collection.name, [selector, options]);
541
+ },
542
+ getQueryState: (selector, options) => {
543
+ return (this.queries[collection.name]?.get(queryId(selector, options)))?.state || "active";
544
+ },
545
+ getQueryError: (selector, options) => {
546
+ return (this.queries[collection.name]?.get(queryId(selector, options)))?.error || null;
547
+ },
548
+ getQueryResult: (selector, options) => {
549
+ const query = this.queries[collection.name]?.get(queryId(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(queryId(selector, options))?.stateChangeCallbacks || [], callback] });
558
+ return () => {
559
+ const currentCallbacks = this.queries[collection.name]?.get(queryId(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
+ }
387
578
  };
388
579
  //#endregion
389
- export { WorkerDataAdapterHost as default };
580
+ export { WorkerDataAdapter as default };