@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
@@ -0,0 +1,484 @@
1
+ import isEqual from "./index2.mjs";
2
+ import deepClone from "./index18.mjs";
3
+ import match from "./index19.mjs";
4
+ import modify from "./index20.mjs";
5
+ import queryId from "./index26.mjs";
6
+ import executeStorageQuery from "./index35.mjs";
7
+ //#region src/AutoFetchDataAdapter.ts
8
+ /**
9
+ * Default merge strategy: shallow spread (right wins)
10
+ * @param a first item
11
+ * @param b second item
12
+ * @returns merged item
13
+ */
14
+ function defaultMergeItems(a, b) {
15
+ return {
16
+ ...a,
17
+ ...b
18
+ };
19
+ }
20
+ /**
21
+ * Generates a stable key for a selector
22
+ * @param selector - the selector
23
+ * @returns the key
24
+ */
25
+ function selectorId(selector) {
26
+ return JSON.stringify(selector ?? {});
27
+ }
28
+ /**
29
+ * AutoFetchDataAdapter
30
+ *
31
+ * A DataAdapter that:
32
+ * - Mirrors the CollectionBackend surface (CRUD + query registry + lifecycle)
33
+ * - Executes queries against a provided StorageAdapter (local cache)
34
+ * - On first registration of a selector, auto-fetches from a remote source and
35
+ * ingests the result into storage (upsert), then pushes query result updates
36
+ * - Optionally purges auto-fetched items for a selector once no observers remain
37
+ * - Can subscribe to remote change notifications to re-fetch active selectors
38
+ *
39
+ * IMPORTANT: Purging only ever deletes items that were introduced via the
40
+ * auto-fetch path and are no longer referenced by any active selector. Items
41
+ * inserted through CRUD calls are never purged.
42
+ */
43
+ var AutoFetchDataAdapter = class {
44
+ options;
45
+ id;
46
+ onError;
47
+ fetchQueryItems;
48
+ mergeItems;
49
+ purgeDelay;
50
+ storageAdapters = /* @__PURE__ */ new Map();
51
+ storageAdapterReady = /* @__PURE__ */ new Map();
52
+ collectionIndices = /* @__PURE__ */ new Map();
53
+ queries = /* @__PURE__ */ new Map();
54
+ activeObservers = /* @__PURE__ */ new Map();
55
+ observerTimeouts = /* @__PURE__ */ new Map();
56
+ selectorIds = /* @__PURE__ */ new Map();
57
+ idRefCounts = /* @__PURE__ */ new Map();
58
+ autoloadIds = /* @__PURE__ */ new Map();
59
+ constructor(options) {
60
+ this.options = options;
61
+ this.id = options.id || "autofetch-data-adapter";
62
+ this.onError = options.onError ?? ((error) => {
63
+ console.error(error);
64
+ });
65
+ this.fetchQueryItems = options.fetchQueryItems;
66
+ this.mergeItems = options.mergeItems ?? defaultMergeItems;
67
+ this.purgeDelay = options.purgeDelay ?? 1e4;
68
+ if (options.registerRemoteChange) options.registerRemoteChange(async () => {
69
+ await this.forceRefetchAll();
70
+ });
71
+ }
72
+ createCollectionBackend(collection, indices) {
73
+ this.collectionIndices.set(collection.name, indices);
74
+ this.queries.set(collection.name, /* @__PURE__ */ new Map());
75
+ this.ensureStorageAdapter(collection.name);
76
+ this.activeObservers.set(collection.name, /* @__PURE__ */ new Map());
77
+ this.observerTimeouts.set(collection.name, /* @__PURE__ */ new Map());
78
+ this.selectorIds.set(collection.name, /* @__PURE__ */ new Map());
79
+ this.idRefCounts.set(collection.name, /* @__PURE__ */ new Map());
80
+ this.autoloadIds.set(collection.name, /* @__PURE__ */ new Set());
81
+ const ready = this.setupStorage(collection.name, indices);
82
+ this.storageAdapterReady.set(collection.name, ready);
83
+ const registerQuery = (selector, options) => {
84
+ const qid = queryId(selector, options);
85
+ const registry = this.queries.get(collection.name);
86
+ if (!registry) throw new Error(`Collection ${collection.name} not initialized!`);
87
+ registry.set(qid, {
88
+ selector,
89
+ options,
90
+ items: [],
91
+ listeners: /* @__PURE__ */ new Set(),
92
+ ...registry.get(qid),
93
+ state: "active",
94
+ error: null
95
+ });
96
+ const key = selectorId(selector);
97
+ const perColObservers = this.activeObservers.get(collection.name);
98
+ const current = perColObservers?.get(key)?.count ?? 0;
99
+ perColObservers?.set(key, {
100
+ selector,
101
+ count: current + 1
102
+ });
103
+ const t = this.observerTimeouts.get(collection.name)?.get(key);
104
+ if (t) clearTimeout(t);
105
+ if (current === 0) this.fetchAndIngest(collection.name, selector).catch(this.onError);
106
+ this.fulfillQuery(collection.name, selector, options).catch(this.onError);
107
+ };
108
+ const unregisterQuery = (selector, options) => {
109
+ const qid = queryId(selector, options);
110
+ this.queries.get(collection.name)?.delete(qid);
111
+ const key = selectorId(selector);
112
+ const perColObservers = this.activeObservers.get(collection.name);
113
+ const current = perColObservers?.get(key)?.count ?? 0;
114
+ const remaining = Math.max(0, current - 1);
115
+ if (remaining > 0) {
116
+ perColObservers?.set(key, {
117
+ selector,
118
+ count: remaining
119
+ });
120
+ return;
121
+ }
122
+ const doPurge = () => {
123
+ perColObservers?.delete(key);
124
+ this.purgeSelector(collection.name, selector).catch(this.onError);
125
+ };
126
+ if (this.purgeDelay === 0) doPurge();
127
+ else {
128
+ const timeouts = this.observerTimeouts.get(collection.name);
129
+ const t = timeouts?.get(key);
130
+ if (t) clearTimeout(t);
131
+ timeouts?.set(key, setTimeout(doPurge, this.purgeDelay));
132
+ }
133
+ };
134
+ const getQueryState = (selector, options) => {
135
+ return (this.queries.get(collection.name)?.get(queryId(selector, options)))?.state ?? "active";
136
+ };
137
+ const getQueryError = (selector, options) => {
138
+ return (this.queries.get(collection.name)?.get(queryId(selector, options)))?.error ?? null;
139
+ };
140
+ const getQueryResult = (selector, options) => {
141
+ return (this.queries.get(collection.name)?.get(queryId(selector, options)))?.items ?? [];
142
+ };
143
+ const onQueryStateChange = (selector, options, callback) => {
144
+ const qid = queryId(selector, options);
145
+ const registry = this.queries.get(collection.name);
146
+ if (!registry) throw new Error(`Collection ${collection.name} not initialized!`);
147
+ if (!registry.has(qid)) registry.set(qid, {
148
+ selector,
149
+ options,
150
+ state: "active",
151
+ error: null,
152
+ items: [],
153
+ listeners: /* @__PURE__ */ new Set()
154
+ });
155
+ registry.get(qid)?.listeners.add(callback);
156
+ return () => {
157
+ registry.get(qid)?.listeners.delete(callback);
158
+ };
159
+ };
160
+ return {
161
+ insert: async (item) => {
162
+ await ready;
163
+ return await this.insert(collection.name, item);
164
+ },
165
+ updateOne: async (selector, modifier) => {
166
+ await ready;
167
+ return this.updateOne(collection.name, selector, modifier);
168
+ },
169
+ updateMany: async (selector, modifier) => {
170
+ await ready;
171
+ return this.updateMany(collection.name, selector, modifier);
172
+ },
173
+ replaceOne: async (selector, replacement) => {
174
+ await ready;
175
+ return this.replaceOne(collection.name, selector, replacement);
176
+ },
177
+ removeOne: async (selector) => {
178
+ await ready;
179
+ return this.removeOne(collection.name, selector);
180
+ },
181
+ removeMany: async (selector) => {
182
+ await ready;
183
+ return this.removeMany(collection.name, selector);
184
+ },
185
+ registerQuery,
186
+ unregisterQuery,
187
+ getQueryState,
188
+ getQueryError,
189
+ getQueryResult,
190
+ onQueryStateChange,
191
+ executeQuery: async (selector, options) => {
192
+ await ready;
193
+ registerQuery(selector, options);
194
+ await new Promise((resolve) => {
195
+ let stop = () => {};
196
+ stop = onQueryStateChange(selector, options, (state) => {
197
+ if (state === "active") return;
198
+ resolve();
199
+ stop();
200
+ });
201
+ });
202
+ const result = getQueryResult(selector, options);
203
+ unregisterQuery(selector, options);
204
+ return result;
205
+ },
206
+ dispose: async () => {
207
+ this.storageAdapters.delete(collection.name);
208
+ this.queries.delete(collection.name);
209
+ this.collectionIndices.delete(collection.name);
210
+ this.storageAdapterReady.delete(collection.name);
211
+ this.activeObservers.delete(collection.name);
212
+ this.observerTimeouts.delete(collection.name);
213
+ this.selectorIds.delete(collection.name);
214
+ this.idRefCounts.delete(collection.name);
215
+ this.autoloadIds.delete(collection.name);
216
+ },
217
+ isReady: async () => {
218
+ await ready;
219
+ }
220
+ };
221
+ }
222
+ async forceRefetchAll() {
223
+ const tasks = [];
224
+ for (const [collectionName, observers] of this.activeObservers.entries()) for (const { selector, count } of observers.values()) if (count > 0) tasks.push(this.fetchAndIngest(collectionName, selector));
225
+ await Promise.all(tasks);
226
+ }
227
+ async fetchAndIngest(collectionName, selector) {
228
+ this.publishForSelector(collectionName, selector, "active", null);
229
+ try {
230
+ const items = await this.fetchQueryItems(collectionName, selector);
231
+ if (!items || !Array.isArray(items)) throw new Error("AutoFetchDataAdapter: fetchQueryItems must resolve to { items: T[] }");
232
+ const ids = items.map((i) => i.id);
233
+ const selectorKey = selectorId(selector);
234
+ const selMap = this.selectorIds.get(collectionName);
235
+ const previous = selMap?.get(selectorKey) ?? /* @__PURE__ */ new Set();
236
+ ids.forEach((id) => previous.add(id));
237
+ selMap?.set(selectorKey, previous);
238
+ await this.upsertMerged(collectionName, items);
239
+ await this.checkQueryUpdates(collectionName, items);
240
+ } catch (error) {
241
+ this.publishForSelector(collectionName, selector, "error", error);
242
+ this.onError(error);
243
+ }
244
+ }
245
+ async purgeSelector(collectionName, selector) {
246
+ const selectorKey = selectorId(selector);
247
+ const selMap = this.selectorIds.get(collectionName);
248
+ const ids = selMap?.get(selectorKey);
249
+ selMap?.delete(selectorKey);
250
+ if (!ids || ids.size === 0) return;
251
+ const referenceMap = this.idRefCounts.get(collectionName);
252
+ const autoload = this.autoloadIds.get(collectionName);
253
+ const toRemove = [];
254
+ for (const id of ids) {
255
+ const current = referenceMap?.get(id) ?? 0;
256
+ const next = Math.max(0, current - 1);
257
+ if (next === 0) {
258
+ referenceMap?.delete(id);
259
+ if (autoload?.has(id)) toRemove.push(id);
260
+ } else referenceMap?.set(id, next);
261
+ }
262
+ if (toRemove.length === 0) return;
263
+ const storage = this.storageAdapters.get(collectionName);
264
+ if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
265
+ const items = await storage.readIds(toRemove);
266
+ if (items.length > 0) {
267
+ await storage.remove(items);
268
+ await this.checkQueryUpdates(collectionName, items);
269
+ }
270
+ toRemove.forEach((id) => autoload?.delete(id));
271
+ }
272
+ async setupStorage(collectionName, indices) {
273
+ const storage = this.storageAdapters.get(collectionName);
274
+ if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
275
+ await storage.setup();
276
+ await Promise.all(indices.map((field) => storage.createIndex(field)));
277
+ }
278
+ ensureStorageAdapter(name) {
279
+ if (this.storageAdapters.has(name)) return;
280
+ const adapter = this.options.storage && this.options.storage(name);
281
+ if (!adapter) return;
282
+ this.storageAdapters.set(name, adapter);
283
+ }
284
+ publishForSelector(collectionName, selector, state, error) {
285
+ const registry = this.queries.get(collectionName);
286
+ if (!registry) return;
287
+ for (const query of registry.values()) {
288
+ if (!isEqual(query.selector, selector)) continue;
289
+ this.publishState(collectionName, queryId(query.selector, query.options), state, error);
290
+ }
291
+ }
292
+ publishState(collectionName, qid, state, error) {
293
+ const query = this.queries.get(collectionName)?.get(qid);
294
+ if (!query) return;
295
+ query.state = state;
296
+ query.error = error;
297
+ for (const callback of query.listeners) try {
298
+ callback(state);
299
+ } catch (error_) {
300
+ this.onError(error_);
301
+ }
302
+ }
303
+ publishResult(collectionName, qid, items) {
304
+ const query = this.queries.get(collectionName)?.get(qid);
305
+ if (!query) return;
306
+ query.items = items;
307
+ this.queries.get(collectionName)?.set(qid, query);
308
+ }
309
+ async fulfillQuery(collectionName, selector, options) {
310
+ const qid = queryId(selector, options);
311
+ const registry = this.queries.get(collectionName);
312
+ if (!registry) throw new Error(`Collection ${collectionName} not initialized!`);
313
+ if (!registry.get(qid)) return;
314
+ this.publishState(collectionName, qid, "active", null);
315
+ try {
316
+ const items = await this.executeQuery(collectionName, selector, options);
317
+ this.publishResult(collectionName, qid, items);
318
+ this.publishState(collectionName, qid, "complete", null);
319
+ } catch (error) {
320
+ this.publishState(collectionName, qid, "error", error);
321
+ }
322
+ }
323
+ /**
324
+ * Reads one query's result from the local storage adapter.
325
+ *
326
+ * Shares `executeStorageQuery` with the other adapters — the projection this
327
+ * used to do by hand was `projectItems` spelled out, so nothing about the
328
+ * result changes.
329
+ * @template T - The type of the items.
330
+ * @template I - The type of the item ids.
331
+ * @param collectionName - The collection to read from.
332
+ * @param selector - The query's selector.
333
+ * @param options - The query's sort, window and projection.
334
+ * @returns The query result.
335
+ */
336
+ async executeQuery(collectionName, selector, options) {
337
+ const storage = this.storageAdapters.get(collectionName);
338
+ if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
339
+ return executeStorageQuery(storage, this.collectionIndices.get(collectionName) ?? [], selector, options);
340
+ }
341
+ async checkQueryUpdates(collectionName, changedItems) {
342
+ const registry = this.queries.get(collectionName);
343
+ if (!registry) throw new Error(`Collection ${collectionName} not initialized!`);
344
+ if (registry.size === 0) return;
345
+ const affected = [...registry.values()].filter(({ selector }) => changedItems.some((item) => match(item, selector)));
346
+ if (affected.length === 0) return;
347
+ for (const { selector, options } of affected) {
348
+ const qid = queryId(selector, options);
349
+ this.publishState(collectionName, qid, "active", null);
350
+ }
351
+ await Promise.all(affected.map(async ({ selector, options }) => {
352
+ const qid = queryId(selector, options);
353
+ try {
354
+ const items = await this.executeQuery(collectionName, selector, options);
355
+ this.publishResult(collectionName, qid, items);
356
+ this.publishState(collectionName, qid, "complete", null);
357
+ } catch (error) {
358
+ this.publishState(collectionName, qid, "error", error);
359
+ }
360
+ }));
361
+ }
362
+ async insert(collectionName, newItem) {
363
+ const storage = this.storageAdapters.get(collectionName);
364
+ if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
365
+ if ((await this.executeQuery(collectionName, { id: newItem.id }, { limit: 1 })).length > 0) throw new Error(`Item with id ${String(newItem.id)} already exists`);
366
+ await storage.insert([newItem]);
367
+ await this.checkQueryUpdates(collectionName, [newItem]);
368
+ return newItem;
369
+ }
370
+ async updateOne(collectionName, selector, modifier) {
371
+ const storage = this.storageAdapters.get(collectionName);
372
+ if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
373
+ const [item] = await this.executeQuery(collectionName, selector, { limit: 1 });
374
+ const { $setOnInsert, ...rest } = modifier;
375
+ if (item == null) return [];
376
+ const modified = modify(deepClone(item), rest);
377
+ if (item.id !== modified.id) {
378
+ if ((await this.executeQuery(collectionName, { id: modified.id }, { limit: 1 })).length > 0) throw new Error(`Item with id ${String(modified.id)} already exists`);
379
+ }
380
+ await storage.replace([modified]);
381
+ await this.checkQueryUpdates(collectionName, [item, modified]);
382
+ return {
383
+ items: [modified],
384
+ previousItems: [item]
385
+ };
386
+ }
387
+ async updateMany(collectionName, selector, modifier) {
388
+ const storage = this.storageAdapters.get(collectionName);
389
+ if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
390
+ const items = await this.executeQuery(collectionName, selector);
391
+ if (items.length === 0) return [];
392
+ const { $setOnInsert, ...rest } = modifier;
393
+ const changed = await Promise.all(items.map(async (item) => {
394
+ const modified = modify(deepClone(item), rest);
395
+ if (item.id !== modified.id) {
396
+ if ((await this.executeQuery(collectionName, { id: modified.id }, { limit: 1 })).length > 0) throw new Error(`Item with id ${String(modified.id)} already exists`);
397
+ }
398
+ return modified;
399
+ }));
400
+ await storage.replace(changed);
401
+ await this.checkQueryUpdates(collectionName, [...items, ...changed]);
402
+ return {
403
+ items: changed,
404
+ previousItems: items
405
+ };
406
+ }
407
+ async replaceOne(collectionName, selector, replacement) {
408
+ const storage = this.storageAdapters.get(collectionName);
409
+ if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
410
+ const [item] = await this.executeQuery(collectionName, selector, { limit: 1 });
411
+ if (item == null) return [];
412
+ const modified = {
413
+ ...replacement,
414
+ id: replacement.id ?? item.id
415
+ };
416
+ if (item.id !== modified.id) {
417
+ if ((await this.executeQuery(collectionName, { id: modified.id }, { limit: 1 })).length > 0) throw new Error(`Item with id ${String(modified.id)} already exists`);
418
+ }
419
+ await storage.replace([modified]);
420
+ await this.checkQueryUpdates(collectionName, [item, modified]);
421
+ return {
422
+ items: [modified],
423
+ previousItems: [item]
424
+ };
425
+ }
426
+ async removeOne(collectionName, selector) {
427
+ const storage = this.storageAdapters.get(collectionName);
428
+ if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
429
+ const [item] = await this.executeQuery(collectionName, selector, { limit: 1 });
430
+ if (item == null) return [];
431
+ await storage.remove([item]);
432
+ await this.checkQueryUpdates(collectionName, [item]);
433
+ return [item];
434
+ }
435
+ async removeMany(collectionName, selector) {
436
+ const storage = this.storageAdapters.get(collectionName);
437
+ if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
438
+ const items = await this.executeQuery(collectionName, selector);
439
+ if (items.length === 0) return [];
440
+ await storage.remove(items);
441
+ await this.checkQueryUpdates(collectionName, items);
442
+ return items;
443
+ }
444
+ async upsertMerged(collectionName, incoming) {
445
+ if (incoming.length === 0) return;
446
+ const storage = this.storageAdapters.get(collectionName);
447
+ if (!storage) throw new Error(`No storage adapter for collection ${collectionName}`);
448
+ const ids = incoming.map((i) => i.id);
449
+ const existing = await storage.readIds(ids);
450
+ const existingById = new Map(existing.map((it) => [it.id, it]));
451
+ const toInsert = [];
452
+ const toReplace = [];
453
+ const referenceMap = this.idRefCounts.get(collectionName);
454
+ const autoload = this.autoloadIds.get(collectionName);
455
+ for (const item of incoming) {
456
+ const previous = existingById.get(item.id);
457
+ if (previous) toReplace.push(this.mergeItems(previous, item));
458
+ else toInsert.push(item);
459
+ autoload?.add(item.id);
460
+ referenceMap?.set(item.id, (referenceMap?.get(item.id) ?? 0) + 1);
461
+ }
462
+ if (toInsert.length > 0) await storage.insert(toInsert);
463
+ if (toReplace.length > 0) await storage.replace(toReplace);
464
+ }
465
+ };
466
+ /**
467
+ * Usage (example):
468
+ *
469
+ * const adapter = new AutoFetchDataAdapter({
470
+ * storage: (name) => new IndexedDBStorage(name),
471
+ * fetchQueryItems: async (collectionName, selector) => {
472
+ * const res = await fetch(`/api/${collectionName}?q=${encodeURIComponent(JSON.stringify(selector||{}))}`)
473
+ * const items = await res.json()
474
+ * return { items }
475
+ * },
476
+ * registerRemoteChange: (onChange) => subscribeToWS(onChange),
477
+ * mergeItems: (a, b) => ({ ...a, ...b }),
478
+ * purgeDelay: 10_000,
479
+ * })
480
+ *
481
+ * const backend = adapter.createCollectionBackend(myCollection, ['status', 'projectId'])
482
+ */
483
+ //#endregion
484
+ export { AutoFetchDataAdapter as default };
@@ -103,12 +103,12 @@ var Observer = class {
103
103
  return;
104
104
  }
