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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +3 -3
  2. package/dist/.vite/manifest.json +61 -59
  3. package/dist/AsyncDataAdapter.d.ts +15 -2
  4. package/dist/AutoFetchDataAdapter.d.ts +13 -2
  5. package/dist/Collection/Observer.d.ts +1 -1
  6. package/dist/Collection/index.d.ts +11 -12
  7. package/dist/DataAdapter.d.ts +19 -3
  8. package/dist/WorkerDataAdapterHost.d.ts +19 -6
  9. package/dist/index.cjs.js +4 -4
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.mjs +4 -4
  12. package/dist/index12.cjs.js +1 -1
  13. package/dist/index12.mjs +1 -1
  14. package/dist/index15.cjs.js +6 -2
  15. package/dist/index15.mjs +6 -2
  16. package/dist/index17.cjs.js +4 -2
  17. package/dist/index17.mjs +4 -3
  18. package/dist/index18.cjs.js +1 -0
  19. package/dist/index18.mjs +1 -1
  20. package/dist/index28.cjs.js +15 -5
  21. package/dist/index28.mjs +15 -5
  22. package/dist/index29.cjs.js +29 -16
  23. package/dist/index29.mjs +29 -16
  24. package/dist/index33.cjs.js +17 -49
  25. package/dist/index33.mjs +17 -49
  26. package/dist/index34.cjs.js +45 -445
  27. package/dist/index34.mjs +45 -445
  28. package/dist/index35.cjs.js +132 -65
  29. package/dist/index35.mjs +132 -65
  30. package/dist/index36.cjs.js +385 -531
  31. package/dist/index36.mjs +386 -533
  32. package/dist/index37.cjs.js +68 -17
  33. package/dist/index37.mjs +68 -17
  34. package/dist/index38.cjs.js +536 -319
  35. package/dist/index38.mjs +538 -321
  36. package/dist/index39.cjs.js +301 -466
  37. package/dist/index39.mjs +301 -466
  38. package/dist/index40.cjs.js +484 -0
  39. package/dist/index40.mjs +484 -0
  40. package/dist/index5.cjs.js +3 -3
  41. package/dist/index5.mjs +3 -3
  42. package/dist/index6.cjs.js +1 -0
  43. package/dist/index6.mjs +1 -1
  44. package/dist/types/StorageAdapter.d.ts +80 -0
  45. package/dist/utils/executeStorageQuery.d.ts +28 -0
  46. package/package.json +1 -1
@@ -1,72 +1,139 @@
1
- //#region src/utils/batchOnNextTick.ts
1
+ const require_isEqual = require("./index2.cjs.js");
2
+ const require_getIndexInfo = require("./index17.cjs.js");
3
+ const require_match = require("./index19.cjs.js");
4
+ const require_projectItems = require("./index23.cjs.js");
5
+ const require_sortItems = require("./index24.cjs.js");
6
+ const require_idIndexQuery = require("./index27.cjs.js");
7
+ const require_compact = require("./index33.cjs.js");
8
+ const require_storageIndexQuery = require("./index34.cjs.js");
9
+ //#region src/utils/executeStorageQuery.ts
2
10
  /**
3
- * Groups multiple calls by key and flushes them on the next tick (macrotask).
4
- * @param onFlush - Function that will be called with the key and all queued items when flushing.
5
- * @returns An object with `enqueue` and `flush` methods.
6
- * @example
7
- * const batcher = batchOnNextTick<string>(async (key, items) => {
8
- * // items is an array of { args, resolve, reject }
9
- * // do something once with all args...
10
- * })
11
+ * Runs one query against a storage adapter and returns its finished result.
11
12
  *
12
- * batcher.enqueue("my-key", [arg1, arg2])
13
+ * This is the single place a query becomes rows. It used to be three copies — `AsyncDataAdapter`,
14
+ * `AutoFetchDataAdapter` and `WorkerDataAdapterHost` each had the same eight lines — which is how a
15
+ * capability like `StorageAdapter#query` ends up supported in one of them and quietly missing from
16
+ * the other two. The copies had already drifted apart in one respect: each maintained its own
17
+ * primary-key fast path.
18
+ *
19
+ * Two paths, and the second is what every adapter had before:
20
+ *
21
+ * - the adapter answers `query` itself, and this applies whatever it declined;
22
+ * - it does not, and this reads through the index (or the whole store) and filters, sorts, windows
23
+ * and projects here.
24
+ * @template T - The type of the items.
25
+ * @template I - The type of the item ids.
26
+ * @param storageAdapter - The storage adapter to read from.
27
+ * @param indices - The fields this collection has declared indices for.
28
+ * @param selector - The query's selector. `null` matches nothing.
29
+ * @param options - The query's sort, window and projection.
30
+ * @returns The query's result.
13
31
  */
