@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
|
@@ -338,7 +338,7 @@ class Collection extends EventEmitter_1.default {
|
|
|
338
338
|
callback();
|
|
339
339
|
}),
|
|
340
340
|
},
|
|
341
|
-
bindEvents: (requery) => {
|
|
341
|
+
bindEvents: (requery, applyDelta) => {
|
|
342
342
|
const handleRequery = () => {
|
|
343
343
|
if (this.batchOperationInProgress) {
|
|
344
344
|
this.postBatchCallbacks.add(requery);
|
|
@@ -346,13 +346,17 @@ class Collection extends EventEmitter_1.default {
|
|
|
346
346
|
}
|
|
347
347
|
requery();
|
|
348
348
|
};
|
|
349
|
+
// A `transformAll` sits between the backend's result and what the cursor holds, and it
|
|
350
|
+
// is free to produce anything at all — so a delta describing the backend's result says
|
|
351
|
+
// nothing about the cursor's. Those collections keep comparing.
|
|
352
|
+
const canApplyDeltas = !this.options.transformAll && !options?.async;
|
|
349
353
|
// register query if not yet registered
|
|
350
354
|
const listeners = this.queryListeners({ selector, options });
|
|
351
355
|
const didRegister = listeners === 0;
|
|
352
356
|
if (didRegister)
|
|
353
357
|
this.backend.registerQuery(selector, options || {});
|
|
354
358
|
this.queryListeners({ selector, options }, listeners + 1);
|
|
355
|
-
const queryStateChangeCleanup = this.backend.onQueryStateChange(selector, options || {}, (state) => {
|
|
359
|
+
const queryStateChangeCleanup = this.backend.onQueryStateChange(selector, options || {}, (state, delta) => {
|
|
356
360
|
// A failed query never reaches `'complete'`, so the cursor keeps
|
|
357
361
|
// serving its neutral empty value — indistinguishable from "no
|
|
358
362
|
// data" for anyone reading it. Surfacing the failure as an event
|
|
@@ -367,6 +371,13 @@ class Collection extends EventEmitter_1.default {
|
|
|
367
371
|
}
|
|
368
372
|
if (state !== 'complete')
|
|
369
373
|
return;
|
|
374
|
+
// Inside a batch the update is deferred to the end of it, by which point this delta
|
|
375
|
+
// is one of several and no longer describes the whole change — so the batch always
|
|
376
|
+
// ends in a comparison.
|
|
377
|
+
if (delta != null && canApplyDeltas && !this.batchOperationInProgress) {
|
|
378
|
+
applyDelta(delta);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
370
381
|
handleRequery();
|
|
371
382
|
});
|
|
372
383
|
this.emit('observer.created', selector, options);
|
|
@@ -516,26 +527,30 @@ class Collection extends EventEmitter_1.default {
|
|
|
516
527
|
if (!modifier)
|
|
517
528
|
throw new Error('Invalid modifier');
|
|
518
529
|
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
|
|
530
|
+
// Reading the item back before writing it is worth a round trip to the data layer only when
|
|
531
|
+
// something is waiting to inspect it: a validator gets to refuse the write, and it can only do
|
|
532
|
+
// that beforehand. Otherwise the backend's own answer says everything there is to know — what
|
|
533
|
+
// it returns is what changed, and an empty answer is what turns an upsert into an insert.
|
|
534
|
+
if (this.listenerCount('validate') > 0) {
|
|
535
|
+
const item = await this.getItem(selector, { async: true });
|
|
536
|
+
if (item != null)
|
|
537
|
+
this.emit('validate', (0, modify_1.default)((0, deepClone_1.default)(item), restModifier));
|
|
534
538
|
}
|
|
535
|
-
const modifiedItem = (0, modify_1.default)((0, deepClone_1.default)(item), restModifier);
|
|
536
|
-
this.emit('validate', modifiedItem);
|
|
537
539
|
const changes = await this.withPushState(() => this.backend.updateOne(selector, modifier));
|
|
538
|
-
|
|
540
|
+
if (changes.length === 0) {
|
|
541
|
+
if (!options?.upsert)
|
|
542
|
+
return 0; // no item found, and upsert is not enabled
|
|
543
|
+
const newItem = (0, modify_1.default)({}, {
|
|
544
|
+
...restModifier,
|
|
545
|
+
$set: {
|
|
546
|
+
...$setOnInsert,
|
|
547
|
+
...restModifier.$set,
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
await this.insert(newItem);
|
|
551
|
+
return 1;
|
|
552
|
+
}
|
|
553
|
+
changes.forEach(item => this.emit('changed', item, restModifier));
|
|
539
554
|
this.emit('updateOne', selector, modifier);
|
|
540
555
|
this.executeInDebugMode(callstack => this.emit('_debug.updateOne', callstack, selector, modifier));
|
|
541
556
|
return changes.length;
|
|
@@ -557,27 +572,27 @@ class Collection extends EventEmitter_1.default {
|
|
|
557
572
|
if (!modifier)
|
|
558
573
|
throw new Error('Invalid modifier');
|
|
559
574
|
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
|
|
575
|
+
// See `updateOne`: the items are only fetched up front for the sake of a validator.
|
|
576
|
+
if (this.listenerCount('validate') > 0) {
|
|
577
|
+
const items = await this.getItems(selector, { async: true });
|
|
578
|
+
items.forEach((item) => {
|
|
579
|
+
this.emit('validate', (0, modify_1.default)((0, deepClone_1.default)(item), restModifier));
|
|
580
|
+
});
|
|
575
581
|
}
|
|
576
|
-
items.forEach((item) => {
|
|
577
|
-
const modifiedItem = (0, modify_1.default)((0, deepClone_1.default)(item), restModifier);
|
|
578
|
-
this.emit('validate', modifiedItem);
|
|
579
|
-
});
|
|
580
582
|
const changes = await this.withPushState(() => this.backend.updateMany(selector, modifier));
|
|
583
|
+
if (changes.length === 0) {
|
|
584
|
+
if (!options?.upsert)
|
|
585
|
+
return 0; // no items found, and upsert is not enabled
|
|
586
|
+
const newItem = (0, modify_1.default)({}, {
|
|
587
|
+
...restModifier,
|
|
588
|
+
$set: {
|
|
589
|
+
...$setOnInsert,
|
|
590
|
+
...restModifier.$set,
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
await this.insert(newItem);
|
|
594
|
+
return 1;
|
|
595
|
+
}
|
|
581
596
|
changes.forEach((item) => {
|
|
582
597
|
this.emit('changed', item, restModifier);
|
|
583
598
|
});
|
|
@@ -599,18 +614,20 @@ class Collection extends EventEmitter_1.default {
|
|
|
599
614
|
throw new Error('Collection is disposed');
|
|
600
615
|
if (!selector)
|
|
601
616
|
throw new Error('Invalid selector');
|
|
602
|
-
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
}
|
|
608
|
-
return 0; // no item found, and upsert is not enabled
|
|
617
|
+
// See `updateOne`: the item is only fetched up front for the sake of a validator.
|
|
618
|
+
if (this.listenerCount('validate') > 0) {
|
|
619
|
+
const item = await this.getItem(selector, { async: true });
|
|
620
|
+
if (item != null)
|
|
621
|
+
this.emit('validate', { id: item.id, ...replacement });
|
|
609
622
|
}
|
|
610
|
-
const modifiedItem = { id: item.id, ...replacement };
|
|
611
|
-
this.emit('validate', modifiedItem);
|
|
612
623
|
const changes = await this.withPushState(() => this.backend.replaceOne(selector, replacement));
|
|
613
|
-
|
|
624
|
+
if (changes.length === 0) {
|
|
625
|
+
if (!options?.upsert)
|
|
626
|
+
return 0; // no item found, and upsert is not enabled
|
|
627
|
+
await this.insert(replacement);
|
|
628
|
+
return 1;
|
|
629
|
+
}
|
|
630
|
+
changes.forEach(item => this.emit('changed', item, replacement));
|
|
614
631
|
this.emit('replaceOne', selector, replacement);
|
|
615
632
|
this.executeInDebugMode(callstack => this.emit('_debug.replaceOne', callstack, selector, replacement));
|
|
616
633
|
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;
|