@signaldb/svelte 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/base/core/src/AsyncDataAdapter.d.ts +112 -0
- package/dist/base/core/src/AsyncDataAdapter.js +549 -0
- package/dist/base/core/src/AutoFetchDataAdapter.d.ts +112 -0
- package/dist/base/core/src/AutoFetchDataAdapter.js +612 -0
- package/dist/base/core/src/Collection/Cursor.d.ts +165 -0
- package/dist/base/core/src/Collection/Cursor.js +352 -0
- package/dist/base/core/src/Collection/Observer.d.ts +95 -0
- package/dist/base/core/src/Collection/Observer.js +227 -0
- package/dist/base/core/src/Collection/index.d.ts +341 -0
- package/dist/base/core/src/Collection/index.js +761 -0
- package/dist/base/core/src/Collection/types.d.ts +28 -0
- package/dist/base/core/src/Collection/types.js +2 -0
- package/dist/base/core/src/DataAdapter.d.ts +54 -0
- package/dist/base/core/src/DataAdapter.js +2 -0
- package/dist/base/core/src/DefaultDataAdapter.d.ts +35 -0
- package/dist/base/core/src/DefaultDataAdapter.js +408 -0
- package/dist/base/core/src/WorkerDataAdapter.d.ts +79 -0
- package/dist/base/core/src/WorkerDataAdapter.js +749 -0
- package/dist/base/core/src/WorkerDataAdapterHost.d.ts +64 -0
- package/dist/base/core/src/WorkerDataAdapterHost.js +430 -0
- package/dist/base/core/src/createIndex.d.ts +7 -0
- package/dist/base/core/src/createIndex.js +120 -0
- package/dist/base/core/src/createIndexProvider.d.ts +8 -0
- package/dist/base/core/src/createIndexProvider.js +11 -0
- package/dist/base/core/src/createReactivityAdapter.d.ts +8 -0
- package/dist/base/core/src/createReactivityAdapter.js +11 -0
- package/dist/base/core/src/createStorageAdapter.d.ts +9 -0
- package/dist/base/core/src/createStorageAdapter.js +11 -0
- package/dist/base/core/src/getIndexInfo.d.ts +39 -0
- package/dist/base/core/src/getIndexInfo.js +188 -0
- package/dist/base/core/src/index.d.ts +26 -0
- package/dist/base/core/src/index.js +39 -0
- package/dist/base/core/src/types/Dependency.d.ts +4 -0
- package/dist/base/core/src/types/Dependency.js +2 -0
- package/dist/base/core/src/types/IndexProvider.d.ts +26 -0
- package/dist/base/core/src/types/IndexProvider.js +2 -0
- package/dist/base/core/src/types/Modifier.d.ts +46 -0
- package/dist/base/core/src/types/Modifier.js +3 -0
- package/dist/base/core/src/types/ReactivityAdapter.d.ts +6 -0
- package/dist/base/core/src/types/ReactivityAdapter.js +2 -0
- package/dist/base/core/src/types/Selector.d.ts +46 -0
- package/dist/base/core/src/types/Selector.js +3 -0
- package/dist/base/core/src/types/Signal.d.ts +4 -0
- package/dist/base/core/src/types/Signal.js +2 -0
- package/dist/base/core/src/types/StorageAdapter.d.ts +28 -0
- package/dist/base/core/src/types/StorageAdapter.js +2 -0
- package/dist/base/core/src/utils/EventEmitter.d.ts +71 -0
- package/dist/base/core/src/utils/EventEmitter.js +144 -0
- package/dist/base/core/src/utils/applyQueryOptions.d.ts +15 -0
- package/dist/base/core/src/utils/applyQueryOptions.js +36 -0
- package/dist/base/core/src/utils/batchOnNextTick.d.ts +16 -0
- package/dist/base/core/src/utils/batchOnNextTick.js +72 -0
- package/dist/base/core/src/utils/compact.d.ts +9 -0
- package/dist/base/core/src/utils/compact.js +21 -0
- package/dist/base/core/src/utils/createSignal.d.ts +14 -0
- package/dist/base/core/src/utils/createSignal.js +38 -0
- package/dist/base/core/src/utils/deepClone.d.ts +17 -0
- package/dist/base/core/src/utils/deepClone.js +68 -0
- package/dist/base/core/src/utils/get.d.ts +9 -0
- package/dist/base/core/src/utils/get.js +27 -0
- package/dist/base/core/src/utils/getMatchingKeys.d.ts +19 -0
- package/dist/base/core/src/utils/getMatchingKeys.js +49 -0
- package/dist/base/core/src/utils/idIndexQuery.d.ts +20 -0
- package/dist/base/core/src/utils/idIndexQuery.js +49 -0
- package/dist/base/core/src/utils/incrementalQueryUpdate.d.ts +62 -0
- package/dist/base/core/src/utils/incrementalQueryUpdate.js +197 -0
- package/dist/base/core/src/utils/intersection.d.ts +9 -0
- package/dist/base/core/src/utils/intersection.js +16 -0
- package/dist/base/core/src/utils/isEqual.d.ts +14 -0
- package/dist/base/core/src/utils/isEqual.js +43 -0
- package/dist/base/core/src/utils/isFieldExpression.d.ts +11 -0
- package/dist/base/core/src/utils/isFieldExpression.js +52 -0
- package/dist/base/core/src/utils/match.d.ts +12 -0
- package/dist/base/core/src/utils/match.js +16 -0
- package/dist/base/core/src/utils/modify.d.ts +14 -0
- package/dist/base/core/src/utils/modify.js +29 -0
- package/dist/base/core/src/utils/project.d.ts +15 -0
- package/dist/base/core/src/utils/project.js +43 -0
- package/dist/base/core/src/utils/projectItems.d.ts +12 -0
- package/dist/base/core/src/utils/projectItems.js +25 -0
- package/dist/base/core/src/utils/queryDelta.d.ts +83 -0
- package/dist/base/core/src/utils/queryDelta.js +231 -0
- package/dist/base/core/src/utils/queryId.d.ts +9 -0
- package/dist/base/core/src/utils/queryId.js +61 -0
- package/dist/base/core/src/utils/randomId.d.ts +7 -0
- package/dist/base/core/src/utils/randomId.js +12 -0
- package/dist/base/core/src/utils/reactiveOrAsync.d.ts +59 -0
- package/dist/base/core/src/utils/reactiveOrAsync.js +93 -0
- package/dist/base/core/src/utils/serializeValue.d.ts +12 -0
- package/dist/base/core/src/utils/serializeValue.js +27 -0
- package/dist/base/core/src/utils/set.d.ts +13 -0
- package/dist/base/core/src/utils/set.js +43 -0
- package/dist/base/core/src/utils/sortItems.d.ts +12 -0
- package/dist/base/core/src/utils/sortItems.js +23 -0
- package/dist/base/core/src/utils/storageIndexQuery.d.ts +20 -0
- package/dist/base/core/src/utils/storageIndexQuery.js +75 -0
- package/dist/base/core/src/utils/uniqueBy.d.ts +10 -0
- package/dist/base/core/src/utils/uniqueBy.js +19 -0
- package/dist/index.cjs +38 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +38 -0
- package/dist/index.mjs.map +1 -0
- package/dist/reactivity-adapters/svelte/index.svelte.d.ts +10 -0
- package/dist/reactivity-adapters/svelte/index.svelte.js +36 -0
- package/package.json +3 -3
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const queryId_1 = __importDefault(require("./utils/queryId"));
|
|
7
|
+
const randomId_1 = __importDefault(require("./utils/randomId"));
|
|
8
|
+
const batchOnNextTick_1 = __importDefault(require("./utils/batchOnNextTick"));
|
|
9
|
+
const incrementalQueryUpdate_1 = require("./utils/incrementalQueryUpdate");
|
|
10
|
+
const queryDelta_1 = require("./utils/queryDelta");
|
|
11
|
+
const match_1 = __importDefault(require("./utils/match"));
|
|
12
|
+
const modify_1 = __importDefault(require("./utils/modify"));
|
|
13
|
+
const deepClone_1 = __importDefault(require("./utils/deepClone"));
|
|
14
|
+
// The ids a selector names outright, or `null` when it asks something the ids alone cannot answer.
|
|
15
|
+
// Deliberately strict: one key, `id`, holding a primitive or a lone `$in`. Anything else — another
|
|
16
|
+
// field alongside it, an operator, a nested condition — falls back to matching, because guessing
|
|
17
|
+
// wrong here would silently drop a row from a write.
|
|
18
|
+
/**
|
|
19
|
+
* Extracts the ids a selector names outright.
|
|
20
|
+
* @param selector - The selector to inspect.
|
|
21
|
+
* @returns The named ids, or `null` when the selector asks more than ids can answer.
|
|
22
|
+
*/
|
|
23
|
+
function selectorIds(selector) {
|
|
24
|
+
if (selector == null || typeof selector !== 'object')
|
|
25
|
+
return null;
|
|
26
|
+
const keys = Object.keys(selector);
|
|
27
|
+
if (keys.length !== 1 || keys[0] !== 'id')
|
|
28
|
+
return null;
|
|
29
|
+
const value = selector.id;
|
|
30
|
+
if (value == null)
|
|
31
|
+
return null;
|
|
32
|
+
if (typeof value !== 'object')
|
|
33
|
+
return [value];
|
|
34
|
+
const valueKeys = Object.keys(value);
|
|
35
|
+
if (valueKeys.length !== 1 || valueKeys[0] !== '$in')
|
|
36
|
+
return null;
|
|
37
|
+
const inValues = value.$in;
|
|
38
|
+
return Array.isArray(inValues) ? inValues : null;
|
|
39
|
+
}
|
|
40
|
+
class WorkerDataAdapter {
|
|
41
|
+
worker;
|
|
42
|
+
options;
|
|
43
|
+
id;
|
|
44
|
+
isDisposed = false;
|
|
45
|
+
workerReady;
|
|
46
|
+
log = () => { };
|
|
47
|
+
collectionReady = new Map();
|
|
48
|
+
batchExecutionHelpers = new Map();
|
|
49
|
+
queries = {};
|
|
50
|
+
// Resolvers for `exec` calls that are still waiting for their response, keyed by message id.
|
|
51
|
+
// Together with the query registry above this is everything the shared dispatcher needs to route
|
|
52
|
+
// a message, which is why there is no longer a listener per request or per query.
|
|
53
|
+
pendingRequests = new Map();
|
|
54
|
+
// Writes that have been issued but not yet confirmed by the worker. Their
|
|
55
|
+
// effect is layered on top of each active query's last authoritative result
|
|
56
|
+
// in `getQueryResult`, so a cursor reflects a write immediately instead of
|
|
57
|
+
// waiting out the postMessage → SQLite write → re-executed query →
|
|
58
|
+
// postMessage round trip. Deliberately scoped to items that active queries
|
|
59
|
+
// already hold: this adapter must not turn into a full in-memory mirror of
|
|
60
|
+
// the backing store, and anything no active query holds isn't observable
|
|
61
|
+
// anyway. Dropped again once the write settles — on success the worker's
|
|
62
|
+
// own `queryUpdate` has already landed (it is posted before the write's
|
|
63
|
+
// response, and message order is preserved), on failure dropping it is the
|
|
64
|
+
// rollback.
|
|
65
|
+
//
|
|
66
|
+
// Held in three shapes, because each answers a different question cheaply and none of them
|
|
67
|
+
// answers all three: `writes` says which ids a settling write touched, `byId` says what an id
|
|
68
|
+
// looked like before the newest write touched it, and `flat` is the collapsed view every reader
|
|
69
|
+
// actually wants. They are maintained together, as writes arrive and settle.
|
|
70
|
+
pendingWrites = new Map();
|
|
71
|
+
pendingWriteSeq = 0;
|
|
72
|
+
// Bumped whenever a collection's pending writes change, in either direction. Anything derived
|
|
73
|
+
// from them is stale from that moment on.
|
|
74
|
+
pendingWriteVersions = new Map();
|
|
75
|
+
bumpPendingWriteVersion(collectionName) {
|
|
76
|
+
const current = this.pendingWriteVersions.get(collectionName) ?? 0;
|
|
77
|
+
this.pendingWriteVersions.set(collectionName, current + 1);
|
|
78
|
+
}
|
|
79
|
+
constructor(worker, options) {
|
|
80
|
+
this.worker = worker;
|
|
81
|
+
this.options = options;
|
|
82
|
+
this.id = this.options.id || 'default-worker-data-adapter';
|
|
83
|
+
if (this.options.log)
|
|
84
|
+
this.log = this.options.log;
|
|
85
|
+
this.workerReady = new Promise((resolve, reject) => {
|
|
86
|
+
const timeoutId = setTimeout(() => {
|
|
87
|
+
reject(new Error('WorkerDataAdapter initialization timed out'));
|
|
88
|
+
}, 5000);
|
|
89
|
+
this.resolveWorkerReady = () => {
|
|
90
|
+
clearTimeout(timeoutId);
|
|
91
|
+
resolve();
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
this.worker.addEventListener('message', this.handleWorkerMessage);
|
|
95
|
+
}
|
|
96
|
+
resolveWorkerReady = () => { };
|
|
97
|
+
// The one and only message listener this adapter installs. Every response and every query update
|
|
98
|
+
// is routed from here by a map lookup. Listening per request and per query instead meant each
|
|
99
|
+
// incoming message was offered to every listener in turn, and each of them re-serialized its own
|
|
100
|
+
// selector to decide the message was not for it — turning a write that touches N queries into
|
|
101
|
+
// N² selector serializations before any of the actual work started.
|
|
102
|
+
handleWorkerMessage = (event) => {
|
|
103
|
+
const message = event.data;
|
|
104
|
+
if (message == null)
|
|
105
|
+
return;
|
|
106
|
+
if (message.workerId !== this.id)
|
|
107
|
+
return;
|
|
108
|
+
if (message.type === 'ready') {
|
|
109
|
+
this.resolveWorkerReady();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (message.type === 'response') {
|
|
113
|
+
if (message.id == null)
|
|
114
|
+
return;
|
|
115
|
+
const pending = this.pendingRequests.get(message.id);
|
|
116
|
+
if (!pending)
|
|
117
|
+
return;
|
|
118
|
+
this.pendingRequests.delete(message.id);
|
|
119
|
+
this.log('response', message.data ?? message.error);
|
|
120
|
+
if (message.error) {
|
|
121
|
+
pending.reject(message.error);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
pending.resolve(message.data);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (message.type === 'queryUpdate')
|
|
129
|
+
this.handleQueryUpdate(message.data, message.error ?? null);
|
|
130
|
+
};
|
|
131
|
+
handleQueryUpdate(data, error) {
|
|
132
|
+
if (data == null)
|
|
133
|
+
return;
|
|
134
|
+
const { collectionName, qid, selector, options, state, items, delta, } = data;
|
|
135
|
+
if (collectionName == null)
|
|
136
|
+
return;
|
|
137
|
+
const collectionQueries = this.queries[collectionName];
|
|
138
|
+
if (!collectionQueries)
|
|
139
|
+
return;
|
|
140
|
+
// The host names the query outright; deriving the id from the selector is only for messages
|
|
141
|
+
// that predate that (and for tests that hand-roll one).
|
|
142
|
+
const id = qid ?? (selector === undefined ? undefined : (0, queryId_1.default)(selector, options));
|
|
143
|
+
if (id == null)
|
|
144
|
+
return;
|
|
145
|
+
const query = collectionQueries.get(id);
|
|
146
|
+
if (!query)
|
|
147
|
+
return;
|
|
148
|
+
this.log('queryUpdate', query.selector, query.options, state, data ?? error);
|
|
149
|
+
let nextItems = items;
|
|
150
|
+
let deltaToPublish;
|
|
151
|
+
if (delta != null) {
|
|
152
|
+
// A delta only makes sense against the result it was computed from. If this adapter is
|
|
153
|
+
// holding something else — a message lost, a query re-registered underneath, a host and an
|
|
154
|
+
// adapter that disagree — applying it anyway would leave a result that silently drifts from
|
|
155
|
+
// the store. Refusing it keeps the last coherent result instead, and the next full answer
|
|
156
|
+
// puts things right. A delta describing no change at all is refused for the simpler reason
|
|
157
|
+
// that there is nothing to apply.
|
|
158
|
+
//
|
|
159
|
+
// The *state* the message carries is not part of that judgement, and used to be dropped
|
|
160
|
+
// along with the delta. A host that cannot answer a query from its previous result announces
|
|
161
|
+
// `'active'`, re-executes, and answers with a delta — which is empty whenever the write left
|
|
162
|
+
// this query's result as it was. The query then stayed `'active'` here for good, because no
|
|
163
|
+
// further message was coming to correct it: `Cursor#isLoading()` reported a first result
|
|
164
|
+
// still pending forever, and every screen gated on it showed its loading state forever.
|
|
165
|
+
const canApply = (0, queryDelta_1.canApplyQueryDelta)(query.items, delta);
|
|
166
|
+
if (!canApply || (0, queryDelta_1.isEmptyQueryDelta)(delta)) {
|
|
167
|
+
if (state === query.state)
|
|
168
|
+
return;
|
|
169
|
+
this.updateQuery(collectionName, {
|
|
170
|
+
selector: query.selector,
|
|
171
|
+
options: query.options,
|
|
172
|
+
}, { state, error });
|
|
173
|
+
const settled = collectionQueries.get(id);
|
|
174
|
+
if (!settled)
|
|
175
|
+
return;
|
|
176
|
+
// An empty delta is passed on as it came: it settles the query without asking anyone to
|
|
177
|
+
// recompute a result that did not change. One that could not be applied is not, so the
|
|
178
|
+
// consumer re-reads and the disagreement ends there.
|
|
179
|
+
settled.stateChangeCallbacks
|
|
180
|
+
.forEach(callback => (0, queryDelta_1.callWithDelta)(callback, state, canApply ? delta : undefined));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const pendingBefore = this.flattenPendingWrites(collectionName);
|
|
184
|
+
const servedBefore = pendingBefore == null
|
|
185
|
+
? null
|
|
186
|
+
: this.servedResult(collectionName, query);
|
|
187
|
+
nextItems = (0, queryDelta_1.applyQueryDelta)(query.items, delta);
|
|
188
|
+
if (servedBefore == null) {
|
|
189
|
+
// Nothing is layered on top of the stored result, so what the host described is exactly
|
|
190
|
+
// what a reader of this query will see change.
|
|
191
|
+
deltaToPublish = delta;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
// A write is still in flight, and its effect has been shown to readers all along. What
|
|
195
|
+
// they see change is the difference between the two layered results — which, for the
|
|
196
|
+
// ordinary case of the host confirming the write that is in flight, is nothing at all.
|
|
197
|
+
this.updateQuery(collectionName, {
|
|
198
|
+
selector: query.selector,
|
|
199
|
+
options: query.options,
|
|
200
|
+
}, { state, error, items: nextItems });
|
|
201
|
+
const stored = collectionQueries.get(id);
|
|
202
|
+
if (!stored)
|
|
203
|
+
return;
|
|
204
|
+
const servedDelta = (0, queryDelta_1.diffQueryResults)(servedBefore, this.servedResult(collectionName, stored));
|
|
205
|
+
if ((0, queryDelta_1.isEmptyQueryDelta)(servedDelta) && state === query.state)
|
|
206
|
+
return;
|
|
207
|
+
stored.stateChangeCallbacks
|
|
208
|
+
.forEach(callback => (0, queryDelta_1.callWithDelta)(callback, state, servedDelta));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
this.updateQuery(collectionName, {
|
|
213
|
+
selector: query.selector,
|
|
214
|
+
options: query.options,
|
|
215
|
+
}, { state, error, items: nextItems });
|
|
216
|
+
const updated = collectionQueries.get(id);
|
|
217
|
+
if (!updated)
|
|
218
|
+
return;
|
|
219
|
+
updated.stateChangeCallbacks.forEach(callback => (0, queryDelta_1.callWithDelta)(callback, state, deltaToPublish));
|
|
220
|
+
}
|
|
221
|
+
async exec(method, collectionName, ...args) {
|
|
222
|
+
await this.workerReady;
|
|
223
|
+
if (method !== 'isReady') {
|
|
224
|
+
const collectionReady = this.collectionReady.get(collectionName);
|
|
225
|
+
if (!collectionReady)
|
|
226
|
+
throw new Error(`Collection "${collectionName}" is not registered in WorkerDataAdapter`);
|
|
227
|
+
await collectionReady;
|
|
228
|
+
}
|
|
229
|
+
if (this.isDisposed) {
|
|
230
|
+
throw new Error('WorkerDataAdapter is disposed');
|
|
231
|
+
}
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
const messageId = (0, randomId_1.default)();
|
|
234
|
+
this.pendingRequests.set(messageId, { resolve, reject });
|
|
235
|
+
this.worker.postMessage({
|
|
236
|
+
id: messageId,
|
|
237
|
+
workerId: this.id,
|
|
238
|
+
method,
|
|
239
|
+
args: [collectionName, ...args],
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Issues a call whose result nobody is waiting for, and makes sure a failure has somewhere to
|
|
245
|
+
* go. A bare rejection here would surface as an uncaught error — which is what a disposed
|
|
246
|
+
* collection produced every time a cursor was cleaned up after it.
|
|
247
|
+
* @param method - The method to call on the worker.
|
|
248
|
+
* @param collectionName - The collection it applies to.
|
|
249
|
+
* @param args - The remaining arguments.
|
|
250
|
+
* @param onError - Called when the call fails, in place of merely logging it.
|
|
251
|
+
*/
|
|
252
|
+
execInBackground(method, collectionName, args = [], onError) {
|
|
253
|
+
this.exec(method, collectionName, ...args).catch((error) => {
|
|
254
|
+
if (onError) {
|
|
255
|
+
onError(error);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
this.log(method, 'failed', error);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
queryItemsById(query) {
|
|
262
|
+
if (!query.itemsById) {
|
|
263
|
+
query.itemsById = new Map(query.items.map(item => [item.id, item]));
|
|
264
|
+
}
|
|
265
|
+
return query.itemsById;
|
|
266
|
+
}
|
|
267
|
+
// The pending writes of a collection collapsed into one upsert/delete view, newest write winning.
|
|
268
|
+
// `null` when there are none, which is the overwhelmingly common case and the one every caller
|
|
269
|
+
// below short-circuits on. Maintained by the two helpers under it rather than rebuilt here, so
|
|
270
|
+
// this is a lookup whatever the size of the pending set — see `PendingWriteState`. The returned
|
|
271
|
+
// view is the live one: callers read it, never mutate it.
|
|
272
|
+
flattenPendingWrites(collectionName) {
|
|
273
|
+
const state = this.pendingWrites.get(collectionName);
|
|
274
|
+
if (!state || state.writes.size === 0)
|
|
275
|
+
return null;
|
|
276
|
+
return state.flat;
|
|
277
|
+
}
|
|
278
|
+
// Records one id's contribution from the write being registered. `seq` only ever grows, so the
|
|
279
|
+
// new entry is by construction the newest one for this id and therefore the one that wins.
|
|
280
|
+
static pushPendingEntry(state, id, item, seq) {
|
|
281
|
+
const stack = state.byId.get(id);
|
|
282
|
+
if (stack)
|
|
283
|
+
stack.push({ seq, item });
|
|
284
|
+
else
|
|
285
|
+
state.byId.set(id, [{ seq, item }]);
|
|
286
|
+
WorkerDataAdapter.writeFlatEntry(state, id, item);
|
|
287
|
+
}
|
|
288
|
+
// Takes one id's contribution back out as its write settles, and restores whatever the write
|
|
289
|
+
// below it said — or removes the id entirely when that was the only one.
|
|
290
|
+
static dropPendingEntry(state, id, seq) {
|
|
291
|
+
const stack = state.byId.get(id);
|
|
292
|
+
if (!stack)
|
|
293
|
+
return;
|
|
294
|
+
const index = stack.findIndex(entry => entry.seq === seq);
|
|
295
|
+
if (index !== -1)
|
|
296
|
+
stack.splice(index, 1);
|
|
297
|
+
const top = stack.at(-1);
|
|
298
|
+
if (!top) {
|
|
299
|
+
state.byId.delete(id);
|
|
300
|
+
state.flat.upserts.delete(id);
|
|
301
|
+
state.flat.deletes.delete(id);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
WorkerDataAdapter.writeFlatEntry(state, id, top.item);
|
|
305
|
+
}
|
|
306
|
+
static writeFlatEntry(state, id, item) {
|
|
307
|
+
if (item === null) {
|
|
308
|
+
state.flat.deletes.add(id);
|
|
309
|
+
state.flat.upserts.delete(id);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
state.flat.upserts.set(id, item);
|
|
313
|
+
state.flat.deletes.delete(id);
|
|
314
|
+
}
|
|
315
|
+
// The items an active query currently holds, deduplicated by id, plus whatever the pending writes
|
|
316
|
+
// add or remove — the only items this adapter knows about, and the set a selector-based write is
|
|
317
|
+
// resolved against locally. One pass over the queries rather than a merge per query.
|
|
318
|
+
// Whether a query's result is the items themselves rather than a projection of them. A write is
|
|
319
|
+
// resolved locally by applying its modifier to the item this adapter holds, and applying it to an
|
|
320
|
+
// item that has had fields removed produces something that is not the item — one that a selector
|
|
321
|
+
// naming a projected-away field no longer matches, so the row would vanish from every other
|
|
322
|
+
// query until the store answered. An item known only through a projection is therefore treated as
|
|
323
|
+
// not known at all: the write still happens, it simply is not shown before the store confirms it.
|
|
324
|
+
static providesFullItems(query) {
|
|
325
|
+
return query.options?.fields == null;
|
|
326
|
+
}
|
|
327
|
+
observableItems(collectionName) {
|
|
328
|
+
const byId = new Map();
|
|
329
|
+
this.queries[collectionName]?.forEach((query) => {
|
|
330
|
+
if (!WorkerDataAdapter.providesFullItems(query))
|
|
331
|
+
return;
|
|
332
|
+
query.items.forEach(item => byId.set(item.id, item));
|
|
333
|
+
});
|
|
334
|
+
const pending = this.flattenPendingWrites(collectionName);
|
|
335
|
+
if (pending) {
|
|
336
|
+
pending.upserts.forEach((item, id) => byId.set(id, item));
|
|
337
|
+
pending.deletes.forEach(id => byId.delete(id));
|
|
338
|
+
}
|
|
339
|
+
return [...byId.values()];
|
|
340
|
+
}
|
|
341
|
+
// The same answer as `observableItems` restricted to known ids — for the selector shapes that
|
|
342
|
+
// name them (`{ id }`, `{ id: { $in } }`), which is what an ordinary `updateOne`/`removeOne`
|
|
343
|
+
// carries. Costs a handful of map lookups instead of materialising every active query's result
|
|
344
|
+
// and running the matcher over all of it.
|
|
345
|
+
observableItemsByIds(collectionName, ids) {
|
|
346
|
+
const pending = this.flattenPendingWrites(collectionName);
|
|
347
|
+
const found = new Map();
|
|
348
|
+
ids.forEach((id) => {
|
|
349
|
+
if (pending?.deletes.has(id))
|
|
350
|
+
return;
|
|
351
|
+
const pendingItem = pending?.upserts.get(id);
|
|
352
|
+
if (pendingItem) {
|
|
353
|
+
found.set(id, pendingItem);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const queries = this.queries[collectionName];
|
|
357
|
+
if (!queries)
|
|
358
|
+
return;
|
|
359
|
+
for (const query of queries.values()) {
|
|
360
|
+
if (!WorkerDataAdapter.providesFullItems(query))
|
|
361
|
+
continue;
|
|
362
|
+
const item = this.queryItemsById(query).get(id);
|
|
363
|
+
if (item) {
|
|
364
|
+
found.set(id, item);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
return [...found.values()];
|
|
370
|
+
}
|
|
371
|
+
// Whether any pending write changes what this query would return. Answered against the pending
|
|
372
|
+
// set (small) and the query's id index, never by walking its items: a query no in-flight write
|
|
373
|
+
// touches — nearly all of them, nearly always — keeps its own array, so its readers skip both the
|
|
374
|
+
// merge and the re-filtering that would follow it.
|
|
375
|
+
// What `getQueryResult` answers: the query's last confirmed result with whatever writes are still
|
|
376
|
+
// in flight folded into it. Only the items those writes touch are examined — the rest matched
|
|
377
|
+
// when the store produced them and are carried over untouched, which is both what makes this cost
|
|
378
|
+
// the size of the pending writes rather than the size of the result, and what keeps a projected
|
|
379
|
+
// result from being re-matched against fields its projection has already dropped.
|
|
380
|
+
servedResult(collectionName, query) {
|
|
381
|
+
const pendingVersion = this.pendingWriteVersions.get(collectionName) ?? 0;
|
|
382
|
+
if (query.served
|
|
383
|
+
&& query.served.fromItems === query.items
|
|
384
|
+
&& query.served.pendingVersion === pendingVersion) {
|
|
385
|
+
return query.served.items;
|
|
386
|
+
}
|
|
387
|
+
const items = this.advanceServedResult(collectionName, query, pendingVersion)
|
|
388
|
+
?? this.computeServedResult(collectionName, query);
|
|
389
|
+
query.served = { items, fromItems: query.items, pendingVersion };
|
|
390
|
+
return items;
|
|
391
|
+
}
|
|
392
|
+
// One step forward from the answer this query already had, instead of rebuilding it from every
|
|
393
|
+
// pending write. Only the ids the newest change touched are looked at, so a burst of writes costs
|
|
394
|
+
// each of them the size of that write — where rebuilding re-matched and re-projected the whole
|
|
395
|
+
// pending set per write, which is quadratic in the burst and was measured at twelve seconds for
|
|
396
|
+
// four thousand unsettled inserts against a single matching query.
|
|
397
|
+
//
|
|
398
|
+
// `null` means the step is not available and the caller has to rebuild: nothing to step from, a
|
|
399
|
+
// gap of more than one version, or a query shape whose result cannot be derived from itself — a
|
|
400
|
+
// projection cannot be re-matched against a selector naming fields it dropped, and a limited
|
|
401
|
+
// query is a window whose content can depend on rows it does not hold.
|
|
402
|
+
advanceServedResult(collectionName, query, pendingVersion) {
|
|
403
|
+
const served = query.served;
|
|
404
|
+
if (!served || served.fromItems !== query.items)
|
|
405
|
+
return null;
|
|
406
|
+
if (served.pendingVersion !== pendingVersion - 1)
|
|
407
|
+
return null;
|
|
408
|
+
if (!WorkerDataAdapter.providesFullItems(query) || query.options?.limit != null)
|
|
409
|
+
return null;
|
|
410
|
+
const state = this.pendingWrites.get(collectionName);
|
|
411
|
+
if (state?.lastChange == null || state.lastChange.version !== pendingVersion)
|
|
412
|
+
return null;
|
|
413
|
+
return (0, incrementalQueryUpdate_1.mergeChangesetIntoResult)(served.items, query.selector, query.options, this.changesetForIds(collectionName, query, state.lastChange.ids));
|
|
414
|
+
}
|
|
415
|
+
// What the named ids look like now — the pending value if one is still in flight for them, and
|
|
416
|
+
// otherwise whatever the last confirmed result said, which is what a settling write reverts to.
|
|
417
|
+
// The same rule answers a write arriving and a write settling, so both take the step above.
|
|
418
|
+
changesetForIds(collectionName, query, ids) {
|
|
419
|
+
const pending = this.flattenPendingWrites(collectionName);
|
|
420
|
+
const stored = this.queryItemsById(query);
|
|
421
|
+
const upserts = [];
|
|
422
|
+
const deletes = [];
|
|
423
|
+
ids.forEach((id) => {
|
|
424
|
+
const pendingItem = pending?.upserts.get(id);
|
|
425
|
+
if (pendingItem) {
|
|
426
|
+
upserts.push(pendingItem);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (pending?.deletes.has(id)) {
|
|
430
|
+
deletes.push(id);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const storedItem = stored.get(id);
|
|
434
|
+
if (storedItem)
|
|
435
|
+
upserts.push(storedItem);
|
|
436
|
+
else
|
|
437
|
+
deletes.push(id);
|
|
438
|
+
});
|
|
439
|
+
return { upserts, deletes };
|
|
440
|
+
}
|
|
441
|
+
computeServedResult(collectionName, query) {
|
|
442
|
+
const pending = this.flattenPendingWrites(collectionName);
|
|
443
|
+
if (!pending)
|
|
444
|
+
return query.items;
|
|
445
|
+
const byId = this.queryItemsById(query);
|
|
446
|
+
let affected = false;
|
|
447
|
+
pending.deletes.forEach((id) => {
|
|
448
|
+
if (byId.has(id))
|
|
449
|
+
affected = true;
|
|
450
|
+
});
|
|
451
|
+
if (!affected) {
|
|
452
|
+
pending.upserts.forEach((item, id) => {
|
|
453
|
+
if (affected)
|
|
454
|
+
return;
|
|
455
|
+
if (byId.has(id))
|
|
456
|
+
affected = true;
|
|
457
|
+
else if (query.selector != null && (0, match_1.default)(item, query.selector))
|
|
458
|
+
affected = true;
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
if (!affected)
|
|
462
|
+
return query.items;
|
|
463
|
+
return (0, incrementalQueryUpdate_1.mergeChangesetIntoResult)(query.items, query.selector, query.options, {
|
|
464
|
+
upserts: [...pending.upserts.values()],
|
|
465
|
+
deletes: [...pending.deletes],
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Registers a write's effect locally and notifies every active query it
|
|
470
|
+
* touches, then returns a function that drops it again once the write
|
|
471
|
+
* settles.
|
|
472
|
+
* @param collectionName - The collection the write targets.
|
|
473
|
+
* @param upserts - Items inserted or updated by the write.
|
|
474
|
+
* @param deletes - Ids removed by the write.
|
|
475
|
+
* @returns A function that drops the pending write and re-notifies.
|
|
476
|
+
*/
|
|
477
|
+
applyPendingWrite(collectionName, upserts, deletes) {
|
|
478
|
+
if (upserts.length === 0 && deletes.length === 0)
|
|
479
|
+
return () => { };
|
|
480
|
+
const affectedIds = new Set([...upserts.map(item => item.id), ...deletes]);
|
|
481
|
+
const affected = this.affectedQueries(collectionName, upserts, affectedIds);
|
|
482
|
+
// Captured before the write is registered, so the notification below can say what actually
|
|
483
|
+
// changed for a reader rather than just that something did.
|
|
484
|
+
const servedBefore = this.servedResults(collectionName, affected);
|
|
485
|
+
const seq = this.pendingWriteSeq += 1;
|
|
486
|
+
const state = this.pendingWrites.get(collectionName)
|
|
487
|
+
?? {
|
|
488
|
+
writes: new Map(),
|
|
489
|
+
byId: new Map(),
|
|
490
|
+
flat: { upserts: new Map(), deletes: new Set() },
|
|
491
|
+
lastChange: null,
|
|
492
|
+
};
|
|
493
|
+
const write = {
|
|
494
|
+
upserts: new Map(upserts.map(item => [item.id, item])),
|
|
495
|
+
deletes: new Set(deletes),
|
|
496
|
+
};
|
|
497
|
+
state.writes.set(seq, write);
|
|
498
|
+
// Upserts first, then deletes, so a write naming the same id in both ends as a delete — the
|
|
499
|
+
// order the collapsed view was folded in when it was still rebuilt from scratch.
|
|
500
|
+
write.upserts.forEach((item, id) => WorkerDataAdapter.pushPendingEntry(state, id, item, seq));
|
|
501
|
+
write.deletes.forEach(id => WorkerDataAdapter.pushPendingEntry(state, id, null, seq));
|
|
502
|
+
this.pendingWrites.set(collectionName, state);
|
|
503
|
+
this.bumpPendingWriteVersion(collectionName);
|
|
504
|
+
const addedVersion = this.pendingWriteVersions.get(collectionName) ?? 0;
|
|
505
|
+
state.lastChange = { version: addedVersion, ids: [...affectedIds] };
|
|
506
|
+
this.notifyWithDeltas(collectionName, affected, servedBefore);
|
|
507
|
+
return () => {
|
|
508
|
+
const current = this.pendingWrites.get(collectionName);
|
|
509
|
+
if (!current)
|
|
510
|
+
return;
|
|
511
|
+
const settled = current.writes.get(seq);
|
|
512
|
+
// Already dropped: settling twice must not take a *later* write's contribution back out.
|
|
513
|
+
if (!settled)
|
|
514
|
+
return;
|
|
515
|
+
const affectedOnDrop = this.affectedQueries(collectionName, upserts, affectedIds);
|
|
516
|
+
const beforeDrop = this.servedResults(collectionName, affectedOnDrop);
|
|
517
|
+
current.writes.delete(seq);
|
|
518
|
+
settled.upserts.forEach((item, id) => WorkerDataAdapter.dropPendingEntry(current, id, seq));
|
|
519
|
+
settled.deletes.forEach(id => WorkerDataAdapter.dropPendingEntry(current, id, seq));
|
|
520
|
+
// With nothing left in flight the state is dropped entirely, and a query's served result is
|
|
521
|
+
// its stored one again — which `computeServedResult` answers without looking at anything.
|
|
522
|
+
if (current.writes.size === 0)
|
|
523
|
+
this.pendingWrites.delete(collectionName);
|
|
524
|
+
this.bumpPendingWriteVersion(collectionName);
|
|
525
|
+
if (current.writes.size > 0) {
|
|
526
|
+
const droppedVersion = this.pendingWriteVersions.get(collectionName) ?? 0;
|
|
527
|
+
current.lastChange = { version: droppedVersion, ids: [...affectedIds] };
|
|
528
|
+
}
|
|
529
|
+
// By the time a write settles the host's own answer has usually already landed, so dropping
|
|
530
|
+
// the optimistic copy changes nothing a reader can see and produces no notification at all.
|
|
531
|
+
this.notifyWithDeltas(collectionName, affectedOnDrop, beforeDrop);
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
// The queries whose result the write can have changed — either because a written item matches
|
|
535
|
+
// their selector, or because they already hold one of the affected items (an update that moves
|
|
536
|
+
// an item out of a query, or a removal).
|
|
537
|
+
affectedQueries(collectionName, upserts, affectedIds) {
|
|
538
|
+
const affected = [];
|
|
539
|
+
this.queries[collectionName]?.forEach((query) => {
|
|
540
|
+
const byId = this.queryItemsById(query);
|
|
541
|
+
let wasHolding = false;
|
|
542
|
+
affectedIds.forEach((id) => {
|
|
543
|
+
if (byId.has(id))
|
|
544
|
+
wasHolding = true;
|
|
545
|
+
});
|
|
546
|
+
const nowMatches = upserts.some(item => query.selector != null
|
|
547
|
+
&& (0, match_1.default)(item, query.selector));
|
|
548
|
+
if (!wasHolding && !nowMatches)
|
|
549
|
+
return;
|
|
550
|
+
affected.push(query);
|
|
551
|
+
});
|
|
552
|
+
return affected;
|
|
553
|
+
}
|
|
554
|
+
servedResults(collectionName, queries) {
|
|
555
|
+
return new Map(queries.map(query => [query, this.servedResult(collectionName, query)]));
|
|
556
|
+
}
|
|
557
|
+
// Tells each query what changed for someone reading it, and says nothing to a query where the
|
|
558
|
+
// answer is the same as before. A reader that has to re-run the query to find that out pays for
|
|
559
|
+
// the whole result to learn nothing.
|
|
560
|
+
notifyWithDeltas(collectionName, queries, servedBefore) {
|
|
561
|
+
queries.forEach((query) => {
|
|
562
|
+
const before = servedBefore.get(query);
|
|
563
|
+
if (before == null)
|
|
564
|
+
return;
|
|
565
|
+
const delta = (0, queryDelta_1.diffQueryResults)(before, this.servedResult(collectionName, query));
|
|
566
|
+
if ((0, queryDelta_1.isEmptyQueryDelta)(delta))
|
|
567
|
+
return;
|
|
568
|
+
query.stateChangeCallbacks.forEach(callback => (0, queryDelta_1.callWithDelta)(callback, query.state, delta));
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
matchObservableItems(collectionName, selector, onlyFirst) {
|
|
572
|
+
if (selector == null)
|
|
573
|
+
return [];
|
|
574
|
+
// `updateOne({ id })` and `removeOne({ id })` are what an application writes most of the time,
|
|
575
|
+
// and they name exactly the rows they touch — no reason to materialise every active query's
|
|
576
|
+
// result and run the matcher over all of it to find them.
|
|
577
|
+
const ids = selectorIds(selector);
|
|
578
|
+
const matches = ids == null
|
|
579
|
+
? this.observableItems(collectionName).filter(item => (0, match_1.default)(item, selector))
|
|
580
|
+
: this.observableItemsByIds(collectionName, ids);
|
|
581
|
+
return onlyFirst ? matches.slice(0, 1) : matches;
|
|
582
|
+
}
|
|
583
|
+
resolveUpdate(collectionName, selector, modifier, onlyFirst) {
|
|
584
|
+
const { $setOnInsert, ...restModifier } = modifier;
|
|
585
|
+
const upserts = [];
|
|
586
|
+
const deletes = [];
|
|
587
|
+
this.matchObservableItems(collectionName, selector, onlyFirst).forEach((item) => {
|
|
588
|
+
const modifiedItem = (0, modify_1.default)((0, deepClone_1.default)(item), restModifier);
|
|
589
|
+
upserts.push(modifiedItem);
|
|
590
|
+
if (modifiedItem.id !== item.id)
|
|
591
|
+
deletes.push(item.id);
|
|
592
|
+
});
|
|
593
|
+
return { upserts, deletes };
|
|
594
|
+
}
|
|
595
|
+
resolveRemoval(collectionName, selector, onlyFirst) {
|
|
596
|
+
return this.matchObservableItems(collectionName, selector, onlyFirst).map(item => item.id);
|
|
597
|
+
}
|
|
598
|
+
resolveReplacement(item, replacement) {
|
|
599
|
+
const modifiedItem = { ...replacement, id: replacement.id ?? item.id };
|
|
600
|
+
return {
|
|
601
|
+
upserts: [modifiedItem],
|
|
602
|
+
deletes: modifiedItem.id === item.id ? [] : [item.id],
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
// Makes a write's effect visible to active queries for the duration of the
|
|
606
|
+
// round trip, and drops it again once the worker has spoken — see
|
|
607
|
+
// `pendingWrites`.
|
|
608
|
+
async withPendingWrite(collectionName, delta, run) {
|
|
609
|
+
const dropPendingWrite = this.applyPendingWrite(collectionName, delta.upserts, delta.deletes);
|
|
610
|
+
try {
|
|
611
|
+
return await run();
|
|
612
|
+
}
|
|
613
|
+
finally {
|
|
614
|
+
dropPendingWrite();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
enqueueBatched(collectionName, method, args) {
|
|
618
|
+
const helper = this.batchExecutionHelpers.get(collectionName);
|
|
619
|
+
if (!helper)
|
|
620
|
+
throw new Error(`Collection "${collectionName}" is not registered in WorkerDataAdapter`);
|
|
621
|
+
return helper.enqueue(method, args);
|
|
622
|
+
}
|
|
623
|
+
// ---------- end batching integration ----------
|
|
624
|
+
updateQuery(collectionName, query, update) {
|
|
625
|
+
const id = (0, queryId_1.default)(query.selector, query.options);
|
|
626
|
+
const collectionQueries = this.queries[collectionName];
|
|
627
|
+
if (!collectionQueries)
|
|
628
|
+
return;
|
|
629
|
+
const existing = collectionQueries.get(id);
|
|
630
|
+
const newState = {
|
|
631
|
+
selector: query.selector,
|
|
632
|
+
options: query.options,
|
|
633
|
+
state: 'active',
|
|
634
|
+
error: null,
|
|
635
|
+
stateChangeCallbacks: [],
|
|
636
|
+
...existing,
|
|
637
|
+
...update,
|
|
638
|
+
// An update that says nothing about the items leaves them alone. A query going back to
|
|
639
|
+
// `'active'` while it is recomputed is exactly that, and letting it blank the result would
|
|
640
|
+
// leave every reader of this query with nothing to show until the recomputation lands.
|
|
641
|
+
...update.items
|
|
642
|
+
? { items: update.items, itemsById: undefined }
|
|
643
|
+
: { items: existing?.items ?? [] },
|
|
644
|
+
};
|
|
645
|
+
collectionQueries.set(id, newState);
|
|
646
|
+
this.queries[collectionName] = collectionQueries;
|
|
647
|
+
}
|
|
648
|
+
createCollectionBackend(collection, indices) {
|
|
649
|
+
this.queries[collection.name] = new Map();
|
|
650
|
+
this.execInBackground('registerCollection', collection.name, [indices]);
|
|
651
|
+
this.collectionReady.set(collection.name, this.exec('isReady', collection.name));
|
|
652
|
+
this.batchExecutionHelpers.set(collection.name, (0, batchOnNextTick_1.default)(async (method, args) => this.exec(method, collection.name, args)));
|
|
653
|
+
return {
|
|
654
|
+
insert: async (item) => {
|
|
655
|
+
return this.withPendingWrite(collection.name, { upserts: [item], deletes: [] }, () => this.enqueueBatched(collection.name, 'insert', [item]));
|
|
656
|
+
},
|
|
657
|
+
updateOne: async (selector, modifier) => {
|
|
658
|
+
return this.withPendingWrite(collection.name, this.resolveUpdate(collection.name, selector, modifier, true), () => this.enqueueBatched(collection.name, 'updateOne', [selector, modifier]));
|
|
659
|
+
},
|
|
660
|
+
updateMany: async (selector, modifier) => {
|
|
661
|
+
return this.withPendingWrite(collection.name, this.resolveUpdate(collection.name, selector, modifier, false), () => this.enqueueBatched(collection.name, 'updateMany', [selector, modifier]));
|
|
662
|
+
},
|
|
663
|
+
replaceOne: async (selector, replacement) => {
|
|
664
|
+
const [item] = this.matchObservableItems(collection.name, selector, true);
|
|
665
|
+
return this.withPendingWrite(collection.name, item == null
|
|
666
|
+
? { upserts: [], deletes: [] }
|
|
667
|
+
: this.resolveReplacement(item, replacement), () => this.enqueueBatched(collection.name, 'replaceOne', [selector, replacement]));
|
|
668
|
+
},
|
|
669
|
+
removeOne: async (selector) => {
|
|
670
|
+
return this.withPendingWrite(collection.name, { upserts: [], deletes: this.resolveRemoval(collection.name, selector, true) }, () => this.enqueueBatched(collection.name, 'removeOne', [selector]));
|
|
671
|
+
},
|
|
672
|
+
removeMany: async (selector) => {
|
|
673
|
+
return this.withPendingWrite(collection.name, { upserts: [], deletes: this.resolveRemoval(collection.name, selector, false) }, () => this.enqueueBatched(collection.name, 'removeMany', [selector]));
|
|
674
|
+
},
|
|
675
|
+
// methods for registering and unregistering queries that will be called from the collection during find/findOne
|
|
676
|
+
registerQuery: (selector, options) => {
|
|
677
|
+
this.updateQuery(collection.name, { selector, options }, { state: 'active', error: null, items: [] });
|
|
678
|
+
// A query the worker could not register will never answer. Left as a bare rejection it
|
|
679
|
+
// would surface as an uncaught error and the cursor would sit on its empty result forever,
|
|
680
|
+
// indistinguishable from a query with nothing to show; published as an error it reaches
|
|
681
|
+
// the collection's `query.error` event, which is what that event is for.
|
|
682
|
+
this.execInBackground('registerQuery', collection.name, [selector, options], (error) => {
|
|
683
|
+
const query = this.queries[collection.name]?.get((0, queryId_1.default)(selector, options));
|
|
684
|
+
if (!query)
|
|
685
|
+
return;
|
|
686
|
+
this.updateQuery(collection.name, { selector, options }, { state: 'error', error });
|
|
687
|
+
query.stateChangeCallbacks.forEach(callback => callback('error'));
|
|
688
|
+
});
|
|
689
|
+
},
|
|
690
|
+
unregisterQuery: (selector, options) => {
|
|
691
|
+
this.queries[collection.name]?.delete((0, queryId_1.default)(selector, options));
|
|
692
|
+
// Nothing holds the query any more, so a failure here has nobody to report to — but it
|
|
693
|
+
// still must not escape as an uncaught error, which is what a disposed collection would
|
|
694
|
+
// otherwise produce every time a cursor was cleaned up after it.
|
|
695
|
+
this.execInBackground('unregisterQuery', collection.name, [selector, options]);
|
|
696
|
+
},
|
|
697
|
+
getQueryState: (selector, options) => {
|
|
698
|
+
const query = this.queries[collection.name]?.get((0, queryId_1.default)(selector, options));
|
|
699
|
+
return query?.state || 'active';
|
|
700
|
+
},
|
|
701
|
+
getQueryError: (selector, options) => {
|
|
702
|
+
const query = this.queries[collection.name]?.get((0, queryId_1.default)(selector, options));
|
|
703
|
+
return query?.error || null;
|
|
704
|
+
},
|
|
705
|
+
getQueryResult: (selector, options) => {
|
|
706
|
+
const query = this.queries[collection.name]?.get((0, queryId_1.default)(selector, options));
|
|
707
|
+
if (!query)
|
|
708
|
+
return [];
|
|
709
|
+
return this.servedResult(collection.name, query);
|
|
710
|
+
},
|
|
711
|
+
onQueryStateChange: (selector, options, callback) => {
|
|
712
|
+
this.updateQuery(collection.name, { selector, options }, {
|
|
713
|
+
stateChangeCallbacks: [
|
|
714
|
+
...this.queries[collection.name]?.get((0, queryId_1.default)(selector, options))?.stateChangeCallbacks || [],
|
|
715
|
+
callback,
|
|
716
|
+
],
|
|
717
|
+
});
|
|
718
|
+
return () => {
|
|
719
|
+
const currentCallbacks = this.queries[collection.name]?.get((0, queryId_1.default)(selector, options))?.stateChangeCallbacks;
|
|
720
|
+
if (!currentCallbacks)
|
|
721
|
+
return;
|
|
722
|
+
this.updateQuery(collection.name, { selector, options }, {
|
|
723
|
+
stateChangeCallbacks: currentCallbacks
|
|
724
|
+
.filter(existingCallback => existingCallback !== callback),
|
|
725
|
+
});
|
|
726
|
+
};
|
|
727
|
+
},
|
|
728
|
+
executeQuery: (selector, options) => this.exec('executeQuery', collection.name, selector, options),
|
|
729
|
+
// lifecycle methods
|
|
730
|
+
dispose: async () => {
|
|
731
|
+
await this.exec('unregisterCollection', collection.name);
|
|
732
|
+
this.isDisposed = true;
|
|
733
|
+
this.worker.terminate?.();
|
|
734
|
+
},
|
|
735
|
+
// Awaits the promise `createCollectionBackend` already started rather
|
|
736
|
+
// than asking the worker again. Readiness happens once and never goes
|
|
737
|
+
// back, so a repeated question can only get the same answer — but it
|
|
738
|
+
// still cost a full round trip each time, and callers ask often: a
|
|
739
|
+
// repository helper that awaits `collection.ready()` before touching a
|
|
740
|
+
// record turns a thousand-record sync into a thousand extra messages.
|
|
741
|
+
// One app measured 2,273 `isReady` messages in a single session, more
|
|
742
|
+
// than any other message type it produced.
|
|
743
|
+
isReady: async () => {
|
|
744
|
+
await this.collectionReady.get(collection.name);
|
|
745
|
+
},
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
exports.default = WorkerDataAdapter;
|