105
105
  const beforeOf = (index) => nextItems[index + 1] || null;
106
- const previousById = delta.removed.length > 0 && this.hasCallbacks(["removed"]) || delta.changed.length > 0 && this.hasCallbacks(["changedField"]) ? new Map(this.previousItems.map((item) => [item.id, item])) : null;
106
+ const previousById = delta.removed.length > 0 && this.hasCallbacks(["removed"]) || delta.changed.length > 0 && this.hasCallbacks(["changed", "changedField"]) ? new Map(this.previousItems.map((item) => [item.id, item])) : null;
107
107
  if (this.hasCallbacks(["changed", "changedField"])) delta.changed.forEach((item) => {
108
- this.call("changed", item);
109
- if (!this.hasCallbacks(["changedField"])) return;
110
108
  const oldItem = previousById?.get(item.id);
111
109
  if (!oldItem) return;
110
+ this.call("changed", item, oldItem);
111
+ if (!this.hasCallbacks(["changedField"])) return;
112
112
  require_uniqueBy.default([...Object.keys(item), ...Object.keys(oldItem)], (value) => value).forEach((key) => {
113
113
  if (require_isEqual.default(item[key], oldItem[key])) return;
114
114
  this.call("changedField", item, key, oldItem[key], item[key]);
package/dist/index5.mjs CHANGED
@@ -103,12 +103,12 @@ var Observer = class {
103
103
  return;
104
104
  }
105
105
  const beforeOf = (index) => nextItems[index + 1] || null;
106
- const previousById = delta.removed.length > 0 && this.hasCallbacks(["removed"]) || delta.changed.length > 0 && this.hasCallbacks(["changedField"]) ? new Map(this.previousItems.map((item) => [item.id, item])) : null;
106
+ const previousById = delta.removed.length > 0 && this.hasCallbacks(["removed"]) || delta.changed.length > 0 && this.hasCallbacks(["changed", "changedField"]) ? new Map(this.previousItems.map((item) => [item.id, item])) : null;
107
107
  if (this.hasCallbacks(["changed", "changedField"])) delta.changed.forEach((item) => {
108
- this.call("changed", item);
109
- if (!this.hasCallbacks(["changedField"])) return;
110
108
  const oldItem = previousById?.get(item.id);
111
109
  if (!oldItem) return;
110
+ this.call("changed", item, oldItem);
111
+ if (!this.hasCallbacks(["changedField"])) return;
112
112
  uniqueBy([...Object.keys(item), ...Object.keys(oldItem)], (value) => value).forEach((key) => {
113
113
  if (isEqual(item[key], oldItem[key])) return;
114
114
  this.call("changedField", item, key, oldItem[key], item[key]);
@@ -301,3 +301,4 @@ var Cursor = class {
301
301
  };
302
302
  //#endregion
303
303
  exports.default = Cursor;
304
+ exports.isInReactiveScope = isInReactiveScope;
package/dist/index6.mjs CHANGED
@@ -300,4 +300,4 @@ var Cursor = class {
300
300
  }
301
301
  };
302
302
  //#endregion
303
- export { Cursor as default };
303
+ export { Cursor as default, isInReactiveScope };
@@ -1,8 +1,63 @@
1
+ import { BaseItem, FieldSpecifier, SortSpecifier } from '../Collection';
2
+ import { default as Selector } from './Selector';
1
3
  export interface Changeset<T> {
2
4
  added: T[];
3
5
  modified: T[];
4
6
  removed: T[];
5
7
  }
8
+ /**
9
+ * What a caller wants answered, in one piece.
10
+ *
11
+ * The same four options a query carries (`DataAdapter`'s `QueryOptions`) plus
12
+ * the selector, so an adapter that can push some of them down to its backing
13
+ * store sees the whole question rather than a filter at a time.
14
+ */
15
+ export interface StorageQuery<T extends BaseItem> {
16
+ selector: Selector<T>;
17
+ sort?: SortSpecifier<T> | undefined;
18
+ skip?: number | undefined;
19
+ limit?: number | undefined;
20
+ fields?: FieldSpecifier<T> | undefined;
21
+ }
22
+ /**
23
+ * What an adapter answered, and how much of the question it actually took on.
24
+ *
25
+ * Every flag defaults to "no": an adapter says only what it did, and the
26
+ * caller does the rest in JavaScript, exactly as it does for an adapter with
27
+ * no `query` at all. That is what makes partial support the normal case rather
28
+ * than a special one — an adapter may translate an equality and decline a
29
+ * `$regex`, or sort but not window, and never has to understand a selector it
30
+ * does not recognise.
31
+ *
32
+ * Three combinations are contradictions rather than choices, and
33
+ * `executeStorageQuery` throws on them rather than returning a wrong result
34
+ * quietly:
35
+ *
36
+ * - `windowed` without having applied the whole selector — the rows it dropped
37
+ * at the window's edge may be rows the caller was going to filter out, so the
38
+ * window is over the wrong set and nothing can repair it.
39
+ * - `windowed` without `sorted`, when a sort was asked for — the same, one step
40
+ * earlier: a window over an unordered set is an arbitrary subset.
41
+ * - `projected` without `sorted`, when the sort is on a field the projection
42
+ * dropped — the caller is then asked to sort by something that is no longer
43
+ * there. This is the trap `incrementalQueryUpdate` already documents.
44
+ */
45
+ export interface StorageQueryAnswer<T extends BaseItem> {
46
+ items: T[];
47
+ /**
48
+ * The part of the selector the adapter could *not* apply, for the caller to
49
+ * evaluate over `items`. Omit it, or give `{}`, to say the whole selector was
50
+ * applied. This mirrors `getIndexInfo`'s `optimizedSelector`: an adapter
51
+ * narrows as far as it can and hands back what is left.
52
+ */
53
+ residualSelector?: Selector<T>;
54
+ /** The items are in the requested order. */
55
+ sorted?: boolean;
56
+ /** `skip` and `limit` have already been applied. */
57
+ windowed?: boolean;
58
+ /** The items already carry only the requested fields. */
59
+ projected?: boolean;
60
+ }
6
61
  export default interface StorageAdapter<T extends {
7
62
  id: I;
8
63
  } & Record<string, any>, I> {
@@ -10,6 +65,28 @@ export default interface StorageAdapter<T extends {
10
65
  teardown(): Promise<void>;
11
66
  readAll(): Promise<T[]>;
12
67
  readIds(positions: I[]): Promise<T[]>;
68
+ /**
69
+ * Answers a whole query — predicate, order, window and projection — as far as
70
+ * the backing store can.
71
+ *
72
+ * **Optional, and a pure optimisation.** An adapter that does not implement
73
+ * it is read through `readIds`/`readAll` and filtered, sorted, windowed and
74
+ * projected in JavaScript, which is what every adapter did before this
75
+ * existed and what several of them can only ever do: a store that holds one
76
+ * blob — `localstorage`, `fs`, `opfs` — has no way to answer less than all of
77
+ * it, and implementing this to do the same work behind a new name would buy
78
+ * nothing.
79
+ *
80
+ * It exists because without it there is no way to *express* a bounded read.
81
+ * `limit` costs exactly what no limit costs when the store hands over every
82
+ * matching row first, so nobody writes one, and a query over a table that
83
+ * grows with a user's history stays proportional to that history however
84
+ * carefully its consumer is written.
85
+ *
86
+ * An adapter may always answer less than it was asked; see
87
+ * `StorageQueryAnswer` for what it must not claim.
88
+ */
89
+ query?(query: StorageQuery<T>): Promise<StorageQueryAnswer<T>>;
13
90
  createIndex(field: string): Promise<void>;
14
91
  dropIndex(field: string): Promise<void>;
15
92
  /**
@@ -19,6 +96,9 @@ export default interface StorageAdapter<T extends {
19
96
  * makes `3`, `'3'` and `new Date(...)` comparable as map keys at all. An
20
97
  * adapter that stores its backend's own keys instead answers nothing for
21
98
  * every non-string field, and everything for a `$ne` on one.
99
+ *
100
+ * Never consulted for a query an adapter answered through `query` itself —
101
+ * that adapter's own store already did the narrowing this index exists for.
22
102
  */
23
103
  readIndex(field: string): Promise<Map<string | null, Set<I>>>;
24
104
  insert(items: T[]): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { BaseItem } from '../Collection';
2
+ import { default as Selector } from '../types/Selector';
3
+ import { default as StorageAdapter, StorageQuery } from '../types/StorageAdapter';
4
+ type QueryShape<T extends BaseItem> = Omit<StorageQuery<T>, 'selector'>;
5
+ /**
6
+ * Runs one query against a storage adapter and returns its finished result.
7
+ *
8
+ * This is the single place a query becomes rows. It used to be three copies — `AsyncDataAdapter`,
9
+ * `AutoFetchDataAdapter` and `WorkerDataAdapterHost` each had the same eight lines — which is how a
10
+ * capability like `StorageAdapter#query` ends up supported in one of them and quietly missing from
11
+ * the other two. The copies had already drifted apart in one respect: each maintained its own
12
+ * primary-key fast path.
13
+ *
14
+ * Two paths, and the second is what every adapter had before:
15
+ *
16
+ * - the adapter answers `query` itself, and this applies whatever it declined;
17
+ * - it does not, and this reads through the index (or the whole store) and filters, sorts, windows
18
+ * and projects here.
19
+ * @template T - The type of the items.
20
+ * @template I - The type of the item ids.
21
+ * @param storageAdapter - The storage adapter to read from.
22
+ * @param indices - The fields this collection has declared indices for.
23
+ * @param selector - The query's selector. `null` matches nothing.
24
+ * @param options - The query's sort, window and projection.
25
+ * @returns The query's result.
26
+ */
27
+ export default function executeStorageQuery<T extends BaseItem<I>, I = any>(storageAdapter: StorageAdapter<any, any>, indices: string[], selector: Selector<T> | null, options?: QueryShape<T>): Promise<T[]>;
28
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaldb/core",
3
- "version": "2.0.0-beta.21",
3
+ "version": "2.0.0-beta.22",
4
4
  "description": "SignalDB is a client-side database that provides a simple MongoDB-like interface to the data with first-class typescript support to achieve an optimistic UI. Data persistence can be achieved by using storage providers that store the data through a JSON interface to places such as localStorage.",
5
5
  "scripts": {
6
6
  "build": "rimraf dist && vite build",