14
- function batchOnNextTick(onFlush) {
15
- const queues = /* @__PURE__ */ new Map();
16
- /**
17
- * Enqueue a call with the given key and arguments.
18
- * @param key key to group calls
19
- * @param args arguments for the call
20
- * @returns A promise that resolves or rejects when the call is flushed.
21
- */
22
- function enqueue(key, args) {
23
- return new Promise((resolve, reject) => {
24
- let q = queues.get(key);
25
- if (!q) {
26
- q = {
27
- timer: null,
28
- items: [],
29
- flush: () => flush(key)
30
- };
31
- queues.set(key, q);
32
- }
33
- q.items.push({
34
- args,
35
- resolve,
36
- reject
37
- });
38
- if (q.timer == null) q.timer = setTimeout(() => {
39
- q.timer = null;
40
- q.flush();
41
- }, 0);
42
- });
43
- }
44
- /**
45
- * Flush the queue for the given key immediately.
46
- * @param key key to flush
47
- * @returns A promise that resolves when the flush is complete.
48
- */
49
- async function flush(key) {
50
- const q = queues.get(key);
51
- if (!q || q.items.length === 0) return;
52
- if (q.timer != null) {
53
- clearTimeout(q.timer);
54
- q.timer = null;
55
- }
56
- const items = q.items.splice(0);
57
- onFlush(key, items.map((i) => i.args)).then((results) => {
58
- for (const [index, result] of results.entries()) {
59
- const { resolve } = items[index];
60
- resolve(result);
61
- }
62
- }).catch((error) => {
63
- for (const { reject } of items) reject(error);
64
- });
32
+ async function executeStorageQuery(storageAdapter, indices, selector, options) {
33
+ if (selector === null) return [];
34
+ const effectiveSelector = selector || {};
35
+ if (storageAdapter.query) return finishAnswer(await storageAdapter.query({
36
+ selector: effectiveSelector,
37
+ ...options
38
+ }), options);
39
+ return finishAnswer({ items: await readMatching(storageAdapter, indices, effectiveSelector) }, options);
40
+ }
41
+ /**
42
+ * Applies whatever the adapter did not, and refuses what it cannot have meant.
43
+ * @template T - The type of the items.
44
+ * @param answer - What the adapter answered, and how much of the question it took on.
45
+ * @param options - The query's sort, window and projection.
46
+ * @returns The finished result.
47
+ */
48
+ function finishAnswer(answer, options) {
49
+ const { sort, skip, limit, fields } = options || {};
50
+ const residual = answer.residualSelector;
51
+ const hasResidual = residual != null && Object.keys(residual).length > 0;
52
+ assertAnswerIsCoherent(answer, hasResidual, sort, fields);
53
+ const matched = hasResidual ? answer.items.filter((item) => require_match.default(item, residual)) : answer.items;
54
+ const sorted = sort && !answer.sorted ? require_sortItems.default(matched, sort) : matched;
55
+ const windowed = answer.windowed ? sorted : applyWindow(sorted, skip, limit);
56
+ return answer.projected ? windowed : require_projectItems.default(windowed, fields);
57
+ }
58
+ /**
59
+ * Fails on the three claims that are contradictions rather than choices.
60
+ *
61
+ * Each of them produces a result nothing downstream can repair, so this throws where the mistake
62
+ * is rather than serving a subset that looks like an answer.
63
+ * @template T - The type of the items.
64
+ * @param answer - What the adapter answered.
65
+ * @param hasResidual - Whether part of the selector was left to the caller.
66
+ * @param sort - The sort the query asked for, if any.
67
+ * @param fields - The projection the query asked for, if any.
68
+ */
69
+ function assertAnswerIsCoherent(answer, hasResidual, sort, fields) {
70
+ if (answer.windowed && hasResidual) throw new Error("StorageAdapter#query claimed `windowed` while leaving part of the selector unapplied; the window is over the wrong set");
71
+ if (answer.windowed && sort != null && !answer.sorted) throw new Error("StorageAdapter#query claimed `windowed` without `sorted` for a sorted query; the window is an arbitrary subset");
72
+ const sortIsStillOwed = sort != null && !answer.sorted;
73
+ if (answer.projected && sortIsStillOwed && !sortKeysSurviveProjection(sort, fields)) throw new Error("StorageAdapter#query claimed `projected` without `sorted`, and the projection drops a key the sort needs");
74
+ }
75
+ /**
76
+ * Applies `skip` and `limit`.
77
+ * @template T - The type of the items.
78
+ * @param items - The items to window.
79
+ * @param skip - How many to drop from the front.
80
+ * @param limit - How many to keep.
81
+ * @returns The windowed items.
82
+ */
83
+ function applyWindow(items, skip, limit) {
84
+ const skipped = skip ? items.slice(skip) : items;
85
+ return limit == null ? skipped : skipped.slice(0, limit);
86
+ }
87
+ /**
88
+ * Whether every key the sort is on survives the projection.
89
+ * @template T - The type of the items.
90
+ * @param sort - The query's sort.
91
+ * @param fields - The query's projection, if any.
92
+ * @returns `true` when the sort can still be applied to the projected items.
93
+ */
94
+ function sortKeysSurviveProjection(sort, fields) {
95
+ if (fields == null) return true;
96
+ const specKeys = Object.keys(fields);
97
+ const spec = fields;
98
+ const excluding = specKeys.length > 0 && specKeys.every((key) => !spec[key]);
99
+ const covers = (key) => specKeys.some((entry) => entry === key || key.startsWith(`${entry}.`));
100
+ return Object.keys(sort).every((key) => excluding ? !covers(key) : covers(key));
101
+ }
102
+ /**
103
+ * The pre-`query` read path: the primary key if the selector is one, then a declared index, then
104
+ * the whole store.
105
+ *
106
+ * The `id` fast path is not an optimisation on top of the index machinery, it is the only way a
107
+ * primary-key lookup works at all: `id` is never a *declared* index — `readIds` is exactly the
108
+ * lookup it describes — so a selector of `{ id: … }` matches no index and would otherwise fall
109
+ * through to `readAll`. All three adapters carried their own copy of it; losing it here turned
110
+ * every point read into a full scan, which is how it was noticed.
111
+ * @template T - The type of the items.
112
+ * @template I - The type of the item ids.
113
+ * @param storageAdapter - The storage adapter to read from.
114
+ * @param indices - The fields this collection has declared indices for.
115
+ * @param selector - The query's selector.
116
+ * @returns Every stored item the selector matches.
117
+ */
118
+ async function readMatching(storageAdapter, indices, selector) {
119
+ if (Object.keys(selector).length === 1 && "id" in selector) {
120
+ const idResult = require_idIndexQuery.default(selector);
121
+ if (idResult.matched) return storageAdapter.readIds(require_compact.default(idResult.ids));
65
122
  }
66
- return {
67
- enqueue,
68
- flush
123
+ const indexInfo = await require_getIndexInfo.default(indices.map((field) => require_storageIndexQuery.default(storageAdapter, field)), selector);
124
+ const matchItems = (item) => {
125
+ if (indexInfo.optimizedSelector == null) return true;
126
+ if (Object.keys(indexInfo.optimizedSelector).length <= 0) return true;
127
+ return require_match.default(item, indexInfo.optimizedSelector);
69
128
  };
129
+ if (indexInfo.matched) {
130
+ const items = await storageAdapter.readIds(indexInfo.ids);
131
+ if (require_isEqual.default(indexInfo.optimizedSelector, {})) return items;
132
+ return items.filter(matchItems);
133
+ }
134
+ const allItems = await storageAdapter.readAll();
135
+ if (require_isEqual.default(selector, {})) return allItems;
136
+ return allItems.filter(matchItems);
70
137
  }
71
138
  //#endregion
72
- exports.default = batchOnNextTick;
139
+ exports.default = executeStorageQuery;
package/dist/index35.mjs CHANGED
@@ -1,72 +1,139 @@
1
- //#region src/utils/batchOnNextTick.ts
1
+ import isEqual from "./index2.mjs";
2
+ import getIndexInfo from "./index17.mjs";
3
+ import match from "./index19.mjs";
4
+ import projectItems from "./index23.mjs";
5
+ import sortItems from "./index24.mjs";
6
+ import idIndexQuery from "./index27.mjs";
7
+ import compact from "./index33.mjs";
8
+ import storageIndexQuery from "./index34.mjs";
9
+ //#region src/utils/executeStorageQuery.ts
2
10
  /**
3
- * Groups multiple calls by key and flushes them on the next tick (macrotask).
4
- * @param onFlush - Function that will be called with the key and all queued items when flushing.
5
- * @returns An object with `enqueue` and `flush` methods.
6
- * @example
7
- * const batcher = batchOnNextTick<string>(async (key, items) => {
8
- * // items is an array of { args, resolve, reject }
9
- * // do something once with all args...
10
- * })
11
+ * Runs one query against a storage adapter and returns its finished result.
11
12
  *
12
- * batcher.enqueue("my-key", [arg1, arg2])
13
+ * This is the single place a query becomes rows. It used to be three copies — `AsyncDataAdapter`,
14
+ * `AutoFetchDataAdapter` and `WorkerDataAdapterHost` each had the same eight lines — which is how a
15
+ * capability like `StorageAdapter#query` ends up supported in one of them and quietly missing from
16
+ * the other two. The copies had already drifted apart in one respect: each maintained its own
17
+ * primary-key fast path.
18
+ *
19
+ * Two paths, and the second is what every adapter had before:
20
+ *
21
+ * - the adapter answers `query` itself, and this applies whatever it declined;
22
+ * - it does not, and this reads through the index (or the whole store) and filters, sorts, windows
23
+ * and projects here.
24
+ * @template T - The type of the items.
25
+ * @template I - The type of the item ids.
26
+ * @param storageAdapter - The storage adapter to read from.
27
+ * @param indices - The fields this collection has declared indices for.
28
+ * @param selector - The query's selector. `null` matches nothing.
29
+ * @param options - The query's sort, window and projection.
30
+ * @returns The query's result.
13
31
  */
14
- function batchOnNextTick(onFlush) {
15
- const queues = /* @__PURE__ */ new Map();
16
- /**
17
- * Enqueue a call with the given key and arguments.
18
- * @param key key to group calls
19
- * @param args arguments for the call
20
- * @returns A promise that resolves or rejects when the call is flushed.
21
- */
22
- function enqueue(key, args) {
23
- return new Promise((resolve, reject) => {
24
- let q = queues.get(key);
25
- if (!q) {
26
- q = {
27
- timer: null,
28
- items: [],
29
- flush: () => flush(key)
30
- };
31
- queues.set(key, q);
32
- }
33
- q.items.push({
34
- args,
35
- resolve,
36
- reject
37
- });
38
- if (q.timer == null) q.timer = setTimeout(() => {
39
- q.timer = null;
40
- q.flush();
41
- }, 0);
42
- });
43
- }
44
- /**
45
- * Flush the queue for the given key immediately.
46
- * @param key key to flush
47
- * @returns A promise that resolves when the flush is complete.
48
- */
49
- async function flush(key) {
50
- const q = queues.get(key);
51
- if (!q || q.items.length === 0) return;
52
- if (q.timer != null) {
53
- clearTimeout(q.timer);
54
- q.timer = null;
55
- }
56
- const items = q.items.splice(0);
57
- onFlush(key, items.map((i) => i.args)).then((results) => {
58
- for (const [index, result] of results.entries()) {
59
- const { resolve } = items[index];
60
- resolve(result);
61
- }
62
- }).catch((error) => {
63
- for (const { reject } of items) reject(error);
64
- });
32
+ async function executeStorageQuery(storageAdapter, indices, selector, options) {
33
+ if (selector === null) return [];
34
+ const effectiveSelector = selector || {};
35
+ if (storageAdapter.query) return finishAnswer(await storageAdapter.query({
36
+ selector: effectiveSelector,
37
+ ...options
38
+ }), options);
39
+ return finishAnswer({ items: await readMatching(storageAdapter, indices, effectiveSelector) }, options);
40
+ }
41
+ /**
42
+ * Applies whatever the adapter did not, and refuses what it cannot have meant.
43
+ * @template T - The type of the items.
44
+ * @param answer - What the adapter answered, and how much of the question it took on.
45
+ * @param options - The query's sort, window and projection.
46
+ * @returns The finished result.
47
+ */
48
+ function finishAnswer(answer, options) {
49
+ const { sort, skip, limit, fields } = options || {};
50
+ const residual = answer.residualSelector;
51
+ const hasResidual = residual != null && Object.keys(residual).length > 0;
52
+ assertAnswerIsCoherent(answer, hasResidual, sort, fields);
53
+ const matched = hasResidual ? answer.items.filter((item) => match(item, residual)) : answer.items;
54
+ const sorted = sort && !answer.sorted ? sortItems(matched, sort) : matched;
55
+ const windowed = answer.windowed ? sorted : applyWindow(sorted, skip, limit);
56
+ return answer.projected ? windowed : projectItems(windowed, fields);
57
+ }
58
+ /**
59
+ * Fails on the three claims that are contradictions rather than choices.
60
+ *
61
+ * Each of them produces a result nothing downstream can repair, so this throws where the mistake
62
+ * is rather than serving a subset that looks like an answer.
63
+ * @template T - The type of the items.
64
+ * @param answer - What the adapter answered.
65
+ * @param hasResidual - Whether part of the selector was left to the caller.
66
+ * @param sort - The sort the query asked for, if any.
67
+ * @param fields - The projection the query asked for, if any.
68
+ */
69
+ function assertAnswerIsCoherent(answer, hasResidual, sort, fields) {
70
+ if (answer.windowed && hasResidual) throw new Error("StorageAdapter#query claimed `windowed` while leaving part of the selector unapplied; the window is over the wrong set");
71
+ if (answer.windowed && sort != null && !answer.sorted) throw new Error("StorageAdapter#query claimed `windowed` without `sorted` for a sorted query; the window is an arbitrary subset");
72
+ const sortIsStillOwed = sort != null && !answer.sorted;
73
+ if (answer.projected && sortIsStillOwed && !sortKeysSurviveProjection(sort, fields)) throw new Error("StorageAdapter#query claimed `projected` without `sorted`, and the projection drops a key the sort needs");
74
+ }
75
+ /**
76
+ * Applies `skip` and `limit`.
77
+ * @template T - The type of the items.
78
+ * @param items - The items to window.
79
+ * @param skip - How many to drop from the front.
80
+ * @param limit - How many to keep.
81
+ * @returns The windowed items.
82
+ */
83
+ function applyWindow(items, skip, limit) {
84
+ const skipped = skip ? items.slice(skip) : items;
85
+ return limit == null ? skipped : skipped.slice(0, limit);
86
+ }
87
+ /**
88
+ * Whether every key the sort is on survives the projection.
89
+ * @template T - The type of the items.
90
+ * @param sort - The query's sort.
91
+ * @param fields - The query's projection, if any.
92
+ * @returns `true` when the sort can still be applied to the projected items.
93
+ */
94
+ function sortKeysSurviveProjection(sort, fields) {
95
+ if (fields == null) return true;
96
+ const specKeys = Object.keys(fields);
97
+ const spec = fields;
98
+ const excluding = specKeys.length > 0 && specKeys.every((key) => !spec[key]);
99
+ const covers = (key) => specKeys.some((entry) => entry === key || key.startsWith(`${entry}.`));
100
+ return Object.keys(sort).every((key) => excluding ? !covers(key) : covers(key));
101
+ }
102
+ /**
103
+ * The pre-`query` read path: the primary key if the selector is one, then a declared index, then
104
+ * the whole store.
105
+ *
106
+ * The `id` fast path is not an optimisation on top of the index machinery, it is the only way a
107
+ * primary-key lookup works at all: `id` is never a *declared* index — `readIds` is exactly the
108
+ * lookup it describes — so a selector of `{ id: … }` matches no index and would otherwise fall
109
+ * through to `readAll`. All three adapters carried their own copy of it; losing it here turned
110
+ * every point read into a full scan, which is how it was noticed.
111
+ * @template T - The type of the items.
112
+ * @template I - The type of the item ids.
113
+ * @param storageAdapter - The storage adapter to read from.
114
+ * @param indices - The fields this collection has declared indices for.
115
+ * @param selector - The query's selector.
116
+ * @returns Every stored item the selector matches.
117
+ */
118
+ async function readMatching(storageAdapter, indices, selector) {
119
+ if (Object.keys(selector).length === 1 && "id" in selector) {
120
+ const idResult = idIndexQuery(selector);
121
+ if (idResult.matched) return storageAdapter.readIds(compact(idResult.ids));
65
122
  }
66
- return {
67
- enqueue,
68
- flush
123
+ const indexInfo = await getIndexInfo(indices.map((field) => storageIndexQuery(storageAdapter, field)), selector);
124
+ const matchItems = (item) => {
125
+ if (indexInfo.optimizedSelector == null) return true;
126
+ if (Object.keys(indexInfo.optimizedSelector).length <= 0) return true;
127
+ return match(item, indexInfo.optimizedSelector);
69
128
  };
129
+ if (indexInfo.matched) {
130
+ const items = await storageAdapter.readIds(indexInfo.ids);
131
+ if (isEqual(indexInfo.optimizedSelector, {})) return items;
132
+ return items.filter(matchItems);
133
+ }
134
+ const allItems = await storageAdapter.readAll();
135
+ if (isEqual(selector, {})) return allItems;
136
+ return allItems.filter(matchItems);
70
137
  }
71
138
  //#endregion
72
- export { batchOnNextTick as default };
139
+ export { executeStorageQuery as default };