@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.
- package/dist/.vite/manifest.json +183 -80
- package/dist/AsyncDataAdapter.d.ts +53 -7
- package/dist/AutoFetchDataAdapter.d.ts +4 -6
- package/dist/Collection/Cursor.d.ts +56 -4
- package/dist/Collection/Observer.d.ts +33 -0
- package/dist/Collection/index.d.ts +66 -19
- package/dist/Collection/types.d.ts +9 -2
- package/dist/DataAdapter.d.ts +24 -6
- package/dist/DefaultDataAdapter.d.ts +5 -5
- package/dist/WorkerDataAdapter.d.ts +57 -5
- package/dist/WorkerDataAdapterHost.d.ts +9 -7
- package/dist/createIndex.d.ts +2 -2
- package/dist/createIndexProvider.d.ts +2 -2
- package/dist/createReactivityAdapter.d.ts +2 -2
- package/dist/createStorageAdapter.d.ts +1 -1
- package/dist/getIndexInfo.d.ts +3 -4
- package/dist/index.cjs.js +33 -31
- package/dist/index.d.ts +6 -1
- package/dist/index.mjs +17 -32
- package/dist/index10.cjs.js +11 -0
- package/dist/index10.mjs +10 -18
- package/dist/index11.cjs.js +23 -0
- package/dist/index11.mjs +22 -15
- package/dist/index12.cjs.js +45 -0
- package/dist/index12.mjs +44 -353
- package/dist/index13.cjs.js +22 -0
- package/dist/index13.mjs +21 -419
- package/dist/index14.cjs.js +45 -0
- package/dist/index14.mjs +44 -206
- package/dist/index15.cjs.js +86 -0
- package/dist/index15.mjs +85 -388
- package/dist/index16.cjs.js +15 -0
- package/dist/index16.mjs +14 -579
- package/dist/index17.cjs.js +141 -0
- package/dist/index17.mjs +140 -152
- package/dist/index18.cjs.js +47 -0
- package/dist/index18.mjs +46 -26
- package/dist/index19.cjs.js +15 -0
- package/dist/index19.mjs +14 -41
- package/dist/index2.cjs.js +33 -0
- package/dist/index2.mjs +32 -258
- package/dist/index20.cjs.js +23 -0
- package/dist/index20.mjs +22 -16
- package/dist/index21.cjs.js +37 -0
- package/dist/index21.mjs +36 -104
- package/dist/index22.cjs.js +35 -0
- package/dist/index22.mjs +34 -128
- package/dist/index23.cjs.js +21 -0
- package/dist/index23.mjs +20 -7
- package/dist/index24.cjs.js +19 -0
- package/dist/index24.mjs +18 -27
- package/dist/index25.cjs.js +157 -0
- package/dist/index25.mjs +156 -11
- package/dist/index26.cjs.js +38 -0
- package/dist/index26.mjs +37 -29
- package/dist/index27.cjs.js +42 -0
- package/dist/index27.mjs +41 -46
- package/dist/index28.cjs.js +347 -0
- package/dist/index28.mjs +345 -7
- package/dist/index29.cjs.js +602 -0
- package/dist/index29.mjs +601 -7
- package/dist/index3.cjs.js +19 -0
- package/dist/index3.mjs +18 -556
- package/dist/index30.cjs.js +11 -0
- package/dist/index30.mjs +10 -42
- package/dist/index31.cjs.js +11 -0
- package/dist/index31.mjs +10 -29
- package/dist/index32.cjs.js +90 -0
- package/dist/index32.mjs +89 -10
- package/dist/index33.cjs.js +53 -0
- package/dist/index33.mjs +52 -5
- package/dist/index34.cjs.js +453 -0
- package/dist/index34.mjs +453 -0
- package/dist/index35.cjs.js +72 -0
- package/dist/index35.mjs +72 -0
- package/dist/index36.cjs.js +580 -0
- package/dist/index36.mjs +580 -0
- package/dist/index37.cjs.js +21 -0
- package/dist/index37.mjs +21 -0
- package/dist/index38.cjs.js +363 -0
- package/dist/index38.mjs +363 -0
- package/dist/index39.cjs.js +513 -0
- package/dist/index39.mjs +513 -0
- package/dist/index4.cjs.js +209 -0
- package/dist/index4.mjs +204 -5
- package/dist/index5.cjs.js +187 -0
- package/dist/index5.mjs +186 -5
- package/dist/index6.cjs.js +303 -0
- package/dist/index6.mjs +301 -28
- package/dist/index7.cjs.js +125 -0
- package/dist/index7.mjs +124 -12
- package/dist/index8.cjs.js +33 -0
- package/dist/index8.mjs +32 -5
- package/dist/index9.cjs.js +12 -0
- package/dist/index9.mjs +11 -133
- package/dist/types/IndexProvider.d.ts +2 -2
- package/dist/types/Modifier.d.ts +1 -1
- package/dist/types/ReactivityAdapter.d.ts +1 -1
- package/dist/types/StorageAdapter.d.ts +9 -1
- package/dist/utils/applyQueryOptions.d.ts +15 -0
- package/dist/utils/createSignal.d.ts +2 -2
- package/dist/utils/getMatchingKeys.d.ts +2 -2
- package/dist/utils/idIndexQuery.d.ts +20 -0
- package/dist/utils/incrementalQueryUpdate.d.ts +62 -0
- package/dist/utils/isFieldExpression.d.ts +1 -1
- package/dist/utils/match.d.ts +1 -1
- package/dist/utils/modify.d.ts +1 -1
- package/dist/utils/projectItems.d.ts +12 -0
- package/dist/utils/queryDelta.d.ts +83 -0
- package/dist/utils/queryId.d.ts +2 -2
- package/dist/utils/reactiveOrAsync.d.ts +59 -0
- package/dist/utils/storageIndexQuery.d.ts +20 -0
- package/package.json +1 -1
- package/dist/index.cjs10.js +0 -18
- package/dist/index.cjs11.js +0 -15
- package/dist/index.cjs12.js +0 -353
- package/dist/index.cjs13.js +0 -419
- package/dist/index.cjs14.js +0 -206
- package/dist/index.cjs15.js +0 -388
- package/dist/index.cjs16.js +0 -579
- package/dist/index.cjs17.js +0 -152
- package/dist/index.cjs18.js +0 -26
- package/dist/index.cjs19.js +0 -42
- package/dist/index.cjs2.js +0 -259
- package/dist/index.cjs20.js +0 -16
- package/dist/index.cjs21.js +0 -104
- package/dist/index.cjs22.js +0 -129
- package/dist/index.cjs23.js +0 -7
- package/dist/index.cjs24.js +0 -27
- package/dist/index.cjs25.js +0 -10
- package/dist/index.cjs26.js +0 -29
- package/dist/index.cjs27.js +0 -46
- package/dist/index.cjs28.js +0 -8
- package/dist/index.cjs29.js +0 -7
- package/dist/index.cjs3.js +0 -557
- package/dist/index.cjs30.js +0 -42
- package/dist/index.cjs31.js +0 -29
- package/dist/index.cjs32.js +0 -9
- package/dist/index.cjs33.js +0 -5
- package/dist/index.cjs4.js +0 -5
- package/dist/index.cjs5.js +0 -5
- package/dist/index.cjs6.js +0 -29
- package/dist/index.cjs7.js +0 -12
- package/dist/index.cjs8.js +0 -5
- package/dist/index.cjs9.js +0 -133
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
const require_isEqual = require("./index2.cjs.js");
|
|
2
|
+
//#region src/utils/queryDelta.ts
|
|
3
|
+
/**
|
|
4
|
+
* Checks whether a delta leaves the result it is applied to unchanged.
|
|
5
|
+
* @param delta - The delta to inspect.
|
|
6
|
+
* @returns `true` when applying the delta would be a no-op.
|
|
7
|
+
*/
|
|
8
|
+
function isEmptyQueryDelta(delta) {
|
|
9
|
+
return delta.added.length === 0 && delta.changed.length === 0 && delta.removed.length === 0 && delta.moved.length === 0;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Calls a state-change callback, passing the delta only when there is one.
|
|
13
|
+
*
|
|
14
|
+
* A callback invoked as `callback(state, undefined)` has been handed two arguments, which is a
|
|
15
|
+
* different thing from being handed one — visible to anything that inspects arity, and to any test
|
|
16
|
+
* that asserts on the call.
|
|
17
|
+
* @template T - The type of the items.
|
|
18
|
+
* @param callback - The callback to invoke.
|
|
19
|
+
* @param state - The state to report.
|
|
20
|
+
* @param delta - The delta to report, if there is one.
|
|
21
|
+
*/
|
|
22
|
+
function callWithDelta(callback, state, delta) {
|
|
23
|
+
if (delta == null) {
|
|
24
|
+
callback(state);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
callback(state, delta);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Checks whether a delta describes a change to the given result.
|
|
31
|
+
*
|
|
32
|
+
* A delta is only meaningful against the exact result it was computed from — it names positions in
|
|
33
|
+
* an array and items by id alone. Applying one to anything else produces a result that looks
|
|
34
|
+
* plausible and is wrong, and from then on every further delta compounds the error. This is the
|
|
35
|
+
* cheap structural check that catches that: every id the delta expects to find is there, every id
|
|
36
|
+
* it expects to be new is not, and the arithmetic on the length works out. It costs the size of the
|
|
37
|
+
* delta, not the size of the result.
|
|
38
|
+
* @template T - The type of the items.
|
|
39
|
+
* @param previous - The result the delta would be applied to.
|
|
40
|
+
* @param delta - The delta to check.
|
|
41
|
+
* @returns `true` when the delta can be applied.
|
|
42
|
+
*/
|
|
43
|
+
function canApplyQueryDelta(previous, delta) {
|
|
44
|
+
const present = new Set(previous.map((item) => item.id));
|
|
45
|
+
const seen = /* @__PURE__ */ new Set();
|
|
46
|
+
const claim = (id, shouldExist) => {
|
|
47
|
+
if (seen.has(id)) return false;
|
|
48
|
+
seen.add(id);
|
|
49
|
+
return present.has(id) === shouldExist;
|
|
50
|
+
};
|
|
51
|
+
if (previous.length - delta.removed.length + delta.added.length !== delta.resultCount) return false;
|
|
52
|
+
if (!delta.removed.every((id) => claim(id, true))) return false;
|
|
53
|
+
if (!delta.added.every(({ index, item }) => claim(item.id, false) && index >= 0 && index < delta.resultCount)) return false;
|
|
54
|
+
if (!delta.changed.every((item) => present.has(item.id))) return false;
|
|
55
|
+
return delta.moved.every(({ index, id }) => present.has(id) && !delta.removed.includes(id) && index >= 0 && index < delta.resultCount);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Indices of the longest strictly increasing subsequence of the given numbers.
|
|
59
|
+
* Used to decide which items keep their place when a result is reordered: everything outside the
|
|
60
|
+
* subsequence has to move, everything inside it is already in the right relative order.
|
|
61
|
+
* @param sequence - The numbers to inspect.
|
|
62
|
+
* @returns The indices into `sequence` that form the longest increasing subsequence.
|
|
63
|
+
*/
|
|
64
|
+
function longestIncreasingSubsequence(sequence) {
|
|
65
|
+
if (sequence.length === 0) return [];
|
|
66
|
+
const tails = [];
|
|
67
|
+
const previous = Array.from({ length: sequence.length }).fill(-1);
|
|
68
|
+
for (let index = 0; index < sequence.length; index += 1) {
|
|
69
|
+
const value = sequence[index];
|
|
70
|
+
let low = 0;
|
|
71
|
+
let high = tails.length;
|
|
72
|
+
while (low < high) {
|
|
73
|
+
const middle = low + high >> 1;
|
|
74
|
+
if (sequence[tails[middle]] < value) low = middle + 1;
|
|
75
|
+
else high = middle;
|
|
76
|
+
}
|
|
77
|
+
if (low > 0) previous[index] = tails[low - 1];
|
|
78
|
+
tails[low] = index;
|
|
79
|
+
}
|
|
80
|
+
const result = [];
|
|
81
|
+
let cursor = tails.at(-1);
|
|
82
|
+
while (cursor !== -1) {
|
|
83
|
+
result.push(cursor);
|
|
84
|
+
cursor = previous[cursor];
|
|
85
|
+
}
|
|
86
|
+
return result.toReversed();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Computes the delta between two results of the same query.
|
|
90
|
+
*
|
|
91
|
+
* A fallback for the cases where the change that produced the new result is not available — a query
|
|
92
|
+
* that had to be re-executed in full, for instance. It costs a pass over both results, but it is
|
|
93
|
+
* paid once, on the side that has both of them, instead of shipping the entire new result to
|
|
94
|
+
* everyone who only needs to know what changed.
|
|
95
|
+
* @template T - The type of the items.
|
|
96
|
+
* @param previous - The result the delta should be relative to.
|
|
97
|
+
* @param next - The result the delta should produce.
|
|
98
|
+
* @returns The delta between the two results.
|
|
99
|
+
*/
|
|
100
|
+
function diffQueryResults(previous, next) {
|
|
101
|
+
if (holdsTheSameItems(previous, next)) return {
|
|
102
|
+
added: [],
|
|
103
|
+
changed: [],
|
|
104
|
+
removed: [],
|
|
105
|
+
moved: [],
|
|
106
|
+
resultCount: next.length
|
|
107
|
+
};
|
|
108
|
+
const previousIndexById = /* @__PURE__ */ new Map();
|
|
109
|
+
previous.forEach((item, index) => previousIndexById.set(item.id, index));
|
|
110
|
+
const added = [];
|
|
111
|
+
const changed = [];
|
|
112
|
+
const survivingPreviousIndices = [];
|
|
113
|
+
const survivingNextIndices = [];
|
|
114
|
+
const survived = Array.from({ length: previous.length }).fill(false);
|
|
115
|
+
let orderPreserved = true;
|
|
116
|
+
let lastPreviousIndex = -1;
|
|
117
|
+
next.forEach((item, index) => {
|
|
118
|
+
const previousIndex = previousIndexById.get(item.id);
|
|
119
|
+
if (previousIndex == null) {
|
|
120
|
+
added.push({
|
|
121
|
+
index,
|
|
122
|
+
item
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
survived[previousIndex] = true;
|
|
127
|
+
if (!require_isEqual.default(previous[previousIndex], item)) changed.push(item);
|
|
128
|
+
if (previousIndex < lastPreviousIndex) orderPreserved = false;
|
|
129
|
+
lastPreviousIndex = previousIndex;
|
|
130
|
+
survivingPreviousIndices.push(previousIndex);
|
|
131
|
+
survivingNextIndices.push(index);
|
|
132
|
+
});
|
|
133
|
+
const removed = [];
|
|
134
|
+
previous.forEach((item, index) => {
|
|
135
|
+
if (!survived[index]) removed.push(item.id);
|
|
136
|
+
});
|
|
137
|
+
const moved = [];
|
|
138
|
+
if (!orderPreserved) {
|
|
139
|
+
const stationary = new Set(longestIncreasingSubsequence(survivingPreviousIndices).map((position) => survivingNextIndices[position]));
|
|
140
|
+
survivingNextIndices.forEach((nextIndex) => {
|
|
141
|
+
if (stationary.has(nextIndex)) return;
|
|
142
|
+
moved.push({
|
|
143
|
+
index: nextIndex,
|
|
144
|
+
id: next[nextIndex].id
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
added,
|
|
150
|
+
changed,
|
|
151
|
+
removed,
|
|
152
|
+
moved,
|
|
153
|
+
resultCount: next.length
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Whether two results hold the same items in the same order, by identity.
|
|
158
|
+
*
|
|
159
|
+
* Items are replaced rather than mutated wherever they come from, so identity is a sound answer to
|
|
160
|
+
* "unchanged" — and a wrong one is impossible, only a missed shortcut.
|
|
161
|
+
* @template T - The type of the items.
|
|
162
|
+
* @param previous - One result.
|
|
163
|
+
* @param next - The other.
|
|
164
|
+
* @returns `true` when the two are element-for-element the same objects.
|
|
165
|
+
*/
|
|
166
|
+
function holdsTheSameItems(previous, next) {
|
|
167
|
+
if (previous === next) return true;
|
|
168
|
+
if (previous.length !== next.length) return false;
|
|
169
|
+
return previous.every((item, index) => item === next[index]);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Applies a delta to the result it was computed against.
|
|
173
|
+
* @template T - The type of the items.
|
|
174
|
+
* @param previous - The result the delta is relative to. Not modified.
|
|
175
|
+
* @param delta - The delta to apply.
|
|
176
|
+
* @returns The resulting items.
|
|
177
|
+
*/
|
|
178
|
+
function applyQueryDelta(previous, delta) {
|
|
179
|
+
const removed = new Set(delta.removed);
|
|
180
|
+
const changedById = new Map(delta.changed.map((item) => [item.id, item]));
|
|
181
|
+
const movedIds = new Set(delta.moved.map(({ id }) => id));
|
|
182
|
+
const stationary = [];
|
|
183
|
+
const byId = /* @__PURE__ */ new Map();
|
|
184
|
+
previous.forEach((item) => {
|
|
185
|
+
if (removed.has(item.id)) return;
|
|
186
|
+
const current = changedById.get(item.id) ?? item;
|
|
187
|
+
byId.set(current.id, current);
|
|
188
|
+
if (!movedIds.has(current.id)) stationary.push(current);
|
|
189
|
+
});
|
|
190
|
+
const insertions = [...delta.added.map(({ index, item }) => ({
|
|
191
|
+
index,
|
|
192
|
+
item
|
|
193
|
+
})), ...delta.moved.map(({ index, id }) => ({
|
|
194
|
+
index,
|
|
195
|
+
item: byId.get(id)
|
|
196
|
+
}))].sort((a, b) => a.index - b.index);
|
|
197
|
+
const result = stationary;
|
|
198
|
+
insertions.forEach(({ index, item }) => {
|
|
199
|
+
if (item == null) return;
|
|
200
|
+
result.splice(index, 0, item);
|
|
201
|
+
});
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
exports.applyQueryDelta = applyQueryDelta;
|
|
206
|
+
exports.callWithDelta = callWithDelta;
|
|
207
|
+
exports.canApplyQueryDelta = canApplyQueryDelta;
|
|
208
|
+
exports.diffQueryResults = diffQueryResults;
|
|
209
|
+
exports.isEmptyQueryDelta = isEmptyQueryDelta;
|
package/dist/index4.mjs
CHANGED
|
@@ -1,6 +1,205 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import isEqual from "./index2.mjs";
|
|
2
|
+
//#region src/utils/queryDelta.ts
|
|
3
|
+
/**
|
|
4
|
+
* Checks whether a delta leaves the result it is applied to unchanged.
|
|
5
|
+
* @param delta - The delta to inspect.
|
|
6
|
+
* @returns `true` when applying the delta would be a no-op.
|
|
7
|
+
*/
|
|
8
|
+
function isEmptyQueryDelta(delta) {
|
|
9
|
+
return delta.added.length === 0 && delta.changed.length === 0 && delta.removed.length === 0 && delta.moved.length === 0;
|
|
3
10
|
}
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Calls a state-change callback, passing the delta only when there is one.
|
|
13
|
+
*
|
|
14
|
+
* A callback invoked as `callback(state, undefined)` has been handed two arguments, which is a
|
|
15
|
+
* different thing from being handed one — visible to anything that inspects arity, and to any test
|
|
16
|
+
* that asserts on the call.
|
|
17
|
+
* @template T - The type of the items.
|
|
18
|
+
* @param callback - The callback to invoke.
|
|
19
|
+
* @param state - The state to report.
|
|
20
|
+
* @param delta - The delta to report, if there is one.
|
|
21
|
+
*/
|
|
22
|
+
function callWithDelta(callback, state, delta) {
|
|
23
|
+
if (delta == null) {
|
|
24
|
+
callback(state);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
callback(state, delta);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Checks whether a delta describes a change to the given result.
|
|
31
|
+
*
|
|
32
|
+
* A delta is only meaningful against the exact result it was computed from — it names positions in
|
|
33
|
+
* an array and items by id alone. Applying one to anything else produces a result that looks
|
|
34
|
+
* plausible and is wrong, and from then on every further delta compounds the error. This is the
|
|
35
|
+
* cheap structural check that catches that: every id the delta expects to find is there, every id
|
|
36
|
+
* it expects to be new is not, and the arithmetic on the length works out. It costs the size of the
|
|
37
|
+
* delta, not the size of the result.
|
|
38
|
+
* @template T - The type of the items.
|
|
39
|
+
* @param previous - The result the delta would be applied to.
|
|
40
|
+
* @param delta - The delta to check.
|
|
41
|
+
* @returns `true` when the delta can be applied.
|
|
42
|
+
*/
|
|
43
|
+
function canApplyQueryDelta(previous, delta) {
|
|
44
|
+
const present = new Set(previous.map((item) => item.id));
|
|
45
|
+
const seen = /* @__PURE__ */ new Set();
|
|
46
|
+
const claim = (id, shouldExist) => {
|
|
47
|
+
if (seen.has(id)) return false;
|
|
48
|
+
seen.add(id);
|
|
49
|
+
return present.has(id) === shouldExist;
|
|
50
|
+
};
|
|
51
|
+
if (previous.length - delta.removed.length + delta.added.length !== delta.resultCount) return false;
|
|
52
|
+
if (!delta.removed.every((id) => claim(id, true))) return false;
|
|
53
|
+
if (!delta.added.every(({ index, item }) => claim(item.id, false) && index >= 0 && index < delta.resultCount)) return false;
|
|
54
|
+
if (!delta.changed.every((item) => present.has(item.id))) return false;
|
|
55
|
+
return delta.moved.every(({ index, id }) => present.has(id) && !delta.removed.includes(id) && index >= 0 && index < delta.resultCount);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Indices of the longest strictly increasing subsequence of the given numbers.
|
|
59
|
+
* Used to decide which items keep their place when a result is reordered: everything outside the
|
|
60
|
+
* subsequence has to move, everything inside it is already in the right relative order.
|
|
61
|
+
* @param sequence - The numbers to inspect.
|
|
62
|
+
* @returns The indices into `sequence` that form the longest increasing subsequence.
|
|
63
|
+
*/
|
|
64
|
+
function longestIncreasingSubsequence(sequence) {
|
|
65
|
+
if (sequence.length === 0) return [];
|
|
66
|
+
const tails = [];
|
|
67
|
+
const previous = Array.from({ length: sequence.length }).fill(-1);
|
|
68
|
+
for (let index = 0; index < sequence.length; index += 1) {
|
|
69
|
+
const value = sequence[index];
|
|
70
|
+
let low = 0;
|
|
71
|
+
let high = tails.length;
|
|
72
|
+
while (low < high) {
|
|
73
|
+
const middle = low + high >> 1;
|
|
74
|
+
if (sequence[tails[middle]] < value) low = middle + 1;
|
|
75
|
+
else high = middle;
|
|
76
|
+
}
|
|
77
|
+
if (low > 0) previous[index] = tails[low - 1];
|
|
78
|
+
tails[low] = index;
|
|
79
|
+
}
|
|
80
|
+
const result = [];
|
|
81
|
+
let cursor = tails.at(-1);
|
|
82
|
+
while (cursor !== -1) {
|
|
83
|
+
result.push(cursor);
|
|
84
|
+
cursor = previous[cursor];
|
|
85
|
+
}
|
|
86
|
+
return result.toReversed();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Computes the delta between two results of the same query.
|
|
90
|
+
*
|
|
91
|
+
* A fallback for the cases where the change that produced the new result is not available — a query
|
|
92
|
+
* that had to be re-executed in full, for instance. It costs a pass over both results, but it is
|
|
93
|
+
* paid once, on the side that has both of them, instead of shipping the entire new result to
|
|
94
|
+
* everyone who only needs to know what changed.
|
|
95
|
+
* @template T - The type of the items.
|
|
96
|
+
* @param previous - The result the delta should be relative to.
|
|
97
|
+
* @param next - The result the delta should produce.
|
|
98
|
+
* @returns The delta between the two results.
|
|
99
|
+
*/
|
|
100
|
+
function diffQueryResults(previous, next) {
|
|
101
|
+
if (holdsTheSameItems(previous, next)) return {
|
|
102
|
+
added: [],
|
|
103
|
+
changed: [],
|
|
104
|
+
removed: [],
|
|
105
|
+
moved: [],
|
|
106
|
+
resultCount: next.length
|
|
107
|
+
};
|
|
108
|
+
const previousIndexById = /* @__PURE__ */ new Map();
|
|
109
|
+
previous.forEach((item, index) => previousIndexById.set(item.id, index));
|
|
110
|
+
const added = [];
|
|
111
|
+
const changed = [];
|
|
112
|
+
const survivingPreviousIndices = [];
|
|
113
|
+
const survivingNextIndices = [];
|
|
114
|
+
const survived = Array.from({ length: previous.length }).fill(false);
|
|
115
|
+
let orderPreserved = true;
|
|
116
|
+
let lastPreviousIndex = -1;
|
|
117
|
+
next.forEach((item, index) => {
|
|
118
|
+
const previousIndex = previousIndexById.get(item.id);
|
|
119
|
+
if (previousIndex == null) {
|
|
120
|
+
added.push({
|
|
121
|
+
index,
|
|
122
|
+
item
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
survived[previousIndex] = true;
|
|
127
|
+
if (!isEqual(previous[previousIndex], item)) changed.push(item);
|
|
128
|
+
if (previousIndex < lastPreviousIndex) orderPreserved = false;
|
|
129
|
+
lastPreviousIndex = previousIndex;
|
|
130
|
+
survivingPreviousIndices.push(previousIndex);
|
|
131
|
+
survivingNextIndices.push(index);
|
|
132
|
+
});
|
|
133
|
+
const removed = [];
|
|
134
|
+
previous.forEach((item, index) => {
|
|
135
|
+
if (!survived[index]) removed.push(item.id);
|
|
136
|
+
});
|
|
137
|
+
const moved = [];
|
|
138
|
+
if (!orderPreserved) {
|
|
139
|
+
const stationary = new Set(longestIncreasingSubsequence(survivingPreviousIndices).map((position) => survivingNextIndices[position]));
|
|
140
|
+
survivingNextIndices.forEach((nextIndex) => {
|
|
141
|
+
if (stationary.has(nextIndex)) return;
|
|
142
|
+
moved.push({
|
|
143
|
+
index: nextIndex,
|
|
144
|
+
id: next[nextIndex].id
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
added,
|
|
150
|
+
changed,
|
|
151
|
+
removed,
|
|
152
|
+
moved,
|
|
153
|
+
resultCount: next.length
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Whether two results hold the same items in the same order, by identity.
|
|
158
|
+
*
|
|
159
|
+
* Items are replaced rather than mutated wherever they come from, so identity is a sound answer to
|
|
160
|
+
* "unchanged" — and a wrong one is impossible, only a missed shortcut.
|
|
161
|
+
* @template T - The type of the items.
|
|
162
|
+
* @param previous - One result.
|
|
163
|
+
* @param next - The other.
|
|
164
|
+
* @returns `true` when the two are element-for-element the same objects.
|
|
165
|
+
*/
|
|
166
|
+
function holdsTheSameItems(previous, next) {
|
|
167
|
+
if (previous === next) return true;
|
|
168
|
+
if (previous.length !== next.length) return false;
|
|
169
|
+
return previous.every((item, index) => item === next[index]);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Applies a delta to the result it was computed against.
|
|
173
|
+
* @template T - The type of the items.
|
|
174
|
+
* @param previous - The result the delta is relative to. Not modified.
|
|
175
|
+
* @param delta - The delta to apply.
|
|
176
|
+
* @returns The resulting items.
|
|
177
|
+
*/
|
|
178
|
+
function applyQueryDelta(previous, delta) {
|
|
179
|
+
const removed = new Set(delta.removed);
|
|
180
|
+
const changedById = new Map(delta.changed.map((item) => [item.id, item]));
|
|
181
|
+
const movedIds = new Set(delta.moved.map(({ id }) => id));
|
|
182
|
+
const stationary = [];
|
|
183
|
+
const byId = /* @__PURE__ */ new Map();
|
|
184
|
+
previous.forEach((item) => {
|
|
185
|
+
if (removed.has(item.id)) return;
|
|
186
|
+
const current = changedById.get(item.id) ?? item;
|
|
187
|
+
byId.set(current.id, current);
|
|
188
|
+
if (!movedIds.has(current.id)) stationary.push(current);
|
|
189
|
+
});
|
|
190
|
+
const insertions = [...delta.added.map(({ index, item }) => ({
|
|
191
|
+
index,
|
|
192
|
+
item
|
|
193
|
+
})), ...delta.moved.map(({ index, id }) => ({
|
|
194
|
+
index,
|
|
195
|
+
item: byId.get(id)
|
|
196
|
+
}))].sort((a, b) => a.index - b.index);
|
|
197
|
+
const result = stationary;
|
|
198
|
+
insertions.forEach(({ index, item }) => {
|
|
199
|
+
if (item == null) return;
|
|
200
|
+
result.splice(index, 0, item);
|
|
201
|
+
});
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
export { applyQueryDelta, callWithDelta, canApplyQueryDelta, diffQueryResults, isEmptyQueryDelta };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const require_isEqual = require("./index2.cjs.js");
|
|
2
|
+
const require_uniqueBy = require("./index3.cjs.js");
|
|
3
|
+
const require_queryDelta = require("./index4.cjs.js");
|
|
4
|
+
//#region src/Collection/Observer.ts
|
|
5
|
+
/**
|
|
6
|
+
* Represents an observer that tracks changes in a collection of items and triggers
|
|
7
|
+
* callbacks for various events such as addition, removal, and modification of items.
|
|
8
|
+
* @template T - The type of the items being observed, which must include an `id` field.
|
|
9
|
+
*/
|
|
10
|
+
var Observer = class {
|
|
11
|
+
previousItems = [];
|
|
12
|
+
callbacks;
|
|
13
|
+
unbindEvents;
|
|
14
|
+
/**
|
|
15
|
+
* Creates a new instance of the `Observer` class.
|
|
16
|
+
* Sets up event bindings and initializes the callbacks for tracking changes in a collection.
|
|
17
|
+
* @param bindEvents - A function to bind external events to the observer. Must return a cleanup function to unbind those events.
|
|
18
|
+
*/
|
|
19
|
+
constructor(bindEvents) {
|
|
20
|
+
this.callbacks = {
|
|
21
|
+
added: [],
|
|
22
|
+
addedBefore: [],
|
|
23
|
+
changed: [],
|
|
24
|
+
changedField: [],
|
|
25
|
+
movedBefore: [],
|
|
26
|
+
removed: []
|
|
27
|
+
};
|
|
28
|
+
this.unbindEvents = bindEvents();
|
|
29
|
+
}
|
|
30
|
+
call(event, ...args) {
|
|
31
|
+
this.callbacks[event].forEach(({ callback, options }) => {
|
|
32
|
+
if (!options.skipInitial || !options.isInitial) callback(...args);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
hasCallbacks(events) {
|
|
36
|
+
return events.some((event) => this.callbacks[event].length > 0);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Determines if the observer has no active callbacks registered for any events.
|
|
40
|
+
* @returns A boolean indicating whether the observer is empty (i.e., no callbacks are registered).
|
|
41
|
+
*/
|
|
42
|
+
isEmpty() {
|
|
43
|
+
return !this.hasCallbacks([
|
|
44
|
+
"added",
|
|
45
|
+
"addedBefore",
|
|
46
|
+
"changed",
|
|
47
|
+
"changedField",
|
|
48
|
+
"movedBefore",
|
|
49
|
+
"removed"
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Compares the previous state of items with the new state and triggers the appropriate callbacks
|
|
54
|
+
* for events such as added, removed, changed, or moved items.
|
|
55
|
+
* @param getItems - A function that returns a promise resolving to the new items or the items themselves.
|
|
56
|
+
*/
|
|
57
|
+
runChecks(getItems) {
|
|
58
|
+
const result = getItems();
|
|
59
|
+
if (result instanceof Promise) result.then((newItems) => this.checkItems(newItems)).catch((error) => {
|
|
60
|
+
console.error("Error while asynchronously querying items", error);
|
|
61
|
+
});
|
|
62
|
+
else this.checkItems(result);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Brings the observer up to date from a description of what changed, instead of from the new
|
|
66
|
+
* result.
|
|
67
|
+
*
|
|
68
|
+
* `runChecks` has to rediscover the change by comparing the whole new result against the whole
|
|
69
|
+
* old one — a cost proportional to the result, paid on every write, to find out that one row
|
|
70
|
+
* moved. When the change is already known it can simply be reported, and the cost becomes
|
|
71
|
+
* proportional to the change.
|
|
72
|
+
*
|
|
73
|
+
* The delta must have been computed against exactly the result this observer holds. If it was
|
|
74
|
+
* not, this reports nothing, falls back to `runChecks`, and returns `false`.
|
|
75
|
+
*
|
|
76
|
+
* Note that the reported moves are minimal, where a comparison reports every item whose
|
|
77
|
+
* neighbour changed. Applying them yields the same order either way — there are simply fewer of
|
|
78
|
+
* them.
|
|
79
|
+
* @param delta - The change to report.
|
|
80
|
+
* @param getItems - Used to fall back to a comparison when the delta cannot be applied.
|
|
81
|
+
* @returns Whether the delta was applied.
|
|
82
|
+
*/
|
|
83
|
+
applyDelta(delta, getItems) {
|
|
84
|
+
if (!require_queryDelta.canApplyQueryDelta(this.previousItems, delta)) {
|
|
85
|
+
this.runChecks(getItems);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (require_queryDelta.isEmptyQueryDelta(delta)) return true;
|
|
89
|
+
this.emitDelta(delta, require_queryDelta.applyQueryDelta(this.previousItems, delta));
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Reports a delta and adopts the result it produces.
|
|
94
|
+
*
|
|
95
|
+
* The single place the callbacks are fired from, whether the change arrived as a delta or was
|
|
96
|
+
* found by comparing two results — so the two can never disagree about what a consumer is told.
|
|
97
|
+
* @param delta - The change to report.
|
|
98
|
+
* @param nextItems - The result the delta produces.
|
|
99
|
+
*/
|
|
100
|
+
emitDelta(delta, nextItems) {
|
|
101
|
+
if (this.isEmpty()) {
|
|
102
|
+
this.finishCheck(nextItems);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
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;
|
|
107
|
+
if (this.hasCallbacks(["changed", "changedField"])) delta.changed.forEach((item) => {
|
|
108
|
+
this.call("changed", item);
|
|
109
|
+
if (!this.hasCallbacks(["changedField"])) return;
|
|
110
|
+
const oldItem = previousById?.get(item.id);
|
|
111
|
+
if (!oldItem) return;
|
|
112
|
+
require_uniqueBy.default([...Object.keys(item), ...Object.keys(oldItem)], (value) => value).forEach((key) => {
|
|
113
|
+
if (require_isEqual.default(item[key], oldItem[key])) return;
|
|
114
|
+
this.call("changedField", item, key, oldItem[key], item[key]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
if (this.hasCallbacks(["removed"])) delta.removed.forEach((id) => {
|
|
118
|
+
const oldItem = previousById?.get(id);
|
|
119
|
+
if (oldItem) this.call("removed", oldItem);
|
|
120
|
+
});
|
|
121
|
+
if (this.hasCallbacks(["added", "addedBefore"])) delta.added.forEach(({ index, item }) => {
|
|
122
|
+
this.call("added", item);
|
|
123
|
+
this.call("addedBefore", item, beforeOf(index));
|
|
124
|
+
});
|
|
125
|
+
if (this.hasCallbacks(["movedBefore"])) delta.moved.forEach(({ index }) => {
|
|
126
|
+
this.call("movedBefore", nextItems[index], beforeOf(index));
|
|
127
|
+
});
|
|
128
|
+
this.finishCheck(nextItems);
|
|
129
|
+
}
|
|
130
|
+
finishCheck(newItems) {
|
|
131
|
+
this.previousItems = newItems;
|
|
132
|
+
Object.keys(this.callbacks).forEach((key) => {
|
|
133
|
+
const event = key;
|
|
134
|
+
const callbacks = this.callbacks[event];
|
|
135
|
+
this.callbacks[event] = callbacks.map((callback) => ({
|
|
136
|
+
...callback,
|
|
137
|
+
options: {
|
|
138
|
+
...callback.options,
|
|
139
|
+
isInitial: false
|
|
140
|
+
}
|
|
141
|
+
}));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
checkItems(newItems) {
|
|
145
|
+
this.emitDelta(require_queryDelta.diffQueryResults(this.previousItems, newItems), newItems);
|
|
146
|
+
}
|
|
147
|
+
stopped = false;
|
|
148
|
+
/**
|
|
149
|
+
* Stops the observer by unbinding all events and cleaning up resources.
|
|
150
|
+
* Safe to call multiple times - will only unbind once.
|
|
151
|
+
*/
|
|
152
|
+
stop() {
|
|
153
|
+
if (this.stopped) return;
|
|
154
|
+
this.stopped = true;
|
|
155
|
+
this.unbindEvents();
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Registers callbacks for specific events to observe changes in the collection.
|
|
159
|
+
* @param callbacks - An object containing the callbacks for various events (e.g., 'added', 'removed').
|
|
160
|
+
* @param skipInitial - A boolean indicating whether to skip invoking the callbacks for the initial state of the collection.
|
|
161
|
+
*/
|
|
162
|
+
addCallbacks(callbacks, skipInitial = false) {
|
|
163
|
+
Object.keys(callbacks).forEach((key) => {
|
|
164
|
+
const typedKey = key;
|
|
165
|
+
this.callbacks[typedKey].push({
|
|
166
|
+
callback: callbacks[typedKey],
|
|
167
|
+
options: {
|
|
168
|
+
skipInitial,
|
|
169
|
+
isInitial: true
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Removes the specified callbacks for specific events, unregistering them from the observer.
|
|
176
|
+
* @param callbacks - An object containing the callbacks to be removed for various events.
|
|
177
|
+
*/
|
|
178
|
+
removeCallbacks(callbacks) {
|
|
179
|
+
Object.keys(callbacks).forEach((key) => {
|
|
180
|
+
const typedKey = key;
|
|
181
|
+
const index = this.callbacks[typedKey].findIndex(({ callback }) => callback === callbacks[typedKey]);
|
|
182
|
+
this.callbacks[typedKey].splice(index, 1);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
//#endregion
|
|
187
|
+
exports.default = Observer;
|