@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.
@@ -11,8 +11,31 @@ const isEqual_1 = __importDefault(require("./utils/isEqual"));
11
11
  const getIndexInfo_1 = __importDefault(require("./getIndexInfo"));
12
12
  const getMatchingKeys_1 = __importDefault(require("./utils/getMatchingKeys"));
13
13
  const sortItems_1 = __importDefault(require("./utils/sortItems"));
14
- const project_1 = __importDefault(require("./utils/project"));
14
+ const projectItems_1 = __importDefault(require("./utils/projectItems"));
15
15
  const compact_1 = __importDefault(require("./utils/compact"));
16
+ const incrementalQueryUpdate_1 = __importDefault(require("./utils/incrementalQueryUpdate"));
17
+ const queryDelta_1 = require("./utils/queryDelta");
18
+ /**
19
+ * Turns the item states a write produced into the upsert/delete split a query update needs.
20
+ *
21
+ * The two lists are not symmetric: an item that is still there after the write is described by
22
+ * its new state, while an item that is gone — removed, or given a new id — is described by the id
23
+ * it used to have and nothing else. Mixing the states from before and after a write into one list
24
+ * loses exactly that distinction.
25
+ * @template T - The type of the items.
26
+ * @param previousItems - The items as they were before the write.
27
+ * @param modifiedItems - The items as they are after it.
28
+ * @returns The changeset describing the write.
29
+ */
30
+ function toChangeset(previousItems, modifiedItems) {
31
+ const modifiedIds = new Set(modifiedItems.map(item => item.id));
32
+ return {
33
+ upserts: modifiedItems,
34
+ deletes: previousItems
35
+ .map(item => item.id)
36
+ .filter(id => !modifiedIds.has(id)),
37
+ };
38
+ }
16
39
  class WorkerDataAdapterHost {
17
40
  workerContext;
18
41
  options;
@@ -180,15 +203,7 @@ class WorkerDataAdapterHost {
180
203
  const sorted = sort ? (0, sortItems_1.default)(items, sort) : items;
181
204
  const skipped = skip ? sorted.slice(skip) : sorted;
182
205
  const limited = limit ? skipped.slice(0, limit) : skipped;
183
- const idExcluded = fields && fields.id === 0;
184
- return limited.map((item) => {
185
- if (!fields)
186
- return item;
187
- return {
188
- ...idExcluded ? {} : { id: item.id },
189
- ...(0, project_1.default)(item, fields),
190
- };
191
- });
206
+ return (0, projectItems_1.default)(limited, fields);
192
207
  }
193
208
  ensureQuery(collectionName, selector, options) {
194
209
  const id = (0, queryId_1.default)(selector, options);
@@ -197,17 +212,33 @@ class WorkerDataAdapterHost {
197
212
  }
198
213
  let query = this.queries.get(collectionName)?.get(id);
199
214
  if (!query) {
200
- query = { selector, options };
215
+ query = { selector, options, items: null };
201
216
  this.queries.get(collectionName)?.set(id, query);
202
217
  }
203
218
  return query;
204
219
  }
205
- emitQueryUpdate(collectionName, selector, options, state, error, items) {
220
+ setQueryItems(query, items) {
221
+ query.items = items;
222
+ query.itemIds = undefined;
223
+ }
224
+ queryItemIds(query) {
225
+ if (!query.itemIds)
226
+ query.itemIds = new Set((query.items ?? []).map(item => item.id));
227
+ return query.itemIds;
228
+ }
229
+ emitQueryUpdate(collectionName, selector, options, state, error, items, delta) {
206
230
  const id = (0, queryId_1.default)(selector, options);
207
231
  const collectionQueries = this.queries.get(collectionName);
208
232
  if (!collectionQueries)
209
233
  throw new Error(`Collection ${collectionName} not initialized!`);
210
- this.respond(id, { collectionName, selector, options, state, error, items }, null, 'queryUpdate');
234
+ // `qid` is what the adapter routes on. It is derived from the same selector/options pair the
235
+ // adapter registered the query with, so sending it saves every recipient from re-deriving it.
236
+ //
237
+ // `items` and `delta` are alternatives: the first answer to a query carries the whole result
238
+ // because the recipient holds nothing yet, every answer after it carries only what changed.
239
+ // Everything in this message is structurally cloned on its way out of the worker, which is why
240
+ // it matters that editing one field of one row no longer costs a copy of the entire result.
241
+ this.respond(id, { collectionName, qid: id, selector, options, state, error, items, delta }, null, 'queryUpdate');
211
242
  }
212
243
  ensureStorageAdapter(name) {
213
244
  if (this.storageAdapters.has(name))
@@ -217,19 +248,49 @@ class WorkerDataAdapterHost {
217
248
  return; // no adapter returned
218
249
  this.storageAdapters.set(name, adapter);
219
250
  }
220
- async checkQueryUpdates(collectionName, items) {
251
+ async checkQueryUpdates(collectionName, changes) {
221
252
  const queries = this.queries.get(collectionName);
222
253
  if (!queries)
223
254
  throw new Error(`Collection ${collectionName} not initialized!`);
224
- const affectedQueries = [...queries.values()].filter(({ selector }) => items.some(item => (0, match_1.default)(item, selector))) ?? [];
255
+ if (changes.upserts.length === 0 && changes.deletes.length === 0)
256
+ return;
257
+ // A query is affected when the write produces something it should hold, or takes away
258
+ // something it already holds. The second half is what the write itself cannot tell us: an
259
+ // item that no longer matches, or that was removed outright, is invisible to the matcher.
260
+ const affectedQueries = [...queries.values()].filter((query) => {
261
+ const ids = this.queryItemIds(query);
262
+ if (changes.deletes.some(id => ids.has(id)))
263
+ return true;
264
+ return changes.upserts.some(item => ids.has(item.id) || (0, match_1.default)(item, query.selector));
265
+ });
225
266
  if (affectedQueries.length === 0)
226
267
  return; // no active queries affected
227
- affectedQueries.forEach(({ selector, options }) => {
268
+ await Promise.all(affectedQueries.map(async (query) => {
269
+ const { selector, options } = query;
270
+ const previous = query.items;
271
+ const incremental = previous == null
272
+ ? null
273
+ : (0, incrementalQueryUpdate_1.default)(previous, selector, options, changes);
274
+ if (incremental != null) {
275
+ // Answered from the previous result, without touching the store — so there is no window in
276
+ // which the query is stale, and nothing to announce with an `'active'` state either.
277
+ const delta = (0, queryDelta_1.diffQueryResults)(previous, incremental);
278
+ if ((0, queryDelta_1.isEmptyQueryDelta)(delta))
279
+ return;
280
+ this.setQueryItems(query, incremental);
281
+ this.emitQueryUpdate(collectionName, selector, options, 'complete', null, undefined, delta);
282
+ return;
283
+ }
228
284
  this.emitQueryUpdate(collectionName, selector, options, 'active', null);
229
- });
230
- await Promise.all(affectedQueries.map(async ({ selector, options }) => {
231
285
  const queryItems = await this.executeQuery(collectionName, selector, options);
232
- this.emitQueryUpdate(collectionName, selector, options, 'complete', null, queryItems);
286
+ if (previous == null) {
287
+ this.setQueryItems(query, queryItems);
288
+ this.emitQueryUpdate(collectionName, selector, options, 'complete', null, queryItems);
289
+ return;
290
+ }
291
+ const delta = (0, queryDelta_1.diffQueryResults)(previous, queryItems);
292
+ this.setQueryItems(query, queryItems);
293
+ this.emitQueryUpdate(collectionName, selector, options, 'complete', null, undefined, delta);
233
294
  }));
234
295
  }
235
296
  registerCollection = async (collectionName, indices) => {
@@ -251,8 +312,11 @@ class WorkerDataAdapterHost {
251
312
  this.queries.delete(collectionName);
252
313
  };
253
314
  registerQuery = async (collectionName, selector, options) => {
254
- this.ensureQuery(collectionName, selector, options);
315
+ const query = this.ensureQuery(collectionName, selector, options);
255
316
  const queryItems = await this.executeQuery(collectionName, selector, options);
317
+ // Always the full result, even for a query that is already registered: whoever is registering
318
+ // holds nothing for it yet, and a delta would be relative to a result only the host has seen.
319
+ this.setQueryItems(query, queryItems);
256
320
  this.emitQueryUpdate(collectionName, selector, options, 'complete', null, queryItems);
257
321
  };
258
322
  unregisterQuery = async (collectionName, selector, options) => {
@@ -276,7 +340,7 @@ class WorkerDataAdapterHost {
276
340
  });
277
341
  const newItems = result.filter(item => !(item instanceof Error));
278
342
  await storageAdapter.insert(newItems);
279
- await this.checkQueryUpdates(collectionName, newItems);
343
+ await this.checkQueryUpdates(collectionName, { upserts: newItems, deletes: [] });
280
344
  return result;
281
345
  };
282
346
  updateOne = async (collectionName, parameters) => {
@@ -302,7 +366,7 @@ class WorkerDataAdapterHost {
302
366
  const modifiedItems = (0, compact_1.default)(result.filter(item => !(item instanceof Error)).flat());
303
367
  if (modifiedItems.length > 0) {
304
368
  await storageAdapter.replace(modifiedItems);
305
- await this.checkQueryUpdates(collectionName, [...modifiedItems, ...previousItems]);
369
+ await this.checkQueryUpdates(collectionName, toChangeset(previousItems, modifiedItems));
306
370
  }
307
371
  return result;
308
372
  };
@@ -337,7 +401,7 @@ class WorkerDataAdapterHost {
337
401
  const modifiedItems = (0, compact_1.default)(result.filter(item => !(item instanceof Error)).flat());
338
402
  if (modifiedItems.length > 0) {
339
403
  await storageAdapter.replace(modifiedItems);
340
- await this.checkQueryUpdates(collectionName, [...modifiedItems, ...previousItems]);
404
+ await this.checkQueryUpdates(collectionName, toChangeset(previousItems, modifiedItems));
341
405
  }
342
406
  return result;
343
407
  };
@@ -366,7 +430,7 @@ class WorkerDataAdapterHost {
366
430
  const modifiedItems = (0, compact_1.default)(result.filter(item => !(item instanceof Error)).flat());
367
431
  if (modifiedItems.length > 0) {
368
432
  await storageAdapter.replace(modifiedItems);
369
- await this.checkQueryUpdates(collectionName, [...modifiedItems, ...previousItems]);
433
+ await this.checkQueryUpdates(collectionName, toChangeset(previousItems, modifiedItems));
370
434
  }
371
435
  return result;
372
436
  };
@@ -383,7 +447,10 @@ class WorkerDataAdapterHost {
383
447
  const items = result.flat();
384
448
  if (items.length > 0) {
385
449
  await storageAdapter.remove(items);
386
- await this.checkQueryUpdates(collectionName, items);
450
+ await this.checkQueryUpdates(collectionName, {
451
+ upserts: [],
452
+ deletes: items.map(item => item.id),
453
+ });
387
454
  }
388
455
  return result;
389
456
  };
@@ -395,7 +462,10 @@ class WorkerDataAdapterHost {
395
462
  const items = result.flat();
396
463
  if (items.length > 0) {
397
464
  await storageAdapter.remove(items);
398
- await this.checkQueryUpdates(collectionName, items);
465
+ await this.checkQueryUpdates(collectionName, {
466
+ upserts: [],
467
+ deletes: items.map(item => item.id),
468
+ });
399
469
  }
400
470
  return result;
401
471
  };
@@ -4,6 +4,8 @@ export type { default as Selector } from './types/Selector';
4
4
  export type { default as Modifier } from './types/Modifier';
5
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
+ export type { CollectionBackend, QueryOptions, StateChangeCallback, } from './DataAdapter';
8
+ export type { QueryDelta } from './utils/queryDelta';
7
9
  export { default as Cursor } from './Collection/Cursor';
8
10
  export { default as Collection } from './Collection';
9
11
  export { default as createStorageAdapter } from './createStorageAdapter';
@@ -0,0 +1,60 @@
1
+ import type { QueryOptions } from '../DataAdapter';
2
+ import type { BaseItem } from '../Collection/types';
3
+ import type Selector from '../types/Selector';
4
+ /**
5
+ * The items a write created, updated or removed, from the point of view of a store.
6
+ *
7
+ * `upserts` are the *current* state of every item that still exists; `deletes` are the ids of items
8
+ * that no longer do. A write that changes an item's id contributes to both. Callers are responsible
9
+ * for that split — an "affected items" list holding an item's state from before and after a write
10
+ * cannot be told apart from two unrelated items here.
11
+ */
12
+ export interface QueryChangeset<T extends BaseItem> {
13
+ upserts: T[];
14
+ deletes: any[];
15
+ }
16
+ /**
17
+ * Recomputes a query's result from its previous result and the change that was just written,
18
+ * without going back to the store.
19
+ *
20
+ * A store re-executing the query instead reads every item it holds (or every item an index points
21
+ * at) and filters, sorts and projects the lot — for a write that touched one row. This does the
22
+ * same job in the size of the write, which is what a query's result costs to keep up to date when
23
+ * the change that affects it is already in hand.
24
+ *
25
+ * Returns `null` when the previous result is not enough to answer, and the caller has to re-execute
26
+ * the query after all:
27
+ * - `limit` or `skip`: the result is a window onto a larger set, and an item leaving the window has
28
+ * to be replaced by one the previous result never contained.
29
+ * - `fields` together with `sort`: the previous items are projected, so the field the sort is keyed
30
+ * on may no longer be there to sort by.
31
+ * - a `null` selector, which matches nothing and is not worth a special case.
32
+ * @template T - The type of the items.
33
+ * @param previous - The query's previous result.
34
+ * @param selector - The query's selector.
35
+ * @param options - The query's options.
36
+ * @param changes - The items the write created, updated or removed.
37
+ * @returns The new result, or `null` when the query has to be re-executed.
38
+ */
39
+ export default function incrementalQueryUpdate<T extends BaseItem>(previous: T[], selector: Selector<T>, options: QueryOptions<T> | undefined, changes: QueryChangeset<T>): T[] | null;
40
+ /**
41
+ * Folds a change into a query's result, whatever the query's options.
42
+ *
43
+ * The unguarded version of `incrementalQueryUpdate`, for the places where the alternative is not a
44
+ * more accurate answer but a wrong one — layering a write that has not been confirmed yet on top of
45
+ * the last confirmed result, say. For a query returning everything it matches, this is exact. For a
46
+ * window onto a larger set it is the closest the window itself can get: an item that no longer
47
+ * belongs is dropped, one that does is placed, and the window is trimmed back to its length — but
48
+ * an item pulled in from beyond the window is not something the window knows about.
49
+ *
50
+ * What it never does is re-examine the items already in the result. They matched when the store
51
+ * produced them, they still match, and asking again is both wasteful and — for a projected result,
52
+ * whose items no longer carry the fields the selector names — wrong.
53
+ * @template T - The type of the items.
54
+ * @param previous - The query's previous result.
55
+ * @param selector - The query's selector.
56
+ * @param options - The query's options.
57
+ * @param changes - The items the write created, updated or removed.
58
+ * @returns The resulting items.
59
+ */
60
+ export declare function mergeChangesetIntoResult<T extends BaseItem>(previous: T[], selector: Selector<T>, options: QueryOptions<T> | undefined, changes: QueryChangeset<T>): T[];
@@ -0,0 +1,159 @@
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 = incrementalQueryUpdate;
7
+ exports.mergeChangesetIntoResult = mergeChangesetIntoResult;
8
+ const match_1 = __importDefault(require("./match"));
9
+ const projectItems_1 = __importDefault(require("./projectItems"));
10
+ const sortItems_1 = __importDefault(require("./sortItems"));
11
+ /**
12
+ * Recomputes a query's result from its previous result and the change that was just written,
13
+ * without going back to the store.
14
+ *
15
+ * A store re-executing the query instead reads every item it holds (or every item an index points
16
+ * at) and filters, sorts and projects the lot — for a write that touched one row. This does the
17
+ * same job in the size of the write, which is what a query's result costs to keep up to date when
18
+ * the change that affects it is already in hand.
19
+ *
20
+ * Returns `null` when the previous result is not enough to answer, and the caller has to re-execute
21
+ * the query after all:
22
+ * - `limit` or `skip`: the result is a window onto a larger set, and an item leaving the window has
23
+ * to be replaced by one the previous result never contained.
24
+ * - `fields` together with `sort`: the previous items are projected, so the field the sort is keyed
25
+ * on may no longer be there to sort by.
26
+ * - a `null` selector, which matches nothing and is not worth a special case.
27
+ * @template T - The type of the items.
28
+ * @param previous - The query's previous result.
29
+ * @param selector - The query's selector.
30
+ * @param options - The query's options.
31
+ * @param changes - The items the write created, updated or removed.
32
+ * @returns The new result, or `null` when the query has to be re-executed.
33
+ */
34
+ function incrementalQueryUpdate(previous, selector, options, changes) {
35
+ if (selector == null)
36
+ return null;
37
+ const { sort, skip, limit, fields } = options || {};
38
+ if (skip != null)
39
+ return null;
40
+ if (fields != null && sort != null)
41
+ return null;
42
+ if (limit != null && !windowStaysClosed(previous, selector, options, changes))
43
+ return null;
44
+ return mergeChangesetIntoResult(previous, selector, options, changes);
45
+ }
46
+ /**
47
+ * Whether two items are in the given order under the given sort, deciding ties against the caller.
48
+ *
49
+ * Uses the sort itself rather than a comparator of its own: a rule about which side of a window an
50
+ * item falls on is only as good as its agreement with the ordering that drew the window. A tie
51
+ * comes back as `false`, because a tie is exactly the case where an item could belong on either
52
+ * side and the answer has to be taken from the store.
53
+ * @template T - The type of the items.
54
+ * @param item - The item whose position is in question.
55
+ * @param edge - The item at the edge of the window.
56
+ * @param sort - The query's sort.
57
+ * @returns `true` when `item` sorts strictly before `edge`.
58
+ */
59
+ function sortsBefore(item, edge, sort) {
60
+ return (0, sortItems_1.default)([edge, item], sort)[0] === item;
61
+ }
62
+ /**
63
+ * Whether a change to a windowed query can be answered from the window alone.
64
+ *
65
+ * A window holds the first `limit` items in sort order, and nothing about what lies beyond it. An
66
+ * item leaving the window therefore has to be replaced by one the window has never seen, and that
67
+ * answer can only come from the store. An item arriving is a different matter: it takes its place
68
+ * and pushes the last one out, and where that one goes is not the window's problem.
69
+ *
70
+ * The one case where none of this applies is a window that was never full, because then the query
71
+ * already returns everything it matches and there is no "beyond".
72
+ * @template T - The type of the items.
73
+ * @param previous - The query's previous result.
74
+ * @param selector - The query's selector.
75
+ * @param options - The query's options.
76
+ * @param changes - The items the write created, updated or removed.
77
+ * @returns `true` when the new window follows from the old one and the change.
78
+ */
79
+ function windowStaysClosed(previous, selector, options, changes) {
80
+ const { sort, limit, fields } = options || {};
81
+ if (limit == null || previous.length < limit)
82
+ return true;
83
+ // A full window needs an edge to compare against, and comparing needs the field the sort is on
84
+ // to still be there.
85
+ if (sort == null || fields != null)
86
+ return false;
87
+ const edge = previous.at(-1);
88
+ const runnerUp = previous.at(-2);
89
+ // Which item is *the* edge has to be beyond doubt. Two items sorting equally at the end of the
90
+ // window are interchangeable, and so is the question of which of them a write displaces.
91
+ if (runnerUp != null && !sortsBefore(runnerUp, edge, sort))
92
+ return false;
93
+ const inWindow = new Set(previous.map(item => item.id));
94
+ if (changes.deletes.some(id => inWindow.has(id)))
95
+ return false;
96
+ return changes.upserts.every((item) => {
97
+ if (item === edge)
98
+ return true;
99
+ const before = sortsBefore(item, edge, sort);
100
+ if (!inWindow.has(item.id)) {
101
+ // Coming from outside: it either takes a place inside, displacing the edge, or stays where
102
+ // it was. Sorting *equally* to the edge is the one answer the window cannot give, because
103
+ // the store may just as well have kept the edge and left this one out.
104
+ if (!(0, match_1.default)(item, selector))
105
+ return true;
106
+ return before || sortsBefore(edge, item, sort);
107
+ }
108
+ // An item that no longer matches leaves a place open, and what fills it is beyond the window.
109
+ if (!(0, match_1.default)(item, selector))
110
+ return false;
111
+ if (before)
112
+ return true;
113
+ // Not before the edge. The one item that may sit *on* the edge is the edge itself, and only
114
+ // while it has not actually moved — an edge that slides outwards gives up the last place, and
115
+ // what takes it is something the window has never seen.
116
+ return item.id === edge.id && !sortsBefore(edge, item, sort);
117
+ });
118
+ }
119
+ /**
120
+ * Folds a change into a query's result, whatever the query's options.
121
+ *
122
+ * The unguarded version of `incrementalQueryUpdate`, for the places where the alternative is not a
123
+ * more accurate answer but a wrong one — layering a write that has not been confirmed yet on top of
124
+ * the last confirmed result, say. For a query returning everything it matches, this is exact. For a
125
+ * window onto a larger set it is the closest the window itself can get: an item that no longer
126
+ * belongs is dropped, one that does is placed, and the window is trimmed back to its length — but
127
+ * an item pulled in from beyond the window is not something the window knows about.
128
+ *
129
+ * What it never does is re-examine the items already in the result. They matched when the store
130
+ * produced them, they still match, and asking again is both wasteful and — for a projected result,
131
+ * whose items no longer carry the fields the selector names — wrong.
132
+ * @template T - The type of the items.
133
+ * @param previous - The query's previous result.
134
+ * @param selector - The query's selector.
135
+ * @param options - The query's options.
136
+ * @param changes - The items the write created, updated or removed.
137
+ * @returns The resulting items.
138
+ */
139
+ function mergeChangesetIntoResult(previous, selector, options, changes) {
140
+ if (selector == null)
141
+ return [];
142
+ const { sort, limit, fields } = options || {};
143
+ const byId = new Map();
144
+ previous.forEach(item => byId.set(item.id, item));
145
+ changes.deletes.forEach(id => byId.delete(id));
146
+ changes.upserts.forEach((item) => {
147
+ // Matched against the unprojected item: the selector is free to name fields the projection
148
+ // drops, and the stored result would have no answer for those.
149
+ if ((0, match_1.default)(item, selector)) {
150
+ byId.set(item.id, (0, projectItems_1.default)([item], fields)[0]);
151
+ }
152
+ else {
153
+ byId.delete(item.id);
154
+ }
155
+ });
156
+ const items = [...byId.values()];
157
+ const sorted = sort ? (0, sortItems_1.default)(items, sort) : items;
158
+ return limit == null ? sorted : sorted.slice(0, limit);
159
+ }
@@ -0,0 +1,12 @@
1
+ import type { QueryOptions } from '../DataAdapter';
2
+ import type { BaseItem } from '../Collection/types';
3
+ /**
4
+ * Applies a query's field projection to a list of items, keeping the primary key unless the
5
+ * projection excludes it outright. Returns the items untouched when there is no projection, so a
6
+ * caller does not have to check for one first.
7
+ * @template T - The type of the items.
8
+ * @param items - The items to project.
9
+ * @param fields - The projection, or `undefined` for none.
10
+ * @returns The projected items.
11
+ */
12
+ export default function projectItems<T extends BaseItem>(items: T[], fields: QueryOptions<T>['fields']): T[];
@@ -0,0 +1,25 @@
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 = projectItems;
7
+ const project_1 = __importDefault(require("./project"));
8
+ /**
9
+ * Applies a query's field projection to a list of items, keeping the primary key unless the
10
+ * projection excludes it outright. Returns the items untouched when there is no projection, so a
11
+ * caller does not have to check for one first.
12
+ * @template T - The type of the items.
13
+ * @param items - The items to project.
14
+ * @param fields - The projection, or `undefined` for none.
15
+ * @returns The projected items.
16
+ */
17
+ function projectItems(items, fields) {
18
+ if (!fields)
19
+ return items;
20
+ const idExcluded = fields.id === 0;
21
+ return items.map(item => ({
22
+ ...idExcluded ? {} : { id: item.id },
23
+ ...(0, project_1.default)(item, fields),
24
+ }));
25
+ }
@@ -0,0 +1,83 @@
1
+ import type { BaseItem } from '../Collection/types';
2
+ /**
3
+ * The change between two consecutive results of the same query, expressed so it can be applied to
4
+ * the earlier result to obtain the later one.
5
+ *
6
+ * Indices in `added` and `moved` are positions in the *resulting* array and are always ascending,
7
+ * which is what makes applying them a matter of splicing in order. Removals and moves name items by
8
+ * id only: whoever applies the delta still holds the previous result and can look the item up
9
+ * there, so there is no reason to send it twice — the point of the whole exercise is that a change
10
+ * costs the size of the change, not the size of the result.
11
+ */
12
+ export interface QueryDelta<T extends BaseItem = BaseItem> {
13
+ /** Items that were not in the previous result, at their position in the new one. */
14
+ added: {
15
+ index: number;
16
+ item: T;
17
+ }[];
18
+ /** Items that were in the previous result and whose contents changed. */
19
+ changed: T[];
20
+ /** Ids of items that are no longer in the result. */
21
+ removed: any[];
22
+ /** Items that stayed, at their new position, because the order around them changed. */
23
+ moved: {
24
+ index: number;
25
+ id: any;
26
+ }[];
27
+ /** Length of the resulting array — lets a recipient verify it applied the delta to the result it was computed against. */
28
+ resultCount: number;
29
+ }
30
+ /**
31
+ * Checks whether a delta leaves the result it is applied to unchanged.
32
+ * @param delta - The delta to inspect.
33
+ * @returns `true` when applying the delta would be a no-op.
34
+ */
35
+ export declare function isEmptyQueryDelta(delta: QueryDelta<any>): boolean;
36
+ /**
37
+ * Calls a state-change callback, passing the delta only when there is one.
38
+ *
39
+ * A callback invoked as `callback(state, undefined)` has been handed two arguments, which is a
40
+ * different thing from being handed one — visible to anything that inspects arity, and to any test
41
+ * that asserts on the call.
42
+ * @template T - The type of the items.
43
+ * @param callback - The callback to invoke.
44
+ * @param state - The state to report.
45
+ * @param delta - The delta to report, if there is one.
46
+ */
47
+ export declare function callWithDelta<T extends BaseItem>(callback: (state: 'active' | 'complete' | 'error', delta?: QueryDelta<T>) => void, state: 'active' | 'complete' | 'error', delta?: QueryDelta<T>): void;
48
+ /**
49
+ * Checks whether a delta describes a change to the given result.
50
+ *
51
+ * A delta is only meaningful against the exact result it was computed from — it names positions in
52
+ * an array and items by id alone. Applying one to anything else produces a result that looks
53
+ * plausible and is wrong, and from then on every further delta compounds the error. This is the
54
+ * cheap structural check that catches that: every id the delta expects to find is there, every id
55
+ * it expects to be new is not, and the arithmetic on the length works out. It costs the size of the
56
+ * delta, not the size of the result.
57
+ * @template T - The type of the items.
58
+ * @param previous - The result the delta would be applied to.
59
+ * @param delta - The delta to check.
60
+ * @returns `true` when the delta can be applied.
61
+ */
62
+ export declare function canApplyQueryDelta<T extends BaseItem>(previous: T[], delta: QueryDelta<T>): boolean;
63
+ /**
64
+ * Computes the delta between two results of the same query.
65
+ *
66
+ * A fallback for the cases where the change that produced the new result is not available — a query
67
+ * that had to be re-executed in full, for instance. It costs a pass over both results, but it is
68
+ * paid once, on the side that has both of them, instead of shipping the entire new result to
69
+ * everyone who only needs to know what changed.
70
+ * @template T - The type of the items.
71
+ * @param previous - The result the delta should be relative to.
72
+ * @param next - The result the delta should produce.
73
+ * @returns The delta between the two results.
74
+ */
75
+ export declare function diffQueryResults<T extends BaseItem>(previous: T[], next: T[]): QueryDelta<T>;
76
+ /**
77
+ * Applies a delta to the result it was computed against.
78
+ * @template T - The type of the items.
79
+ * @param previous - The result the delta is relative to. Not modified.
80
+ * @param delta - The delta to apply.
81
+ * @returns The resulting items.
82
+ */
83
+ export declare function applyQueryDelta<T extends BaseItem>(previous: T[], delta: QueryDelta<T>): T[];