@dxos/echo-db 2.29.1 → 2.29.2-dev.f64f2a6f

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.
Files changed (84) hide show
  1. package/dist/src/api/database.d.ts +11 -7
  2. package/dist/src/api/database.d.ts.map +1 -1
  3. package/dist/src/api/database.js +20 -23
  4. package/dist/src/api/database.js.map +1 -1
  5. package/dist/src/api/database.test.js +13 -13
  6. package/dist/src/api/database.test.js.map +1 -1
  7. package/dist/src/api/item.d.ts.map +1 -1
  8. package/dist/src/api/item.js +1 -1
  9. package/dist/src/api/item.js.map +1 -1
  10. package/dist/src/api/result-set.d.ts.map +1 -1
  11. package/dist/src/api/result-set.js +1 -0
  12. package/dist/src/api/result-set.js.map +1 -1
  13. package/dist/src/api/selection/index.d.ts +5 -0
  14. package/dist/src/api/selection/index.d.ts.map +1 -0
  15. package/dist/src/api/selection/index.js +20 -0
  16. package/dist/src/api/selection/index.js.map +1 -0
  17. package/dist/src/api/selection/queries.d.ts +51 -0
  18. package/dist/src/api/selection/queries.d.ts.map +1 -0
  19. package/dist/src/api/selection/queries.js +70 -0
  20. package/dist/src/api/selection/queries.js.map +1 -0
  21. package/dist/src/api/selection/result.d.ts +50 -0
  22. package/dist/src/api/selection/result.d.ts.map +1 -0
  23. package/dist/src/api/selection/result.js +91 -0
  24. package/dist/src/api/selection/result.js.map +1 -0
  25. package/dist/src/api/selection/selection.d.ts +96 -0
  26. package/dist/src/api/selection/selection.d.ts.map +1 -0
  27. package/dist/src/api/selection/selection.js +164 -0
  28. package/dist/src/api/selection/selection.js.map +1 -0
  29. package/dist/src/api/{selection.test.d.ts → selection/selection.test.d.ts} +0 -0
  30. package/dist/src/api/selection/selection.test.d.ts.map +1 -0
  31. package/dist/src/api/{selection.test.js → selection/selection.test.js} +46 -44
  32. package/dist/src/api/selection/selection.test.js.map +1 -0
  33. package/dist/src/api/selection/util.d.ts +7 -0
  34. package/dist/src/api/selection/util.d.ts.map +1 -0
  35. package/dist/src/api/selection/util.js +25 -0
  36. package/dist/src/api/selection/util.js.map +1 -0
  37. package/dist/src/echo.test.js +20 -20
  38. package/dist/src/echo.test.js.map +1 -1
  39. package/dist/src/halo/contact-manager.js +2 -2
  40. package/dist/src/halo/contact-manager.js.map +1 -1
  41. package/dist/src/halo/preferences.js +6 -6
  42. package/dist/src/halo/preferences.js.map +1 -1
  43. package/dist/src/parties/party-core.test.js +3 -3
  44. package/dist/src/parties/party-core.test.js.map +1 -1
  45. package/dist/src/parties/party-factory.d.ts.map +1 -1
  46. package/dist/src/parties/party-factory.js +3 -3
  47. package/dist/src/parties/party-factory.js.map +1 -1
  48. package/dist/src/parties/party-internal.js +3 -3
  49. package/dist/src/parties/party-internal.js.map +1 -1
  50. package/dist/src/parties/party-manager.js +1 -1
  51. package/dist/src/parties/party-manager.js.map +1 -1
  52. package/dist/src/parties/party-manager.test.js +8 -10
  53. package/dist/src/parties/party-manager.test.js.map +1 -1
  54. package/dist/src/testing/testing-factories.d.ts +2 -2
  55. package/dist/src/testing/testing-factories.d.ts.map +1 -1
  56. package/dist/src/testing/testing-factories.js +1 -1
  57. package/dist/src/testing/testing-factories.js.map +1 -1
  58. package/dist/tsconfig.tsbuildinfo +1 -1
  59. package/package.json +27 -27
  60. package/src/api/database.test.ts +13 -13
  61. package/src/api/database.ts +28 -29
  62. package/src/api/item.ts +2 -2
  63. package/src/api/result-set.ts +1 -0
  64. package/src/api/selection/index.ts +8 -0
  65. package/src/api/selection/queries.ts +108 -0
  66. package/src/api/selection/result.ts +112 -0
  67. package/src/api/{selection.test.ts → selection/selection.test.ts} +50 -48
  68. package/src/api/{selection.ts → selection/selection.ts} +30 -231
  69. package/src/api/selection/util.ts +27 -0
  70. package/src/echo.test.ts +20 -20
  71. package/src/halo/contact-manager.ts +2 -2
  72. package/src/halo/preferences.ts +6 -6
  73. package/src/parties/party-core.test.ts +3 -3
  74. package/src/parties/party-factory.ts +3 -4
  75. package/src/parties/party-internal.ts +3 -3
  76. package/src/parties/party-manager.test.ts +8 -10
  77. package/src/parties/party-manager.ts +1 -1
  78. package/src/testing/testing-factories.ts +3 -3
  79. package/dist/src/api/selection.d.ts +0 -183
  80. package/dist/src/api/selection.d.ts.map +0 -1
  81. package/dist/src/api/selection.js +0 -308
  82. package/dist/src/api/selection.js.map +0 -1
  83. package/dist/src/api/selection.test.d.ts.map +0 -1
  84. package/dist/src/api/selection.test.js.map +0 -1
