@signaldb/svelte 2.0.0-beta.13 → 2.0.0-beta.15
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.d.ts +40 -0
- package/dist/base/core/src/Collection/index.js +149 -53
- 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 +369 -94
- 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
|
@@ -29,9 +29,32 @@ class Collection extends EventEmitter_1.default {
|
|
|
29
29
|
static fieldTracking = false;
|
|
30
30
|
static onCreationCallbacks = [];
|
|
31
31
|
static onDisposeCallbacks = [];
|
|
32
|
+
// How many rows a live query may hold before it is reported as expensive.
|
|
33
|
+
// `null` disables the check, which is the default: it costs a stack capture
|
|
34
|
+
// per registered query, which is worth paying while developing and not in
|
|
35
|
+
// production. `enableDebugMode()` turns it on.
|
|
36
|
+
static largeQueryWarningThreshold = null;
|
|
37
|
+
static reportedLargeQueries = new Set();
|
|
32
38
|
static getCollections() {
|
|
33
39
|
return Collection.collections;
|
|
34
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Reports live queries whose result is larger than `rows`, once each, with
|
|
43
|
+
* the stack that registered them.
|
|
44
|
+
*
|
|
45
|
+
* A reactive query is re-evaluated whenever the data under it changes, and
|
|
46
|
+
* one registered from a long-lived place — a navigation bar, a provider
|
|
47
|
+
* near the root — keeps that cost for the lifetime of the application. There
|
|
48
|
+
* is otherwise nothing to see: the query works, and its price is only
|
|
49
|
+
* visible as an application that has grown slow. Finding one such query in a
|
|
50
|
+
* real app took a purpose-built profiler and the better part of a day.
|
|
51
|
+
* @param rows - Result size to report above, or `null` to switch the check off.
|
|
52
|
+
*/
|
|
53
|
+
static reportLargeQueries(rows) {
|
|
54
|
+
Collection.largeQueryWarningThreshold = rows;
|
|
55
|
+
if (rows == null)
|
|
56
|
+
Collection.reportedLargeQueries.clear();
|
|
57
|
+
}
|
|
35
58
|
static onCreation(callback) {
|
|
36
59
|
Collection.onCreationCallbacks.push(callback);
|
|
37
60
|
}
|
|
@@ -43,6 +66,12 @@ class Collection extends EventEmitter_1.default {
|
|
|
43
66
|
*/
|
|
44
67
|
static enableDebugMode = () => {
|
|
45
68
|
Collection.debugMode = true;
|
|
69
|
+
// A query large enough to matter is exactly the kind of thing debug mode
|
|
70
|
+
// exists to surface, and it is invisible otherwise. Call
|
|
71
|
+
// `reportLargeQueries()` afterwards to pick a different threshold or turn
|
|
72
|
+
// it off again.
|
|
73
|
+
if (Collection.largeQueryWarningThreshold == null)
|
|
74
|
+
Collection.reportLargeQueries(500);
|
|
46
75
|
Collection.collections.forEach((collection) => {
|
|
47
76
|
collection.setDebugMode(true);
|
|
48
77
|
});
|
|
@@ -57,13 +86,23 @@ class Collection extends EventEmitter_1.default {
|
|
|
57
86
|
collection.setFieldTracking(enable);
|
|
58
87
|
});
|
|
59
88
|
};
|
|
60
|
-
static batch(
|
|
61
|
-
|
|
62
|
-
const
|
|
89
|
+
static batch(collectionsOrCallback, maybeCallback) {
|
|
90
|
+
const scoped = Array.isArray(collectionsOrCallback);
|
|
91
|
+
const callback = (scoped ? maybeCallback : collectionsOrCallback);
|
|
92
|
+
if (typeof callback !== 'function')
|
|
93
|
+
throw new TypeError('Collection.batch requires a callback');
|
|
94
|
+
const collections = scoped ? collectionsOrCallback : Collection.collections;
|
|
95
|
+
// Only a batch that really covers every collection may claim the global
|
|
96
|
+
// flag; a scoped one must not make unrelated collections report themselves
|
|
97
|
+
// as batching through `isBatchOperationInProgress()`.
|
|
98
|
+
if (!scoped)
|
|
99
|
+
Collection.batchOperationInProgress = true;
|
|
100
|
+
const execute = () => collections.reduce((memo, collection) => () => {
|
|
63
101
|
return collection.batch(memo);
|
|
64
102
|
}, callback)();
|
|
65
103
|
const afterBatch = () => {
|
|
66
|
-
|
|
104
|
+
if (!scoped)
|
|
105
|
+
Collection.batchOperationInProgress = false;
|
|
67
106
|
};
|
|
68
107
|
let maybePromise;
|
|
69
108
|
try {
|
|
@@ -141,6 +180,34 @@ class Collection extends EventEmitter_1.default {
|
|
|
141
180
|
.catch(() => { });
|
|
142
181
|
Collection.onCreationCallbacks.forEach(callback => callback(this));
|
|
143
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Reports a live query the first time its result is found to be larger than
|
|
185
|
+
* the configured threshold. Once per query, because it re-runs on every
|
|
186
|
+
* write and a warning per write would be its own performance problem.
|
|
187
|
+
* @param selector - The query's selector.
|
|
188
|
+
* @param options - The query's options.
|
|
189
|
+
* @param registrationStack - Where the query was registered, if captured.
|
|
190
|
+
*/
|
|
191
|
+
reportIfLargeQuery(selector, options, registrationStack) {
|
|
192
|
+
const threshold = Collection.largeQueryWarningThreshold;
|
|
193
|
+
if (threshold == null)
|
|
194
|
+
return;
|
|
195
|
+
const id = `${this.name}:${(0, queryId_1.default)(selector, options)}`;
|
|
196
|
+
if (Collection.reportedLargeQueries.has(id))
|
|
197
|
+
return;
|
|
198
|
+
const rows = this.backend.getQueryResult(selector, options || {}).length;
|
|
199
|
+
if (rows <= threshold)
|
|
200
|
+
return;
|
|
201
|
+
Collection.reportedLargeQueries.add(id);
|
|
202
|
+
// The selector's *keys*, never its values: the shape is what identifies the
|
|
203
|
+
// problem — an empty one means the query holds the whole collection — and
|
|
204
|
+
// the values would put user data into a log.
|
|
205
|
+
const keys = selector && typeof selector === 'object' ? Object.keys(selector) : [];
|
|
206
|
+
// eslint-disable-next-line no-console
|
|
207
|
+
console.warn(`[SignalDB] Live query on "${this.name}" holds ${rows} rows `
|
|
208
|
+
+ `with selector {${keys.join(', ')}}. It is re-evaluated on every write to this `
|
|
209
|
+
+ `collection, for as long as it stays registered. ${registrationStack ?? ''}`);
|
|
210
|
+
}
|
|
144
211
|
isBatchOperationInProgress() {
|
|
145
212
|
return Collection.batchOperationInProgress || this.batchOperationInProgress;
|
|
146
213
|
}
|
|
@@ -338,7 +405,7 @@ class Collection extends EventEmitter_1.default {
|
|
|
338
405
|
callback();
|
|
339
406
|
}),
|
|
340
407
|
},
|
|
341
|
-
bindEvents: (requery) => {
|
|
408
|
+
bindEvents: (requery, applyDelta) => {
|
|
342
409
|
const handleRequery = () => {
|
|
343
410
|
if (this.batchOperationInProgress) {
|
|
344
411
|
this.postBatchCallbacks.add(requery);
|
|
@@ -346,13 +413,28 @@ class Collection extends EventEmitter_1.default {
|
|
|
346
413
|
}
|
|
347
414
|
requery();
|
|
348
415
|
};
|
|
416
|
+
// A `transformAll` sits between the backend's result and what the cursor holds, and it
|
|
417
|
+
// is free to produce anything at all — so a delta describing the backend's result says
|
|
418
|
+
// nothing about the cursor's. Those collections keep comparing.
|
|
419
|
+
const canApplyDeltas = !this.options.transformAll && !options?.async;
|
|
349
420
|
// register query if not yet registered
|
|
350
421
|
const listeners = this.queryListeners({ selector, options });
|
|
351
422
|
const didRegister = listeners === 0;
|
|
352
423
|
if (didRegister)
|
|
353
424
|
this.backend.registerQuery(selector, options || {});
|
|
354
425
|
this.queryListeners({ selector, options }, listeners + 1);
|
|
355
|
-
|
|
426
|
+
// Captured at registration, not at completion: by the time the result
|
|
427
|
+
// arrives the stack is the adapter's, and the only useful thing to
|
|
428
|
+
// report is where the query was asked for.
|
|
429
|
+
const registrationStack = didRegister && Collection.largeQueryWarningThreshold != null
|
|
430
|
+
? new Error('query registered here').stack
|
|
431
|
+
: undefined;
|
|
432
|
+
// A synchronous adapter already holds the result here and never
|
|
433
|
+
// reports `'complete'`, so the check has to happen at both points.
|
|
434
|
+
// Reporting is once per query, which makes the overlap harmless.
|
|
435
|
+
if (didRegister)
|
|
436
|
+
this.reportIfLargeQuery(selector, options, registrationStack);
|
|
437
|
+
const queryStateChangeCleanup = this.backend.onQueryStateChange(selector, options || {}, (state, delta) => {
|
|
356
438
|
// A failed query never reaches `'complete'`, so the cursor keeps
|
|
357
439
|
// serving its neutral empty value — indistinguishable from "no
|
|
358
440
|
// data" for anyone reading it. Surfacing the failure as an event
|
|
@@ -367,6 +449,14 @@ class Collection extends EventEmitter_1.default {
|
|
|
367
449
|
}
|
|
368
450
|
if (state !== 'complete')
|
|
369
451
|
return;
|
|
452
|
+
this.reportIfLargeQuery(selector, options, registrationStack);
|
|
453
|
+
// Inside a batch the update is deferred to the end of it, by which point this delta
|
|
454
|
+
// is one of several and no longer describes the whole change — so the batch always
|
|
455
|
+
// ends in a comparison.
|
|
456
|
+
if (delta != null && canApplyDeltas && !this.batchOperationInProgress) {
|
|
457
|
+
applyDelta(delta);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
370
460
|
handleRequery();
|
|
371
461
|
});
|
|
372
462
|
this.emit('observer.created', selector, options);
|
|
@@ -516,26 +606,30 @@ class Collection extends EventEmitter_1.default {
|
|
|
516
606
|
if (!modifier)
|
|
517
607
|
throw new Error('Invalid modifier');
|
|
518
608
|
const { $setOnInsert, ...restModifier } = modifier;
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
...restModifier.$set,
|
|
528
|
-
},
|
|
529
|
-
});
|
|
530
|
-
await this.insert(newItem);
|
|
531
|
-
return 1;
|
|
532
|
-
}
|
|
533
|
-
return 0; // no item found, and upsert is not enabled
|
|
609
|
+
// Reading the item back before writing it is worth a round trip to the data layer only when
|
|
610
|
+
// something is waiting to inspect it: a validator gets to refuse the write, and it can only do
|
|
611
|
+
// that beforehand. Otherwise the backend's own answer says everything there is to know — what
|
|
612
|
+
// it returns is what changed, and an empty answer is what turns an upsert into an insert.
|
|
613
|
+
if (this.listenerCount('validate') > 0) {
|
|
614
|
+
const item = await this.getItem(selector, { async: true });
|
|
615
|
+
if (item != null)
|
|
616
|
+
this.emit('validate', (0, modify_1.default)((0, deepClone_1.default)(item), restModifier));
|
|
534
617
|
}
|
|
535
|
-
const modifiedItem = (0, modify_1.default)((0, deepClone_1.default)(item), restModifier);
|
|
536
|
-
this.emit('validate', modifiedItem);
|
|
537
618
|
const changes = await this.withPushState(() => this.backend.updateOne(selector, modifier));
|
|
538
|
-
|
|
619
|
+
if (changes.length === 0) {
|
|
620
|
+
if (!options?.upsert)
|
|
621
|
+
return 0; // no item found, and upsert is not enabled
|
|
622
|
+
const newItem = (0, modify_1.default)({}, {
|
|
623
|
+
...restModifier,
|
|
624
|
+
$set: {
|
|
625
|
+
...$setOnInsert,
|
|
626
|
+
...restModifier.$set,
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
await this.insert(newItem);
|
|
630
|
+
return 1;
|
|
631
|
+
}
|
|
632
|
+
changes.forEach(item => this.emit('changed', item, restModifier));
|
|
539
633
|
this.emit('updateOne', selector, modifier);
|
|
540
634
|
this.executeInDebugMode(callstack => this.emit('_debug.updateOne', callstack, selector, modifier));
|
|
541
635
|
return changes.length;
|
|
@@ -557,27 +651,27 @@ class Collection extends EventEmitter_1.default {
|
|
|
557
651
|
if (!modifier)
|
|
558
652
|
throw new Error('Invalid modifier');
|
|
559
653
|
const { $setOnInsert, ...restModifier } = modifier;
|
|
560
|
-
|
|
561
|
-
if (
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
$set: {
|
|
567
|
-
...$setOnInsert,
|
|
568
|
-
...restModifier.$set,
|
|
569
|
-
},
|
|
570
|
-
});
|
|
571
|
-
await this.insert(newItem);
|
|
572
|
-
return 1;
|
|
573
|
-
}
|
|
574
|
-
return 0; // no items found, and upsert is not enabled
|
|
654
|
+
// See `updateOne`: the items are only fetched up front for the sake of a validator.
|
|
655
|
+
if (this.listenerCount('validate') > 0) {
|
|
656
|
+
const items = await this.getItems(selector, { async: true });
|
|
657
|
+
items.forEach((item) => {
|
|
658
|
+
this.emit('validate', (0, modify_1.default)((0, deepClone_1.default)(item), restModifier));
|
|
659
|
+
});
|
|
575
660
|
}
|
|
576
|
-
items.forEach((item) => {
|
|
577
|
-
const modifiedItem = (0, modify_1.default)((0, deepClone_1.default)(item), restModifier);
|
|
578
|
-
this.emit('validate', modifiedItem);
|
|
579
|
-
});
|
|
580
661
|
const changes = await this.withPushState(() => this.backend.updateMany(selector, modifier));
|
|
662
|
+
if (changes.length === 0) {
|
|
663
|
+
if (!options?.upsert)
|
|
664
|
+
return 0; // no items found, and upsert is not enabled
|
|
665
|
+
const newItem = (0, modify_1.default)({}, {
|
|
666
|
+
...restModifier,
|
|
667
|
+
$set: {
|
|
668
|
+
...$setOnInsert,
|
|
669
|
+
...restModifier.$set,
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
await this.insert(newItem);
|
|
673
|
+
return 1;
|
|
674
|
+
}
|
|
581
675
|
changes.forEach((item) => {
|
|
582
676
|
this.emit('changed', item, restModifier);
|
|
583
677
|
});
|
|
@@ -599,18 +693,20 @@ class Collection extends EventEmitter_1.default {
|
|
|
599
693
|
throw new Error('Collection is disposed');
|
|
600
694
|
if (!selector)
|
|
601
695
|
throw new Error('Invalid selector');
|
|
602
|
-
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
}
|
|
608
|
-
return 0; // no item found, and upsert is not enabled
|
|
696
|
+
// See `updateOne`: the item is only fetched up front for the sake of a validator.
|
|
697
|
+
if (this.listenerCount('validate') > 0) {
|
|
698
|
+
const item = await this.getItem(selector, { async: true });
|
|
699
|
+
if (item != null)
|
|
700
|
+
this.emit('validate', { id: item.id, ...replacement });
|
|
609
701
|
}
|
|
610
|
-
const modifiedItem = { id: item.id, ...replacement };
|
|
611
|
-
this.emit('validate', modifiedItem);
|
|
612
702
|
const changes = await this.withPushState(() => this.backend.replaceOne(selector, replacement));
|
|
613
|
-
|
|
703
|
+
if (changes.length === 0) {
|
|
704
|
+
if (!options?.upsert)
|
|
705
|
+
return 0; // no item found, and upsert is not enabled
|
|
706
|
+
await this.insert(replacement);
|
|
707
|
+
return 1;
|
|
708
|
+
}
|
|
709
|
+
changes.forEach(item => this.emit('changed', item, replacement));
|
|
614
710
|
this.emit('replaceOne', selector, replacement);
|
|
615
711
|
this.executeInDebugMode(callstack => this.emit('_debug.replaceOne', callstack, selector, replacement));
|
|
616
712
|
return changes.length;
|
|
@@ -2,6 +2,7 @@ import type { BaseItem, FieldSpecifier, SortSpecifier } from './Collection';
|
|
|
2
2
|
import type Collection from './Collection';
|
|
3
3
|
import type Modifier from './types/Modifier';
|
|
4
4
|
import type Selector from './types/Selector';
|
|
5
|
+
import type { QueryDelta } from './utils/queryDelta';
|
|
5
6
|
export interface QueryOptions<T extends BaseItem> {
|
|
6
7
|
/** Sort order (default: natural order) */
|
|
7
8
|
sort?: SortSpecifier<T> | undefined;
|
|
@@ -12,7 +13,19 @@ export interface QueryOptions<T extends BaseItem> {
|
|
|
12
13
|
/** Dictionary of fields to return or exclude. */
|
|
13
14
|
fields?: FieldSpecifier<T> | undefined;
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Notified when a query's state changes.
|
|
18
|
+
*
|
|
19
|
+
* A `'complete'` notification may carry a delta describing how the result changed since the last
|
|
20
|
+
* one. An adapter that can produce one saves its listeners from rediscovering the change by
|
|
21
|
+
* comparing the whole result against the whole previous result; one that cannot simply omits it,
|
|
22
|
+
* and its listeners fall back to exactly that comparison.
|
|
23
|
+
*
|
|
24
|
+
* A delta is only ever passed when it is relative to what `getQueryResult` returned the last time
|
|
25
|
+
* it was asked. An adapter that layers anything on top of its stored result — an optimistic write
|
|
26
|
+
* still in flight, for instance — must omit the delta for as long as it does.
|
|
27
|
+
*/
|
|
28
|
+
export type StateChangeCallback<T extends BaseItem = BaseItem> = (state: 'active' | 'complete' | 'error', delta?: QueryDelta<T>) => void;
|
|
16
29
|
export interface CollectionBackend<T extends BaseItem<I>, I> {
|
|
17
30
|
insert(item: T): Promise<T>;
|
|
18
31
|
updateOne(selector: Selector<T>, modifier: Modifier<T>): Promise<T[]>;
|
|
@@ -32,7 +45,7 @@ export interface CollectionBackend<T extends BaseItem<I>, I> {
|
|
|
32
45
|
getQueryError<O extends QueryOptions<T>>(selector: Selector<T>, options: O): Error | null;
|
|
33
46
|
getQueryResult<O extends QueryOptions<T>>(selector: Selector<T>, options: O): T[];
|
|
34
47
|
executeQuery<O extends QueryOptions<T>>(selector: Selector<T>, options: O): Promise<T[]>;
|
|
35
|
-
onQueryStateChange<O extends QueryOptions<T>>(selector: Selector<T>, options: O, callback: StateChangeCallback): () => void;
|
|
48
|
+
onQueryStateChange<O extends QueryOptions<T>>(selector: Selector<T>, options: O, callback: StateChangeCallback<T>): () => void;
|
|
36
49
|
dispose(): Promise<void>;
|
|
37
50
|
isReady(): Promise<void>;
|
|
38
51
|
}
|
|
@@ -10,7 +10,9 @@ const EventEmitter_1 = __importDefault(require("./utils/EventEmitter"));
|
|
|
10
10
|
const isEqual_1 = __importDefault(require("./utils/isEqual"));
|
|
11
11
|
const match_1 = __importDefault(require("./utils/match"));
|
|
12
12
|
const modify_1 = __importDefault(require("./utils/modify"));
|
|
13
|
-
const
|
|
13
|
+
const projectItems_1 = __importDefault(require("./utils/projectItems"));
|
|
14
|
+
const incrementalQueryUpdate_1 = __importDefault(require("./utils/incrementalQueryUpdate"));
|
|
15
|
+
const queryDelta_1 = require("./utils/queryDelta");
|
|
14
16
|
const queryId_1 = __importDefault(require("./utils/queryId"));
|
|
15
17
|
const serializeValue_1 = __importDefault(require("./utils/serializeValue"));
|
|
16
18
|
const sortItems_1 = __importDefault(require("./utils/sortItems"));
|
|
@@ -142,15 +144,7 @@ class DefaultDataAdapter {
|
|
|
142
144
|
const sorted = sort ? (0, sortItems_1.default)(items, sort) : items;
|
|
143
145
|
const skipped = skip ? sorted.slice(skip) : sorted;
|
|
144
146
|
const limited = limit ? skipped.slice(0, limit) : skipped;
|
|
145
|
-
|
|
146
|
-
return limited.map((item) => {
|
|
147
|
-
if (!fields)
|
|
148
|
-
return item;
|
|
149
|
-
return {
|
|
150
|
-
...idExcluded ? {} : { id: item.id },
|
|
151
|
-
...(0, project_1.default)(item, fields),
|
|
152
|
-
};
|
|
153
|
-
});
|
|
147
|
+
return (0, projectItems_1.default)(limited, fields);
|
|
154
148
|
}
|
|
155
149
|
flushQueuedQueryUpdates(collection) {
|
|
156
150
|
if (!this.queuedQueryUpdates.get(collection.name))
|
|
@@ -159,6 +153,10 @@ class DefaultDataAdapter {
|
|
|
159
153
|
if (!changes || !hasPendingUpdates(changes))
|
|
160
154
|
return;
|
|
161
155
|
this.queuedQueryUpdates.set(collection.name, { added: [], modified: [], removed: [] });
|
|
156
|
+
const changeset = {
|
|
157
|
+
upserts: [...changes.added, ...changes.modified],
|
|
158
|
+
deletes: changes.removed.map(item => item.id),
|
|
159
|
+
};
|
|
162
160
|
const flatItems = [...changes.added, ...changes.modified, ...changes.removed];
|
|
163
161
|
const itemIds = new Set(flatItems.map(i => i.id));
|
|
164
162
|
const queries = [
|
|
@@ -173,17 +171,27 @@ class DefaultDataAdapter {
|
|
|
173
171
|
return flatItems.some(item => (0, match_1.default)(item, selector));
|
|
174
172
|
});
|
|
175
173
|
queries.forEach(({ selector, options }) => {
|
|
176
|
-
this.executeAndCacheQuery(collection, selector, options);
|
|
174
|
+
this.executeAndCacheQuery(collection, selector, options, changeset);
|
|
177
175
|
});
|
|
178
176
|
}
|
|
179
|
-
executeAndCacheQuery(collection, selector, options) {
|
|
180
|
-
const
|
|
177
|
+
executeAndCacheQuery(collection, selector, options, changes) {
|
|
178
|
+
const cached = this.cachedQueryResults.get(collection.name)?.get((0, queryId_1.default)(selector, options));
|
|
179
|
+
// With the change in hand and the previous result cached, the query can be brought up to date
|
|
180
|
+
// without walking every item this collection holds — and the listeners can be told what
|
|
181
|
+
// changed instead of being left to compare the two results themselves.
|
|
182
|
+
const incremental = changes && cached
|
|
183
|
+
? (0, incrementalQueryUpdate_1.default)(cached, selector, options, changes)
|
|
184
|
+
: null;
|
|
185
|
+
const result = incremental ?? this.executeQuery(collection, selector, options);
|
|
186
|
+
const delta = cached ? (0, queryDelta_1.diffQueryResults)(cached, result) : undefined;
|
|
181
187
|
this.cachedQueryResults.set(collection.name, this.cachedQueryResults.get(collection.name) || new Map());
|
|
182
188
|
this.cachedQueryResults.get(collection.name)?.set((0, queryId_1.default)(selector, options), result);
|
|
183
189
|
const emitter = this.queryEmitters.get(collection.name);
|
|
184
190
|
if (!emitter)
|
|
185
191
|
return;
|
|
186
|
-
|
|
192
|
+
if (delta && (0, queryDelta_1.isEmptyQueryDelta)(delta))
|
|
193
|
+
return;
|
|
194
|
+
emitter.emit('change', selector, options, 'complete', delta);
|
|
187
195
|
}
|
|
188
196
|
updateQueries(collection, changes) {
|
|
189
197
|
this.queuedQueryUpdates.set(collection.name, this.queuedQueryUpdates.get(collection.name) || { added: [], modified: [], removed: [] });
|
|
@@ -336,10 +344,10 @@ class DefaultDataAdapter {
|
|
|
336
344
|
const emitter = this.queryEmitters.get(collection.name);
|
|
337
345
|
if (!emitter)
|
|
338
346
|
throw new Error(`Query emitter not found for collection ${collection.name}`);
|
|
339
|
-
const handler = (querySelector, queryOptions, state) => {
|
|
347
|
+
const handler = (querySelector, queryOptions, state, delta) => {
|
|
340
348
|
if ((0, queryId_1.default)(querySelector, queryOptions) !== (0, queryId_1.default)(selector, options))
|
|
341
349
|
return;
|
|
342
|
-
callback
|
|
350
|
+
(0, queryDelta_1.callWithDelta)(callback, state, delta);
|
|
343
351
|
};
|
|
344
352
|
emitter.on('change', handler);
|
|
345
353
|
return () => {
|
|
@@ -22,12 +22,33 @@ export default class WorkerDataAdapter implements DataAdapter {
|
|
|
22
22
|
private collectionReady;
|
|
23
23
|
private batchExecutionHelpers;
|
|
24
24
|
private queries;
|
|
25
|
+
private pendingRequests;
|
|
25
26
|
private pendingWrites;
|
|
26
27
|
private pendingWriteSeq;
|
|
28
|
+
private pendingWriteVersions;
|
|
29
|
+
private bumpPendingWriteVersion;
|
|
27
30
|
constructor(worker: WorkerDataAdapterEndpoint, options: WorkerDataAdapterOptions);
|
|
31
|
+
private resolveWorkerReady;
|
|
32
|
+
private handleWorkerMessage;
|
|
33
|
+
private handleQueryUpdate;
|
|
28
34
|
private exec;
|
|
35
|
+
/**
|
|
36
|
+
* Issues a call whose result nobody is waiting for, and makes sure a failure has somewhere to
|
|
37
|
+
* go. A bare rejection here would surface as an uncaught error — which is what a disposed
|
|
38
|
+
* collection produced every time a cursor was cleaned up after it.
|
|
39
|
+
* @param method - The method to call on the worker.
|
|
40
|
+
* @param collectionName - The collection it applies to.
|
|
41
|
+
* @param args - The remaining arguments.
|
|
42
|
+
* @param onError - Called when the call fails, in place of merely logging it.
|
|
43
|
+
*/
|
|
44
|
+
private execInBackground;
|
|
45
|
+
private queryItemsById;
|
|
46
|
+
private flattenPendingWrites;
|
|
47
|
+
private static providesFullItems;
|
|
29
48
|
private observableItems;
|
|
30
|
-
private
|
|
49
|
+
private observableItemsByIds;
|
|
50
|
+
private servedResult;
|
|
51
|
+
private computeServedResult;
|
|
31
52
|
/**
|
|
32
53
|
* Registers a write's effect locally and notifies every active query it
|
|
33
54
|
* touches, then returns a function that drops it again once the write
|
|
@@ -38,7 +59,9 @@ export default class WorkerDataAdapter implements DataAdapter {
|
|
|
38
59
|
* @returns A function that drops the pending write and re-notifies.
|
|
39
60
|
*/
|
|
40
61
|
private applyPendingWrite;
|
|
41
|
-
private
|
|
62
|
+
private affectedQueries;
|
|
63
|
+
private servedResults;
|
|
64
|
+
private notifyWithDeltas;
|
|
42
65
|
private matchObservableItems;
|
|
43
66
|
private resolveUpdate;
|
|
44
67
|
private resolveRemoval;
|