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

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 (145) hide show
  1. package/dist/.vite/manifest.json +183 -80
  2. package/dist/AsyncDataAdapter.d.ts +53 -7
  3. package/dist/AutoFetchDataAdapter.d.ts +4 -6
  4. package/dist/Collection/Cursor.d.ts +56 -4
  5. package/dist/Collection/Observer.d.ts +33 -0
  6. package/dist/Collection/index.d.ts +66 -19
  7. package/dist/Collection/types.d.ts +9 -2
  8. package/dist/DataAdapter.d.ts +24 -6
  9. package/dist/DefaultDataAdapter.d.ts +5 -5
  10. package/dist/WorkerDataAdapter.d.ts +57 -5
  11. package/dist/WorkerDataAdapterHost.d.ts +9 -7
  12. package/dist/createIndex.d.ts +2 -2
  13. package/dist/createIndexProvider.d.ts +2 -2
  14. package/dist/createReactivityAdapter.d.ts +2 -2
  15. package/dist/createStorageAdapter.d.ts +1 -1
  16. package/dist/getIndexInfo.d.ts +3 -4
  17. package/dist/index.cjs.js +33 -31
  18. package/dist/index.d.ts +6 -1
  19. package/dist/index.mjs +17 -32
  20. package/dist/index10.cjs.js +11 -0
  21. package/dist/index10.mjs +10 -18
  22. package/dist/index11.cjs.js +23 -0
  23. package/dist/index11.mjs +22 -15
  24. package/dist/index12.cjs.js +45 -0
  25. package/dist/index12.mjs +44 -353
  26. package/dist/index13.cjs.js +22 -0
  27. package/dist/index13.mjs +21 -419
  28. package/dist/index14.cjs.js +45 -0
  29. package/dist/index14.mjs +44 -206
  30. package/dist/index15.cjs.js +86 -0
  31. package/dist/index15.mjs +85 -388
  32. package/dist/index16.cjs.js +15 -0
  33. package/dist/index16.mjs +14 -579
  34. package/dist/index17.cjs.js +141 -0
  35. package/dist/index17.mjs +140 -152
  36. package/dist/index18.cjs.js +47 -0
  37. package/dist/index18.mjs +46 -26
  38. package/dist/index19.cjs.js +15 -0
  39. package/dist/index19.mjs +14 -41
  40. package/dist/index2.cjs.js +33 -0
  41. package/dist/index2.mjs +32 -258
  42. package/dist/index20.cjs.js +23 -0
  43. package/dist/index20.mjs +22 -16
  44. package/dist/index21.cjs.js +37 -0
  45. package/dist/index21.mjs +36 -104
  46. package/dist/index22.cjs.js +35 -0
  47. package/dist/index22.mjs +34 -128
  48. package/dist/index23.cjs.js +21 -0
  49. package/dist/index23.mjs +20 -7
  50. package/dist/index24.cjs.js +19 -0
  51. package/dist/index24.mjs +18 -27
  52. package/dist/index25.cjs.js +157 -0
  53. package/dist/index25.mjs +156 -11
  54. package/dist/index26.cjs.js +38 -0
  55. package/dist/index26.mjs +37 -29
  56. package/dist/index27.cjs.js +42 -0
  57. package/dist/index27.mjs +41 -46
  58. package/dist/index28.cjs.js +347 -0
  59. package/dist/index28.mjs +345 -7
  60. package/dist/index29.cjs.js +602 -0
  61. package/dist/index29.mjs +601 -7
  62. package/dist/index3.cjs.js +19 -0
  63. package/dist/index3.mjs +18 -556
  64. package/dist/index30.cjs.js +11 -0
  65. package/dist/index30.mjs +10 -42
  66. package/dist/index31.cjs.js +11 -0
  67. package/dist/index31.mjs +10 -29
  68. package/dist/index32.cjs.js +90 -0
  69. package/dist/index32.mjs +89 -10
  70. package/dist/index33.cjs.js +53 -0
  71. package/dist/index33.mjs +52 -5
  72. package/dist/index34.cjs.js +453 -0
  73. package/dist/index34.mjs +453 -0
  74. package/dist/index35.cjs.js +72 -0
  75. package/dist/index35.mjs +72 -0
  76. package/dist/index36.cjs.js +580 -0
  77. package/dist/index36.mjs +580 -0
  78. package/dist/index37.cjs.js +21 -0
  79. package/dist/index37.mjs +21 -0
  80. package/dist/index38.cjs.js +363 -0
  81. package/dist/index38.mjs +363 -0
  82. package/dist/index39.cjs.js +513 -0
  83. package/dist/index39.mjs +513 -0
  84. package/dist/index4.cjs.js +209 -0
  85. package/dist/index4.mjs +204 -5
  86. package/dist/index5.cjs.js +187 -0
  87. package/dist/index5.mjs +186 -5
  88. package/dist/index6.cjs.js +303 -0
  89. package/dist/index6.mjs +301 -28
  90. package/dist/index7.cjs.js +125 -0
  91. package/dist/index7.mjs +124 -12
  92. package/dist/index8.cjs.js +33 -0
  93. package/dist/index8.mjs +32 -5
  94. package/dist/index9.cjs.js +12 -0
  95. package/dist/index9.mjs +11 -133
  96. package/dist/types/IndexProvider.d.ts +2 -2
  97. package/dist/types/Modifier.d.ts +1 -1
  98. package/dist/types/ReactivityAdapter.d.ts +1 -1
  99. package/dist/types/StorageAdapter.d.ts +9 -1
  100. package/dist/utils/applyQueryOptions.d.ts +15 -0
  101. package/dist/utils/createSignal.d.ts +2 -2
  102. package/dist/utils/getMatchingKeys.d.ts +2 -2
  103. package/dist/utils/idIndexQuery.d.ts +20 -0
  104. package/dist/utils/incrementalQueryUpdate.d.ts +62 -0
  105. package/dist/utils/isFieldExpression.d.ts +1 -1
  106. package/dist/utils/match.d.ts +1 -1
  107. package/dist/utils/modify.d.ts +1 -1
  108. package/dist/utils/projectItems.d.ts +12 -0
  109. package/dist/utils/queryDelta.d.ts +83 -0
  110. package/dist/utils/queryId.d.ts +2 -2
  111. package/dist/utils/reactiveOrAsync.d.ts +59 -0
  112. package/dist/utils/storageIndexQuery.d.ts +20 -0
  113. package/package.json +1 -1
  114. package/dist/index.cjs10.js +0 -18
  115. package/dist/index.cjs11.js +0 -15
  116. package/dist/index.cjs12.js +0 -353
  117. package/dist/index.cjs13.js +0 -419
  118. package/dist/index.cjs14.js +0 -206
  119. package/dist/index.cjs15.js +0 -388
  120. package/dist/index.cjs16.js +0 -579
  121. package/dist/index.cjs17.js +0 -152
  122. package/dist/index.cjs18.js +0 -26
  123. package/dist/index.cjs19.js +0 -42
  124. package/dist/index.cjs2.js +0 -259
  125. package/dist/index.cjs20.js +0 -16
  126. package/dist/index.cjs21.js +0 -104
  127. package/dist/index.cjs22.js +0 -129
  128. package/dist/index.cjs23.js +0 -7
  129. package/dist/index.cjs24.js +0 -27
  130. package/dist/index.cjs25.js +0 -10
  131. package/dist/index.cjs26.js +0 -29
  132. package/dist/index.cjs27.js +0 -46
  133. package/dist/index.cjs28.js +0 -8
  134. package/dist/index.cjs29.js +0 -7
  135. package/dist/index.cjs3.js +0 -557
  136. package/dist/index.cjs30.js +0 -42
  137. package/dist/index.cjs31.js +0 -29
  138. package/dist/index.cjs32.js +0 -9
  139. package/dist/index.cjs33.js +0 -5
  140. package/dist/index.cjs4.js +0 -5
  141. package/dist/index.cjs5.js +0 -5
  142. package/dist/index.cjs6.js +0 -29
  143. package/dist/index.cjs7.js +0 -12
  144. package/dist/index.cjs8.js +0 -5
  145. package/dist/index.cjs9.js +0 -133
