@signaldb/svelte 2.0.0-beta.13 → 2.0.0-beta.14
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 +11 -2
- package/dist/base/core/src/AsyncDataAdapter.js +91 -28
- package/dist/base/core/src/Collection/Cursor.d.ts +11 -1
- package/dist/base/core/src/Collection/Cursor.js +18 -1
- package/dist/base/core/src/Collection/Observer.d.ts +31 -0
- package/dist/base/core/src/Collection/Observer.js +92 -46
- package/dist/base/core/src/Collection/index.js +66 -49
- package/dist/base/core/src/DataAdapter.d.ts +15 -2
- package/dist/base/core/src/DefaultDataAdapter.js +24 -16
- package/dist/base/core/src/WorkerDataAdapter.d.ts +25 -2
- package/dist/base/core/src/WorkerDataAdapter.js +360 -93
- package/dist/base/core/src/WorkerDataAdapterHost.d.ts +2 -0
- package/dist/base/core/src/WorkerDataAdapterHost.js +96 -26
- package/dist/base/core/src/index.d.ts +2 -0
- package/dist/base/core/src/utils/incrementalQueryUpdate.d.ts +60 -0
- package/dist/base/core/src/utils/incrementalQueryUpdate.js +159 -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.js +32 -1
- package/package.json +1 -1
|
@@ -11,8 +11,31 @@ const isEqual_1 = __importDefault(require("./utils/isEqual"));
|
|
|
11
11
|
const getIndexInfo_1 = __importDefault(require("./getIndexInfo"));
|
|
12
12
|
const getMatchingKeys_1 = __importDefault(require("./utils/getMatchingKeys"));
|
|
13
13
|
const sortItems_1 = __importDefault(require("./utils/sortItems"));
|
|
14
|
-
const
|
|
14
|
+
const projectItems_1 = __importDefault(require("./utils/projectItems"));
|
|
15
15
|
const compact_1 = __importDefault(require("./utils/compact"));
|
|
16
|
+
const incrementalQueryUpdate_1 = __importDefault(require("./utils/incrementalQueryUpdate"));
|
|
17
|
+
const queryDelta_1 = require("./utils/queryDelta");
|
|
18
|
+
/**
|
|
19
|
+
* Turns the item states a write produced into the upsert/delete split a query update needs.
|
|
20
|
+
*
|
|
21
|
+
* The two lists are not symmetric: an item that is still there after the write is described by
|
|
22
|
+
* its new state, while an item that is gone — removed, or given a new id — is described by the id
|
|
23
|
+
* it used to have and nothing else. Mixing the states from before and after a write into one list
|
|
24
|
+
* loses exactly that distinction.
|
|
25
|
+
* @template T - The type of the items.
|
|
26
|
+
* @param previousItems - The items as they were before the write.
|
|
27
|
+
* @param modifiedItems - The items as they are after it.
|
|
28
|
+
* @returns The changeset describing the write.
|
|
29
|
+
*/
|
|
30
|
+
function toChangeset(previousItems, modifiedItems) {
|
|
31
|
+
const modifiedIds = new Set(modifiedItems.map(item => item.id));
|
|
32
|
+
return {
|
|
33
|
+
upserts: modifiedItems,
|
|
34
|
+
deletes: previousItems
|
|
35
|
+
.map(item => item.id)
|
|
36
|
+
.filter(id => !modifiedIds.has(id)),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
16
39
|
class WorkerDataAdapterHost {
|
|
17
40
|
workerContext;
|
|
18
41
|
options;
|
|
@@ -180,15 +203,7 @@ class WorkerDataAdapterHost {
|
|
|
180
203
|
const sorted = sort ? (0, sortItems_1.default)(items, sort) : items;
|
|
181
204
|
const skipped = skip ? sorted.slice(skip) : sorted;
|
|
182
205
|
const limited = limit ? skipped.slice(0, limit) : skipped;
|
|
183
|
-
|
|
184
|
-
return limited.map((item) => {
|
|
185
|
-
if (!fields)
|
|
186
|
-
return item;
|
|
187
|
-
return {
|
|
188
|
-
...idExcluded ? {} : { id: item.id },
|
|
189
|
-
...(0, project_1.default)(item, fields),
|
|
190
|
-
};
|
|
191
|
-
});
|
|
206
|
+
return (0, projectItems_1.default)(limited, fields);
|
|
192
207
|
}
|
|
193
208
|
ensureQuery(collectionName, selector, options) {
|
|
194
209
|
const id = (0, queryId_1.default)(selector, options);
|
|
@@ -197,17 +212,33 @@ class WorkerDataAdapterHost {
|
|
|
197
212
|
}
|
|
198
213
|
let query = this.queries.get(collectionName)?.get(id);
|
|
199
214
|
if (!query) {
|
|
200
|
-
query = { selector, options };
|
|
215
|
+
query = { selector, options, items: null };
|
|
201
216
|
this.queries.get(collectionName)?.set(id, query);
|
|
202
217
|
}
|
|
203
218
|
return query;
|
|
204
219
|
}
|
|
205
|
-
|
|
220
|
+
setQueryItems(query, items) {
|
|
221
|
+
query.items = items;
|
|
222
|
+
query.itemIds = undefined;
|
|
223
|
+
}
|
|
224
|
+
queryItemIds(query) {
|
|
225
|
+
if (!query.itemIds)
|
|
226
|
+
query.itemIds = new Set((query.items ?? []).map(item => item.id));
|
|
227
|
+
return query.itemIds;
|
|
228
|
+
}
|
|
229
|
+
emitQueryUpdate(collectionName, selector, options, state, error, items, delta) {
|
|
206
230
|
const id = (0, queryId_1.default)(selector, options);
|
|
207
231
|
const collectionQueries = this.queries.get(collectionName);
|
|
208
232
|
if (!collectionQueries)
|
|
209
233
|
throw new Error(`Collection ${collectionName} not initialized!`);
|
|
210
|
-
|
|
234
|
+
// `qid` is what the adapter routes on. It is derived from the same selector/options pair the
|
|
235
|
+
// adapter registered the query with, so sending it saves every recipient from re-deriving it.
|
|
236
|
+
//
|
|
237
|
+
// `items` and `delta` are alternatives: the first answer to a query carries the whole result
|
|
238
|
+
// because the recipient holds nothing yet, every answer after it carries only what changed.
|
|
239
|
+
// Everything in this message is structurally cloned on its way out of the worker, which is why
|
|
240
|
+
// it matters that editing one field of one row no longer costs a copy of the entire result.
|
|
241
|
+
this.respond(id, { collectionName, qid: id, selector, options, state, error, items, delta }, null, 'queryUpdate');
|
|
211
242
|
}
|
|
212
243
|
ensureStorageAdapter(name) {
|
|
213
244
|
if (this.storageAdapters.has(name))
|
|
@@ -217,19 +248,49 @@ class WorkerDataAdapterHost {
|
|
|
217
248
|
return; // no adapter returned
|
|
218
249
|
this.storageAdapters.set(name, adapter);
|
|
219
250
|
}
|
|
220
|
-
async checkQueryUpdates(collectionName,
|
|
251
|
+
async checkQueryUpdates(collectionName, changes) {
|
|
221
252
|
const queries = this.queries.get(collectionName);
|
|
222
253
|
if (!queries)
|
|
223
254
|
throw new Error(`Collection ${collectionName} not initialized!`);
|
|
224
|
-
|
|
255
|
+
if (changes.upserts.length === 0 && changes.deletes.length === 0)
|
|
256
|
+
return;
|
|
257
|
+
// A query is affected when the write produces something it should hold, or takes away
|
|
258
|
+
// something it already holds. The second half is what the write itself cannot tell us: an
|
|
259
|
+
// item that no longer matches, or that was removed outright, is invisible to the matcher.
|
|
260
|
+
const affectedQueries = [...queries.values()].filter((query) => {
|
|
261
|
+
const ids = this.queryItemIds(query);
|
|
262
|
+
if (changes.deletes.some(id => ids.has(id)))
|
|
263
|
+
return true;
|
|
264
|
+
return changes.upserts.some(item => ids.has(item.id) || (0, match_1.default)(item, query.selector));
|
|
265
|
+
});
|
|
225
266
|
if (affectedQueries.length === 0)
|
|
226
267
|
return; // no active queries affected
|
|
227
|
-
affectedQueries.
|
|
268
|
+
await Promise.all(affectedQueries.map(async (query) => {
|
|
269
|
+
const { selector, options } = query;
|
|
270
|
+
const previous = query.items;
|
|
271
|
+
const incremental = previous == null
|
|
272
|
+
? null
|
|
273
|
+
: (0, incrementalQueryUpdate_1.default)(previous, selector, options, changes);
|
|
274
|
+
if (incremental != null) {
|
|
275
|
+
// Answered from the previous result, without touching the store — so there is no window in
|
|
276
|
+
// which the query is stale, and nothing to announce with an `'active'` state either.
|
|
277
|
+
const delta = (0, queryDelta_1.diffQueryResults)(previous, incremental);
|
|
278
|
+
if ((0, queryDelta_1.isEmptyQueryDelta)(delta))
|
|
279
|
+
return;
|
|
280
|
+
this.setQueryItems(query, incremental);
|
|
281
|
+
this.emitQueryUpdate(collectionName, selector, options, 'complete', null, undefined, delta);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
228
284
|
this.emitQueryUpdate(collectionName, selector, options, 'active', null);
|
|
229
|
-
});
|
|
230
|
-
await Promise.all(affectedQueries.map(async ({ selector, options }) => {
|
|
231
285
|
const queryItems = await this.executeQuery(collectionName, selector, options);
|
|
232
|
-
|
|
286
|
+
if (previous == null) {
|
|
287
|
+
this.setQueryItems(query, queryItems);
|
|
288
|
+
this.emitQueryUpdate(collectionName, selector, options, 'complete', null, queryItems);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const delta = (0, queryDelta_1.diffQueryResults)(previous, queryItems);
|
|
292
|
+
this.setQueryItems(query, queryItems);
|
|
293
|
+
this.emitQueryUpdate(collectionName, selector, options, 'complete', null, undefined, delta);
|
|
233
294
|
}));
|
|
234
295
|
}
|
|
235
296
|
registerCollection = async (collectionName, indices) => {
|
|
@@ -251,8 +312,11 @@ class WorkerDataAdapterHost {
|
|
|
251
312
|
this.queries.delete(collectionName);
|
|
252
313
|
};
|
|
253
314
|
registerQuery = async (collectionName, selector, options) => {
|
|
254
|
-
this.ensureQuery(collectionName, selector, options);
|
|
315
|
+
const query = this.ensureQuery(collectionName, selector, options);
|
|
255
316
|
const queryItems = await this.executeQuery(collectionName, selector, options);
|
|
317
|
+
// Always the full result, even for a query that is already registered: whoever is registering
|
|
318
|
+
// holds nothing for it yet, and a delta would be relative to a result only the host has seen.
|
|
319
|
+
this.setQueryItems(query, queryItems);
|
|
256
320
|
this.emitQueryUpdate(collectionName, selector, options, 'complete', null, queryItems);
|
|
257
321
|
};
|
|
258
322
|
unregisterQuery = async (collectionName, selector, options) => {
|
|
@@ -276,7 +340,7 @@ class WorkerDataAdapterHost {
|
|
|
276
340
|
});
|
|
277
341
|
const newItems = result.filter(item => !(item instanceof Error));
|
|
278
342
|
await storageAdapter.insert(newItems);
|
|
279
|
-
await this.checkQueryUpdates(collectionName, newItems);
|
|
343
|
+
await this.checkQueryUpdates(collectionName, { upserts: newItems, deletes: [] });
|
|
280
344
|
return result;
|
|
281
345
|
};
|
|
282
346
|
updateOne = async (collectionName, parameters) => {
|
|
@@ -302,7 +366,7 @@ class WorkerDataAdapterHost {
|
|
|
302
366
|
const modifiedItems = (0, compact_1.default)(result.filter(item => !(item instanceof Error)).flat());
|
|
303
367
|
if (modifiedItems.length > 0) {
|
|
304
368
|
await storageAdapter.replace(modifiedItems);
|
|
305
|
-
await this.checkQueryUpdates(collectionName,
|
|
369
|
+
await this.checkQueryUpdates(collectionName, toChangeset(previousItems, modifiedItems));
|
|
306
370
|
}
|
|
307
371
|
return result;
|
|
308
372
|
};
|
|
@@ -337,7 +401,7 @@ class WorkerDataAdapterHost {
|
|
|
337
401
|
const modifiedItems = (0, compact_1.default)(result.filter(item => !(item instanceof Error)).flat());
|
|
338
402
|
if (modifiedItems.length > 0) {
|
|
339
403
|
await storageAdapter.replace(modifiedItems);
|
|
340
|
-
await this.checkQueryUpdates(collectionName,
|
|
404
|
+
await this.checkQueryUpdates(collectionName, toChangeset(previousItems, modifiedItems));
|
|
341
405
|
}
|
|
342
406
|
return result;
|
|
343
407
|
};
|
|
@@ -366,7 +430,7 @@ class WorkerDataAdapterHost {
|
|
|
366
430
|
const modifiedItems = (0, compact_1.default)(result.filter(item => !(item instanceof Error)).flat());
|
|
367
431
|
if (modifiedItems.length > 0) {
|
|
368
432
|
await storageAdapter.replace(modifiedItems);
|
|
369
|
-
await this.checkQueryUpdates(collectionName,
|
|
433
|
+
await this.checkQueryUpdates(collectionName, toChangeset(previousItems, modifiedItems));
|
|
370
434
|
}
|
|
371
435
|
return result;
|
|
372
436
|
};
|
|
@@ -383,7 +447,10 @@ class WorkerDataAdapterHost {
|
|
|
383
447
|
const items = result.flat();
|
|
384
448
|
if (items.length > 0) {
|
|
385
449
|
await storageAdapter.remove(items);
|
|
386
|
-
await this.checkQueryUpdates(collectionName,
|
|
450
|
+
await this.checkQueryUpdates(collectionName, {
|
|
451
|
+
upserts: [],
|
|
452
|
+
deletes: items.map(item => item.id),
|
|
453
|
+
});
|
|
387
454
|
}
|
|
388
455
|
return result;
|
|
389
456
|
};
|
|
@@ -395,7 +462,10 @@ class WorkerDataAdapterHost {
|
|
|
395
462
|
const items = result.flat();
|
|
396
463
|
if (items.length > 0) {
|
|
397
464
|
await storageAdapter.remove(items);
|
|
398
|
-
await this.checkQueryUpdates(collectionName,
|
|
465
|
+
await this.checkQueryUpdates(collectionName, {
|
|
466
|
+
upserts: [],
|
|
467
|
+
deletes: items.map(item => item.id),
|
|
468
|
+
});
|
|
399
469
|
}
|
|
400
470
|
return result;
|
|
401
471
|
};
|
|
@@ -4,6 +4,8 @@ export type { default as Selector } from './types/Selector';
|
|
|
4
4
|
export type { default as Modifier } from './types/Modifier';
|
|
5
5
|
export type { BaseItem, ObserveCallbacks, CursorOptions, Transform, TransformAll, SortSpecifier, FieldSpecifier, AnyFindOptions, AsyncFindOptions, FindOptions, SyncFindOptions, CollectionOptions, QueryStateAccessor, } from './Collection';
|
|
6
6
|
export type { default as DataAdapter } from './DataAdapter';
|
|
7
|
+
export type { CollectionBackend, QueryOptions, StateChangeCallback, } from './DataAdapter';
|
|
8
|
+
export type { QueryDelta } from './utils/queryDelta';
|
|
7
9
|
export { default as Cursor } from './Collection/Cursor';
|
|
8
10
|
export { default as Collection } from './Collection';
|
|
9
11
|
export { default as createStorageAdapter } from './createStorageAdapter';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { QueryOptions } from '../DataAdapter';
|
|
2
|
+
import type { BaseItem } from '../Collection/types';
|
|
3
|
+
import type Selector from '../types/Selector';
|
|
4
|
+
/**
|
|
5
|
+
* The items a write created, updated or removed, from the point of view of a store.
|
|
6
|
+
*
|
|
7
|
+
* `upserts` are the *current* state of every item that still exists; `deletes` are the ids of items
|
|
8
|
+
* that no longer do. A write that changes an item's id contributes to both. Callers are responsible
|
|
9
|
+
* for that split — an "affected items" list holding an item's state from before and after a write
|
|
10
|
+
* cannot be told apart from two unrelated items here.
|
|
11
|
+
*/
|
|
12
|
+
export interface QueryChangeset<T extends BaseItem> {
|
|
13
|
+
upserts: T[];
|
|
14
|
+
deletes: any[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Recomputes a query's result from its previous result and the change that was just written,
|
|
18
|
+
* without going back to the store.
|
|
19
|
+
*
|
|
20
|
+
* A store re-executing the query instead reads every item it holds (or every item an index points
|
|
21
|
+
* at) and filters, sorts and projects the lot — for a write that touched one row. This does the
|
|
22
|
+
* same job in the size of the write, which is what a query's result costs to keep up to date when
|
|
23
|
+
* the change that affects it is already in hand.
|
|
24
|
+
*
|
|
25
|
+
* Returns `null` when the previous result is not enough to answer, and the caller has to re-execute
|
|
26
|
+
* the query after all:
|
|
27
|
+
* - `limit` or `skip`: the result is a window onto a larger set, and an item leaving the window has
|
|
28
|
+
* to be replaced by one the previous result never contained.
|
|
29
|
+
* - `fields` together with `sort`: the previous items are projected, so the field the sort is keyed
|
|
30
|
+
* on may no longer be there to sort by.
|
|
31
|
+
* - a `null` selector, which matches nothing and is not worth a special case.
|
|
32
|
+
* @template T - The type of the items.
|
|
33
|
+
* @param previous - The query's previous result.
|
|
34
|
+
* @param selector - The query's selector.
|
|
35
|
+
* @param options - The query's options.
|
|
36
|
+
* @param changes - The items the write created, updated or removed.
|
|
37
|
+
* @returns The new result, or `null` when the query has to be re-executed.
|
|
38
|
+
*/
|
|
39
|
+
export default function incrementalQueryUpdate<T extends BaseItem>(previous: T[], selector: Selector<T>, options: QueryOptions<T> | undefined, changes: QueryChangeset<T>): T[] | null;
|
|
40
|
+
/**
|
|
41
|
+
* Folds a change into a query's result, whatever the query's options.
|
|
42
|
+
*
|
|
43
|
+
* The unguarded version of `incrementalQueryUpdate`, for the places where the alternative is not a
|
|
44
|
+
* more accurate answer but a wrong one — layering a write that has not been confirmed yet on top of
|
|
45
|
+
* the last confirmed result, say. For a query returning everything it matches, this is exact. For a
|
|
46
|
+
* window onto a larger set it is the closest the window itself can get: an item that no longer
|
|
47
|
+
* belongs is dropped, one that does is placed, and the window is trimmed back to its length — but
|
|
48
|
+
* an item pulled in from beyond the window is not something the window knows about.
|
|
49
|
+
*
|
|
50
|
+
* What it never does is re-examine the items already in the result. They matched when the store
|
|
51
|
+
* produced them, they still match, and asking again is both wasteful and — for a projected result,
|
|
52
|
+
* whose items no longer carry the fields the selector names — wrong.
|
|
53
|
+
* @template T - The type of the items.
|
|
54
|
+
* @param previous - The query's previous result.
|
|
55
|
+
* @param selector - The query's selector.
|
|
56
|
+
* @param options - The query's options.
|
|
57
|
+
* @param changes - The items the write created, updated or removed.
|
|
58
|
+
* @returns The resulting items.
|
|
59
|
+
*/
|
|
60
|
+
export declare function mergeChangesetIntoResult<T extends BaseItem>(previous: T[], selector: Selector<T>, options: QueryOptions<T> | undefined, changes: QueryChangeset<T>): T[];
|
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
exports.default = incrementalQueryUpdate;
|
|
7
|
+
exports.mergeChangesetIntoResult = mergeChangesetIntoResult;
|
|
8
|
+
const match_1 = __importDefault(require("./match"));
|
|
9
|
+
const projectItems_1 = __importDefault(require("./projectItems"));
|
|
10
|
+
const sortItems_1 = __importDefault(require("./sortItems"));
|
|
11
|
+
/**
|
|
12
|
+
* Recomputes a query's result from its previous result and the change that was just written,
|
|
13
|
+
* without going back to the store.
|
|
14
|
+
*
|
|
15
|
+
* A store re-executing the query instead reads every item it holds (or every item an index points
|
|
16
|
+
* at) and filters, sorts and projects the lot — for a write that touched one row. This does the
|
|
17
|
+
* same job in the size of the write, which is what a query's result costs to keep up to date when
|
|
18
|
+
* the change that affects it is already in hand.
|
|
19
|
+
*
|
|
20
|
+
* Returns `null` when the previous result is not enough to answer, and the caller has to re-execute
|
|
21
|
+
* the query after all:
|
|
22
|
+
* - `limit` or `skip`: the result is a window onto a larger set, and an item leaving the window has
|
|
23
|
+
* to be replaced by one the previous result never contained.
|
|
24
|
+
* - `fields` together with `sort`: the previous items are projected, so the field the sort is keyed
|
|
25
|
+
* on may no longer be there to sort by.
|
|
26
|
+
* - a `null` selector, which matches nothing and is not worth a special case.
|
|
27
|
+
* @template T - The type of the items.
|
|
28
|
+
* @param previous - The query's previous result.
|
|
29
|
+
* @param selector - The query's selector.
|
|
30
|
+
* @param options - The query's options.
|
|
31
|
+
* @param changes - The items the write created, updated or removed.
|
|
32
|
+
* @returns The new result, or `null` when the query has to be re-executed.
|
|
33
|
+
*/
|
|
34
|
+
function incrementalQueryUpdate(previous, selector, options, changes) {
|
|
35
|
+
if (selector == null)
|
|
36
|
+
return null;
|
|
37
|
+
const { sort, skip, limit, fields } = options || {};
|
|
38
|
+
if (skip != null)
|
|
39
|
+
return null;
|
|
40
|
+
if (fields != null && sort != null)
|
|
41
|
+
return null;
|
|
42
|
+
if (limit != null && !windowStaysClosed(previous, selector, options, changes))
|
|
43
|
+
return null;
|
|
44
|
+
return mergeChangesetIntoResult(previous, selector, options, changes);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Whether two items are in the given order under the given sort, deciding ties against the caller.
|
|
48
|
+
*
|
|
49
|
+
* Uses the sort itself rather than a comparator of its own: a rule about which side of a window an
|
|
50
|
+
* item falls on is only as good as its agreement with the ordering that drew the window. A tie
|
|
51
|
+
* comes back as `false`, because a tie is exactly the case where an item could belong on either
|
|
52
|
+
* side and the answer has to be taken from the store.
|
|
53
|
+
* @template T - The type of the items.
|
|
54
|
+
* @param item - The item whose position is in question.
|
|
55
|
+
* @param edge - The item at the edge of the window.
|
|
56
|
+
* @param sort - The query's sort.
|
|
57
|
+
* @returns `true` when `item` sorts strictly before `edge`.
|
|
58
|
+
*/
|
|
59
|
+
function sortsBefore(item, edge, sort) {
|
|
60
|
+
return (0, sortItems_1.default)([edge, item], sort)[0] === item;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether a change to a windowed query can be answered from the window alone.
|
|
64
|
+
*
|
|
65
|
+
* A window holds the first `limit` items in sort order, and nothing about what lies beyond it. An
|
|
66
|
+
* item leaving the window therefore has to be replaced by one the window has never seen, and that
|
|
67
|
+
* answer can only come from the store. An item arriving is a different matter: it takes its place
|
|
68
|
+
* and pushes the last one out, and where that one goes is not the window's problem.
|
|
69
|
+
*
|
|
70
|
+
* The one case where none of this applies is a window that was never full, because then the query
|
|
71
|
+
* already returns everything it matches and there is no "beyond".
|
|
72
|
+
* @template T - The type of the items.
|
|
73
|
+
* @param previous - The query's previous result.
|
|
74
|
+
* @param selector - The query's selector.
|
|
75
|
+
* @param options - The query's options.
|
|
76
|
+
* @param changes - The items the write created, updated or removed.
|
|
77
|
+
* @returns `true` when the new window follows from the old one and the change.
|
|
78
|
+
*/
|
|
79
|
+
function windowStaysClosed(previous, selector, options, changes) {
|
|
80
|
+
const { sort, limit, fields } = options || {};
|
|
81
|
+
if (limit == null || previous.length < limit)
|
|
82
|
+
return true;
|
|
83
|
+
// A full window needs an edge to compare against, and comparing needs the field the sort is on
|
|
84
|
+
// to still be there.
|
|
85
|
+
if (sort == null || fields != null)
|
|
86
|
+
return false;
|
|
87
|
+
const edge = previous.at(-1);
|
|
88
|
+
const runnerUp = previous.at(-2);
|
|
89
|
+
// Which item is *the* edge has to be beyond doubt. Two items sorting equally at the end of the
|
|
90
|
+
// window are interchangeable, and so is the question of which of them a write displaces.
|
|
91
|
+
if (runnerUp != null && !sortsBefore(runnerUp, edge, sort))
|
|
92
|
+
return false;
|
|
93
|
+
const inWindow = new Set(previous.map(item => item.id));
|
|
94
|
+
if (changes.deletes.some(id => inWindow.has(id)))
|
|
95
|
+
return false;
|
|
96
|
+
return changes.upserts.every((item) => {
|
|
97
|
+
if (item === edge)
|
|
98
|
+
return true;
|
|
99
|
+
const before = sortsBefore(item, edge, sort);
|
|
100
|
+
if (!inWindow.has(item.id)) {
|
|
101
|
+
// Coming from outside: it either takes a place inside, displacing the edge, or stays where
|
|
102
|
+
// it was. Sorting *equally* to the edge is the one answer the window cannot give, because
|
|
103
|
+
// the store may just as well have kept the edge and left this one out.
|
|
104
|
+
if (!(0, match_1.default)(item, selector))
|
|
105
|
+
return true;
|
|
106
|
+
return before || sortsBefore(edge, item, sort);
|
|
107
|
+
}
|
|
108
|
+
// An item that no longer matches leaves a place open, and what fills it is beyond the window.
|
|
109
|
+
if (!(0, match_1.default)(item, selector))
|
|
110
|
+
return false;
|
|
111
|
+
if (before)
|
|
112
|
+
return true;
|
|
113
|
+
// Not before the edge. The one item that may sit *on* the edge is the edge itself, and only
|
|
114
|
+
// while it has not actually moved — an edge that slides outwards gives up the last place, and
|
|
115
|
+
// what takes it is something the window has never seen.
|
|
116
|
+
return item.id === edge.id && !sortsBefore(edge, item, sort);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Folds a change into a query's result, whatever the query's options.
|
|
121
|
+
*
|
|
122
|
+
* The unguarded version of `incrementalQueryUpdate`, for the places where the alternative is not a
|
|
123
|
+
* more accurate answer but a wrong one — layering a write that has not been confirmed yet on top of
|
|
124
|
+
* the last confirmed result, say. For a query returning everything it matches, this is exact. For a
|
|
125
|
+
* window onto a larger set it is the closest the window itself can get: an item that no longer
|
|
126
|
+
* belongs is dropped, one that does is placed, and the window is trimmed back to its length — but
|
|
127
|
+
* an item pulled in from beyond the window is not something the window knows about.
|
|
128
|
+
*
|
|
129
|
+
* What it never does is re-examine the items already in the result. They matched when the store
|
|
130
|
+
* produced them, they still match, and asking again is both wasteful and — for a projected result,
|
|
131
|
+
* whose items no longer carry the fields the selector names — wrong.
|
|
132
|
+
* @template T - The type of the items.
|
|
133
|
+
* @param previous - The query's previous result.
|
|
134
|
+
* @param selector - The query's selector.
|
|
135
|
+
* @param options - The query's options.
|
|
136
|
+
* @param changes - The items the write created, updated or removed.
|
|
137
|
+
* @returns The resulting items.
|
|
138
|
+
*/
|
|
139
|
+
function mergeChangesetIntoResult(previous, selector, options, changes) {
|
|
140
|
+
if (selector == null)
|
|
141
|
+
return [];
|
|
142
|
+
const { sort, limit, fields } = options || {};
|
|
143
|
+
const byId = new Map();
|
|
144
|
+
previous.forEach(item => byId.set(item.id, item));
|
|
145
|
+
changes.deletes.forEach(id => byId.delete(id));
|
|
146
|
+
changes.upserts.forEach((item) => {
|
|
147
|
+
// Matched against the unprojected item: the selector is free to name fields the projection
|
|
148
|
+
// drops, and the stored result would have no answer for those.
|
|
149
|
+
if ((0, match_1.default)(item, selector)) {
|
|
150
|
+
byId.set(item.id, (0, projectItems_1.default)([item], fields)[0]);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
byId.delete(item.id);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
const items = [...byId.values()];
|
|
157
|
+
const sorted = sort ? (0, sortItems_1.default)(items, sort) : items;
|
|
158
|
+
return limit == null ? sorted : sorted.slice(0, limit);
|
|
159
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { QueryOptions } from '../DataAdapter';
|
|
2
|
+
import type { BaseItem } from '../Collection/types';
|
|
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
|
+
export default function projectItems<T extends BaseItem>(items: T[], fields: QueryOptions<T>['fields']): T[];
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
exports.default = projectItems;
|
|
7
|
+
const project_1 = __importDefault(require("./project"));
|
|
8
|
+
/**
|
|
9
|
+
* Applies a query's field projection to a list of items, keeping the primary key unless the
|
|
10
|
+
* projection excludes it outright. Returns the items untouched when there is no projection, so a
|
|
11
|
+
* caller does not have to check for one first.
|
|
12
|
+
* @template T - The type of the items.
|
|
13
|
+
* @param items - The items to project.
|
|
14
|
+
* @param fields - The projection, or `undefined` for none.
|
|
15
|
+
* @returns The projected items.
|
|
16
|
+
*/
|
|
17
|
+
function projectItems(items, fields) {
|
|
18
|
+
if (!fields)
|
|
19
|
+
return items;
|
|
20
|
+
const idExcluded = fields.id === 0;
|
|
21
|
+
return items.map(item => ({
|
|
22
|
+
...idExcluded ? {} : { id: item.id },
|
|
23
|
+
...(0, project_1.default)(item, fields),
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { BaseItem } from '../Collection/types';
|
|
2
|
+
/**
|
|
3
|
+
* The change between two consecutive results of the same query, expressed so it can be applied to
|
|
4
|
+
* the earlier result to obtain the later one.
|
|
5
|
+
*
|
|
6
|
+
* Indices in `added` and `moved` are positions in the *resulting* array and are always ascending,
|
|
7
|
+
* which is what makes applying them a matter of splicing in order. Removals and moves name items by
|
|
8
|
+
* id only: whoever applies the delta still holds the previous result and can look the item up
|
|
9
|
+
* there, so there is no reason to send it twice — the point of the whole exercise is that a change
|
|
10
|
+
* costs the size of the change, not the size of the result.
|
|
11
|
+
*/
|
|
12
|
+
export interface QueryDelta<T extends BaseItem = BaseItem> {
|
|
13
|
+
/** Items that were not in the previous result, at their position in the new one. */
|
|
14
|
+
added: {
|
|
15
|
+
index: number;
|
|
16
|
+
item: T;
|
|
17
|
+
}[];
|
|
18
|
+
/** Items that were in the previous result and whose contents changed. */
|
|
19
|
+
changed: T[];
|
|
20
|
+
/** Ids of items that are no longer in the result. */
|
|
21
|
+
removed: any[];
|
|
22
|
+
/** Items that stayed, at their new position, because the order around them changed. */
|
|
23
|
+
moved: {
|
|
24
|
+
index: number;
|
|
25
|
+
id: any;
|
|
26
|
+
}[];
|
|
27
|
+
/** Length of the resulting array — lets a recipient verify it applied the delta to the result it was computed against. */
|
|
28
|
+
resultCount: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Checks whether a delta leaves the result it is applied to unchanged.
|
|
32
|
+
* @param delta - The delta to inspect.
|
|
33
|
+
* @returns `true` when applying the delta would be a no-op.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isEmptyQueryDelta(delta: QueryDelta<any>): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Calls a state-change callback, passing the delta only when there is one.
|
|
38
|
+
*
|
|
39
|
+
* A callback invoked as `callback(state, undefined)` has been handed two arguments, which is a
|
|
40
|
+
* different thing from being handed one — visible to anything that inspects arity, and to any test
|
|
41
|
+
* that asserts on the call.
|
|
42
|
+
* @template T - The type of the items.
|
|
43
|
+
* @param callback - The callback to invoke.
|
|
44
|
+
* @param state - The state to report.
|
|
45
|
+
* @param delta - The delta to report, if there is one.
|
|
46
|
+
*/
|
|
47
|
+
export declare function callWithDelta<T extends BaseItem>(callback: (state: 'active' | 'complete' | 'error', delta?: QueryDelta<T>) => void, state: 'active' | 'complete' | 'error', delta?: QueryDelta<T>): void;
|
|
48
|
+
/**
|
|
49
|
+
* Checks whether a delta describes a change to the given result.
|
|
50
|
+
*
|
|
51
|
+
* A delta is only meaningful against the exact result it was computed from — it names positions in
|
|
52
|
+
* an array and items by id alone. Applying one to anything else produces a result that looks
|
|
53
|
+
* plausible and is wrong, and from then on every further delta compounds the error. This is the
|
|
54
|
+
* cheap structural check that catches that: every id the delta expects to find is there, every id
|
|
55
|
+
* it expects to be new is not, and the arithmetic on the length works out. It costs the size of the
|
|
56
|
+
* delta, not the size of the result.
|
|
57
|
+
* @template T - The type of the items.
|
|
58
|
+
* @param previous - The result the delta would be applied to.
|
|
59
|
+
* @param delta - The delta to check.
|
|
60
|
+
* @returns `true` when the delta can be applied.
|
|
61
|
+
*/
|
|
62
|
+
export declare function canApplyQueryDelta<T extends BaseItem>(previous: T[], delta: QueryDelta<T>): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Computes the delta between two results of the same query.
|
|
65
|
+
*
|
|
66
|
+
* A fallback for the cases where the change that produced the new result is not available — a query
|
|
67
|
+
* that had to be re-executed in full, for instance. It costs a pass over both results, but it is
|
|
68
|
+
* paid once, on the side that has both of them, instead of shipping the entire new result to
|
|
69
|
+
* everyone who only needs to know what changed.
|
|
70
|
+
* @template T - The type of the items.
|
|
71
|
+
* @param previous - The result the delta should be relative to.
|
|
72
|
+
* @param next - The result the delta should produce.
|
|
73
|
+
* @returns The delta between the two results.
|
|
74
|
+
*/
|
|
75
|
+
export declare function diffQueryResults<T extends BaseItem>(previous: T[], next: T[]): QueryDelta<T>;
|
|
76
|
+
/**
|
|
77
|
+
* Applies a delta to the result it was computed against.
|
|
78
|
+
* @template T - The type of the items.
|
|
79
|
+
* @param previous - The result the delta is relative to. Not modified.
|
|
80
|
+
* @param delta - The delta to apply.
|
|
81
|
+
* @returns The resulting items.
|
|
82
|
+
*/
|
|
83
|
+
export declare function applyQueryDelta<T extends BaseItem>(previous: T[], delta: QueryDelta<T>): T[];
|