@signaldb/svelte 2.0.0-beta.10 → 2.0.0-beta.12
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.js +7 -2
- package/dist/base/core/src/Collection/Cursor.d.ts +42 -0
- package/dist/base/core/src/Collection/Cursor.js +43 -1
- package/dist/base/core/src/Collection/index.d.ts +2 -1
- package/dist/base/core/src/Collection/index.js +31 -1
- package/dist/base/core/src/WorkerDataAdapter.d.ts +27 -1
- package/dist/base/core/src/WorkerDataAdapter.js +147 -9
- package/dist/base/core/src/WorkerDataAdapterHost.d.ts +2 -2
- package/dist/base/core/src/WorkerDataAdapterHost.js +0 -3
- package/dist/base/core/src/index.d.ts +3 -1
- package/dist/base/core/src/utils/applyQueryOptions.d.ts +15 -0
- package/dist/base/core/src/utils/applyQueryOptions.js +36 -0
- package/package.json +1 -1
|
@@ -281,8 +281,13 @@ class AsyncDataAdapter {
|
|
|
281
281
|
return;
|
|
282
282
|
rec.state = state;
|
|
283
283
|
rec.error = error;
|
|
284
|
-
//
|
|
285
|
-
|
|
284
|
+
// Notify over a snapshot, never the live set: a subscriber is free to
|
|
285
|
+
// resubscribe from inside its own callback — a reactive scope that reads
|
|
286
|
+
// the query state does exactly that when it re-runs — and `Set` iteration
|
|
287
|
+
// visits entries added while it is still running, so notifying in place
|
|
288
|
+
// turns one state change into an unbounded loop.
|
|
289
|
+
const subscribers = [...rec.listeners];
|
|
290
|
+
for (const callback of subscribers) {
|
|
286
291
|
try {
|
|
287
292
|
callback(state);
|
|
288
293
|
}
|
|
@@ -7,9 +7,26 @@ import type { ObserveCallbacks } from './Observer';
|
|
|
7
7
|
* @returns A boolean indicating if the current scope is reactive.
|
|
8
8
|
*/
|
|
9
9
|
export declare function isInReactiveScope(reactivity: ReactivityAdapter | undefined | false): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Reports whether the query a cursor stands for has produced an outcome yet.
|
|
12
|
+
* Supplied by the collection, which owns the query registration; a cursor
|
|
13
|
+
* built without one simply never reports itself as loading.
|
|
14
|
+
*/
|
|
15
|
+
export interface QueryStateAccessor {
|
|
16
|
+
/**
|
|
17
|
+
* Whether the query has settled — completed or failed — at least once
|
|
18
|
+
* since it was registered.
|
|
19
|
+
*/
|
|
20
|
+
hasSettled: () => boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Subscribes to the query settling. Returns a cleanup function.
|
|
23
|
+
*/
|
|
24
|
+
onSettled: (callback: () => void) => () => void;
|
|
25
|
+
}
|
|
10
26
|
export interface CursorOptions<T extends BaseItem, U = T, Async extends boolean = false> extends FindOptions<T, Async> {
|
|
11
27
|
transform?: Transform<T, U>;
|
|
12
28
|
bindEvents?: (requery: () => void) => () => void;
|
|
29
|
+
queryState?: QueryStateAccessor;
|
|
13
30
|
}
|
|
14
31
|
/**
|
|
15
32
|
* Represents a cursor for querying and observing a filtered, sorted, and transformed
|
|
@@ -90,6 +107,31 @@ export default class Cursor<T extends BaseItem, U = T, Async extends boolean = f
|
|
|
90
107
|
* @returns The total number of items in the result set.
|
|
91
108
|
*/
|
|
92
109
|
count(): Async extends true ? Promise<number> : number;
|
|
110
|
+
/**
|
|
111
|
+
* Whether this cursor's query has yet to deliver a first result.
|
|
112
|
+
* ⚡️ this function is reactive!
|
|
113
|
+
*
|
|
114
|
+
* An asynchronous data adapter answers a newly registered query only after a
|
|
115
|
+
* round trip, and serves a neutral empty result until it does — which a
|
|
116
|
+
* consumer cannot otherwise tell apart from "there is nothing to show". This
|
|
117
|
+
* is that distinction, per query rather than per collection, so one screen
|
|
118
|
+
* waiting on its own data says nothing about any other query.
|
|
119
|
+
*
|
|
120
|
+
* Follows the usual `isLoading`/`isFetching` split: it reports "no result
|
|
121
|
+
* yet", not "an execution is in flight". A write that re-runs an
|
|
122
|
+
* already-settled query drives it through `'active'` again while this stays
|
|
123
|
+
* `false`, so a list does not fall back to a loading state every time one of
|
|
124
|
+
* its rows changes.
|
|
125
|
+
*
|
|
126
|
+
* A query that fails counts as settled — the `query.error` event on the
|
|
127
|
+
* collection is what surfaces the failure, and a loading state that never
|
|
128
|
+
* ends is the worse answer. Reading this registers the query if nothing else
|
|
129
|
+
* has, so it cannot wait on something nobody asked for. It is always `false`
|
|
130
|
+
* for an `{ async: true }` cursor, whose `fetch()` awaits the real result
|
|
131
|
+
* anyway, and for a data adapter that answers synchronously.
|
|
132
|
+
* @returns A boolean indicating whether the first result is still pending.
|
|
133
|
+
*/
|
|
134
|
+
isLoading(): boolean;
|
|
93
135
|
/**
|
|
94
136
|
* Observes changes to the cursor's result set and triggers the specified callbacks
|
|
95
137
|
* when items are added, removed, or updated. Supports reactivity and transformation.
|
|
@@ -79,7 +79,7 @@ class Cursor {
|
|
|
79
79
|
return item;
|
|
80
80
|
return this.options.transform(item);
|
|
81
81
|
}
|
|
82
|
-
depend(changeEvents) {
|
|
82
|
+
depend(changeEvents, bindExtraNotifier) {
|
|
83
83
|
if (this.options?.async)
|
|
84
84
|
return;
|
|
85
85
|
if (!isInReactiveScope(this.options.reactive)) {
|
|
@@ -124,6 +124,17 @@ class Cursor {
|
|
|
124
124
|
this.options.reactive.onDispose(() => stop(), signal);
|
|
125
125
|
}
|
|
126
126
|
this.onCleanup(stop);
|
|
127
|
+
// A notifier that isn't driven by the result set itself — `isLoading()`
|
|
128
|
+
// needs to re-run its scope when the query settles, which is precisely
|
|
129
|
+
// the moment the result set may *not* have changed (an empty query
|
|
130
|
+
// completing produces no diff, so the observer above stays silent).
|
|
131
|
+
if (!bindExtraNotifier)
|
|
132
|
+
return;
|
|
133
|
+
const stopExtraNotifier = bindExtraNotifier(notify);
|
|
134
|
+
if (this.options.reactive.onDispose) {
|
|
135
|
+
this.options.reactive.onDispose(() => stopExtraNotifier(), signal);
|
|
136
|
+
}
|
|
137
|
+
this.onCleanup(stopExtraNotifier);
|
|
127
138
|
}
|
|
128
139
|
ensureObserver() {
|
|
129
140
|
if (!this.observer) {
|
|
@@ -247,6 +258,37 @@ class Cursor {
|
|
|
247
258
|
? maybePromise.then(items => items.length)
|
|
248
259
|
: maybePromise.length);
|
|
249
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Whether this cursor's query has yet to deliver a first result.
|
|
263
|
+
* ⚡️ this function is reactive!
|
|
264
|
+
*
|
|
265
|
+
* An asynchronous data adapter answers a newly registered query only after a
|
|
266
|
+
* round trip, and serves a neutral empty result until it does — which a
|
|
267
|
+
* consumer cannot otherwise tell apart from "there is nothing to show". This
|
|
268
|
+
* is that distinction, per query rather than per collection, so one screen
|
|
269
|
+
* waiting on its own data says nothing about any other query.
|
|
270
|
+
*
|
|
271
|
+
* Follows the usual `isLoading`/`isFetching` split: it reports "no result
|
|
272
|
+
* yet", not "an execution is in flight". A write that re-runs an
|
|
273
|
+
* already-settled query drives it through `'active'` again while this stays
|
|
274
|
+
* `false`, so a list does not fall back to a loading state every time one of
|
|
275
|
+
* its rows changes.
|
|
276
|
+
*
|
|
277
|
+
* A query that fails counts as settled — the `query.error` event on the
|
|
278
|
+
* collection is what surfaces the failure, and a loading state that never
|
|
279
|
+
* ends is the worse answer. Reading this registers the query if nothing else
|
|
280
|
+
* has, so it cannot wait on something nobody asked for. It is always `false`
|
|
281
|
+
* for an `{ async: true }` cursor, whose `fetch()` awaits the real result
|
|
282
|
+
* anyway, and for a data adapter that answers synchronously.
|
|
283
|
+
* @returns A boolean indicating whether the first result is still pending.
|
|
284
|
+
*/
|
|
285
|
+
isLoading() {
|
|
286
|
+
const queryState = this.options.queryState;
|
|
287
|
+
if (!queryState || this.options.async)
|
|
288
|
+
return false;
|
|
289
|
+
this.depend({}, notify => queryState.onSettled(notify));
|
|
290
|
+
return !queryState.hasSettled();
|
|
291
|
+
}
|
|
250
292
|
/**
|
|
251
293
|
* Observes changes to the cursor's result set and triggers the specified callbacks
|
|
252
294
|
* when items are added, removed, or updated. Supports reactivity and transformation.
|
|
@@ -8,7 +8,7 @@ import type StorageAdapter from '../types/StorageAdapter';
|
|
|
8
8
|
import Cursor from './Cursor';
|
|
9
9
|
import type { AsyncFindOptions, BaseItem, FindOptions, SyncFindOptions, Transform, TransformAll } from './types';
|
|
10
10
|
export type { AnyFindOptions, AsyncFindOptions, BaseItem, Transform, TransformAll, SortSpecifier, FieldSpecifier, FindOptions, SyncFindOptions, } from './types';
|
|
11
|
-
export type { CursorOptions } from './Cursor';
|
|
11
|
+
export type { CursorOptions, QueryStateAccessor } from './Cursor';
|
|
12
12
|
export type { ObserveCallbacks } from './Observer';
|
|
13
13
|
export { default as createIndex } from '../createIndex';
|
|
14
14
|
export interface CollectionOptions<T extends BaseItem<I>, I, E extends BaseItem = T, U = E> {
|
|
@@ -110,6 +110,7 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, E ext
|
|
|
110
110
|
private postBatchCallbacks;
|
|
111
111
|
private fieldTracking;
|
|
112
112
|
private queryListenersMap;
|
|
113
|
+
private settledQueriesSet;
|
|
113
114
|
/**
|
|
114
115
|
* Initializes a new instance of the `Collection` class with optional configuration.
|
|
115
116
|
* Sets up memory, persistence, reactivity, and indices as specified in the options.
|
|
@@ -102,6 +102,15 @@ class Collection extends EventEmitter_1.default {
|
|
|
102
102
|
postBatchCallbacks = new Set();
|
|
103
103
|
fieldTracking = false;
|
|
104
104
|
queryListenersMap = new Map();
|
|
105
|
+
// Which registered queries have delivered an outcome at least once, backing
|
|
106
|
+
// `Cursor#isLoading()`. Kept here rather than on a cursor because a cursor is
|
|
107
|
+
// rebuilt on every reactive re-run, and rather than in the data adapters
|
|
108
|
+
// because deriving it from the state they already publish needs no change to
|
|
109
|
+
// the `CollectionBackend` contract. Keyed and dropped exactly like
|
|
110
|
+
// `queryListenersMap`, so a query that gets unregistered starts out pending
|
|
111
|
+
// again — which is correct, since the adapter re-executes it on the next
|
|
112
|
+
// registration.
|
|
113
|
+
settledQueriesSet = new Set();
|
|
105
114
|
constructor(nameOrOptions, maybeDataAdapter, maybeOptions) {
|
|
106
115
|
super();
|
|
107
116
|
const name = typeof nameOrOptions === 'string'
|
|
@@ -310,6 +319,25 @@ class Collection extends EventEmitter_1.default {
|
|
|
310
319
|
fieldTracking: this.fieldTracking,
|
|
311
320
|
...options,
|
|
312
321
|
transform: this.transform.bind(this),
|
|
322
|
+
queryState: {
|
|
323
|
+
hasSettled: () => {
|
|
324
|
+
if (this.settledQueriesSet.has((0, queryId_1.default)(selector, options)))
|
|
325
|
+
return true;
|
|
326
|
+
// An adapter that answers synchronously reports `'complete'` from
|
|
327
|
+
// the start, so a cursor over one is never in a loading state.
|
|
328
|
+
const state = this.backend.getQueryState(selector, options || {});
|
|
329
|
+
return state === 'complete' || state === 'error';
|
|
330
|
+
},
|
|
331
|
+
// The latch is set by the very callback that notifies, so a cursor
|
|
332
|
+
// can never be woken to read a state that has not been recorded yet,
|
|
333
|
+
// whatever order the backend runs its subscribers in.
|
|
334
|
+
onSettled: callback => this.backend.onQueryStateChange(selector, options || {}, (state) => {
|
|
335
|
+
if (state !== 'complete' && state !== 'error')
|
|
336
|
+
return;
|
|
337
|
+
this.settledQueriesSet.add((0, queryId_1.default)(selector, options));
|
|
338
|
+
callback();
|
|
339
|
+
}),
|
|
340
|
+
},
|
|
313
341
|
bindEvents: (requery) => {
|
|
314
342
|
const handleRequery = () => {
|
|
315
343
|
if (this.batchOperationInProgress) {
|
|
@@ -351,8 +379,10 @@ class Collection extends EventEmitter_1.default {
|
|
|
351
379
|
// Only unregister if this observer was the one that registered
|
|
352
380
|
// This prevents race conditions where a new observer registers
|
|
353
381
|
// before the old one's cleanup runs
|
|
354
|
-
if (newListeners === 0 && didRegister)
|
|
382
|
+
if (newListeners === 0 && didRegister) {
|
|
355
383
|
this.backend.unregisterQuery(selector, options || {});
|
|
384
|
+
this.settledQueriesSet.delete((0, queryId_1.default)(selector, options));
|
|
385
|
+
}
|
|
356
386
|
this.queryListeners({ selector, options }, newListeners);
|
|
357
387
|
queryStateChangeCleanup();
|
|
358
388
|
this.emit('observer.disposed', selector, options);
|
|
@@ -6,6 +6,12 @@ interface WorkerDataAdapterOptions {
|
|
|
6
6
|
id?: string;
|
|
7
7
|
log?: (message: string, ...args: any[]) => void;
|
|
8
8
|
}
|
|
9
|
+
export interface WorkerDataAdapterEndpoint {
|
|
10
|
+
addEventListener: (type: 'message', listener: (event: MessageEvent) => void) => void;
|
|
11
|
+
removeEventListener: (type: 'message', listener: (event: MessageEvent) => void) => void;
|
|
12
|
+
postMessage: (message: unknown) => void;
|
|
13
|
+
terminate?: () => void;
|
|
14
|
+
}
|
|
9
15
|
export default class WorkerDataAdapter implements DataAdapter {
|
|
10
16
|
private worker;
|
|
11
17
|
private options;
|
|
@@ -16,8 +22,28 @@ export default class WorkerDataAdapter implements DataAdapter {
|
|
|
16
22
|
private collectionReady;
|
|
17
23
|
private batchExecutionHelpers;
|
|
18
24
|
private queries;
|
|
19
|
-
|
|
25
|
+
private pendingWrites;
|
|
26
|
+
private pendingWriteSeq;
|
|
27
|
+
constructor(worker: WorkerDataAdapterEndpoint, options: WorkerDataAdapterOptions);
|
|
20
28
|
private exec;
|
|
29
|
+
private observableItems;
|
|
30
|
+
private mergePendingWrites;
|
|
31
|
+
/**
|
|
32
|
+
* Registers a write's effect locally and notifies every active query it
|
|
33
|
+
* touches, then returns a function that drops it again once the write
|
|
34
|
+
* settles.
|
|
35
|
+
* @param collectionName - The collection the write targets.
|
|
36
|
+
* @param upserts - Items inserted or updated by the write.
|
|
37
|
+
* @param deletes - Ids removed by the write.
|
|
38
|
+
* @returns A function that drops the pending write and re-notifies.
|
|
39
|
+
*/
|
|
40
|
+
private applyPendingWrite;
|
|
41
|
+
private notifyAffectedQueries;
|
|
42
|
+
private matchObservableItems;
|
|
43
|
+
private resolveUpdate;
|
|
44
|
+
private resolveRemoval;
|
|
45
|
+
private resolveReplacement;
|
|
46
|
+
private withPendingWrite;
|
|
21
47
|
private enqueueBatched;
|
|
22
48
|
private updateQuery;
|
|
23
49
|
createCollectionBackend<T extends BaseItem<I>, I = any, E extends BaseItem = T, U = E>(collection: Collection<T, I, E, U>, indices: string[]): CollectionBackend<T, I>;
|
|
@@ -6,6 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const queryId_1 = __importDefault(require("./utils/queryId"));
|
|
7
7
|
const randomId_1 = __importDefault(require("./utils/randomId"));
|
|
8
8
|
const batchOnNextTick_1 = __importDefault(require("./utils/batchOnNextTick"));
|
|
9
|
+
const applyQueryOptions_1 = __importDefault(require("./utils/applyQueryOptions"));
|
|
10
|
+
const match_1 = __importDefault(require("./utils/match"));
|
|
11
|
+
const modify_1 = __importDefault(require("./utils/modify"));
|
|
12
|
+
const deepClone_1 = __importDefault(require("./utils/deepClone"));
|
|
9
13
|
class WorkerDataAdapter {
|
|
10
14
|
worker;
|
|
11
15
|
options;
|
|
@@ -16,6 +20,19 @@ class WorkerDataAdapter {
|
|
|
16
20
|
collectionReady = new Map();
|
|
17
21
|
batchExecutionHelpers = new Map();
|
|
18
22
|
queries = {};
|
|
23
|
+
// Writes that have been issued but not yet confirmed by the worker. Their
|
|
24
|
+
// effect is layered on top of each active query's last authoritative result
|
|
25
|
+
// in `getQueryResult`, so a cursor reflects a write immediately instead of
|
|
26
|
+
// waiting out the postMessage → SQLite write → re-executed query →
|
|
27
|
+
// postMessage round trip. Deliberately scoped to items that active queries
|
|
28
|
+
// already hold: this adapter must not turn into a full in-memory mirror of
|
|
29
|
+
// the backing store, and anything no active query holds isn't observable
|
|
30
|
+
// anyway. Dropped again once the write settles — on success the worker's
|
|
31
|
+
// own `queryUpdate` has already landed (it is posted before the write's
|
|
32
|
+
// response, and message order is preserved), on failure dropping it is the
|
|
33
|
+
// rollback.
|
|
34
|
+
pendingWrites = new Map();
|
|
35
|
+
pendingWriteSeq = 0;
|
|
19
36
|
constructor(worker, options) {
|
|
20
37
|
this.worker = worker;
|
|
21
38
|
this.options = options;
|
|
@@ -78,6 +95,117 @@ class WorkerDataAdapter {
|
|
|
78
95
|
});
|
|
79
96
|
});
|
|
80
97
|
}
|
|
98
|
+
// The items an active query currently holds, deduplicated by id — the only
|
|
99
|
+
// items this adapter knows about, and the set a selector-based write can be
|
|
100
|
+
// resolved against locally.
|
|
101
|
+
observableItems(collectionName) {
|
|
102
|
+
const byId = new Map();
|
|
103
|
+
this.queries[collectionName]?.forEach((query) => {
|
|
104
|
+
this.mergePendingWrites(collectionName, query.items).forEach((item) => {
|
|
105
|
+
byId.set(item.id, item);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
return [...byId.values()];
|
|
109
|
+
}
|
|
110
|
+
mergePendingWrites(collectionName, items) {
|
|
111
|
+
const pending = this.pendingWrites.get(collectionName);
|
|
112
|
+
if (!pending || pending.size === 0)
|
|
113
|
+
return items;
|
|
114
|
+
const merged = new Map(items.map(item => [item.id, item]));
|
|
115
|
+
[...pending.entries()]
|
|
116
|
+
.toSorted(([a], [b]) => a - b)
|
|
117
|
+
.forEach(([, write]) => {
|
|
118
|
+
write.upserts.forEach((item, id) => merged.set(id, item));
|
|
119
|
+
write.deletes.forEach(id => merged.delete(id));
|
|
120
|
+
});
|
|
121
|
+
return [...merged.values()];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Registers a write's effect locally and notifies every active query it
|
|
125
|
+
* touches, then returns a function that drops it again once the write
|
|
126
|
+
* settles.
|
|
127
|
+
* @param collectionName - The collection the write targets.
|
|
128
|
+
* @param upserts - Items inserted or updated by the write.
|
|
129
|
+
* @param deletes - Ids removed by the write.
|
|
130
|
+
* @returns A function that drops the pending write and re-notifies.
|
|
131
|
+
*/
|
|
132
|
+
applyPendingWrite(collectionName, upserts, deletes) {
|
|
133
|
+
if (upserts.length === 0 && deletes.length === 0)
|
|
134
|
+
return () => { };
|
|
135
|
+
const seq = this.pendingWriteSeq += 1;
|
|
136
|
+
const pending = this.pendingWrites.get(collectionName)
|
|
137
|
+
?? new Map();
|
|
138
|
+
pending.set(seq, {
|
|
139
|
+
upserts: new Map(upserts.map(item => [item.id, item])),
|
|
140
|
+
deletes: new Set(deletes),
|
|
141
|
+
});
|
|
142
|
+
this.pendingWrites.set(collectionName, pending);
|
|
143
|
+
const affectedIds = new Set([...upserts.map(item => item.id), ...deletes]);
|
|
144
|
+
this.notifyAffectedQueries(collectionName, upserts, affectedIds);
|
|
145
|
+
return () => {
|
|
146
|
+
const current = this.pendingWrites.get(collectionName);
|
|
147
|
+
if (!current)
|
|
148
|
+
return;
|
|
149
|
+
current.delete(seq);
|
|
150
|
+
if (current.size === 0)
|
|
151
|
+
this.pendingWrites.delete(collectionName);
|
|
152
|
+
this.notifyAffectedQueries(collectionName, upserts, affectedIds);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// Re-runs the state-change callbacks of every query whose result the write
|
|
156
|
+
// can have changed — either because a written item matches its selector, or
|
|
157
|
+
// because it already held one of the affected items (an update that moves an
|
|
158
|
+
// item out of a query, or a removal).
|
|
159
|
+
notifyAffectedQueries(collectionName, upserts, affectedIds) {
|
|
160
|
+
this.queries[collectionName]?.forEach((query) => {
|
|
161
|
+
const wasHolding = query.items.some(item => affectedIds.has(item.id));
|
|
162
|
+
const nowMatches = upserts.some(item => query.selector != null
|
|
163
|
+
&& (0, match_1.default)(item, query.selector));
|
|
164
|
+
if (!wasHolding && !nowMatches)
|
|
165
|
+
return;
|
|
166
|
+
query.stateChangeCallbacks.forEach(callback => callback(query.state));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
matchObservableItems(collectionName, selector, onlyFirst) {
|
|
170
|
+
if (selector == null)
|
|
171
|
+
return [];
|
|
172
|
+
const matches = this.observableItems(collectionName).filter(item => (0, match_1.default)(item, selector));
|
|
173
|
+
return onlyFirst ? matches.slice(0, 1) : matches;
|
|
174
|
+
}
|
|
175
|
+
resolveUpdate(collectionName, selector, modifier, onlyFirst) {
|
|
176
|
+
const { $setOnInsert, ...restModifier } = modifier;
|
|
177
|
+
const upserts = [];
|
|
178
|
+
const deletes = [];
|
|
179
|
+
this.matchObservableItems(collectionName, selector, onlyFirst).forEach((item) => {
|
|
180
|
+
const modifiedItem = (0, modify_1.default)((0, deepClone_1.default)(item), restModifier);
|
|
181
|
+
upserts.push(modifiedItem);
|
|
182
|
+
if (modifiedItem.id !== item.id)
|
|
183
|
+
deletes.push(item.id);
|
|
184
|
+
});
|
|
185
|
+
return { upserts, deletes };
|
|
186
|
+
}
|
|
187
|
+
resolveRemoval(collectionName, selector, onlyFirst) {
|
|
188
|
+
return this.matchObservableItems(collectionName, selector, onlyFirst).map(item => item.id);
|
|
189
|
+
}
|
|
190
|
+
resolveReplacement(item, replacement) {
|
|
191
|
+
const modifiedItem = { ...replacement, id: replacement.id ?? item.id };
|
|
192
|
+
return {
|
|
193
|
+
upserts: [modifiedItem],
|
|
194
|
+
deletes: modifiedItem.id === item.id ? [] : [item.id],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// Makes a write's effect visible to active queries for the duration of the
|
|
198
|
+
// round trip, and drops it again once the worker has spoken — see
|
|
199
|
+
// `pendingWrites`.
|
|
200
|
+
async withPendingWrite(collectionName, delta, run) {
|
|
201
|
+
const dropPendingWrite = this.applyPendingWrite(collectionName, delta.upserts, delta.deletes);
|
|
202
|
+
try {
|
|
203
|
+
return await run();
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
dropPendingWrite();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
81
209
|
enqueueBatched(collectionName, method, args) {
|
|
82
210
|
const helper = this.batchExecutionHelpers.get(collectionName);
|
|
83
211
|
if (!helper)
|
|
@@ -92,6 +220,8 @@ class WorkerDataAdapter {
|
|
|
92
220
|
return;
|
|
93
221
|
const existing = collectionQueries.get(id);
|
|
94
222
|
const newState = {
|
|
223
|
+
selector: query.selector,
|
|
224
|
+
options: query.options,
|
|
95
225
|
state: 'active',
|
|
96
226
|
error: null,
|
|
97
227
|
items: [],
|
|
@@ -110,22 +240,25 @@ class WorkerDataAdapter {
|
|
|
110
240
|
this.batchExecutionHelpers.set(collection.name, (0, batchOnNextTick_1.default)(async (method, args) => this.exec(method, collection.name, args)));
|
|
111
241
|
return {
|
|
112
242
|
insert: async (item) => {
|
|
113
|
-
return this.enqueueBatched(collection.name, 'insert', [item]);
|
|
243
|
+
return this.withPendingWrite(collection.name, { upserts: [item], deletes: [] }, () => this.enqueueBatched(collection.name, 'insert', [item]));
|
|
114
244
|
},
|
|
115
245
|
updateOne: async (selector, modifier) => {
|
|
116
|
-
return this.enqueueBatched(collection.name, 'updateOne', [selector, modifier]);
|
|
246
|
+
return this.withPendingWrite(collection.name, this.resolveUpdate(collection.name, selector, modifier, true), () => this.enqueueBatched(collection.name, 'updateOne', [selector, modifier]));
|
|
117
247
|
},
|
|
118
248
|
updateMany: async (selector, modifier) => {
|
|
119
|
-
return this.enqueueBatched(collection.name, 'updateMany', [selector, modifier]);
|
|
249
|
+
return this.withPendingWrite(collection.name, this.resolveUpdate(collection.name, selector, modifier, false), () => this.enqueueBatched(collection.name, 'updateMany', [selector, modifier]));
|
|
120
250
|
},
|
|
121
251
|
replaceOne: async (selector, replacement) => {
|
|
122
|
-
|
|
252
|
+
const [item] = this.matchObservableItems(collection.name, selector, true);
|
|
253
|
+
return this.withPendingWrite(collection.name, item == null
|
|
254
|
+
? { upserts: [], deletes: [] }
|
|
255
|
+
: this.resolveReplacement(item, replacement), () => this.enqueueBatched(collection.name, 'replaceOne', [selector, replacement]));
|
|
123
256
|
},
|
|
124
257
|
removeOne: async (selector) => {
|
|
125
|
-
return this.enqueueBatched(collection.name, 'removeOne', [selector]);
|
|
258
|
+
return this.withPendingWrite(collection.name, { upserts: [], deletes: this.resolveRemoval(collection.name, selector, true) }, () => this.enqueueBatched(collection.name, 'removeOne', [selector]));
|
|
126
259
|
},
|
|
127
260
|
removeMany: async (selector) => {
|
|
128
|
-
return this.enqueueBatched(collection.name, 'removeMany', [selector]);
|
|
261
|
+
return this.withPendingWrite(collection.name, { upserts: [], deletes: this.resolveRemoval(collection.name, selector, false) }, () => this.enqueueBatched(collection.name, 'removeMany', [selector]));
|
|
129
262
|
},
|
|
130
263
|
// methods for registering and unregistering queries that will be called from the collection during find/findOne
|
|
131
264
|
registerQuery: (selector, options) => {
|
|
@@ -176,7 +309,12 @@ class WorkerDataAdapter {
|
|
|
176
309
|
},
|
|
177
310
|
getQueryResult: (selector, options) => {
|
|
178
311
|
const query = this.queries[collection.name]?.get((0, queryId_1.default)(selector, options));
|
|
179
|
-
|
|
312
|
+
if (!query)
|
|
313
|
+
return [];
|
|
314
|
+
const pending = this.pendingWrites.get(collection.name);
|
|
315
|
+
if (!pending || pending.size === 0)
|
|
316
|
+
return query.items;
|
|
317
|
+
return (0, applyQueryOptions_1.default)(this.mergePendingWrites(collection.name, query.items), selector, options);
|
|
180
318
|
},
|
|
181
319
|
onQueryStateChange: (selector, options, callback) => {
|
|
182
320
|
this.updateQuery(collection.name, { selector, options }, {
|
|
@@ -188,7 +326,7 @@ class WorkerDataAdapter {
|
|
|
188
326
|
return () => {
|
|
189
327
|
const currentCallbacks = this.queries[collection.name]?.get((0, queryId_1.default)(selector, options))?.stateChangeCallbacks;
|
|
190
328
|
if (!currentCallbacks)
|
|
191
|
-
|
|
329
|
+
return;
|
|
192
330
|
this.updateQuery(collection.name, { selector, options }, {
|
|
193
331
|
stateChangeCallbacks: currentCallbacks
|
|
194
332
|
.filter(existingCallback => existingCallback !== callback),
|
|
@@ -200,7 +338,7 @@ class WorkerDataAdapter {
|
|
|
200
338
|
dispose: async () => {
|
|
201
339
|
await this.exec('unregisterCollection', collection.name);
|
|
202
340
|
this.isDisposed = true;
|
|
203
|
-
this.worker.terminate();
|
|
341
|
+
this.worker.terminate?.();
|
|
204
342
|
},
|
|
205
343
|
isReady: async () => {
|
|
206
344
|
await this.exec('isReady', collection.name);
|
|
@@ -3,7 +3,7 @@ import type { QueryOptions } from './DataAdapter';
|
|
|
3
3
|
import type Modifier from './types/Modifier';
|
|
4
4
|
import type StorageAdapter from './types/StorageAdapter';
|
|
5
5
|
import type Selector from './types/Selector';
|
|
6
|
-
interface
|
|
6
|
+
export interface WorkerDataAdapterHostEndpoint {
|
|
7
7
|
addEventListener: (type: 'message', listener: (event: MessageEvent) => any) => void;
|
|
8
8
|
postMessage: (message: any) => void;
|
|
9
9
|
}
|
|
@@ -37,7 +37,7 @@ export default class WorkerDataAdapterHost<T extends BaseItem<I>, I = any> {
|
|
|
37
37
|
private collectionIndices;
|
|
38
38
|
private queries;
|
|
39
39
|
private onError;
|
|
40
|
-
constructor(workerContext:
|
|
40
|
+
constructor(workerContext: WorkerDataAdapterHostEndpoint, options: WorkerDataAdapterHostOptions);
|
|
41
41
|
private respond;
|
|
42
42
|
private handleMessage;
|
|
43
43
|
private getIndexInfo;
|
|
@@ -35,9 +35,6 @@ class WorkerDataAdapterHost {
|
|
|
35
35
|
}
|
|
36
36
|
if (this.options.log)
|
|
37
37
|
this.log = this.options.log;
|
|
38
|
-
if (typeof addEventListener === 'undefined' || typeof postMessage === 'undefined') {
|
|
39
|
-
throw new TypeError('WorkerDataAdapterHost can only be used in a Web Worker context');
|
|
40
|
-
}
|
|
41
38
|
this.workerContext.addEventListener('message', async (event) => {
|
|
42
39
|
try {
|
|
43
40
|
const { workerId, id, method, args } = event.data;
|
|
@@ -2,7 +2,7 @@ export type { default as ReactivityAdapter } from './types/ReactivityAdapter';
|
|
|
2
2
|
export type { default as StorageAdapter, Changeset, } from './types/StorageAdapter';
|
|
3
3
|
export type { default as Selector } from './types/Selector';
|
|
4
4
|
export type { default as Modifier } from './types/Modifier';
|
|
5
|
-
export type { BaseItem, ObserveCallbacks, CursorOptions, Transform, TransformAll, SortSpecifier, FieldSpecifier, AnyFindOptions, AsyncFindOptions, FindOptions, SyncFindOptions, CollectionOptions, } from './Collection';
|
|
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
7
|
export { default as Cursor } from './Collection/Cursor';
|
|
8
8
|
export { default as Collection } from './Collection';
|
|
@@ -19,4 +19,6 @@ export { default as DefaultDataAdapter } from './DefaultDataAdapter';
|
|
|
19
19
|
export { default as AsyncDataAdapter } from './AsyncDataAdapter';
|
|
20
20
|
export { default as WorkerDataAdapter } from './WorkerDataAdapter';
|
|
21
21
|
export { default as WorkerDataAdapterHost } from './WorkerDataAdapterHost';
|
|
22
|
+
export type { WorkerDataAdapterEndpoint } from './WorkerDataAdapter';
|
|
23
|
+
export type { WorkerDataAdapterHostEndpoint } from './WorkerDataAdapterHost';
|
|
22
24
|
export { default as AutoFetchDataAdapter } from './AutoFetchDataAdapter';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { QueryOptions } from '../DataAdapter';
|
|
2
|
+
import type Selector from '../types/Selector';
|
|
3
|
+
import type { BaseItem } from '../Collection/types';
|
|
4
|
+
/**
|
|
5
|
+
* Filters, sorts, paginates and projects a plain in-memory array the same way
|
|
6
|
+
* DefaultDataAdapter and WorkerDataAdapterHost apply a selector/QueryOptions
|
|
7
|
+
* pair to their stored items. Used to re-derive a query's result locally after
|
|
8
|
+
* a write, without asking the backing store again.
|
|
9
|
+
* @template T - The type of the items.
|
|
10
|
+
* @param items - The items to filter, sort, paginate and project.
|
|
11
|
+
* @param selector - The selector to match items against.
|
|
12
|
+
* @param options - Sort, skip, limit and field projection options.
|
|
13
|
+
* @returns The resulting items.
|
|
14
|
+
*/
|
|
15
|
+
export default function applyQueryOptions<T extends BaseItem>(items: T[], selector: Selector<T>, options?: QueryOptions<T>): T[];
|
|
@@ -0,0 +1,36 @@
|
|
|
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 = applyQueryOptions;
|
|
7
|
+
const match_1 = __importDefault(require("./match"));
|
|
8
|
+
const sortItems_1 = __importDefault(require("./sortItems"));
|
|
9
|
+
const project_1 = __importDefault(require("./project"));
|
|
10
|
+
/**
|
|
11
|
+
* Filters, sorts, paginates and projects a plain in-memory array the same way
|
|
12
|
+
* DefaultDataAdapter and WorkerDataAdapterHost apply a selector/QueryOptions
|
|
13
|
+
* pair to their stored items. Used to re-derive a query's result locally after
|
|
14
|
+
* a write, without asking the backing store again.
|
|
15
|
+
* @template T - The type of the items.
|
|
16
|
+
* @param items - The items to filter, sort, paginate and project.
|
|
17
|
+
* @param selector - The selector to match items against.
|
|
18
|
+
* @param options - Sort, skip, limit and field projection options.
|
|
19
|
+
* @returns The resulting items.
|
|
20
|
+
*/
|
|
21
|
+
function applyQueryOptions(items, selector, options) {
|
|
22
|
+
const matched = selector == null ? [] : items.filter(item => (0, match_1.default)(item, selector));
|
|
23
|
+
const { sort, skip, limit, fields } = options || {};
|
|
24
|
+
const sorted = sort ? (0, sortItems_1.default)(matched, sort) : matched;
|
|
25
|
+
const skipped = skip ? sorted.slice(skip) : sorted;
|
|
26
|
+
const limited = limit ? skipped.slice(0, limit) : skipped;
|
|
27
|
+
const idExcluded = fields && fields.id === 0;
|
|
28
|
+
return limited.map((item) => {
|
|
29
|
+
if (!fields)
|
|
30
|
+
return item;
|
|
31
|
+
return {
|
|
32
|
+
...idExcluded ? {} : { id: item.id },
|
|
33
|
+
...(0, project_1.default)(item, fields),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signaldb/svelte",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.0.0-beta.
|
|
4
|
+
"version": "2.0.0-beta.12",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "tsc -d --noEmit false",
|
|
7
7
|
"analyze-bundle": "bundle-analyzer ./dist --upload-token=$BUNDLE_ANALYZER_UPLOAD_TOKEN --bundle-name=@signaldb/svelte",
|