package/dist/index22.mjs CHANGED
@@ -1,129 +1,35 @@
1
- import intersection from "./index29.mjs";
2
- function getMergedIndexInfo(queryFunctions, selector) {
3
- return queryFunctions.reduce((memoOrPromise, queryFunction) => {
4
- const resultOrPromise = queryFunction(selector);
5
- const processResult = (memo2, result) => {
6
- if (!result.matched)
7
- return memo2;
8
- const optimizedSelector = result.keepSelector ? memo2.optimizedSelector : Object.fromEntries(Object.entries(memo2.optimizedSelector).filter(([key]) => !result.fields.includes(key)));
9
- return {
10
- matched: true,
11
- ids: [...new Set(memo2.matched ? intersection(memo2.ids, result.ids) : result.ids)],
12
- optimizedSelector
13
- };
14
- };
15
- if (resultOrPromise instanceof Promise) {
16
- return resultOrPromise.then(async (result) => {
17
- const memo2 = memoOrPromise instanceof Promise ? await memoOrPromise : memoOrPromise;
18
- return processResult(memo2, result);
19
- });
20
- }
21
- const memo = memoOrPromise;
22
- if (memo instanceof Promise)
23
- throw new Error("Mixing async and sync index providers is not supported");
24
- return processResult(memo, resultOrPromise);
25
- }, {
26
- matched: false,
27
- ids: [],
28
- optimizedSelector: { ...selector }
29
- });
1
+ import get from "./index11.mjs";
2
+ import set from "./index21.mjs";
3
+ //#region src/utils/project.ts
4
+ /**
5
+ * Projects the fields of an object based on a specified fields configuration.
6
+ * Supports inclusion (`1`) and exclusion (`0`) of specific fields. Creates a new object
7
+ * with the desired fields included or excluded, based on the configuration.
8
+ * @template T - The type of the object being projected.
9
+ * @param item - The original object to project fields from.
10
+ * @param fields - An object defining the fields to include (`1`) or exclude (`0`).
11
+ * - Keys are the field names, and values are either `1` (include) or `0` (exclude).
12
+ * @returns A new object with the specified fields included or excluded.
13
+ * - If all fields are set to `0`, the excluded fields are removed from the result.
14
+ * - If fields are set to `1`, only the included fields are retained.
15
+ */
16
+ function project(item, fields) {
17
+ if (Object.values(fields).every((value) => value === 0)) {
18
+ const result = { ...item };
19
+ Object.keys(fields).forEach((key) => {
20
+ if (get(item, key) === void 0) return;
21
+ set(result, key, void 0, true);
22
+ });
23
+ return result;
24
+ }
25
+ const result = {};
26
+ Object.entries(fields).forEach(([key, value]) => {
27
+ const fieldValue = get(item, key);
28
+ if (fieldValue === void 0) return;
29
+ if (fieldValue == null && value !== 1) return;
30
+ set(result, key, value === 1 ? fieldValue : void 0);
31
+ });
32
+ return result;
30
33
  }
31
- function optimizeLogicGate(queryFunctions, logicGate, idsCallback) {
32
- return logicGate.reduce((memoOrPromise, sel) => {
33
- const getSelector = (indexInfo) => {
34
- const { matched: selMatched, ids: selIds, optimizedSelector: optimizedSelector2 } = indexInfo;
35
- if (selMatched) {
36
- idsCallback(true, selIds);
37
- if (Object.keys(optimizedSelector2).length > 0) {
38
- return optimizedSelector2;
39
- }
40
- } else {
41
- idsCallback(false, []);
42
- return sel;
43
- }
44
- };
45
- const indexInfoOrPromise = getIndexInfo(queryFunctions, sel);
46
- if (indexInfoOrPromise instanceof Promise) {
47
- return indexInfoOrPromise.then(async (indexInfo) => {
48
- const memo2 = memoOrPromise instanceof Promise ? await memoOrPromise : memoOrPromise;
49
- const optimizedSelector2 = getSelector(indexInfo);
50
- if (optimizedSelector2)
51
- memo2.push(optimizedSelector2);
52
- return memo2;
53
- });
54
- }
55
- const memo = memoOrPromise;
56
- if (memo instanceof Promise)
57
- throw new Error("Mixing async and sync index providers is not supported");
58
- const optimizedSelector = getSelector(indexInfoOrPromise);
59
- if (optimizedSelector)
60
- memo.push(optimizedSelector);
61
- return memo;
62
- }, []);
63
- }
64
- function getIndexInfo(queryFunctions, selector) {
65
- if (selector == null || Object.keys(selector).length <= 0) {
66
- return {
67
- matched: false,
68
- ids: [],
69
- optimizedSelector: selector
70
- };
71
- }
72
- const { $and, $or, ...rest } = selector;
73
- const flatInfoOrPromise = getMergedIndexInfo(queryFunctions, rest);
74
- const processFlatInfo = (flatInfo) => {
75
- let { matched, ids } = flatInfo;
76
- const newSelector = flatInfo.optimizedSelector;
77
- const $andNewOrPromise = Array.isArray($and) ? optimizeLogicGate(queryFunctions, $and, (match, selIds) => {
78
- if (!match)
79
- return;
80
- ids = matched ? intersection(ids, selIds) : selIds;
81
- matched = true;
82
- }) : void 0;
83
- const process$and = ($andNew) => {
84
- if ($andNew && $andNew.length > 0)
85
- newSelector.$and = $andNew;
86
- let hasNonIndexField = false;
87
- const matchedBefore = matched;
88
- const idsBefore = ids;
89
- const process$or = ($orNew) => {
90
- if ($orNew && $orNew.length > 0)
91
- newSelector.$or = $orNew;
92
- if (hasNonIndexField) {
93
- newSelector.$or = $or;
94
- matched = matchedBefore;
95
- ids = idsBefore;
96
- }
97
- return {
98
- matched,
99
- ids: ids || [],
100
- optimizedSelector: newSelector
101
- };
102
- };
103
- const $orNewOrPromise = Array.isArray($or) ? optimizeLogicGate(queryFunctions, $or, (match, selIds) => {
104
- if (match) {
105
- ids = [.../* @__PURE__ */ new Set([...ids, ...selIds])];
106
- matched = true;
107
- } else {
108
- hasNonIndexField = true;
109
- }
110
- }) : void 0;
111
- if ($orNewOrPromise instanceof Promise) {
112
- return $orNewOrPromise.then(($orNew) => process$or($orNew));
113
- }
114
- return process$or($orNewOrPromise);
115
- };
116
- if ($andNewOrPromise instanceof Promise) {
117
- return $andNewOrPromise.then(($andNew) => process$and($andNew));
118
- }
119
- return process$and($andNewOrPromise);
120
- };
121
- if (flatInfoOrPromise instanceof Promise) {
122
- return flatInfoOrPromise.then(processFlatInfo);
123
- }
124
- return processFlatInfo(flatInfoOrPromise);
125
- }
126
- export {
127
- getIndexInfo as default,
128
- getMergedIndexInfo
129
- };
34
+ //#endregion
35
+ export { project as default };
@@ -0,0 +1,21 @@
1
+ const require_project = require("./index22.cjs.js");
2
+ //#region src/utils/projectItems.ts
3
+ /**
4
+ * Applies a query's field projection to a list of items, keeping the primary key unless the
5
+ * projection excludes it outright. Returns the items untouched when there is no projection, so a
6
+ * caller does not have to check for one first.
7
+ * @template T - The type of the items.
8
+ * @param items - The items to project.
9
+ * @param fields - The projection, or `undefined` for none.
10
+ * @returns The projected items.
11
+ */
12
+ function projectItems(items, fields) {
13
+ if (!fields) return items;
14
+ const idExcluded = fields.id === 0;
15
+ return items.map((item) => ({
16
+ ...idExcluded ? {} : { id: item.id },
17
+ ...require_project.default(item, fields)
18
+ }));
19
+ }
20
+ //#endregion
21
+ exports.default = projectItems;
package/dist/index23.mjs CHANGED
@@ -1,8 +1,21 @@
1
- import { Query } from "mingo";
2
- function match(item, selector) {
3
- const query = new Query(selector);
4
- return query.test(item);
1
+ import project from "./index22.mjs";
2
+ //#region src/utils/projectItems.ts
3
+ /**
4
+ * Applies a query's field projection to a list of items, keeping the primary key unless the
5
+ * projection excludes it outright. Returns the items untouched when there is no projection, so a
6
+ * caller does not have to check for one first.
7
+ * @template T - The type of the items.
8
+ * @param items - The items to project.
9
+ * @param fields - The projection, or `undefined` for none.
10
+ * @returns The projected items.
11
+ */
12
+ function projectItems(items, fields) {
13
+ if (!fields) return items;
14
+ const idExcluded = fields.id === 0;
15
+ return items.map((item) => ({
16
+ ...idExcluded ? {} : { id: item.id },
17
+ ...project(item, fields)
18
+ }));
5
19
  }
6
- export {
7
- match as default
8
- };
20
+ //#endregion
21
+ export { projectItems as default };
@@ -0,0 +1,19 @@
1
+ const require_get = require("./index11.cjs.js");
2
+ let fast_sort = require("fast-sort");
3
+ //#region src/utils/sortItems.ts
4
+ /**
5
+ * Sorts an array of items based on multiple fields and their specified sort order.
6
+ * Uses the `fast-sort` library for efficient sorting.
7
+ * @template T - The type of the items in the array.
8
+ * @param items - The array of items to be sorted.
9
+ * @param sortFields - An object defining the sort order for each field.
10
+ * - Keys are the field names, and values are either `1` (ascending) or `-1` (descending).
11
+ * @returns A new array of items sorted based on the specified fields and their order.
12
+ */
13
+ function sortItems(items, sortFields) {
14
+ return (0, fast_sort.sort)(items).by(Object.entries(sortFields).map(([key, value]) => {
15
+ return { [value === 1 ? "asc" : "desc"]: (i) => require_get.default(i, key) };
16
+ }));
17
+ }
18
+ //#endregion
19
+ exports.default = sortItems;
package/dist/index24.mjs CHANGED
@@ -1,28 +1,19 @@
1
- import get from "./index10.mjs";
2
- import set from "./index31.mjs";
3
- function project(item, fields) {
4
- const allFieldsDeactivated = Object.values(fields).every((value) => value === 0);
5
- if (allFieldsDeactivated) {
6
- const result2 = { ...item };
7
- Object.keys(fields).forEach((key) => {
8
- const fieldValue = get(item, key);
9
- if (fieldValue === void 0)
10
- return;
11
- set(result2, key, void 0, true);
12
- });
13
- return result2;
14
- }
15
- const result = {};
16
- Object.entries(fields).forEach(([key, value]) => {
17
- const fieldValue = get(item, key);
18
- if (fieldValue === void 0)
19
- return;
20
- if (fieldValue == null && value !== 1)
21
- return;
22
- set(result, key, value === 1 ? fieldValue : void 0);
23
- });
24
- return result;
1
+ import get from "./index11.mjs";
2
+ import { sort } from "fast-sort";
3
+ //#region src/utils/sortItems.ts
4
+ /**
5
+ * Sorts an array of items based on multiple fields and their specified sort order.
6
+ * Uses the `fast-sort` library for efficient sorting.
7
+ * @template T - The type of the items in the array.
8
+ * @param items - The array of items to be sorted.
9
+ * @param sortFields - An object defining the sort order for each field.
10
+ * - Keys are the field names, and values are either `1` (ascending) or `-1` (descending).
11
+ * @returns A new array of items sorted based on the specified fields and their order.
12
+ */
13
+ function sortItems(items, sortFields) {
14
+ return sort(items).by(Object.entries(sortFields).map(([key, value]) => {
15
+ return { [value === 1 ? "asc" : "desc"]: (i) => get(i, key) };
16
+ }));
25
17
  }
26
- export {
27
- project as default
28
- };
18
+ //#endregion
19
+ export { sortItems as default };
@@ -0,0 +1,157 @@
1
+ const require_match = require("./index19.cjs.js");
2
+ const require_projectItems = require("./index23.cjs.js");
3
+ const require_sortItems = require("./index24.cjs.js");
4
+ //#region src/utils/incrementalQueryUpdate.ts
5
+ /**
6
+ * Recomputes a query's result from its previous result and the change that was just written,
7
+ * without going back to the store.
8
+ *
9
+ * A store re-executing the query instead reads every item it holds (or every item an index points
10
+ * at) and filters, sorts and projects the lot — for a write that touched one row. This does the
11
+ * same job in the size of the write, which is what a query's result costs to keep up to date when
12
+ * the change that affects it is already in hand.
13
+ *
14
+ * Returns `null` when the previous result is not enough to answer, and the caller has to re-execute
15
+ * the query after all:
16
+ * - `limit` or `skip`: the result is a window onto a larger set, and an item leaving the window has
17
+ * to be replaced by one the previous result never contained.
18
+ * - `fields` together with a `sort` the projection does not keep: the previous items are
19
+ * projected, so a sort key the projection dropped is no longer there to sort by. A projection
20
+ * that keeps every sort key is fine, and is the common case — a list sorted by the same date it
21
+ * displays.
22
+ * - a `null` selector, which matches nothing and is not worth a special case.
23
+ * @template T - The type of the items.
24
+ * @param previous - The query's previous result.
25
+ * @param selector - The query's selector.
26
+ * @param options - The query's options.
27
+ * @param changes - The items the write created, updated or removed.
28
+ * @returns The new result, or `null` when the query has to be re-executed.
29
+ */
30
+ function incrementalQueryUpdate(previous, selector, options, changes) {
31
+ if (selector == null) return null;
32
+ const { sort, skip, limit, fields } = options || {};
33
+ if (skip != null) return null;
34
+ if (fields != null && sort != null && !sortKeysSurviveProjection(sort, fields)) return null;
35
+ if (limit != null && !windowStaysClosed(previous, selector, options, changes)) return null;
36
+ return mergeChangesetIntoResult(previous, selector, options, changes);
37
+ }
38
+ /**
39
+ * Whether a projection keeps every field a sort is keyed on.
40
+ *
41
+ * The previous result is the projected items, so this decides whether they still carry what the
42
+ * sort needs. Both projection modes are covered, because `project` treats an all-zero spec as an
43
+ * exclusion and anything else as an inclusion:
44
+ *
45
+ * - An inclusion keeps a key when the key itself is included, or an ancestor of it is (`{a: 1}`
46
+ * keeps `a.b`), and always keeps `id` unless the spec excludes it outright.
47
+ * - An exclusion keeps a key unless the key or an ancestor of it is excluded.
48
+ *
49
+ * Anything it cannot account for is a "no": re-executing is slower, not wrong.
50
+ * @template T - The type of the items.
51
+ * @param sort - The query's sort.
52
+ * @param fields - The query's projection.
53
+ * @returns `true` when every sort key survives the projection.
54
+ */
55
+ function sortKeysSurviveProjection(sort, fields) {
56
+ const entries = Object.entries(fields);
57
+ if (entries.length === 0) return true;
58
+ const isExclusion = entries.every(([, value]) => value === 0);
59
+ const pathsFor = (key) => key.split(".").map((_, index, parts) => parts.slice(0, index + 1).join("."));
60
+ return Object.keys(sort).every((key) => {
61
+ const paths = pathsFor(key);
62
+ if (isExclusion) return paths.every((path) => fields[path] !== 0);
63
+ if (key === "id") return fields.id !== 0;
64
+ return paths.some((path) => fields[path] === 1);
65
+ });
66
+ }
67
+ /**
68
+ * Whether two items are in the given order under the given sort, deciding ties against the caller.
69
+ *
70
+ * Uses the sort itself rather than a comparator of its own: a rule about which side of a window an
71
+ * item falls on is only as good as its agreement with the ordering that drew the window. A tie
72
+ * comes back as `false`, because a tie is exactly the case where an item could belong on either
73
+ * side and the answer has to be taken from the store.
74
+ * @template T - The type of the items.
75
+ * @param item - The item whose position is in question.
76
+ * @param edge - The item at the edge of the window.
77
+ * @param sort - The query's sort.
78
+ * @returns `true` when `item` sorts strictly before `edge`.
79
+ */
80
+ function sortsBefore(item, edge, sort) {
81
+ return require_sortItems.default([edge, item], sort)[0] === item;
82
+ }
83
+ /**
84
+ * Whether a change to a windowed query can be answered from the window alone.
85
+ *
86
+ * A window holds the first `limit` items in sort order, and nothing about what lies beyond it. An
87
+ * item leaving the window therefore has to be replaced by one the window has never seen, and that
88
+ * answer can only come from the store. An item arriving is a different matter: it takes its place
89
+ * and pushes the last one out, and where that one goes is not the window's problem.
90
+ *
91
+ * The one case where none of this applies is a window that was never full, because then the query
92
+ * already returns everything it matches and there is no "beyond".
93
+ * @template T - The type of the items.
94
+ * @param previous - The query's previous result.
95
+ * @param selector - The query's selector.
96
+ * @param options - The query's options.
97
+ * @param changes - The items the write created, updated or removed.
98
+ * @returns `true` when the new window follows from the old one and the change.
99
+ */
100
+ function windowStaysClosed(previous, selector, options, changes) {
101
+ const { sort, limit, fields } = options || {};
102
+ if (limit == null || previous.length < limit) return true;
103
+ if (sort == null || fields != null) return false;
104
+ const edge = previous.at(-1);
105
+ const runnerUp = previous.at(-2);
106
+ if (runnerUp != null && !sortsBefore(runnerUp, edge, sort)) return false;
107
+ const inWindow = new Set(previous.map((item) => item.id));
108
+ if (changes.deletes.some((id) => inWindow.has(id))) return false;
109
+ return changes.upserts.every((item) => {
110
+ if (item === edge) return true;
111
+ const before = sortsBefore(item, edge, sort);
112
+ if (!inWindow.has(item.id)) {
113
+ if (!require_match.default(item, selector)) return true;
114
+ return before || sortsBefore(edge, item, sort);
115
+ }
116
+ if (!require_match.default(item, selector)) return false;
117
+ if (before) return true;
118
+ return item.id === edge.id && !sortsBefore(edge, item, sort);
119
+ });
120
+ }
121
+ /**
122
+ * Folds a change into a query's result, whatever the query's options.
123
+ *
124
+ * The unguarded version of `incrementalQueryUpdate`, for the places where the alternative is not a
125
+ * more accurate answer but a wrong one — layering a write that has not been confirmed yet on top of
126
+ * the last confirmed result, say. For a query returning everything it matches, this is exact. For a
127
+ * window onto a larger set it is the closest the window itself can get: an item that no longer
128
+ * belongs is dropped, one that does is placed, and the window is trimmed back to its length — but
129
+ * an item pulled in from beyond the window is not something the window knows about.
130
+ *
131
+ * What it never does is re-examine the items already in the result. They matched when the store
132
+ * produced them, they still match, and asking again is both wasteful and — for a projected result,
133
+ * whose items no longer carry the fields the selector names — wrong.
134
+ * @template T - The type of the items.
135
+ * @param previous - The query's previous result.
136
+ * @param selector - The query's selector.
137
+ * @param options - The query's options.
138
+ * @param changes - The items the write created, updated or removed.
139
+ * @returns The resulting items.
140
+ */
141
+ function mergeChangesetIntoResult(previous, selector, options, changes) {
142
+ if (selector == null) return [];
143
+ const { sort, limit, fields } = options || {};
144
+ const byId = /* @__PURE__ */ new Map();
145
+ previous.forEach((item) => byId.set(item.id, item));
146
+ changes.deletes.forEach((id) => byId.delete(id));
147
+ changes.upserts.forEach((item) => {
148
+ if (require_match.default(item, selector)) byId.set(item.id, require_projectItems.default([item], fields)[0]);
149
+ else byId.delete(item.id);
150
+ });
151
+ const items = [...byId.values()];
152
+ const sorted = sort ? require_sortItems.default(items, sort) : items;
153
+ return limit == null ? sorted : sorted.slice(0, limit);
154
+ }
155
+ //#endregion
156
+ exports.default = incrementalQueryUpdate;
157
+ exports.mergeChangesetIntoResult = mergeChangesetIntoResult;
package/dist/index25.mjs CHANGED
@@ -1,11 +1,156 @@
1
- import { sort } from "fast-sort";
2
- import get from "./index10.mjs";
3
- function sortItems(items, sortFields) {
4
- return sort(items).by(Object.entries(sortFields).map(([key, value]) => {
5
- const order = value === 1 ? "asc" : "desc";
6
- return { [order]: (i) => get(i, key) };
7
- }));
8
- }
9
- export {
10
- sortItems as default
11
- };
1
+ import match from "./index19.mjs";
2
+ import projectItems from "./index23.mjs";
3
+ import sortItems from "./index24.mjs";
4
+ //#region src/utils/incrementalQueryUpdate.ts
5
+ /**
6
+ * Recomputes a query's result from its previous result and the change that was just written,
7
+ * without going back to the store.
8
+ *
9
+ * A store re-executing the query instead reads every item it holds (or every item an index points
10
+ * at) and filters, sorts and projects the lot — for a write that touched one row. This does the
11
+ * same job in the size of the write, which is what a query's result costs to keep up to date when
12
+ * the change that affects it is already in hand.
13
+ *
14
+ * Returns `null` when the previous result is not enough to answer, and the caller has to re-execute
15
+ * the query after all:
16
+ * - `limit` or `skip`: the result is a window onto a larger set, and an item leaving the window has
17
+ * to be replaced by one the previous result never contained.
18
+ * - `fields` together with a `sort` the projection does not keep: the previous items are
19
+ * projected, so a sort key the projection dropped is no longer there to sort by. A projection
20
+ * that keeps every sort key is fine, and is the common case — a list sorted by the same date it
21
+ * displays.
22
+ * - a `null` selector, which matches nothing and is not worth a special case.
23
+ * @template T - The type of the items.
24
+ * @param previous - The query's previous result.
25
+ * @param selector - The query's selector.
26
+ * @param options - The query's options.
27
+ * @param changes - The items the write created, updated or removed.
28
+ * @returns The new result, or `null` when the query has to be re-executed.
29
+ */
30
+ function incrementalQueryUpdate(previous, selector, options, changes) {
31
+ if (selector == null) return null;
32
+ const { sort, skip, limit, fields } = options || {};
33
+ if (skip != null) return null;
34
+ if (fields != null && sort != null && !sortKeysSurviveProjection(sort, fields)) return null;
35
+ if (limit != null && !windowStaysClosed(previous, selector, options, changes)) return null;
36
+ return mergeChangesetIntoResult(previous, selector, options, changes);
37
+ }
38
+ /**
39
+ * Whether a projection keeps every field a sort is keyed on.
40
+ *
41
+ * The previous result is the projected items, so this decides whether they still carry what the
42
+ * sort needs. Both projection modes are covered, because `project` treats an all-zero spec as an
43
+ * exclusion and anything else as an inclusion:
44
+ *
45
+ * - An inclusion keeps a key when the key itself is included, or an ancestor of it is (`{a: 1}`
46
+ * keeps `a.b`), and always keeps `id` unless the spec excludes it outright.
47
+ * - An exclusion keeps a key unless the key or an ancestor of it is excluded.
48
+ *
49
+ * Anything it cannot account for is a "no": re-executing is slower, not wrong.
50
+ * @template T - The type of the items.
51
+ * @param sort - The query's sort.
52
+ * @param fields - The query's projection.
53
+ * @returns `true` when every sort key survives the projection.
54
+ */
55
+ function sortKeysSurviveProjection(sort, fields) {
56
+ const entries = Object.entries(fields);
57
+ if (entries.length === 0) return true;
58
+ const isExclusion = entries.every(([, value]) => value === 0);
59
+ const pathsFor = (key) => key.split(".").map((_, index, parts) => parts.slice(0, index + 1).join("."));
60
+ return Object.keys(sort).every((key) => {
61
+ const paths = pathsFor(key);
62
+ if (isExclusion) return paths.every((path) => fields[path] !== 0);
63
+ if (key === "id") return fields.id !== 0;
64
+ return paths.some((path) => fields[path] === 1);
65
+ });
66
+ }
67
+ /**
68
+ * Whether two items are in the given order under the given sort, deciding ties against the caller.
69
+ *
70
+ * Uses the sort itself rather than a comparator of its own: a rule about which side of a window an
71
+ * item falls on is only as good as its agreement with the ordering that drew the window. A tie
72
+ * comes back as `false`, because a tie is exactly the case where an item could belong on either
73
+ * side and the answer has to be taken from the store.
74
+ * @template T - The type of the items.
75
+ * @param item - The item whose position is in question.
76
+ * @param edge - The item at the edge of the window.
77
+ * @param sort - The query's sort.
78
+ * @returns `true` when `item` sorts strictly before `edge`.
79
+ */
80
+ function sortsBefore(item, edge, sort) {
81
+ return sortItems([edge, item], sort)[0] === item;
82
+ }
83
+ /**
84
+ * Whether a change to a windowed query can be answered from the window alone.
85
+ *
86
+ * A window holds the first `limit` items in sort order, and nothing about what lies beyond it. An
87
+ * item leaving the window therefore has to be replaced by one the window has never seen, and that
88
+ * answer can only come from the store. An item arriving is a different matter: it takes its place
89
+ * and pushes the last one out, and where that one goes is not the window's problem.
90
+ *
91
+ * The one case where none of this applies is a window that was never full, because then the query
92
+ * already returns everything it matches and there is no "beyond".
93
+ * @template T - The type of the items.
94
+ * @param previous - The query's previous result.
95
+ * @param selector - The query's selector.
96
+ * @param options - The query's options.
97
+ * @param changes - The items the write created, updated or removed.
98
+ * @returns `true` when the new window follows from the old one and the change.
99
+ */
100
+ function windowStaysClosed(previous, selector, options, changes) {
101
+ const { sort, limit, fields } = options || {};
102
+ if (limit == null || previous.length < limit) return true;
103
+ if (sort == null || fields != null) return false;
104
+ const edge = previous.at(-1);
105
+ const runnerUp = previous.at(-2);
106
+ if (runnerUp != null && !sortsBefore(runnerUp, edge, sort)) return false;
107
+ const inWindow = new Set(previous.map((item) => item.id));
108
+ if (changes.deletes.some((id) => inWindow.has(id))) return false;
109
+ return changes.upserts.every((item) => {
110
+ if (item === edge) return true;
111
+ const before = sortsBefore(item, edge, sort);
112
+ if (!inWindow.has(item.id)) {
113
+ if (!match(item, selector)) return true;
114
+ return before || sortsBefore(edge, item, sort);
115
+ }
116
+ if (!match(item, selector)) return false;
117
+ if (before) return true;
118
+ return item.id === edge.id && !sortsBefore(edge, item, sort);
119
+ });
120
+ }
121
+ /**
122
+ * Folds a change into a query's result, whatever the query's options.
123
+ *
124
+ * The unguarded version of `incrementalQueryUpdate`, for the places where the alternative is not a
125
+ * more accurate answer but a wrong one — layering a write that has not been confirmed yet on top of
126
+ * the last confirmed result, say. For a query returning everything it matches, this is exact. For a
127
+ * window onto a larger set it is the closest the window itself can get: an item that no longer
128
+ * belongs is dropped, one that does is placed, and the window is trimmed back to its length — but
129
+ * an item pulled in from beyond the window is not something the window knows about.
130
+ *
131
+ * What it never does is re-examine the items already in the result. They matched when the store
132
+ * produced them, they still match, and asking again is both wasteful and — for a projected result,
133
+ * whose items no longer carry the fields the selector names — wrong.
134
+ * @template T - The type of the items.
135
+ * @param previous - The query's previous result.
136
+ * @param selector - The query's selector.
137
+ * @param options - The query's options.
138
+ * @param changes - The items the write created, updated or removed.
139
+ * @returns The resulting items.
140
+ */
141
+ function mergeChangesetIntoResult(previous, selector, options, changes) {
142
+ if (selector == null) return [];
143
+ const { sort, limit, fields } = options || {};
144
+ const byId = /* @__PURE__ */ new Map();
145
+ previous.forEach((item) => byId.set(item.id, item));
146
+ changes.deletes.forEach((id) => byId.delete(id));
147
+ changes.upserts.forEach((item) => {
148
+ if (match(item, selector)) byId.set(item.id, projectItems([item], fields)[0]);
149
+ else byId.delete(item.id);
150
+ });
151
+ const items = [...byId.values()];
152
+ const sorted = sort ? sortItems(items, sort) : items;
153
+ return limit == null ? sorted : sorted.slice(0, limit);
154
+ }
155
+ //#endregion
156
+ export { incrementalQueryUpdate as default, mergeChangesetIntoResult };
@@ -0,0 +1,38 @@
1
+ //#region src/utils/queryId.ts
2
+ /**
3
+ * Returns true when `options` is effectively "empty" for the purpose of queryId generation.
4
+ * Treats `undefined`/`null` as empty, and only considers plain objects with zero own enumerable keys as empty.
5
+ * Arrays and non-object values are not considered empty.
6
+ * @param options - Query options to test.
7
+ * @returns `true` if `options` is `null`/`undefined` or a plain object with no own keys; otherwise `false`.
8
+ */
9
+ function isEmptyOptions(options) {
10
+ if (options == null) return true;
11
+ if (typeof options !== "object") return false;
12
+ if (Array.isArray(options)) return false;
13
+ return Object.keys(options).length === 0;
14
+ }
15
+ var noOptions = {};
16
+ var cache = /* @__PURE__ */ new WeakMap();
17
+ /**
18
+ * Generates a unique identifier for a query based on its selector and options.
19
+ * @param selector - The selector object.
20
+ * @param options - The query options object (optional).
21
+ * @returns A unique identifier string for the query.
22
+ */
23
+ function queryId(selector, options) {
24
+ if (!(selector != null && typeof selector === "object" && (options == null || typeof options === "object"))) {
25
+ const optionsId = isEmptyOptions(options) ? -1 : JSON.stringify(options);
26
+ return `${JSON.stringify(selector)}:${optionsId}`;
27
+ }
28
+ const optionsKey = options ?? noOptions;
29
+ const cachedForSelector = cache.get(selector);
30
+ const cached = cachedForSelector?.get(optionsKey);
31
+ if (cached != null) return cached;
32
+ const id = `${JSON.stringify(selector)}:${isEmptyOptions(options) ? -1 : JSON.stringify(options)}`;
33
+ if (cachedForSelector) cachedForSelector.set(optionsKey, id);
34
+ else cache.set(selector, new WeakMap([[optionsKey, id]]));
35
+ return id;
36
+ }
37
+ //#endregion
38
+ exports.default = queryId;