@@ -80,7 +80,7 @@ describe('Database', () => {
80
80
  expect(item.id).not.toBeUndefined();
81
81
  expect(item.model).toBeInstanceOf(ObjectModel);
82
82
 
83
- const result = database.select().query();
83
+ const result = database.select().exec();
84
84
  expect(result.expectOne()).toBeTruthy();
85
85
  });
86
86
 
@@ -111,7 +111,7 @@ describe('Database', () => {
111
111
  const parent = await database.createItem({ model: ObjectModel });
112
112
  const child = await database.createItem({ model: ObjectModel, parent: parent.id });
113
113
 
114
- const result = database.select().query();
114
+ const result = database.select().exec();
115
115
  expect(result.entities).toHaveLength(2);
116
116
  expect(result.entities).toEqual([parent, child]);
117
117
 
@@ -203,14 +203,14 @@ describe('Database', () => {
203
203
  const database = await setupBackend(modelFactory);
204
204
 
205
205
  {
206
- const waiting = database.waitForItem({ type: 'example:type.test' });
207
- const item = await database.createItem({ model: ObjectModel, type: 'example:type.test' });
206
+ const waiting = database.waitForItem({ type: 'example:type/test-1' });
207
+ const item = await database.createItem({ model: ObjectModel, type: 'example:type/test-1' });
208
208
  expect(await promiseTimeout(waiting, 100, new Error('timeout'))).toEqual(item);
209
209
  }
210
210
 
211
211
  {
212
- const item = await database.createItem({ model: ObjectModel, type: 'example:type.test-2' });
213
- const waiting = database.waitForItem({ type: 'example:type.test-2' });
212
+ const item = await database.createItem({ model: ObjectModel, type: 'example:type/test-2' });
213
+ const waiting = database.waitForItem({ type: 'example:type/test-2' });
214
214
  expect(await promiseTimeout(waiting, 100, new Error('timeout'))).toEqual(item);
215
215
  }
216
216
  });
@@ -223,7 +223,7 @@ describe('Database', () => {
223
223
  database.createItem({ model: TestListModel })
224
224
  ));
225
225
 
226
- const result = database.select().query();
226
+ const result = database.select().exec();
227
227
  const items = result.entities;
228
228
  expect(items).toHaveLength(10);
229
229
 
@@ -232,17 +232,17 @@ describe('Database', () => {
232
232
  await update;
233
233
 
234
234
  {
235
- const result = database.select().query();
235
+ const result = database.select().exec();
236
236
  expect(result.entities).toHaveLength(9);
237
237
  }
238
238
 
239
239
  {
240
- const result = database.select().query({ deleted: ItemFilterDeleted.SHOW_DELETED });
240
+ const result = database.select().exec({ deleted: ItemFilterDeleted.SHOW_DELETED });
241
241
  expect(result.entities).toHaveLength(10);
242
242
  }
243
243
 
244
244
  {
245
- const result = database.select().query({ deleted: ItemFilterDeleted.SHOW_DELETED_ONLY });
245
+ const result = database.select().exec({ deleted: ItemFilterDeleted.SHOW_DELETED_ONLY });
246
246
  expect(result.entities).toHaveLength(1);
247
247
  }
248
248
  });
@@ -255,7 +255,7 @@ describe('Database', () => {
255
255
  const item2 = await database.createItem({ model: ObjectModel });
256
256
 
257
257
  // 1. Create a query
258
- const query = database.select().query();
258
+ const query = database.select().exec();
259
259
  const update = query.update.waitForCount(1);
260
260
 
261
261
  // 2. Create a link
@@ -277,7 +277,7 @@ describe('Database', () => {
277
277
 
278
278
  const parentItem = await database.createItem({ model: ObjectModel });
279
279
 
280
- const query = database.select({ id: parentItem.id }).query();
280
+ const query = database.select({ id: parentItem.id }).exec();
281
281
  const update = query.update.waitForCount(1);
282
282
 
283
283
  const childItem = await database.createItem({
@@ -295,7 +295,7 @@ describe('Database', () => {
295
295
  const database = await setupBackend(modelFactory);
296
296
 
297
297
  await Promise.all(Array.from({ length: 8 }).map(() => database.createItem({ model: ObjectModel })));
298
- const { value } = database.reduce(0).call((items) => items.length).query();
298
+ const { value } = database.reduce(0).call((items) => items.length).exec();
299
299
  expect(value).toBe(8);
300
300
  });
301
301
  });
@@ -14,16 +14,16 @@ import { DatabaseBackend, DataServiceHost, ItemManager } from '../database';
14
14
  import { Entity } from './entity';
15
15
  import { Item } from './item';
16
16
  import { Link } from './link';
17
- import { RootFilter, Selection, createSelector } from './selection';
17
+ import { RootFilter, Selection, createSelection } from './selection';
18
18
 
19
- export interface ItemCreationOptions<M extends Model> {
19
+ export interface CreateItemOption<M extends Model> {
20
20
  model?: ModelConstructor<M>
21
21
  type?: ItemType
22
22
  parent?: ItemID
23
23
  props?: any // TODO(marik-d): Type this better. Rename properties?
24
24
  }
25
25
 
26
- export interface LinkCreationOptions<M extends Model, L extends Model, R extends Model> {
26
+ export interface CreateLinkOptions<M extends Model, L extends Model, R extends Model> {
27
27
  model?: ModelConstructor<M>
28
28
  type?: ItemType
29
29
  source: Item<L>
@@ -31,20 +31,19 @@ export interface LinkCreationOptions<M extends Model, L extends Model, R extends
31
31
  props?: any // TODO(marik-d): Type this better.
32
32
  }
33
33
 
34
- enum State {
35
- INITIAL = 'INITIAL',
36
- OPEN = 'OPEN',
34
+ export enum State {
35
+ NULL = 'NULL',
36
+ INITIALIZED = 'INITIALIZED',
37
37
  DESTROYED = 'DESTROYED',
38
38
  }
39
39
 
40
40
  /**
41
- * Represents a shared dataset containing queryable Items that are constructed from an ordered stream
42
- * of mutations.
41
+ * Represents a shared dataset containing queryable Items that are constructed from an ordered stream of mutations.
43
42
  */
44
43
  export class Database {
45
44
  private readonly _itemManager: ItemManager;
46
45
 
47
- private _state = State.INITIAL;
46
+ private _state = State.NULL;
48
47
 
49
48
  /**
50
49
  * Creates a new database instance. `database.initialize()` must be called afterwards to complete the initialization.
@@ -57,6 +56,10 @@ export class Database {
57
56
  this._itemManager = new ItemManager(this._modelFactory, memberKey, this._backend.getWriteStream());
58
57
  }
59
58
 
59
+ get state () {
60
+ return this._state;
61
+ }
62
+
60
63
  get isReadOnly () {
61
64
  return this._backend.isReadOnly;
62
65
  }
@@ -71,26 +74,26 @@ export class Database {
71
74
 
72
75
  /**
73
76
  * Fired immediately after any update in the entities.
74
- *
75
77
  * If the information about which entity got updated is not required prefer using `update`.
76
78
  */
79
+ // TODO(burdon): Unused?
77
80
  get entityUpdate (): Event<Entity<any>> {
78
81
  return this._itemManager.update;
79
82
  }
80
83
 
81
84
  @synchronized
82
85
  async initialize () {
83
- if (this._state !== State.INITIAL) {
86
+ if (this._state !== State.NULL) {
84
87
  throw new Error('Invalid state: database was already initialized.');
85
88
  }
86
89
 
87
90
  await this._backend.open(this._itemManager, this._modelFactory);
88
- this._state = State.OPEN;
91
+ this._state = State.INITIALIZED;
89
92
  }
90
93
 
91
94
  @synchronized
92
95
  async destroy () {
93
- if (this._state === State.DESTROYED || this._state === State.INITIAL) {
96
+ if (this._state === State.DESTROYED || this._state === State.NULL) {
94
97
  return;
95
98
  }
96
99
 
@@ -101,7 +104,7 @@ export class Database {
101
104
  /**
102
105
  * Creates a new item with the given queryable type and model.
103
106
  */
104
- async createItem <M extends Model<any>> (options: ItemCreationOptions<M>): Promise<Item<M>> {
107
+ async createItem <M extends Model<any>> (options: CreateItemOption<M> = {}): Promise<Item<M>> {
105
108
  this._assertInitialized();
106
109
  if (!options.model) {
107
110
  options.model = ObjectModel as any as ModelConstructor<M>;
@@ -109,22 +112,21 @@ export class Database {
109
112
 
110
113
  validateModelClass(options.model);
111
114
 
112
- if (options.type && typeof options.type !== 'string') {
115
+ if (options.type && typeof options.type !== 'string' as ItemType) {
113
116
  throw new TypeError('Invalid type.');
114
117
  }
115
118
 
116
- if (options.parent && typeof options.parent !== 'string') {
119
+ if (options.parent && typeof options.parent !== 'string' as ItemID) {
117
120
  throw new TypeError('Optional parent item id must be a string id of an existing item.');
118
121
  }
119
122
 
120
123
  // TODO(burdon): Get modelType from somewhere other than `ObjectModel.meta.type`.
121
- const item = await this._itemManager.createItem(
124
+ return await this._itemManager.createItem(
122
125
  options.model.meta.type, options.type, options.parent, options.props) as any;
123
- return item;
124
126
  }
125
127
 
126
128
  async createLink<M extends Model<any>, S extends Model<any>, T extends Model<any>> (
127
- options: LinkCreationOptions<M, S, T>
129
+ options: CreateLinkOptions<M, S, T>
128
130
  ): Promise<Link<M, S, T>> {
129
131
  this._assertInitialized();
130
132
 
@@ -135,15 +137,12 @@ export class Database {
135
137
 
136
138
  validateModelClass(model);
137
139
 
138
- if (options.type && typeof options.type !== 'string') {
140
+ if (options.type && typeof options.type !== 'string' as ItemType) {
139
141
  throw new TypeError('Invalid type.');
140
142
  }
141
143
 
142
- assert(options.source instanceof Item);
143
- assert(options.target instanceof Item);
144
-
145
- return this._itemManager
146
- .createLink(model.meta.type, options.type, options.source.id, options.target.id, options.props);
144
+ return this._itemManager.createLink(
145
+ model.meta.type, options.type, options.source.id, options.target.id, options.props);
147
146
  }
148
147
 
149
148
  /**
@@ -159,7 +158,7 @@ export class Database {
159
158
  * Waits for item matching the filter to be present and returns it.
160
159
  */
161
160
  async waitForItem<T extends Model<any>> (filter: RootFilter): Promise<Item<T>> {
162
- const result = this.select(filter).query();
161
+ const result = this.select(filter).exec();
163
162
  await result.update.waitForCondition(() => result.entities.length > 0);
164
163
  const item = result.expectOne();
165
164
  assert(item, 'Possible condition detected.');
@@ -171,7 +170,7 @@ export class Database {
171
170
  * @param filter
172
171
  */
173
172
  select (filter?: RootFilter): Selection<Item<any>> {
174
- return createSelector<void>(
173
+ return createSelection<void>(
175
174
  () => this._itemManager.items,
176
175
  () => this._itemManager.debouncedUpdate,
177
176
  this,
@@ -186,7 +185,7 @@ export class Database {
186
185
  * @param filter
187
186
  */
188
187
  reduce<R> (result: R, filter?: RootFilter): Selection<Item<any>, R> {
189
- return createSelector<R>(
188
+ return createSelection<R>(
190
189
  () => this._itemManager.items,
191
190
  () => this._itemManager.debouncedUpdate,
192
191
  this,
@@ -205,7 +204,7 @@ export class Database {
205
204
  }
206
205
 
207
206
  private _assertInitialized () {
208
- if (this._state !== State.OPEN) {
207
+ if (this._state !== State.INITIALIZED) {
209
208
  throw new Error('Database not initialized.');
210
209
  }
211
210
  }
package/src/api/item.ts CHANGED
@@ -10,7 +10,7 @@ import { Model, StateManager } from '@dxos/model-factory';
10
10
  import { ItemManager } from '../database';
11
11
  import { Entity } from './entity';
12
12
  import type { Link } from './link';
13
- import { Selection, createItemSelector } from './selection';
13
+ import { Selection, createItemSelection } from './selection';
14
14
 
15
15
  const log = debug('dxos:echo-db:item');
16
16
 
@@ -102,7 +102,7 @@ export class Item<M extends Model | null = Model> extends Entity<M> {
102
102
  * Returns a selection context, which can be used to traverse the object graph starting from this item.
103
103
  */
104
104
  select (): Selection<Item<any>> {
105
- return createItemSelector(this as Item, this._itemManager.debouncedUpdate, undefined);
105
+ return createItemSelection(this as Item, this._itemManager.debouncedUpdate, undefined);
106
106
  }
107
107
 
108
108
  /**
@@ -9,6 +9,7 @@ import { Event, ReadOnlyEvent } from '@dxos/async';
9
9
  /**
10
10
  * Reactive query results.
11
11
  */
12
+ // TODO(burdon): Replace with Selection?
12
13
  export class ResultSet<T> {
13
14
  private readonly _resultsUpdate = new Event<T[]>();
14
15
  private readonly _itemUpdate: ReadOnlyEvent;
@@ -0,0 +1,8 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ export * from './queries';
6
+ export * from './result';
7
+ export * from './selection';
8
+ export * from './util';
@@ -0,0 +1,108 @@
1
+ //
2
+ // Copyright 2020 DXOS.org
3
+ //
4
+
5
+ import { ItemID } from '@dxos/echo-protocol';
6
+
7
+ import { Entity } from '../entity';
8
+ import { Item } from '../item';
9
+ import { Link } from '../link';
10
+ import { coerceToId, OneOrMultiple, testOneOrMultiple } from './util';
11
+
12
+ //
13
+ // Types
14
+ //
15
+
16
+ export type ItemIdFilter = {
17
+ id: ItemID
18
+ }
19
+
20
+ export type ItemFilter = {
21
+ type?: OneOrMultiple<string>
22
+ parent?: ItemID | Item
23
+ }
24
+
25
+ export type LinkFilter = {
26
+ type?: OneOrMultiple<string>;
27
+ }
28
+
29
+ export type Predicate<T extends Entity> = (entity: T) => boolean;
30
+
31
+ export type RootFilter = ItemIdFilter | ItemFilter | Predicate<Item>
32
+
33
+ /**
34
+ * Visitor callback.
35
+ * The visitor is passed the current entities and result (accumulator),
36
+ * which may be modified and returned.
37
+ */
38
+ export type Callable<T extends Entity, R> = (entities: T[], result: R) => R
39
+
40
+ /**
41
+ * Controls how deleted items are filtered.
42
+ */
43
+ export enum ItemFilterDeleted {
44
+ /**
45
+ * Do not return deleted items. Default behaviour.
46
+ */
47
+ HIDE_DELETED = 0,
48
+ /**
49
+ * Return deleted and regular items.
50
+ */
51
+ SHOW_DELETED = 1,
52
+ /**
53
+ * Return only deleted items.
54
+ */
55
+ SHOW_DELETED_ONLY = 2
56
+ }
57
+
58
+ export type QueryOptions = {
59
+ /**
60
+ * Controls how deleted items are filtered.
61
+ */
62
+ deleted?: ItemFilterDeleted
63
+ }
64
+
65
+ //
66
+ // Filters
67
+ //
68
+
69
+ export const filterToPredicate = (filter: ItemFilter | ItemIdFilter | Predicate<any>): Predicate<any> => {
70
+ if (typeof filter === 'function') {
71
+ return filter;
72
+ }
73
+
74
+ return itemFilterToPredicate(filter);
75
+ };
76
+
77
+ export const itemFilterToPredicate = (filter: ItemFilter | ItemIdFilter): Predicate<Item> => {
78
+ if ('id' in filter) {
79
+ return item => item.id === filter.id;
80
+ } else {
81
+ return item =>
82
+ (!filter.type || testOneOrMultiple(filter.type, item.type)) &&
83
+ (!filter.parent || item.parent?.id === coerceToId(filter.parent));
84
+ }
85
+ };
86
+
87
+ export const linkFilterToPredicate = (filter: LinkFilter): Predicate<Link> => {
88
+ return link => (!filter.type || testOneOrMultiple(filter.type, link.type));
89
+ };
90
+
91
+ export const createQueryOptionsFilter = ({
92
+ deleted = ItemFilterDeleted.HIDE_DELETED
93
+ }: QueryOptions): Predicate<Entity> => {
94
+ return entity => {
95
+ if (entity.model === null) {
96
+ return false;
97
+ }
98
+
99
+ switch (deleted) {
100
+ case ItemFilterDeleted.HIDE_DELETED:
101
+ return !(entity instanceof Item) || !entity.deleted;
102
+ case ItemFilterDeleted.SHOW_DELETED:
103
+ return true;
104
+ case ItemFilterDeleted.SHOW_DELETED_ONLY:
105
+ return entity instanceof Item && entity.deleted;
106
+ }
107
+ };
108
+ };
@@ -0,0 +1,112 @@
1
+ //
2
+ // Copyright 2020 DXOS.org
3
+ //
4
+
5
+ import assert from 'assert';
6
+
7
+ import { Event } from '@dxos/async';
8
+
9
+ import { Database } from '../database';
10
+ import { Entity } from '../entity';
11
+ import { dedupe } from './util';
12
+
13
+ /**
14
+ * Represents where the selection has started.
15
+ */
16
+ export type SelectionRoot = Database | Entity
17
+
18
+ /**
19
+ * Returned from each stage of the visitor.
20
+ */
21
+ export type SelectionContext<T extends Entity, R> = [entities: T[], result?: R]
22
+
23
+ /**
24
+ * Query subscription.
25
+ * Represents a live-query (subscription) that can notify about future updates to the relevant subset of items.
26
+ */
27
+ export class SelectionResult<T extends Entity, R = any> {
28
+ /**
29
+ * Fired when there are updates in the selection.
30
+ * Only update that are relevant to the selection cause the update.
31
+ */
32
+ readonly update = new Event<SelectionResult<T>>(); // TODO(burdon): Result result object.
33
+
34
+ private _lastResult: SelectionContext<T, R> = [[]];
35
+
36
+ constructor (
37
+ private readonly _execute: () => SelectionContext<T, R>,
38
+ private readonly _update: Event<Entity[]>,
39
+ private readonly _root: SelectionRoot,
40
+ private readonly _reducer: boolean
41
+ ) {
42
+ this.refresh();
43
+
44
+ // Re-run if deps change.
45
+ this.update.addEffect(() => _update.on(currentEntities => {
46
+ const [previousEntities] = this._lastResult;
47
+ this.refresh();
48
+
49
+ // Filters mutation events only if selection (since we can't reason about deps of call methods).
50
+ const set = new Set([...previousEntities, ...this._lastResult![0]]);
51
+ if (this._reducer || currentEntities.some(entity => set.has(entity as any))) {
52
+ this.update.emit(this);
53
+ }
54
+ }));
55
+ }
56
+
57
+ toString () {
58
+ const [entities] = this._lastResult;
59
+ return `SelectionResult<${JSON.stringify({
60
+ entities: entities.length
61
+ })}>`;
62
+ }
63
+
64
+ /**
65
+ * Re-run query.
66
+ */
67
+ refresh () {
68
+ const [entities, result] = this._execute();
69
+ this._lastResult = [dedupe(entities), result];
70
+ return this;
71
+ }
72
+
73
+ /**
74
+ * The root of the selection. Either a database or an item. Must be a stable reference.
75
+ */
76
+ get root (): SelectionRoot {
77
+ return this._root;
78
+ }
79
+
80
+ /**
81
+ * Get the result of this selection.
82
+ */
83
+ get entities (): T[] {
84
+ if (!this._lastResult) {
85
+ this.refresh();
86
+ }
87
+
88
+ const [entities] = this._lastResult!;
89
+ return entities;
90
+ }
91
+
92
+ /**
93
+ * Returns the selection or reducer result.
94
+ */
95
+ get value (): R extends void ? T[] : R {
96
+ if (!this._lastResult) {
97
+ this.refresh();
98
+ }
99
+
100
+ const [entities, value] = this._lastResult!;
101
+ return (this._reducer ? value : entities) as any;
102
+ }
103
+
104
+ /**
105
+ * Return the first element if the set has exactly one element.
106
+ */
107
+ expectOne (): T {
108
+ const entities = this.entities;
109
+ assert(entities.length === 1, `Expected one result; got ${entities.length}`);
110
+ return entities[0];
111
+ }
112
+ }