@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
|
@@ -82,16 +82,25 @@ export default class AsyncDataAdapter implements DataAdapter {
|
|
|
82
82
|
* @param qid - query id
|
|
83
83
|
* @param state - new state
|
|
84
84
|
* @param error - error if state is 'error', null otherwise
|
|
85
|
+
* @param delta - what changed about the result, when that is known
|
|
85
86
|
*/
|
|
86
87
|
private publishState;
|
|
87
88
|
private publishResult;
|
|
89
|
+
private queryItemIds;
|
|
88
90
|
private getIndexInfo;
|
|
89
91
|
private queryItems;
|
|
90
92
|
private executeQuery;
|
|
91
93
|
/**
|
|
92
|
-
* After mutations,
|
|
94
|
+
* After mutations, bring every affected active query up to date.
|
|
95
|
+
*
|
|
96
|
+
* A query whose previous result is enough to answer the change is brought up to date from that
|
|
97
|
+
* result alone — no round trip to the storage, and no detour through `'active'`, because there is
|
|
98
|
+
* no window in which the query is stale. Only a query the change cannot be reasoned about
|
|
99
|
+
* locally — a window onto a larger set, or one that has never been answered — goes back to the
|
|
100
|
+
* store, and that one gets the same retry and reporting behaviour a freshly registered query
|
|
101
|
+
* gets: a refresh that fails silently leaves exactly the same dead cursor.
|
|
93
102
|
* @param collectionName - name of the collection
|
|
94
|
-
* @param
|
|
103
|
+
* @param changes - the items the write created, updated or removed
|
|
95
104
|
*/
|
|
96
105
|
private checkQueryUpdates;
|
|
97
106
|
private insert;
|
|
@@ -12,7 +12,9 @@ const isEqual_1 = __importDefault(require("./utils/isEqual"));
|
|
|
12
12
|
const getIndexInfo_1 = __importDefault(require("./getIndexInfo"));
|
|
13
13
|
const getMatchingKeys_1 = __importDefault(require("./utils/getMatchingKeys"));
|
|
14
14
|
const sortItems_1 = __importDefault(require("./utils/sortItems"));
|
|
15
|
-
const
|
|
15
|
+
const projectItems_1 = __importDefault(require("./utils/projectItems"));
|
|
16
|
+
const incrementalQueryUpdate_1 = __importDefault(require("./utils/incrementalQueryUpdate"));
|
|
17
|
+
const queryDelta_1 = require("./utils/queryDelta");
|
|
16
18
|
/**
|
|
17
19
|
* Carries the context needed to act on a failed query. The bare storage error
|
|
18
20
|
* on its own does not say which collection or selector produced it, which made
|
|
@@ -35,6 +37,27 @@ class QueryError extends Error {
|
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
exports.QueryError = QueryError;
|
|
40
|
+
/**
|
|
41
|
+
* Turns the item states a write produced into the upsert/delete split a query update needs.
|
|
42
|
+
*
|
|
43
|
+
* The two lists are not symmetric: an item that is still there after the write is described by its
|
|
44
|
+
* new state, while an item that is gone — removed, or given a new id — is described by the id it
|
|
45
|
+
* used to have and nothing else. Mixing the states from before and after a write into one list
|
|
46
|
+
* loses exactly that distinction.
|
|
47
|
+
* @template T - The type of the items.
|
|
48
|
+
* @param previousItems - The items as they were before the write.
|
|
49
|
+
* @param modifiedItems - The items as they are after it.
|
|
50
|
+
* @returns The changeset describing the write.
|
|
51
|
+
*/
|
|
52
|
+
function toChangeset(previousItems, modifiedItems) {
|
|
53
|
+
const modifiedIds = new Set(modifiedItems.map(item => item.id));
|
|
54
|
+
return {
|
|
55
|
+
upserts: modifiedItems,
|
|
56
|
+
deletes: previousItems
|
|
57
|
+
.map(item => item.id)
|
|
58
|
+
.filter(id => !modifiedIds.has(id)),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
38
61
|
const DEFAULT_RETRY_ATTEMPTS = 3;
|
|
39
62
|
const defaultRetryDelay = (attempt) => 100 * (4 ** (attempt - 1));
|
|
40
63
|
const wait = (ms) => new Promise((resolve) => {
|
|
@@ -254,8 +277,14 @@ class AsyncDataAdapter {
|
|
|
254
277
|
return;
|
|
255
278
|
try {
|
|
256
279
|
const items = await this.executeQuery(collectionName, selector, options);
|
|
280
|
+
const rec = this.queries.get(collectionName)?.get(qid);
|
|
281
|
+
// A query answered for the first time has no previous result to be relative to; whoever is
|
|
282
|
+
// waiting on it holds nothing yet and needs the whole thing.
|
|
283
|
+
const delta = rec?.answered
|
|
284
|
+
? (0, queryDelta_1.diffQueryResults)(rec.items, items)
|
|
285
|
+
: undefined;
|
|
257
286
|
this.publishResult(collectionName, qid, items);
|
|
258
|
-
this.publishState(collectionName, qid, 'complete', null);
|
|
287
|
+
this.publishState(collectionName, qid, 'complete', null, delta);
|
|
259
288
|
return;
|
|
260
289
|
}
|
|
261
290
|
catch (error) {
|
|
@@ -274,8 +303,9 @@ class AsyncDataAdapter {
|
|
|
274
303
|
* @param qid - query id
|
|
275
304
|
* @param state - new state
|
|
276
305
|
* @param error - error if state is 'error', null otherwise
|
|
306
|
+
* @param delta - what changed about the result, when that is known
|
|
277
307
|
*/
|
|
278
|
-
publishState(collectionName, qid, state, error) {
|
|
308
|
+
publishState(collectionName, qid, state, error, delta) {
|
|
279
309
|
const rec = this.queries.get(collectionName)?.get(qid);
|
|
280
310
|
if (!rec)
|
|
281
311
|
return;
|
|
@@ -289,7 +319,7 @@ class AsyncDataAdapter {
|
|
|
289
319
|
const subscribers = [...rec.listeners];
|
|
290
320
|
for (const callback of subscribers) {
|
|
291
321
|
try {
|
|
292
|
-
callback
|
|
322
|
+
(0, queryDelta_1.callWithDelta)(callback, state, delta);
|
|
293
323
|
}
|
|
294
324
|
catch (error_) {
|
|
295
325
|
this.onError(error_);
|
|
@@ -301,6 +331,13 @@ class AsyncDataAdapter {
|
|
|
301
331
|
if (!rec)
|
|
302
332
|
return;
|
|
303
333
|
rec.items = items;
|
|
334
|
+
rec.itemIds = undefined;
|
|
335
|
+
rec.answered = true;
|
|
336
|
+
}
|
|
337
|
+
queryItemIds(rec) {
|
|
338
|
+
if (!rec.itemIds)
|
|
339
|
+
rec.itemIds = new Set(rec.items.map(item => item.id));
|
|
340
|
+
return rec.itemIds;
|
|
304
341
|
}
|
|
305
342
|
async getIndexInfo(collectionName, selector) {
|
|
306
343
|
const storageAdapter = this.storageAdapters.get(collectionName);
|
|
@@ -395,37 +432,60 @@ class AsyncDataAdapter {
|
|
|
395
432
|
const sorted = sort ? (0, sortItems_1.default)(items, sort) : items;
|
|
396
433
|
const skipped = skip ? sorted.slice(skip) : sorted;
|
|
397
434
|
const limited = limit ? skipped.slice(0, limit) : skipped;
|
|
398
|
-
|
|
399
|
-
return limited.map((item) => {
|
|
400
|
-
if (!fields)
|
|
401
|
-
return item;
|
|
402
|
-
return { ...(idExcluded ? {} : { id: item.id }), ...(0, project_1.default)(item, fields) };
|
|
403
|
-
});
|
|
435
|
+
return (0, projectItems_1.default)(limited, fields);
|
|
404
436
|
}
|
|
405
437
|
/**
|
|
406
|
-
* After mutations,
|
|
438
|
+
* After mutations, bring every affected active query up to date.
|
|
439
|
+
*
|
|
440
|
+
* A query whose previous result is enough to answer the change is brought up to date from that
|
|
441
|
+
* result alone — no round trip to the storage, and no detour through `'active'`, because there is
|
|
442
|
+
* no window in which the query is stale. Only a query the change cannot be reasoned about
|
|
443
|
+
* locally — a window onto a larger set, or one that has never been answered — goes back to the
|
|
444
|
+
* store, and that one gets the same retry and reporting behaviour a freshly registered query
|
|
445
|
+
* gets: a refresh that fails silently leaves exactly the same dead cursor.
|
|
407
446
|
* @param collectionName - name of the collection
|
|
408
|
-
* @param
|
|
447
|
+
* @param changes - the items the write created, updated or removed
|
|
409
448
|
*/
|
|
410
|
-
async checkQueryUpdates(collectionName,
|
|
449
|
+
async checkQueryUpdates(collectionName, changes) {
|
|
411
450
|
const registry = this.queries.get(collectionName);
|
|
412
451
|
if (!registry)
|
|
413
452
|
throw new Error(`Collection ${collectionName} not initialized!`);
|
|
414
453
|
if (registry.size === 0)
|
|
415
454
|
return;
|
|
416
|
-
|
|
417
|
-
|
|
455
|
+
if (changes.upserts.length === 0 && changes.deletes.length === 0)
|
|
456
|
+
return;
|
|
457
|
+
// A query is affected when the write produces something it should hold, or takes away
|
|
458
|
+
// something it already holds. The second half is what the written items cannot tell us on
|
|
459
|
+
// their own: an item that no longer matches, or that is gone, is invisible to the matcher.
|
|
460
|
+
const affected = [...registry.values()].filter((rec) => {
|
|
461
|
+
const ids = this.queryItemIds(rec);
|
|
462
|
+
if (changes.deletes.some(id => ids.has(id)))
|
|
463
|
+
return true;
|
|
464
|
+
return changes.upserts.some(item => ids.has(item.id) || (0, match_1.default)(item, rec.selector));
|
|
465
|
+
});
|
|
418
466
|
if (affected.length === 0)
|
|
419
467
|
return;
|
|
420
|
-
|
|
421
|
-
for (const
|
|
468
|
+
const needsReExecution = [];
|
|
469
|
+
for (const rec of affected) {
|
|
470
|
+
const { selector, options } = rec;
|
|
471
|
+
const incremental = rec.answered
|
|
472
|
+
? (0, incrementalQueryUpdate_1.default)(rec.items, selector, options, changes)
|
|
473
|
+
: null;
|
|
474
|
+
if (incremental == null) {
|
|
475
|
+
needsReExecution.push(rec);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
422
478
|
const qid = (0, queryId_1.default)(selector, options);
|
|
423
|
-
|
|
479
|
+
const delta = (0, queryDelta_1.diffQueryResults)(rec.items, incremental);
|
|
480
|
+
if ((0, queryDelta_1.isEmptyQueryDelta)(delta))
|
|
481
|
+
continue;
|
|
482
|
+
this.publishResult(collectionName, qid, incremental);
|
|
483
|
+
this.publishState(collectionName, qid, 'complete', null, delta);
|
|
424
484
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
await Promise.all(
|
|
485
|
+
for (const { selector, options } of needsReExecution) {
|
|
486
|
+
this.publishState(collectionName, (0, queryId_1.default)(selector, options), 'active', null);
|
|
487
|
+
}
|
|
488
|
+
await Promise.all(needsReExecution.map(({ selector, options }) => this.runQuery(collectionName, selector, options)));
|
|
429
489
|
}
|
|
430
490
|
async insert(collectionName, newItem) {
|
|
431
491
|
const storage = this.storageAdapters.get(collectionName);
|
|
@@ -435,7 +495,7 @@ class AsyncDataAdapter {
|
|
|
435
495
|
if (existingItems.length > 0)
|
|
436
496
|
throw new Error(`Item with id ${String(newItem.id)} already exists`);
|
|
437
497
|
await storage.insert([newItem]);
|
|
438
|
-
await this.checkQueryUpdates(collectionName, [newItem]);
|
|
498
|
+
await this.checkQueryUpdates(collectionName, { upserts: [newItem], deletes: [] });
|
|
439
499
|
return newItem;
|
|
440
500
|
}
|
|
441
501
|
async updateOne(collectionName, selector, modifier) {
|
|
@@ -454,7 +514,7 @@ class AsyncDataAdapter {
|
|
|
454
514
|
throw new Error(`Item with id ${String(modified.id)} already exists`);
|
|
455
515
|
}
|
|
456
516
|
await storage.replace([modified]);
|
|
457
|
-
await this.checkQueryUpdates(collectionName, [item, modified]);
|
|
517
|
+
await this.checkQueryUpdates(collectionName, toChangeset([item], [modified]));
|
|
458
518
|
return [modified];
|
|
459
519
|
}
|
|
460
520
|
async updateMany(collectionName, selector, modifier) {
|
|
@@ -475,7 +535,7 @@ class AsyncDataAdapter {
|
|
|
475
535
|
return modified;
|
|
476
536
|
}));
|
|
477
537
|
await storage.replace(changed);
|
|
478
|
-
await this.checkQueryUpdates(collectionName,
|
|
538
|
+
await this.checkQueryUpdates(collectionName, toChangeset(items, changed));
|
|
479
539
|
return changed;
|
|
480
540
|
}
|
|
481
541
|
async replaceOne(collectionName, selector, replacement) {
|
|
@@ -493,7 +553,7 @@ class AsyncDataAdapter {
|
|
|
493
553
|
throw new Error(`Item with id ${String(modified.id)} already exists`);
|
|
494
554
|
}
|
|
495
555
|
await storage.replace([modified]);
|
|
496
|
-
await this.checkQueryUpdates(collectionName, [item, modified]);
|
|
556
|
+
await this.checkQueryUpdates(collectionName, toChangeset([item], [modified]));
|
|
497
557
|
return [modified];
|
|
498
558
|
}
|
|
499
559
|
async removeOne(collectionName, selector) {
|
|
@@ -505,7 +565,7 @@ class AsyncDataAdapter {
|
|
|
505
565
|
if (item == null)
|
|
506
566
|
return [];
|
|
507
567
|
await storage.remove([item]);
|
|
508
|
-
await this.checkQueryUpdates(collectionName, [item]);
|
|
568
|
+
await this.checkQueryUpdates(collectionName, { upserts: [], deletes: [item.id] });
|
|
509
569
|
return [item];
|
|
510
570
|
}
|
|
511
571
|
async removeMany(collectionName, selector) {
|
|
@@ -516,7 +576,10 @@ class AsyncDataAdapter {
|
|
|
516
576
|
if (items.length === 0)
|
|
517
577
|
return [];
|
|
518
578
|
await storage.remove(items);
|
|
519
|
-
await this.checkQueryUpdates(collectionName,
|
|
579
|
+
await this.checkQueryUpdates(collectionName, {
|
|
580
|
+
upserts: [],
|
|
581
|
+
deletes: items.map(item => item.id),
|
|
582
|
+
});
|
|
520
583
|
return items;
|
|
521
584
|
}
|
|
522
585
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type ReactivityAdapter from '../types/ReactivityAdapter';
|
|
2
|
+
import type { QueryDelta } from '../utils/queryDelta';
|
|
2
3
|
import type { BaseItem, FindOptions, Transform } from './types';
|
|
3
4
|
import type { ObserveCallbacks } from './Observer';
|
|
4
5
|
/**
|
|
@@ -25,7 +26,7 @@ export interface QueryStateAccessor {
|
|
|
25
26
|
}
|
|
26
27
|
export interface CursorOptions<T extends BaseItem, U = T, Async extends boolean = false> extends FindOptions<T, Async> {
|
|
27
28
|
transform?: Transform<T, U>;
|
|
28
|
-
bindEvents?: (requery: () => void) => () => void;
|
|
29
|
+
bindEvents?: (requery: () => void, applyDelta: (delta: QueryDelta<T>) => void) => () => void;
|
|
29
30
|
queryState?: QueryStateAccessor;
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
@@ -152,4 +153,13 @@ export default class Cursor<T extends BaseItem, U = T, Async extends boolean = f
|
|
|
152
153
|
* criteria have changed, and you want to ensure the cursor reflects the latest state.
|
|
153
154
|
*/
|
|
154
155
|
requery(): void;
|
|
156
|
+
/**
|
|
157
|
+
* Brings the cursor up to date from a description of what changed, rather than by re-running the
|
|
158
|
+
* query and comparing the result with the previous one.
|
|
159
|
+
*
|
|
160
|
+
* Falls back to `requery` when the delta does not fit the result the cursor currently holds, so
|
|
161
|
+
* a caller never has to decide which of the two is safe.
|
|
162
|
+
* @param delta - The change to apply.
|
|
163
|
+
*/
|
|
164
|
+
applyDelta(delta: QueryDelta<T>): void;
|
|
155
165
|
}
|
|
@@ -142,7 +142,11 @@ class Cursor {
|
|
|
142
142
|
const requery = () => {
|
|
143
143
|
observer.runChecks(this.getItems);
|
|
144
144
|
};
|
|
145
|
-
const
|
|
145
|
+
const applyDelta = (delta) => {
|
|
146
|
+
observer.applyDelta(delta, this.getItems);
|
|
147
|
+
};
|
|
148
|
+
const cleanup = this.options.bindEvents
|
|
149
|
+
&& this.options.bindEvents(requery, applyDelta);
|
|
146
150
|
return () => {
|
|
147
151
|
if (cleanup)
|
|
148
152
|
cleanup();
|
|
@@ -331,5 +335,18 @@ class Cursor {
|
|
|
331
335
|
return;
|
|
332
336
|
this.observer.runChecks(this.getItems);
|
|
333
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Brings the cursor up to date from a description of what changed, rather than by re-running the
|
|
340
|
+
* query and comparing the result with the previous one.
|
|
341
|
+
*
|
|
342
|
+
* Falls back to `requery` when the delta does not fit the result the cursor currently holds, so
|
|
343
|
+
* a caller never has to decide which of the two is safe.
|
|
344
|
+
* @param delta - The change to apply.
|
|
345
|
+
*/
|
|
346
|
+
applyDelta(delta) {
|
|
347
|
+
if (!this.observer)
|
|
348
|
+
return;
|
|
349
|
+
this.observer.applyDelta(delta, this.getItems);
|
|
350
|
+
}
|
|
334
351
|
}
|
|
335
352
|
exports.default = Cursor;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { QueryDelta } from '../utils/queryDelta';
|
|
1
2
|
type AddedCallback<T> = (item: T) => void;
|
|
2
3
|
type AddedBeforeCallback<T> = (item: T, before: T) => void;
|
|
3
4
|
type ChangedCallback<T> = (item: T) => void;
|
|
@@ -42,6 +43,36 @@ export default class Observer<T extends {
|
|
|
42
43
|
* @param getItems - A function that returns a promise resolving to the new items or the items themselves.
|
|
43
44
|
*/
|
|
44
45
|
runChecks(getItems: () => Promise<T[]> | T[]): void;
|
|
46
|
+
/**
|
|
47
|
+
* Brings the observer up to date from a description of what changed, instead of from the new
|
|
48
|
+
* result.
|
|
49
|
+
*
|
|
50
|
+
* `runChecks` has to rediscover the change by comparing the whole new result against the whole
|
|
51
|
+
* old one — a cost proportional to the result, paid on every write, to find out that one row
|
|
52
|
+
* moved. When the change is already known it can simply be reported, and the cost becomes
|
|
53
|
+
* proportional to the change.
|
|
54
|
+
*
|
|
55
|
+
* The delta must have been computed against exactly the result this observer holds. If it was
|
|
56
|
+
* not, this reports nothing, falls back to `runChecks`, and returns `false`.
|
|
57
|
+
*
|
|
58
|
+
* Note that the reported moves are minimal, where a comparison reports every item whose
|
|
59
|
+
* neighbour changed. Applying them yields the same order either way — there are simply fewer of
|
|
60
|
+
* them.
|
|
61
|
+
* @param delta - The change to report.
|
|
62
|
+
* @param getItems - Used to fall back to a comparison when the delta cannot be applied.
|
|
63
|
+
* @returns Whether the delta was applied.
|
|
64
|
+
*/
|
|
65
|
+
applyDelta(delta: QueryDelta<T>, getItems: () => Promise<T[]> | T[]): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Reports a delta and adopts the result it produces.
|
|
68
|
+
*
|
|
69
|
+
* The single place the callbacks are fired from, whether the change arrived as a delta or was
|
|
70
|
+
* found by comparing two results — so the two can never disagree about what a consumer is told.
|
|
71
|
+
* @param delta - The change to report.
|
|
72
|
+
* @param nextItems - The result the delta produces.
|
|
73
|
+
*/
|
|
74
|
+
private emitDelta;
|
|
75
|
+
private finishCheck;
|
|
45
76
|
private checkItems;
|
|
46
77
|
private stopped;
|
|
47
78
|
/**
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const isEqual_1 = __importDefault(require("../utils/isEqual"));
|
|
7
7
|
const uniqueBy_1 = __importDefault(require("../utils/uniqueBy"));
|
|
8
|
+
const queryDelta_1 = require("../utils/queryDelta");
|
|
8
9
|
/**
|
|
9
10
|
* Represents an observer that tracks changes in a collection of items and triggers
|
|
10
11
|
* callbacks for various events such as addition, removal, and modification of items.
|
|
@@ -74,58 +75,97 @@ class Observer {
|
|
|
74
75
|
this.checkItems(result);
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Brings the observer up to date from a description of what changed, instead of from the new
|
|
80
|
+
* result.
|
|
81
|
+
*
|
|
82
|
+
* `runChecks` has to rediscover the change by comparing the whole new result against the whole
|
|
83
|
+
* old one — a cost proportional to the result, paid on every write, to find out that one row
|
|
84
|
+
* moved. When the change is already known it can simply be reported, and the cost becomes
|
|
85
|
+
* proportional to the change.
|
|
86
|
+
*
|
|
87
|
+
* The delta must have been computed against exactly the result this observer holds. If it was
|
|
88
|
+
* not, this reports nothing, falls back to `runChecks`, and returns `false`.
|
|
89
|
+
*
|
|
90
|
+
* Note that the reported moves are minimal, where a comparison reports every item whose
|
|
91
|
+
* neighbour changed. Applying them yields the same order either way — there are simply fewer of
|
|
92
|
+
* them.
|
|
93
|
+
* @param delta - The change to report.
|
|
94
|
+
* @param getItems - Used to fall back to a comparison when the delta cannot be applied.
|
|
95
|
+
* @returns Whether the delta was applied.
|
|
96
|
+
*/
|
|
97
|
+
applyDelta(delta, getItems) {
|
|
98
|
+
if (!(0, queryDelta_1.canApplyQueryDelta)(this.previousItems, delta)) {
|
|
99
|
+
this.runChecks(getItems);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if ((0, queryDelta_1.isEmptyQueryDelta)(delta))
|
|
103
|
+
return true;
|
|
104
|
+
this.emitDelta(delta, (0, queryDelta_1.applyQueryDelta)(this.previousItems, delta));
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Reports a delta and adopts the result it produces.
|
|
109
|
+
*
|
|
110
|
+
* The single place the callbacks are fired from, whether the change arrived as a delta or was
|
|
111
|
+
* found by comparing two results — so the two can never disagree about what a consumer is told.
|
|
112
|
+
* @param delta - The change to report.
|
|
113
|
+
* @param nextItems - The result the delta produces.
|
|
114
|
+
*/
|
|
115
|
+
emitDelta(delta, nextItems) {
|
|
116
|
+
if (this.isEmpty()) {
|
|
117
|
+
this.finishCheck(nextItems);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const beforeOf = (index) => nextItems[index + 1] || null;
|
|
121
|
+
// Indexing the previous result costs its whole length, and most deltas do not need it: an item
|
|
122
|
+
// that changed is reported by its new value, and only a listener asking which *field* changed,
|
|
123
|
+
// or one asking about a removal, needs to see what was there before.
|
|
124
|
+
const needsPreviousItems = (delta.removed.length > 0 && this.hasCallbacks(['removed']))
|
|
125
|
+
|| (delta.changed.length > 0 && this.hasCallbacks(['changedField']));
|
|
126
|
+
const previousById = needsPreviousItems
|
|
127
|
+
? new Map(this.previousItems.map(item => [item.id, item]))
|
|
128
|
+
: null;
|
|
129
|
+
if (this.hasCallbacks(['changed', 'changedField'])) {
|
|
130
|
+
delta.changed.forEach((item) => {
|
|
131
|
+
this.call('changed', item);
|
|
132
|
+
if (!this.hasCallbacks(['changedField']))
|
|
133
|
+
return;
|
|
134
|
+
const oldItem = previousById?.get(item.id);
|
|
135
|
+
if (!oldItem)
|
|
136
|
+
return;
|
|
137
|
+
const keys = (0, uniqueBy_1.default)([
|
|
138
|
+
...Object.keys(item),
|
|
139
|
+
...Object.keys(oldItem),
|
|
140
|
+
], value => value);
|
|
141
|
+
keys.forEach((key) => {
|
|
142
|
+
if ((0, isEqual_1.default)(item[key], oldItem[key]))
|
|
143
|
+
return;
|
|
144
|
+
this.call('changedField', item, key, oldItem[key], item[key]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (this.hasCallbacks(['removed'])) {
|
|
149
|
+
delta.removed.forEach((id) => {
|
|
150
|
+
const oldItem = previousById?.get(id);
|
|
151
|
+
if (oldItem)
|
|
114
152
|
this.call('removed', oldItem);
|
|
115
|
-
}
|
|
116
153
|
});
|
|
117
154
|
}
|
|
118
155
|
if (this.hasCallbacks(['added', 'addedBefore'])) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (oldItem)
|
|
123
|
-
return;
|
|
124
|
-
// If the item is newly added, call 'added' and 'addedBefore' callbacks
|
|
125
|
-
this.call('added', newItem);
|
|
126
|
-
this.call('addedBefore', newItem, newItems[index + 1] || null);
|
|
156
|
+
delta.added.forEach(({ index, item }) => {
|
|
157
|
+
this.call('added', item);
|
|
158
|
+
this.call('addedBefore', item, beforeOf(index));
|
|
127
159
|
});
|
|
128
160
|
}
|
|
161
|
+
if (this.hasCallbacks(['movedBefore'])) {
|
|
162
|
+
delta.moved.forEach(({ index }) => {
|
|
163
|
+
this.call('movedBefore', nextItems[index], beforeOf(index));
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
this.finishCheck(nextItems);
|
|
167
|
+
}
|
|
168
|
+
finishCheck(newItems) {
|
|
129
169
|
// Store new items as previous items for next check
|
|
130
170
|
this.previousItems = newItems;
|
|
131
171
|
Object.keys(this.callbacks).forEach((key) => {
|
|
@@ -140,6 +180,12 @@ class Observer {
|
|
|
140
180
|
}));
|
|
141
181
|
});
|
|
142
182
|
}
|
|
183
|
+
checkItems(newItems) {
|
|
184
|
+
// Derives the change and reports it through the same path a change that arrived ready-made
|
|
185
|
+
// takes. Comparing and then reporting item by item, as this used to, meant the two paths could
|
|
186
|
+
// describe the same change differently — most visibly in how many moves they reported.
|
|
187
|
+
this.emitDelta((0, queryDelta_1.diffQueryResults)(this.previousItems, newItems), newItems);
|
|
188
|
+
}
|
|
143
189
|
stopped = false;
|
|
144
190
|
/**
|
|
145
191
|
* Stops the observer by unbinding all events and cleaning up resources.
|