@signaldb/svelte 2.0.0-beta.17 → 2.0.0-beta.19
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 +12 -49
- package/dist/base/core/src/AutoFetchDataAdapter.js +8 -48
- package/dist/base/core/src/DefaultDataAdapter.js +11 -9
- package/dist/base/core/src/WorkerDataAdapter.d.ts +5 -0
- package/dist/base/core/src/WorkerDataAdapter.js +130 -27
- package/dist/base/core/src/WorkerDataAdapterHost.js +17 -63
- package/dist/base/core/src/getIndexInfo.js +14 -2
- package/dist/base/core/src/types/StorageAdapter.d.ts +9 -1
- package/dist/base/core/src/utils/idIndexQuery.d.ts +20 -0
- package/dist/base/core/src/utils/idIndexQuery.js +49 -0
- package/dist/base/core/src/utils/storageIndexQuery.d.ts +20 -0
- package/dist/base/core/src/utils/storageIndexQuery.js +75 -0
- package/package.json +1 -1
|
@@ -10,7 +10,8 @@ const modify_1 = __importDefault(require("./utils/modify"));
|
|
|
10
10
|
const queryId_1 = __importDefault(require("./utils/queryId"));
|
|
11
11
|
const isEqual_1 = __importDefault(require("./utils/isEqual"));
|
|
12
12
|
const getIndexInfo_1 = __importDefault(require("./getIndexInfo"));
|
|
13
|
-
const
|
|
13
|
+
const storageIndexQuery_1 = __importDefault(require("./utils/storageIndexQuery"));
|
|
14
|
+
const idIndexQuery_1 = __importDefault(require("./utils/idIndexQuery"));
|
|
14
15
|
const sortItems_1 = __importDefault(require("./utils/sortItems"));
|
|
15
16
|
const projectItems_1 = __importDefault(require("./utils/projectItems"));
|
|
16
17
|
const incrementalQueryUpdate_1 = __importDefault(require("./utils/incrementalQueryUpdate"));
|
|
@@ -343,15 +344,15 @@ class AsyncDataAdapter {
|
|
|
343
344
|
const storageAdapter = this.storageAdapters.get(collectionName);
|
|
344
345
|
if (!storageAdapter)
|
|
345
346
|
throw new Error(`No persistence adapter for collection ${collectionName}`);
|
|
346
|
-
if (selector != null
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
347
|
+
if (selector != null && Object.keys(selector).length === 1 && 'id' in selector) {
|
|
348
|
+
const idResult = (0, idIndexQuery_1.default)(selector);
|
|
349
|
+
if (idResult.matched) {
|
|
350
|
+
return {
|
|
351
|
+
matched: true,
|
|
352
|
+
ids: idResult.ids.filter(Boolean),
|
|
353
|
+
optimizedSelector: {},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
355
356
|
}
|
|
356
357
|
if (selector == null) {
|
|
357
358
|
return {
|
|
@@ -361,45 +362,7 @@ class AsyncDataAdapter {
|
|
|
361
362
|
};
|
|
362
363
|
}
|
|
363
364
|
const indices = this.collectionIndices.get(collectionName) ?? [];
|
|
364
|
-
return (0, getIndexInfo_1.default)(indices.map(field =>
|
|
365
|
-
if (!Object.hasOwnProperty.call(flatSelector, field))
|
|
366
|
-
return { matched: false };
|
|
367
|
-
const index = await storageAdapter.readIndex(field);
|
|
368
|
-
const fieldSelector = flatSelector[field];
|
|
369
|
-
const filtersForNull = fieldSelector == null || fieldSelector.$exists === false;
|
|
370
|
-
const keys = filtersForNull
|
|
371
|
-
? { include: null, exclude: [...index.keys()].filter(key => key != null) }
|
|
372
|
-
: (0, getMatchingKeys_1.default)(field, flatSelector);
|
|
373
|
-
if (keys.include == null && keys.exclude == null)
|
|
374
|
-
return { matched: false };
|
|
375
|
-
// Build included ids
|
|
376
|
-
let includedIds = [];
|
|
377
|
-
if (keys.include == null) {
|
|
378
|
-
for (const set of index.values())
|
|
379
|
-
for (const pos of set)
|
|
380
|
-
includedIds.push(pos);
|
|
381
|
-
}
|
|
382
|
-
else {
|
|
383
|
-
for (const key of keys.include) {
|
|
384
|
-
const idSet = index.get(key);
|
|
385
|
-
if (idSet)
|
|
386
|
-
for (const id of idSet)
|
|
387
|
-
includedIds.push(id);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
// Apply exclusions
|
|
391
|
-
if (keys.exclude != null) {
|
|
392
|
-
const excludeIds = new Set();
|
|
393
|
-
for (const key of keys.exclude) {
|
|
394
|
-
const idSet = index.get(key);
|
|
395
|
-
if (idSet)
|
|
396
|
-
for (const id of idSet)
|
|
397
|
-
excludeIds.add(id);
|
|
398
|
-
}
|
|
399
|
-
includedIds = includedIds.filter(pos => !excludeIds.has(pos));
|
|
400
|
-
}
|
|
401
|
-
return { matched: true, ids: includedIds, fields: [field], keepSelector: filtersForNull };
|
|
402
|
-
}), selector);
|
|
365
|
+
return (0, getIndexInfo_1.default)(indices.map(field => (0, storageIndexQuery_1.default)(storageAdapter, field)), selector);
|
|
403
366
|
}
|
|
404
367
|
async queryItems(collectionName, selector) {
|
|
405
368
|
const storage = this.storageAdapters.get(collectionName);
|
|
@@ -9,7 +9,8 @@ const modify_1 = __importDefault(require("./utils/modify"));
|
|
|
9
9
|
const queryId_1 = __importDefault(require("./utils/queryId"));
|
|
10
10
|
const isEqual_1 = __importDefault(require("./utils/isEqual"));
|
|
11
11
|
const getIndexInfo_1 = __importDefault(require("./getIndexInfo"));
|
|
12
|
-
const
|
|
12
|
+
const storageIndexQuery_1 = __importDefault(require("./utils/storageIndexQuery"));
|
|
13
|
+
const idIndexQuery_1 = __importDefault(require("./utils/idIndexQuery"));
|
|
13
14
|
const sortItems_1 = __importDefault(require("./utils/sortItems"));
|
|
14
15
|
const project_1 = __importDefault(require("./utils/project"));
|
|
15
16
|
/**
|
|
@@ -388,58 +389,17 @@ class AutoFetchDataAdapter {
|
|
|
388
389
|
const storage = this.storageAdapters.get(collectionName);
|
|
389
390
|
if (!storage)
|
|
390
391
|
throw new Error(`No persistence adapter for collection ${collectionName}`);
|
|
391
|
-
//
|
|
392
|
-
if (selector != null && Object.keys(selector).length === 1 && 'id' in selector
|
|
393
|
-
|
|
392
|
+
// `id` needs no declared index — `readIds` is exactly that lookup.
|
|
393
|
+
if (selector != null && Object.keys(selector).length === 1 && 'id' in selector) {
|
|
394
|
+
const idResult = (0, idIndexQuery_1.default)(selector);
|
|
395
|
+
if (idResult.matched)
|
|
396
|
+
return { matched: true, ids: idResult.ids, optimizedSelector: {} };
|
|
394
397
|
}
|
|
395
398
|
if (selector == null) {
|
|
396
399
|
return { matched: false, ids: [], optimizedSelector: {} };
|
|
397
400
|
}
|
|
398
401
|
const indices = this.collectionIndices.get(collectionName) ?? [];
|
|
399
|
-
return (0, getIndexInfo_1.default)(indices.map(field =>
|
|
400
|
-
if (!Object.hasOwnProperty.call(flatSelector, field))
|
|
401
|
-
return { matched: false };
|
|
402
|
-
const index = await storage.readIndex(field);
|
|
403
|
-
const fieldSelector = flatSelector[field];
|
|
404
|
-
const filtersForNull = fieldSelector == null || fieldSelector.$exists === false;
|
|
405
|
-
const keys = filtersForNull
|
|
406
|
-
? { include: null, exclude: [...index.keys()].filter(key => key != null) }
|
|
407
|
-
: (0, getMatchingKeys_1.default)(field, flatSelector);
|
|
408
|
-
if (keys.include == null && keys.exclude == null)
|
|
409
|
-
return { matched: false };
|
|
410
|
-
// Build included ids
|
|
411
|
-
let includedIds = [];
|
|
412
|
-
if (keys.include == null) {
|
|
413
|
-
for (const set of index.values())
|
|
414
|
-
for (const pos of set)
|
|
415
|
-
includedIds.push(pos);
|
|
416
|
-
}
|
|
417
|
-
else {
|
|
418
|
-
for (const key of keys.include) {
|
|
419
|
-
const idSet = index.get(key);
|
|
420
|
-
if (idSet)
|
|
421
|
-
for (const id of idSet)
|
|
422
|
-
includedIds.push(id);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
// Apply exclusions
|
|
426
|
-
if (keys.exclude != null) {
|
|
427
|
-
const excludeIds = new Set();
|
|
428
|
-
for (const key of keys.exclude) {
|
|
429
|
-
const idSet = index.get(key);
|
|
430
|
-
if (idSet)
|
|
431
|
-
for (const id of idSet)
|
|
432
|
-
excludeIds.add(id);
|
|
433
|
-
}
|
|
434
|
-
includedIds = includedIds.filter(pos => !excludeIds.has(pos));
|
|
435
|
-
}
|
|
436
|
-
return {
|
|
437
|
-
matched: true,
|
|
438
|
-
ids: includedIds,
|
|
439
|
-
fields: [field],
|
|
440
|
-
keepSelector: filtersForNull,
|
|
441
|
-
};
|
|
442
|
-
}), selector);
|
|
402
|
+
return (0, getIndexInfo_1.default)(indices.map(field => (0, storageIndexQuery_1.default)(storage, field)), selector);
|
|
443
403
|
}
|
|
444
404
|
async queryItems(collectionName, selector) {
|
|
445
405
|
const storage = this.storageAdapters.get(collectionName);
|
|
@@ -15,6 +15,7 @@ const incrementalQueryUpdate_1 = __importDefault(require("./utils/incrementalQue
|
|
|
15
15
|
const queryDelta_1 = require("./utils/queryDelta");
|
|
16
16
|
const queryId_1 = __importDefault(require("./utils/queryId"));
|
|
17
17
|
const serializeValue_1 = __importDefault(require("./utils/serializeValue"));
|
|
18
|
+
const idIndexQuery_1 = __importDefault(require("./utils/idIndexQuery"));
|
|
18
19
|
const sortItems_1 = __importDefault(require("./utils/sortItems"));
|
|
19
20
|
/**
|
|
20
21
|
* Checks if there are any pending updates in the given changeset.
|
|
@@ -77,15 +78,16 @@ class DefaultDataAdapter {
|
|
|
77
78
|
});
|
|
78
79
|
}
|
|
79
80
|
getIndexInfo(collection, selector) {
|
|
80
|
-
if (selector != null
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
if (selector != null && Object.keys(selector).length === 1 && 'id' in selector) {
|
|
82
|
+
const idResult = (0, idIndexQuery_1.default)(selector);
|
|
83
|
+
if (idResult.matched) {
|
|
84
|
+
return {
|
|
85
|
+
// This adapter keys its in-memory map by `serializeValue(id)`.
|
|
86
|
+
matched: true,
|
|
87
|
+
ids: idResult.ids.map(id => (0, serializeValue_1.default)(id)),
|
|
88
|
+
optimizedSelector: {},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
89
91
|
}
|
|
90
92
|
if (selector == null) {
|
|
91
93
|
return {
|
|
@@ -44,10 +44,15 @@ export default class WorkerDataAdapter implements DataAdapter {
|
|
|
44
44
|
private execInBackground;
|
|
45
45
|
private queryItemsById;
|
|
46
46
|
private flattenPendingWrites;
|
|
47
|
+
private static pushPendingEntry;
|
|
48
|
+
private static dropPendingEntry;
|
|
49
|
+
private static writeFlatEntry;
|
|
47
50
|
private static providesFullItems;
|
|
48
51
|
private observableItems;
|
|
49
52
|
private observableItemsByIds;
|
|
50
53
|
private servedResult;
|
|
54
|
+
private advanceServedResult;
|
|
55
|
+
private changesetForIds;
|
|
51
56
|
private computeServedResult;
|
|
52
57
|
/**
|
|
53
58
|
* Registers a write's effect locally and notifies every active query it
|
|
@@ -62,6 +62,11 @@ class WorkerDataAdapter {
|
|
|
62
62
|
// own `queryUpdate` has already landed (it is posted before the write's
|
|
63
63
|
// response, and message order is preserved), on failure dropping it is the
|
|
64
64
|
// rollback.
|
|
65
|
+
//
|
|
66
|
+
// Held in three shapes, because each answers a different question cheaply and none of them
|
|
67
|
+
// answers all three: `writes` says which ids a settling write touched, `byId` says what an id
|
|
68
|
+
// looked like before the newest write touched it, and `flat` is the collapsed view every reader
|
|
69
|
+
// actually wants. They are maintained together, as writes arrive and settle.
|
|
65
70
|
pendingWrites = new Map();
|
|
66
71
|
pendingWriteSeq = 0;
|
|
67
72
|
// Bumped whenever a collection's pending writes change, in either direction. Anything derived
|
|
@@ -261,27 +266,51 @@ class WorkerDataAdapter {
|
|
|
261
266
|
}
|
|
262
267
|
// The pending writes of a collection collapsed into one upsert/delete view, newest write winning.
|
|
263
268
|
// `null` when there are none, which is the overwhelmingly common case and the one every caller
|
|
264
|
-
// below short-circuits on.
|
|
265
|
-
// a
|
|
269
|
+
// below short-circuits on. Maintained by the two helpers under it rather than rebuilt here, so
|
|
270
|
+
// this is a lookup whatever the size of the pending set — see `PendingWriteState`. The returned
|
|
271
|
+
// view is the live one: callers read it, never mutate it.
|
|
266
272
|
flattenPendingWrites(collectionName) {
|
|
267
|
-
const
|
|
268
|
-
if (!
|
|
273
|
+
const state = this.pendingWrites.get(collectionName);
|
|
274
|
+
if (!state || state.writes.size === 0)
|
|
269
275
|
return null;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
276
|
+
return state.flat;
|
|
277
|
+
}
|
|
278
|
+
// Records one id's contribution from the write being registered. `seq` only ever grows, so the
|
|
279
|
+
// new entry is by construction the newest one for this id and therefore the one that wins.
|
|
280
|
+
static pushPendingEntry(state, id, item, seq) {
|
|
281
|
+
const stack = state.byId.get(id);
|
|
282
|
+
if (stack)
|
|
283
|
+
stack.push({ seq, item });
|
|
284
|
+
else
|
|
285
|
+
state.byId.set(id, [{ seq, item }]);
|
|
286
|
+
WorkerDataAdapter.writeFlatEntry(state, id, item);
|
|
287
|
+
}
|
|
288
|
+
// Takes one id's contribution back out as its write settles, and restores whatever the write
|
|
289
|
+
// below it said — or removes the id entirely when that was the only one.
|
|
290
|
+
static dropPendingEntry(state, id, seq) {
|
|
291
|
+
const stack = state.byId.get(id);
|
|
292
|
+
if (!stack)
|
|
293
|
+
return;
|
|
294
|
+
const index = stack.findIndex(entry => entry.seq === seq);
|
|
295
|
+
if (index !== -1)
|
|
296
|
+
stack.splice(index, 1);
|
|
297
|
+
const top = stack.at(-1);
|
|
298
|
+
if (!top) {
|
|
299
|
+
state.byId.delete(id);
|
|
300
|
+
state.flat.upserts.delete(id);
|
|
301
|
+
state.flat.deletes.delete(id);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
WorkerDataAdapter.writeFlatEntry(state, id, top.item);
|
|
305
|
+
}
|
|
306
|
+
static writeFlatEntry(state, id, item) {
|
|
307
|
+
if (item === null) {
|
|
308
|
+
state.flat.deletes.add(id);
|
|
309
|
+
state.flat.upserts.delete(id);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
state.flat.upserts.set(id, item);
|
|
313
|
+
state.flat.deletes.delete(id);
|
|
285
314
|
}
|
|
286
315
|
// The items an active query currently holds, deduplicated by id, plus whatever the pending writes
|
|
287
316
|
// add or remove — the only items this adapter knows about, and the set a selector-based write is
|
|
@@ -355,10 +384,60 @@ class WorkerDataAdapter {
|
|
|
355
384
|
&& query.served.pendingVersion === pendingVersion) {
|
|
356
385
|
return query.served.items;
|
|
357
386
|
}
|
|
358
|
-
const items = this.
|
|
387
|
+
const items = this.advanceServedResult(collectionName, query, pendingVersion)
|
|
388
|
+
?? this.computeServedResult(collectionName, query);
|
|
359
389
|
query.served = { items, fromItems: query.items, pendingVersion };
|
|
360
390
|
return items;
|
|
361
391
|
}
|
|
392
|
+
// One step forward from the answer this query already had, instead of rebuilding it from every
|
|
393
|
+
// pending write. Only the ids the newest change touched are looked at, so a burst of writes costs
|
|
394
|
+
// each of them the size of that write — where rebuilding re-matched and re-projected the whole
|
|
395
|
+
// pending set per write, which is quadratic in the burst and was measured at twelve seconds for
|
|
396
|
+
// four thousand unsettled inserts against a single matching query.
|
|
397
|
+
//
|
|
398
|
+
// `null` means the step is not available and the caller has to rebuild: nothing to step from, a
|
|
399
|
+
// gap of more than one version, or a query shape whose result cannot be derived from itself — a
|
|
400
|
+
// projection cannot be re-matched against a selector naming fields it dropped, and a limited
|
|
401
|
+
// query is a window whose content can depend on rows it does not hold.
|
|
402
|
+
advanceServedResult(collectionName, query, pendingVersion) {
|
|
403
|
+
const served = query.served;
|
|
404
|
+
if (!served || served.fromItems !== query.items)
|
|
405
|
+
return null;
|
|
406
|
+
if (served.pendingVersion !== pendingVersion - 1)
|
|
407
|
+
return null;
|
|
408
|
+
if (!WorkerDataAdapter.providesFullItems(query) || query.options?.limit != null)
|
|
409
|
+
return null;
|
|
410
|
+
const state = this.pendingWrites.get(collectionName);
|
|
411
|
+
if (state?.lastChange == null || state.lastChange.version !== pendingVersion)
|
|
412
|
+
return null;
|
|
413
|
+
return (0, incrementalQueryUpdate_1.mergeChangesetIntoResult)(served.items, query.selector, query.options, this.changesetForIds(collectionName, query, state.lastChange.ids));
|
|
414
|
+
}
|
|
415
|
+
// What the named ids look like now — the pending value if one is still in flight for them, and
|
|
416
|
+
// otherwise whatever the last confirmed result said, which is what a settling write reverts to.
|
|
417
|
+
// The same rule answers a write arriving and a write settling, so both take the step above.
|
|
418
|
+
changesetForIds(collectionName, query, ids) {
|
|
419
|
+
const pending = this.flattenPendingWrites(collectionName);
|
|
420
|
+
const stored = this.queryItemsById(query);
|
|
421
|
+
const upserts = [];
|
|
422
|
+
const deletes = [];
|
|
423
|
+
ids.forEach((id) => {
|
|
424
|
+
const pendingItem = pending?.upserts.get(id);
|
|
425
|
+
if (pendingItem) {
|
|
426
|
+
upserts.push(pendingItem);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (pending?.deletes.has(id)) {
|
|
430
|
+
deletes.push(id);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const storedItem = stored.get(id);
|
|
434
|
+
if (storedItem)
|
|
435
|
+
upserts.push(storedItem);
|
|
436
|
+
else
|
|
437
|
+
deletes.push(id);
|
|
438
|
+
});
|
|
439
|
+
return { upserts, deletes };
|
|
440
|
+
}
|
|
362
441
|
computeServedResult(collectionName, query) {
|
|
363
442
|
const pending = this.flattenPendingWrites(collectionName);
|
|
364
443
|
if (!pending)
|
|
@@ -404,25 +483,49 @@ class WorkerDataAdapter {
|
|
|
404
483
|
// changed for a reader rather than just that something did.
|
|
405
484
|
const servedBefore = this.servedResults(collectionName, affected);
|
|
406
485
|
const seq = this.pendingWriteSeq += 1;
|
|
407
|
-
const
|
|
408
|
-
??
|
|
409
|
-
|
|
486
|
+
const state = this.pendingWrites.get(collectionName)
|
|
487
|
+
?? {
|
|
488
|
+
writes: new Map(),
|
|
489
|
+
byId: new Map(),
|
|
490
|
+
flat: { upserts: new Map(), deletes: new Set() },
|
|
491
|
+
lastChange: null,
|
|
492
|
+
};
|
|
493
|
+
const write = {
|
|
410
494
|
upserts: new Map(upserts.map(item => [item.id, item])),
|
|
411
495
|
deletes: new Set(deletes),
|
|
412
|
-
}
|
|
413
|
-
|
|
496
|
+
};
|
|
497
|
+
state.writes.set(seq, write);
|
|
498
|
+
// Upserts first, then deletes, so a write naming the same id in both ends as a delete — the
|
|
499
|
+
// order the collapsed view was folded in when it was still rebuilt from scratch.
|
|
500
|
+
write.upserts.forEach((item, id) => WorkerDataAdapter.pushPendingEntry(state, id, item, seq));
|
|
501
|
+
write.deletes.forEach(id => WorkerDataAdapter.pushPendingEntry(state, id, null, seq));
|
|
502
|
+
this.pendingWrites.set(collectionName, state);
|
|
414
503
|
this.bumpPendingWriteVersion(collectionName);
|
|
504
|
+
const addedVersion = this.pendingWriteVersions.get(collectionName) ?? 0;
|
|
505
|
+
state.lastChange = { version: addedVersion, ids: [...affectedIds] };
|
|
415
506
|
this.notifyWithDeltas(collectionName, affected, servedBefore);
|
|
416
507
|
return () => {
|
|
417
508
|
const current = this.pendingWrites.get(collectionName);
|
|
418
509
|
if (!current)
|
|
419
510
|
return;
|
|
511
|
+
const settled = current.writes.get(seq);
|
|
512
|
+
// Already dropped: settling twice must not take a *later* write's contribution back out.
|
|
513
|
+
if (!settled)
|
|
514
|
+
return;
|
|
420
515
|
const affectedOnDrop = this.affectedQueries(collectionName, upserts, affectedIds);
|
|
421
516
|
const beforeDrop = this.servedResults(collectionName, affectedOnDrop);
|
|
422
|
-
current.delete(seq);
|
|
423
|
-
|
|
517
|
+
current.writes.delete(seq);
|
|
518
|
+
settled.upserts.forEach((item, id) => WorkerDataAdapter.dropPendingEntry(current, id, seq));
|
|
519
|
+
settled.deletes.forEach(id => WorkerDataAdapter.dropPendingEntry(current, id, seq));
|
|
520
|
+
// With nothing left in flight the state is dropped entirely, and a query's served result is
|
|
521
|
+
// its stored one again — which `computeServedResult` answers without looking at anything.
|
|
522
|
+
if (current.writes.size === 0)
|
|
424
523
|
this.pendingWrites.delete(collectionName);
|
|
425
524
|
this.bumpPendingWriteVersion(collectionName);
|
|
525
|
+
if (current.writes.size > 0) {
|
|
526
|
+
const droppedVersion = this.pendingWriteVersions.get(collectionName) ?? 0;
|
|
527
|
+
current.lastChange = { version: droppedVersion, ids: [...affectedIds] };
|
|
528
|
+
}
|
|
426
529
|
// By the time a write settles the host's own answer has usually already landed, so dropping
|
|
427
530
|
// the optimistic copy changes nothing a reader can see and produces no notification at all.
|
|
428
531
|
this.notifyWithDeltas(collectionName, affectedOnDrop, beforeDrop);
|
|
@@ -9,7 +9,8 @@ const modify_1 = __importDefault(require("./utils/modify"));
|
|
|
9
9
|
const queryId_1 = __importDefault(require("./utils/queryId"));
|
|
10
10
|
const isEqual_1 = __importDefault(require("./utils/isEqual"));
|
|
11
11
|
const getIndexInfo_1 = __importDefault(require("./getIndexInfo"));
|
|
12
|
-
const
|
|
12
|
+
const idIndexQuery_1 = __importDefault(require("./utils/idIndexQuery"));
|
|
13
|
+
const storageIndexQuery_1 = __importDefault(require("./utils/storageIndexQuery"));
|
|
13
14
|
const sortItems_1 = __importDefault(require("./utils/sortItems"));
|
|
14
15
|
const projectItems_1 = __importDefault(require("./utils/projectItems"));
|
|
15
16
|
const compact_1 = __importDefault(require("./utils/compact"));
|
|
@@ -96,15 +97,18 @@ class WorkerDataAdapterHost {
|
|
|
96
97
|
const storageAdapter = this.storageAdapters.get(collectionName);
|
|
97
98
|
if (!storageAdapter)
|
|
98
99
|
throw new Error(`No persistence adapter for collection ${collectionName}`);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
// `id` needs no declared index: `readIds` is exactly the lookup it
|
|
101
|
+
// describes, so every inclusive form of it is answered here rather than by
|
|
102
|
+
// reading the whole collection (utils/idIndexQuery.ts).
|
|
103
|
+
if (selector != null && Object.keys(selector).length === 1 && 'id' in selector) {
|
|
104
|
+
const idResult = (0, idIndexQuery_1.default)(selector);
|
|
105
|
+
if (idResult.matched) {
|
|
106
|
+
return {
|
|
107
|
+
matched: true,
|
|
108
|
+
ids: (0, compact_1.default)(idResult.ids),
|
|
109
|
+
optimizedSelector: {},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
108
112
|
}
|
|
109
113
|
if (selector == null) {
|
|
110
114
|
return {
|
|
@@ -114,59 +118,9 @@ class WorkerDataAdapterHost {
|
|
|
114
118
|
};
|
|
115
119
|
}
|
|
116
120
|
const indices = this.collectionIndices.get(collectionName) ?? [];
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return { matched: false };
|
|
121
|
-
}
|
|
122
|
-
const index = await storageAdapter.readIndex(field);
|
|
123
|
-
const fieldSelector = flatSelector[field];
|
|
124
|
-
const filteresForNull = fieldSelector == null || fieldSelector.$exists === false;
|
|
125
|
-
const keys = filteresForNull
|
|
126
|
-
? { include: null, exclude: [...index.keys()].filter(key => key != null) }
|
|
127
|
-
: (0, getMatchingKeys_1.default)(field, flatSelector);
|
|
128
|
-
if (keys.include == null && keys.exclude == null)
|
|
129
|
-
return { matched: false };
|
|
130
|
-
// Accumulate included positions
|
|
131
|
-
let includedIds = [];
|
|
132
|
-
if (keys.include == null) {
|
|
133
|
-
for (const set of index.values()) {
|
|
134
|
-
for (const pos of set) {
|
|
135
|
-
includedIds.push(pos);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
for (const key of keys.include) {
|
|
141
|
-
const idSet = index.get(key);
|
|
142
|
-
if (idSet) {
|
|
143
|
-
for (const id of idSet) {
|
|
144
|
-
includedIds.push(id);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
// If exclusion is specified, build a single set of all positions to exclude.
|
|
150
|
-
if (keys.exclude != null) {
|
|
151
|
-
const excludeIds = new Set();
|
|
152
|
-
for (const key of keys.exclude) {
|
|
153
|
-
const idSet = index.get(key);
|
|
154
|
-
if (idSet) {
|
|
155
|
-
for (const id of idSet) {
|
|
156
|
-
excludeIds.add(id);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// Filter out any position that exists in the exclude set.
|
|
161
|
-
includedIds = includedIds.filter(pos => !excludeIds.has(pos));
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
matched: true,
|
|
165
|
-
ids: includedIds,
|
|
166
|
-
fields: [field],
|
|
167
|
-
keepSelector: filteresForNull,
|
|
168
|
-
};
|
|
169
|
-
}), selector);
|
|
121
|
+
// `id` first, and always present: it needs no declared index, because
|
|
122
|
+
// `readIds` is exactly the lookup it describes (utils/idIndexQuery.ts).
|
|
123
|
+
return (0, getIndexInfo_1.default)(indices.map(field => (0, storageIndexQuery_1.default)(storageAdapter, field)), selector);
|
|
170
124
|
}
|
|
171
125
|
async queryItems(collectionName, selector) {
|
|
172
126
|
const storageAdapter = this.storageAdapters.get(collectionName);
|
|
@@ -135,12 +135,24 @@ function getIndexInfo(queryFunctions, selector) {
|
|
|
135
135
|
let hasNonIndexField = false;
|
|
136
136
|
const matchedBefore = matched;
|
|
137
137
|
const idsBefore = ids;
|
|
138
|
+
// The branches of an $or union with each other, but the $or as a whole
|
|
139
|
+
// *intersects* with what the flat fields and $and already narrowed down
|
|
140
|
+
// to. Collecting the branches separately is what keeps those two apart —
|
|
141
|
+
// folding them into `ids` turns `{ a: 1, $or: [...] }` into a union and
|
|
142
|
+
// returns items matching neither half of the selector, with an empty
|
|
143
|
+
// optimized selector that leaves nothing to filter them out again.
|
|
144
|
+
let orIds = [];
|
|
145
|
+
let orMatched = false;
|
|
138
146
|
const process$or = ($orNew) => {
|
|
139
147
|
if (hasNonIndexField || ($orNew && $orNew.length > 0)) {
|
|
140
148
|
newSelector.$or = $or;
|
|
141
149
|
matched = matchedBefore;
|
|
142
150
|
ids = idsBefore;
|
|
143
151
|
}
|
|
152
|
+
else if (orMatched) {
|
|
153
|
+
ids = matchedBefore ? (0, intersection_1.default)(idsBefore, orIds) : orIds;
|
|
154
|
+
matched = true;
|
|
155
|
+
}
|
|
144
156
|
return {
|
|
145
157
|
matched,
|
|
146
158
|
ids: ids || [],
|
|
@@ -150,8 +162,8 @@ function getIndexInfo(queryFunctions, selector) {
|
|
|
150
162
|
const $orNewOrPromise = Array.isArray($or)
|
|
151
163
|
? optimizeLogicGate(queryFunctions, $or, (match, selIds) => {
|
|
152
164
|
if (match) {
|
|
153
|
-
|
|
154
|
-
|
|
165
|
+
orIds = [...new Set([...orIds, ...selIds])];
|
|
166
|
+
orMatched = true;
|
|
155
167
|
}
|
|
156
168
|
else {
|
|
157
169
|
hasNonIndexField = true;
|
|
@@ -12,7 +12,15 @@ export default interface StorageAdapter<T extends {
|
|
|
12
12
|
readIds(positions: I[]): Promise<T[]>;
|
|
13
13
|
createIndex(field: string): Promise<void>;
|
|
14
14
|
dropIndex(field: string): Promise<void>;
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The index, keyed by `serializeValue(value)` — not by the raw field value.
|
|
17
|
+
*
|
|
18
|
+
* SignalDB looks an index up with the serialized form, because that is what
|
|
19
|
+
* makes `3`, `'3'` and `new Date(...)` comparable as map keys at all. An
|
|
20
|
+
* adapter that stores its backend's own keys instead answers nothing for
|
|
21
|
+
* every non-string field, and everything for a `$ne` on one.
|
|
22
|
+
*/
|
|
23
|
+
readIndex(field: string): Promise<Map<string | null, Set<I>>>;
|
|
16
24
|
insert(items: T[]): Promise<void>;
|
|
17
25
|
replace(items: T[]): Promise<void>;
|
|
18
26
|
remove(items: T[]): Promise<void>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BaseItem } from '../Collection/types';
|
|
2
|
+
import type { IndexResult } from '../types/IndexProvider';
|
|
3
|
+
import type { FlatSelector } from '../types/Selector';
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a selector on `id` into the ids it names, without consulting an index.
|
|
6
|
+
*
|
|
7
|
+
* `id` is the one field every storage adapter can look up directly — that is what
|
|
8
|
+
* `readIds` is — so a query on it never needs an index to be declared and never
|
|
9
|
+
* needs the whole collection to be read. This behaves like an index provider that
|
|
10
|
+
* happens to need no stored index, because the ids are already in the selector.
|
|
11
|
+
*
|
|
12
|
+
* Only inclusive forms can be answered this way. `$ne`/`$nin` describe everything
|
|
13
|
+
* except* something, which cannot be enumerated without knowing every id, so they
|
|
14
|
+
* report no match and take the ordinary path.
|
|
15
|
+
* @template T - The type of the items in the collection.
|
|
16
|
+
* @template I - The type of the unique identifier for the items.
|
|
17
|
+
* @param selector - The flat selector to resolve.
|
|
18
|
+
* @returns An index result naming the matched ids, or `{ matched: false }`.
|
|
19
|
+
*/
|
|
20
|
+
export default function idIndexQuery<T extends BaseItem<I> = BaseItem, I = any>(selector: FlatSelector<T>): IndexResult<I>;
|
|
@@ -0,0 +1,49 @@
|
|
|
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 = idIndexQuery;
|
|
7
|
+
const isFieldExpression_1 = __importDefault(require("./isFieldExpression"));
|
|
8
|
+
/**
|
|
9
|
+
* Resolves a selector on `id` into the ids it names, without consulting an index.
|
|
10
|
+
*
|
|
11
|
+
* `id` is the one field every storage adapter can look up directly — that is what
|
|
12
|
+
* `readIds` is — so a query on it never needs an index to be declared and never
|
|
13
|
+
* needs the whole collection to be read. This behaves like an index provider that
|
|
14
|
+
* happens to need no stored index, because the ids are already in the selector.
|
|
15
|
+
*
|
|
16
|
+
* Only inclusive forms can be answered this way. `$ne`/`$nin` describe everything
|
|
17
|
+
* except* something, which cannot be enumerated without knowing every id, so they
|
|
18
|
+
* report no match and take the ordinary path.
|
|
19
|
+
* @template T - The type of the items in the collection.
|
|
20
|
+
* @template I - The type of the unique identifier for the items.
|
|
21
|
+
* @param selector - The flat selector to resolve.
|
|
22
|
+
* @returns An index result naming the matched ids, or `{ matched: false }`.
|
|
23
|
+
*/
|
|
24
|
+
function idIndexQuery(selector) {
|
|
25
|
+
if (selector == null || !Object.hasOwnProperty.call(selector, 'id'))
|
|
26
|
+
return { matched: false };
|
|
27
|
+
const fieldSelector = selector.id;
|
|
28
|
+
if (fieldSelector == null || fieldSelector instanceof RegExp)
|
|
29
|
+
return { matched: false };
|
|
30
|
+
if ((0, isFieldExpression_1.default)(fieldSelector)) {
|
|
31
|
+
const values = fieldSelector.$in;
|
|
32
|
+
if (!Array.isArray(values) || values.length <= 0)
|
|
33
|
+
return { matched: false };
|
|
34
|
+
return {
|
|
35
|
+
matched: true,
|
|
36
|
+
ids: values,
|
|
37
|
+
fields: ['id'],
|
|
38
|
+
keepSelector: false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (typeof fieldSelector === 'object')
|
|
42
|
+
return { matched: false };
|
|
43
|
+
return {
|
|
44
|
+
matched: true,
|
|
45
|
+
ids: [fieldSelector],
|
|
46
|
+
fields: ['id'],
|
|
47
|
+
keepSelector: false,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BaseItem } from '../Collection/types';
|
|
2
|
+
import type StorageAdapter from '../types/StorageAdapter';
|
|
3
|
+
import type { AsynchronousQueryFunction } from '../types/IndexProvider';
|
|
4
|
+
/**
|
|
5
|
+
* Builds the index provider a data adapter uses to narrow a selector down through
|
|
6
|
+
* a storage adapter's index.
|
|
7
|
+
*
|
|
8
|
+
* Every adapter that keeps its data in a `StorageAdapter` needs exactly this, and
|
|
9
|
+
* each of them used to carry its own copy — three transcriptions of one set of
|
|
10
|
+
* rules about null, `$exists`, inclusion and exclusion, which is how they drift
|
|
11
|
+
* apart without anyone noticing. The index is keyed by `serializeValue(value)`,
|
|
12
|
+
* which is what `getMatchingKeys` produces, so the two only agree while they stay
|
|
13
|
+
* in one place.
|
|
14
|
+
* @template T - The type of the items in the collection.
|
|
15
|
+
* @template I - The type of the unique identifier for the items.
|
|
16
|
+
* @param storage - The storage adapter holding the index.
|
|
17
|
+
* @param field - The indexed field this provider answers for.
|
|
18
|
+
* @returns A query function for `getIndexInfo`.
|
|
19
|
+
*/
|
|
20
|
+
export default function storageIndexQuery<T extends BaseItem<I>, I = any>(storage: Pick<StorageAdapter<T, I>, 'readIndex'>, field: string): AsynchronousQueryFunction<T, I>;
|
|
@@ -0,0 +1,75 @@
|
|
|
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 = storageIndexQuery;
|
|
7
|
+
const getMatchingKeys_1 = __importDefault(require("./getMatchingKeys"));
|
|
8
|
+
/**
|
|
9
|
+
* Builds the index provider a data adapter uses to narrow a selector down through
|
|
10
|
+
* a storage adapter's index.
|
|
11
|
+
*
|
|
12
|
+
* Every adapter that keeps its data in a `StorageAdapter` needs exactly this, and
|
|
13
|
+
* each of them used to carry its own copy — three transcriptions of one set of
|
|
14
|
+
* rules about null, `$exists`, inclusion and exclusion, which is how they drift
|
|
15
|
+
* apart without anyone noticing. The index is keyed by `serializeValue(value)`,
|
|
16
|
+
* which is what `getMatchingKeys` produces, so the two only agree while they stay
|
|
17
|
+
* in one place.
|
|
18
|
+
* @template T - The type of the items in the collection.
|
|
19
|
+
* @template I - The type of the unique identifier for the items.
|
|
20
|
+
* @param storage - The storage adapter holding the index.
|
|
21
|
+
* @param field - The indexed field this provider answers for.
|
|
22
|
+
* @returns A query function for `getIndexInfo`.
|
|
23
|
+
*/
|
|
24
|
+
function storageIndexQuery(storage, field) {
|
|
25
|
+
return async (flatSelector) => {
|
|
26
|
+
if (!Object.hasOwnProperty.call(flatSelector, field)) {
|
|
27
|
+
// The field is not in the selector, so this index says nothing about it.
|
|
28
|
+
return { matched: false };
|
|
29
|
+
}
|
|
30
|
+
const index = await storage.readIndex(field);
|
|
31
|
+
const fieldSelector = flatSelector[field];
|
|
32
|
+
// A query for null, or for the field being absent, is the one case the index
|
|
33
|
+
// cannot answer by naming keys — it is answered by naming every key it is
|
|
34
|
+
// *not*, and the selector has to stay for the matcher to confirm it.
|
|
35
|
+
const filtersForNull = fieldSelector == null || fieldSelector.$exists === false;
|
|
36
|
+
const keys = filtersForNull
|
|
37
|
+
? { include: null, exclude: [...index.keys()].filter(key => key != null) }
|
|
38
|
+
: (0, getMatchingKeys_1.default)(field, flatSelector);
|
|
39
|
+
if (keys.include == null && keys.exclude == null)
|
|
40
|
+
return { matched: false };
|
|
41
|
+
let includedIds = [];
|
|
42
|
+
if (keys.include == null) {
|
|
43
|
+
for (const set of index.values()) {
|
|
44
|
+
for (const id of set)
|
|
45
|
+
includedIds.push(id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
for (const key of keys.include) {
|
|
50
|
+
const idSet = index.get(key);
|
|
51
|
+
if (idSet) {
|
|
52
|
+
for (const id of idSet)
|
|
53
|
+
includedIds.push(id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (keys.exclude != null) {
|
|
58
|
+
const excludedIds = new Set();
|
|
59
|
+
for (const key of keys.exclude) {
|
|
60
|
+
const idSet = index.get(key);
|
|
61
|
+
if (idSet) {
|
|
62
|
+
for (const id of idSet)
|
|
63
|
+
excludedIds.add(id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
includedIds = includedIds.filter(id => !excludedIds.has(id));
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
matched: true,
|
|
70
|
+
ids: includedIds,
|
|
71
|
+
fields: [field],
|
|
72
|
+
keepSelector: filtersForNull,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
}
|
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.19",
|
|
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",
|