@dxos/echo-atom 0.8.4-main.ef1bc66f44 → 0.8.4-main.fcfe5033a5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/echo-atom",
3
- "version": "0.8.4-main.ef1bc66f44",
3
+ "version": "0.8.4-main.fcfe5033a5",
4
4
  "description": "Effect Atom wrappers for ECHO objects with explicit subscriptions.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -29,16 +29,19 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "@effect-atom/atom": "^0.5.1",
32
- "lodash.isequal": "^4.5.0",
33
- "@dxos/echo": "0.8.4-main.ef1bc66f44",
34
- "@dxos/invariant": "0.8.4-main.ef1bc66f44",
35
- "@dxos/echo-db": "0.8.4-main.ef1bc66f44",
36
- "@dxos/util": "0.8.4-main.ef1bc66f44"
32
+ "@dxos/echo-db": "0.8.4-main.fcfe5033a5",
33
+ "@dxos/echo": "0.8.4-main.fcfe5033a5",
34
+ "@dxos/invariant": "0.8.4-main.fcfe5033a5",
35
+ "@dxos/util": "0.8.4-main.fcfe5033a5"
37
36
  },
38
37
  "devDependencies": {
39
- "@types/lodash.isequal": "^4.5.0",
40
- "@dxos/random": "0.8.4-main.ef1bc66f44",
41
- "@dxos/test-utils": "0.8.4-main.ef1bc66f44"
38
+ "effect": "3.20.0",
39
+ "@dxos/context": "0.8.4-main.fcfe5033a5",
40
+ "@dxos/random": "0.8.4-main.fcfe5033a5",
41
+ "@dxos/test-utils": "0.8.4-main.fcfe5033a5"
42
+ },
43
+ "peerDependencies": {
44
+ "effect": "3.20.0"
42
45
  },
43
46
  "publishConfig": {
44
47
  "access": "public"
package/src/atom.test.ts CHANGED
@@ -3,11 +3,12 @@
3
3
  //
4
4
 
5
5
  import * as Registry from '@effect-atom/atom/Registry';
6
- import { describe, expect, test } from 'vitest';
6
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
7
7
 
8
8
  import { Obj, Ref } from '@dxos/echo';
9
- import { TestSchema } from '@dxos/echo/testing';
10
9
  import { createObject } from '@dxos/echo-db';
10
+ import { EchoTestBuilder } from '@dxos/echo-db/testing';
11
+ import { TestSchema } from '@dxos/echo/testing';
11
12
 
12
13
  import * as AtomObj from './atom';
13
14
 
@@ -72,8 +73,8 @@ describe('Echo Atom - Basic Functionality', () => {
72
73
  );
73
74
 
74
75
  // Mutate object via Obj.change.
75
- Obj.change(obj, (o) => {
76
- o.name = 'Updated';
76
+ Obj.change(obj, (obj) => {
77
+ obj.name = 'Updated';
77
78
  });
78
79
 
79
80
  // Subscription should have fired: immediate + update.
@@ -104,8 +105,8 @@ describe('Echo Atom - Basic Functionality', () => {
104
105
  );
105
106
 
106
107
  // Update through Obj.change.
107
- Obj.change(obj, (o) => {
108
- o.title = (o.title ?? '') + ' Updated';
108
+ Obj.change(obj, (obj) => {
109
+ obj.title = (obj.title ?? '') + ' Updated';
109
110
  });
110
111
 
111
112
  // Subscription should have fired: immediate + update.
@@ -139,8 +140,8 @@ describe('Echo Atom - Basic Functionality', () => {
139
140
  expect(propertyUpdateCount).toBe(1);
140
141
 
141
142
  // Mutate the standalone object.
142
- Obj.change(obj, (o) => {
143
- o.name = 'Updated Standalone';
143
+ Obj.change(obj, (obj) => {
144
+ obj.name = 'Updated Standalone';
144
145
  });
145
146
 
146
147
  // Both atoms should have received updates.
@@ -243,8 +244,8 @@ describe('Echo Atom - Referential Equality', () => {
243
244
  expect(updateCount).toBe(1);
244
245
 
245
246
  // Mutate the object.
246
- Obj.change(obj, (o) => {
247
- o.name = 'Updated';
247
+ Obj.change(obj, (obj) => {
248
+ obj.name = 'Updated';
248
249
  });
249
250
 
250
251
  // The subscription should still work.
@@ -275,8 +276,8 @@ describe('Echo Atom - Referential Equality', () => {
275
276
  expect(updateCount).toBe(1);
276
277
 
277
278
  // Mutate the specific property.
278
- Obj.change(obj, (o) => {
279
- o.name = 'Updated';
279
+ Obj.change(obj, (obj) => {
280
+ obj.name = 'Updated';
280
281
  });
281
282
 
282
283
  // The subscription should still work.
@@ -306,3 +307,92 @@ describe('Echo Atom - Referential Equality', () => {
306
307
  expect(atom1).toBe(atom2);
307
308
  });
308
309
  });
310
+
311
+ describe('AtomObj.makeWithReactive', () => {
312
+ test('returns object for direct obj input', () => {
313
+ const obj = createObject(
314
+ Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
315
+ );
316
+
317
+ const registry = Registry.make();
318
+ const atom = AtomObj.makeWithReactive(obj);
319
+ const result = registry.get(atom);
320
+
321
+ expect(result).toBe(obj);
322
+ expect(result?.name).toBe('Test');
323
+ });
324
+
325
+ test('returns same atom for same object', () => {
326
+ const obj = createObject(
327
+ Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
328
+ );
329
+
330
+ const atom1 = AtomObj.makeWithReactive(obj);
331
+ const atom2 = AtomObj.makeWithReactive(obj);
332
+
333
+ expect(atom1).toBe(atom2);
334
+ });
335
+
336
+ describe('with ref input', () => {
337
+ let testBuilder: InstanceType<typeof EchoTestBuilder>;
338
+
339
+ beforeEach(async () => {
340
+ testBuilder = await new EchoTestBuilder().open();
341
+ });
342
+
343
+ afterEach(async () => {
344
+ await testBuilder.close();
345
+ });
346
+
347
+ test('returns resolved object for ref', async ({ expect }) => {
348
+ const { db } = await testBuilder.createDatabase({
349
+ types: [TestSchema.Person, TestSchema.Task],
350
+ });
351
+ const task = db.add(Obj.make(TestSchema.Task, { title: 'Task 1' }));
352
+ const person = db.add(
353
+ Obj.make(TestSchema.Person, {
354
+ name: 'Test',
355
+ username: 'test',
356
+ email: 'test@example.com',
357
+ tasks: [Ref.make(task)],
358
+ }),
359
+ );
360
+ await db.flush();
361
+
362
+ const ref = person.tasks![0];
363
+ const registry = Registry.make();
364
+ const atom = AtomObj.makeWithReactive(ref);
365
+ const result = registry.get(atom);
366
+
367
+ expect(result).toBe(task);
368
+ expect(result?.title).toBe('Task 1');
369
+ });
370
+
371
+ test('returns undefined when ref target was removed', async ({ expect }) => {
372
+ const { db } = await testBuilder.createDatabase({
373
+ types: [TestSchema.Person, TestSchema.Task],
374
+ });
375
+ const task = db.add(Obj.make(TestSchema.Task, { title: 'Task 1' }));
376
+ const person = db.add(
377
+ Obj.make(TestSchema.Person, {
378
+ name: 'Test',
379
+ username: 'test',
380
+ email: 'test@example.com',
381
+ tasks: [Ref.make(task)],
382
+ }),
383
+ );
384
+ await db.flush();
385
+
386
+ const ref = person.tasks![0];
387
+ const registry = Registry.make();
388
+ const atom = AtomObj.makeWithReactive(ref);
389
+
390
+ expect(registry.get(atom)).toBe(task);
391
+
392
+ db.remove(task);
393
+ await db.flush();
394
+
395
+ expect(registry.get(atom)).toBeUndefined();
396
+ });
397
+ });
398
+ });
package/src/atom.ts CHANGED
@@ -3,9 +3,12 @@
3
3
  //
4
4
 
5
5
  import * as Atom from '@effect-atom/atom/Atom';
6
- import isEqual from 'lodash.isequal';
6
+ import * as Result from '@effect-atom/atom/Result';
7
+ import * as Effect from 'effect/Effect';
8
+ import * as Function from 'effect/Function';
9
+ import * as Option from 'effect/Option';
7
10
 
8
- import { Obj, Ref } from '@dxos/echo';
11
+ import { type Entity, Obj, Ref, Relation } from '@dxos/echo';
9
12
  import { assertArgument } from '@dxos/invariant';
10
13
 
11
14
  import { loadRefTarget } from './ref-utils';
@@ -23,7 +26,7 @@ const objectFamily = Atom.family(<T extends Obj.Unknown>(obj: T): Atom.Atom<Obj.
23
26
  get.addFinalizer(() => unsubscribe());
24
27
 
25
28
  return Obj.getSnapshot(obj);
26
- });
29
+ }).pipe(Atom.keepAlive);
27
30
  });
28
31
 
29
32
  /**
@@ -48,7 +51,7 @@ const refFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T>): Atom.Ato
48
51
  });
49
52
 
50
53
  return loadRefTarget(ref, get, setupTargetSubscription);
51
- });
54
+ }).pipe(Atom.keepAlive);
52
55
  });
53
56
 
54
57
  /**
@@ -80,7 +83,7 @@ const propertyFamily = Atom.family(<T extends Obj.Unknown>(obj: T) =>
80
83
 
81
84
  const unsubscribe = Obj.subscribe(obj, () => {
82
85
  const newValue = obj[key];
83
- if (!isEqual(previousSnapshot, newValue)) {
86
+ if (previousSnapshot !== newValue) {
84
87
  previousSnapshot = snapshotForComparison(newValue);
85
88
  // Return a snapshot copy so React sees a new reference.
86
89
  get.setSelf(snapshotForComparison(newValue));
@@ -91,43 +94,46 @@ const propertyFamily = Atom.family(<T extends Obj.Unknown>(obj: T) =>
91
94
 
92
95
  // Return a snapshot copy so React sees a new reference.
93
96
  return snapshotForComparison(obj[key]);
94
- });
97
+ }).pipe(Atom.keepAlive);
95
98
  }),
96
99
  );
97
100
 
98
101
  /**
99
- * Create a read-only atom for a reactive object or ref.
100
- * Works with Echo objects, plain reactive objects (from Obj.make), and Refs.
101
- * Returns immutable snapshots of the object data (branded with SnapshotKindId).
102
+ * Create a read-only atom for a single reactive object or ref.
103
+ * Returns {@link Obj.Snapshot} (immutable plain data), not the live reactive object.
104
+ * Use this when you need one object's data for display or React dependency tracking.
102
105
  * The atom updates automatically when the object is mutated.
103
106
  * For refs, automatically handles async loading.
104
- * Uses Atom.family internally - same object/ref reference returns same atom instance.
107
+ * Uses Atom.family internally - same object/ref returns same atom instance.
105
108
  *
106
109
  * @param objOrRef - The reactive object or ref to create an atom for, or undefined.
107
- * @returns An atom that returns the object snapshot. Returns undefined only for refs (async loading) or undefined input.
110
+ * @returns An atom that returns the object snapshot (plain data). Returns undefined only for refs (async loading) or undefined input.
108
111
  */
109
112
  export function make<T extends Obj.Unknown>(obj: T): Atom.Atom<Obj.Snapshot<T>>;
113
+ export function make<T extends Relation.Unknown>(relation: T): Atom.Atom<Relation.Snapshot<T>>;
114
+ export function make<T extends Entity.Unknown>(entity: T): Atom.Atom<Entity.Snapshot>;
110
115
  export function make<T extends Obj.Unknown>(ref: Ref.Ref<T>): Atom.Atom<Obj.Snapshot<T> | undefined>;
111
116
  export function make<T extends Obj.Unknown>(
112
117
  objOrRef: T | Ref.Ref<T> | undefined,
113
118
  ): Atom.Atom<Obj.Snapshot<T> | undefined>;
114
- export function make<T extends Obj.Unknown>(
119
+ export function make<T extends Entity.Unknown>(
115
120
  objOrRef: T | Ref.Ref<T> | undefined,
116
- ): Atom.Atom<Obj.Snapshot<T> | undefined> {
121
+ ): Atom.Atom<Entity.Snapshot | undefined> {
117
122
  if (objOrRef === undefined) {
118
- return Atom.make<Obj.Snapshot<T> | undefined>(() => undefined);
123
+ return Atom.make<Entity.Snapshot | undefined>(() => undefined);
119
124
  }
120
125
 
121
126
  // Handle Ref inputs.
122
127
  if (Ref.isRef(objOrRef)) {
123
- return refFamily(objOrRef as Ref.Ref<T>);
128
+ return refFamily(objOrRef as any);
124
129
  }
125
130
 
126
131
  // At this point, objOrRef is definitely T (not a Ref).
127
132
  const obj = objOrRef as T;
128
- assertArgument(Obj.isObject(obj), 'obj', 'Object must be a reactive object');
133
+ assertArgument(Obj.isObject(obj) || Relation.isRelation(obj), 'obj', 'Object must be a reactive object');
129
134
 
130
- return objectFamily(obj);
135
+ // TODO(dmaretskyi): Fix echo types during review.
136
+ return objectFamily(obj as any);
131
137
  }
132
138
 
133
139
  /**
@@ -155,6 +161,67 @@ export function makeProperty<T extends Obj.Unknown, K extends keyof T>(
155
161
  }
156
162
 
157
163
  assertArgument(Obj.isObject(obj), 'obj', 'Object must be a reactive object');
158
- assertArgument(key in obj, 'key', 'Property must exist on object');
159
164
  return propertyFamily(obj)(key);
160
165
  }
166
+
167
+ /**
168
+ * Atom family for ECHO objects - returns the live object, not a snapshot.
169
+ * Same as objectFamily but returns T instead of Obj.Snapshot<T>.
170
+ */
171
+ const objectWithReactiveFamily = Atom.family(<T extends Obj.Unknown>(obj: T): Atom.Atom<T> => {
172
+ return Atom.make<T>((get) => {
173
+ const unsubscribe = Obj.subscribe(obj, () => {
174
+ get.setSelf(obj);
175
+ });
176
+
177
+ get.addFinalizer(() => unsubscribe());
178
+
179
+ return obj;
180
+ }).pipe(Atom.keepAlive);
181
+ });
182
+
183
+ /**
184
+ * Atom family for ECHO refs - returns the live reactive object, not a snapshot.
185
+ * Resolves the ref via the database; returns undefined while loading or if unresolved.
186
+ */
187
+ const refWithReactiveFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T>): Atom.Atom<T | undefined> => {
188
+ const effect = (get: Atom.Context) =>
189
+ Effect.gen(function* () {
190
+ const snapshot = get(make(ref));
191
+ if (snapshot == null) return undefined;
192
+ const option = yield* Obj.getReactiveOption(snapshot);
193
+ return Option.getOrElse(option, () => undefined);
194
+ });
195
+
196
+ return Function.pipe(
197
+ Atom.make(effect),
198
+ Atom.map((result) => Result.getOrElse(result, () => undefined)),
199
+ );
200
+ });
201
+
202
+ /**
203
+ * Like {@link make} but returns the live reactive object instead of a snapshot.
204
+ * Same input: Obj or Ref.Ref. Same output shape: Atom that updates when the object mutates.
205
+ * Prefer {@link make} (snapshot) unless you need the live Obj.Obj for generic mutations (e.g. Obj.change).
206
+ *
207
+ * @param objOrRef - The reactive object or ref.
208
+ * @returns An atom that returns the live object. Returns undefined for refs (async loading) or undefined input.
209
+ */
210
+ export function makeWithReactive<T extends Obj.Unknown>(obj: T): Atom.Atom<T>;
211
+ export function makeWithReactive<T extends Obj.Unknown>(ref: Ref.Ref<T>): Atom.Atom<T | undefined>;
212
+ export function makeWithReactive<T extends Obj.Unknown>(objOrRef: T | Ref.Ref<T> | undefined): Atom.Atom<T | undefined>;
213
+ export function makeWithReactive<T extends Obj.Unknown>(
214
+ objOrRef: T | Ref.Ref<T> | undefined,
215
+ ): Atom.Atom<T | undefined> {
216
+ if (objOrRef === undefined) {
217
+ return Atom.make<T | undefined>(() => undefined);
218
+ }
219
+
220
+ if (Ref.isRef(objOrRef)) {
221
+ return refWithReactiveFamily(objOrRef as Ref.Ref<T>);
222
+ }
223
+
224
+ const obj = objOrRef as T;
225
+ assertArgument(Obj.isObject(obj), 'obj', 'Object must be a reactive object');
226
+ return objectWithReactiveFamily(obj);
227
+ }
@@ -6,8 +6,8 @@ import * as Registry from '@effect-atom/atom/Registry';
6
6
  import { describe, expect, test } from 'vitest';
7
7
 
8
8
  import { Obj } from '@dxos/echo';
9
- import { TestSchema } from '@dxos/echo/testing';
10
9
  import { createObject } from '@dxos/echo-db';
10
+ import { TestSchema } from '@dxos/echo/testing';
11
11
 
12
12
  import * as AtomObj from './atom';
13
13
 
@@ -34,10 +34,10 @@ describe('Echo Atom - Batch Updates', () => {
34
34
  expect(initialCount).toBe(1); // Verify immediate update fired.
35
35
 
36
36
  // Make multiple updates to the same object in a single Obj.change call.
37
- Obj.change(obj, (o) => {
38
- o.name = 'Updated1';
39
- o.email = 'updated@example.com';
40
- o.username = 'updated';
37
+ Obj.change(obj, (obj) => {
38
+ obj.name = 'Updated1';
39
+ obj.email = 'updated@example.com';
40
+ obj.username = 'updated';
41
41
  });
42
42
 
43
43
  // Should have fired once for initial + once for the Obj.change (not once per property update).
@@ -72,14 +72,14 @@ describe('Echo Atom - Batch Updates', () => {
72
72
  expect(initialCount).toBe(1);
73
73
 
74
74
  // Make multiple separate Obj.change calls.
75
- Obj.change(obj, (o) => {
76
- o.name = 'Updated1';
75
+ Obj.change(obj, (obj) => {
76
+ obj.name = 'Updated1';
77
77
  });
78
- Obj.change(obj, (o) => {
79
- o.email = 'updated@example.com';
78
+ Obj.change(obj, (obj) => {
79
+ obj.email = 'updated@example.com';
80
80
  });
81
- Obj.change(obj, (o) => {
82
- o.username = 'updated';
81
+ Obj.change(obj, (obj) => {
82
+ obj.username = 'updated';
83
83
  });
84
84
 
85
85
  // Should have fired once for initial + once per Obj.change call.
@@ -114,10 +114,10 @@ describe('Echo Atom - Batch Updates', () => {
114
114
  expect(initialCount).toBe(1);
115
115
 
116
116
  // Make multiple updates to the same property in a single Obj.change call.
117
- Obj.change(obj, (o) => {
118
- o.name = 'Updated1';
119
- o.name = 'Updated2';
120
- o.name = 'Updated3';
117
+ Obj.change(obj, (obj) => {
118
+ obj.name = 'Updated1';
119
+ obj.name = 'Updated2';
120
+ obj.name = 'Updated3';
121
121
  });
122
122
 
123
123
  // Should have fired once for initial + once for the Obj.change (not once per assignment).
@@ -6,10 +6,12 @@ import * as Registry from '@effect-atom/atom/Registry';
6
6
  import * as Schema from 'effect/Schema';
7
7
  import { afterEach, beforeEach, describe, expect, test } from 'vitest';
8
8
 
9
+ import { sleep } from '@dxos/async';
9
10
  import { Obj, type QueryResult, Type } from '@dxos/echo';
10
- import { TestSchema } from '@dxos/echo/testing';
11
- import { type EchoDatabase, Filter, Query } from '@dxos/echo-db';
11
+ import { Filter, Query } from '@dxos/echo';
12
+ import { type EchoDatabase, RuntimeSchemaRegistry } from '@dxos/echo-db';
12
13
  import { EchoTestBuilder } from '@dxos/echo-db/testing';
14
+ import { TestSchema } from '@dxos/echo/testing';
13
15
  import { SpaceId } from '@dxos/keys';
14
16
 
15
17
  import * as AtomQuery from './query-atom';
@@ -22,7 +24,7 @@ const TestItem = Schema.Struct({
22
24
  value: Schema.Number,
23
25
  }).pipe(
24
26
  Type.object({
25
- typename: 'example.com/type/TestItem',
27
+ typename: 'com.example.type.test-item',
26
28
  version: '0.1.0',
27
29
  }),
28
30
  );
@@ -47,7 +49,7 @@ describe('AtomQuery', () => {
47
49
  test('creates atom with initial results', async () => {
48
50
  db.add(Obj.make(TestItem, { name: 'Object 1', value: 100 }));
49
51
  db.add(Obj.make(TestItem, { name: 'Object 2', value: 100 }));
50
- await db.flush({ indexes: true });
52
+ await db.flush();
51
53
 
52
54
  const queryResult: QueryResult.QueryResult<TestItem> = db.query(
53
55
  Query.select(Filter.type(TestItem, { value: 100 })),
@@ -63,7 +65,7 @@ describe('AtomQuery', () => {
63
65
 
64
66
  test('registry.subscribe fires on QueryResult changes', async () => {
65
67
  db.add(Obj.make(TestItem, { name: 'Initial', value: 200 }));
66
- await db.flush({ indexes: true });
68
+ await db.flush();
67
69
 
68
70
  const queryResult: QueryResult.QueryResult<TestItem> = db.query(
69
71
  Query.select(Filter.type(TestItem, { value: 200 })),
@@ -86,7 +88,7 @@ describe('AtomQuery', () => {
86
88
 
87
89
  // Add a new object that matches the query.
88
90
  db.add(Obj.make(TestItem, { name: 'New Object', value: 200 }));
89
- await db.flush({ indexes: true, updates: true });
91
+ await db.flush({ updates: true });
90
92
 
91
93
  // Subscription should have fired.
92
94
  expect(updateCount).toBeGreaterThan(0);
@@ -96,7 +98,7 @@ describe('AtomQuery', () => {
96
98
  test('registry.subscribe fires when objects are removed', async () => {
97
99
  const obj1 = db.add(Obj.make(TestItem, { name: 'Object 1', value: 300 }));
98
100
  db.add(Obj.make(TestItem, { name: 'Object 2', value: 300 }));
99
- await db.flush({ indexes: true });
101
+ await db.flush();
100
102
 
101
103
  const queryResult: QueryResult.QueryResult<TestItem> = db.query(
102
104
  Query.select(Filter.type(TestItem, { value: 300 })),
@@ -119,7 +121,7 @@ describe('AtomQuery', () => {
119
121
 
120
122
  // Remove an object.
121
123
  db.remove(obj1);
122
- await db.flush({ indexes: true, updates: true });
124
+ await db.flush({ updates: true });
123
125
 
124
126
  // Subscription should have fired.
125
127
  expect(updateCount).toBeGreaterThan(0);
@@ -129,7 +131,7 @@ describe('AtomQuery', () => {
129
131
 
130
132
  test('unsubscribing from registry stops receiving updates', async () => {
131
133
  db.add(Obj.make(TestItem, { name: 'Initial', value: 400 }));
132
- await db.flush({ indexes: true });
134
+ await db.flush();
133
135
 
134
136
  const queryResult: QueryResult.QueryResult<TestItem> = db.query(
135
137
  Query.select(Filter.type(TestItem, { value: 400 })),
@@ -150,7 +152,7 @@ describe('AtomQuery', () => {
150
152
 
151
153
  // Add object and verify subscription fires.
152
154
  db.add(Obj.make(TestItem, { name: 'Object 2', value: 400 }));
153
- await db.flush({ indexes: true, updates: true });
155
+ await db.flush({ updates: true });
154
156
  const countAfterFirstAdd = updateCount;
155
157
  expect(countAfterFirstAdd).toBeGreaterThan(0);
156
158
 
@@ -159,7 +161,7 @@ describe('AtomQuery', () => {
159
161
 
160
162
  // Add another object.
161
163
  db.add(Obj.make(TestItem, { name: 'Object 3', value: 400 }));
162
- await db.flush({ indexes: true, updates: true });
164
+ await db.flush({ updates: true });
163
165
 
164
166
  // Update count should not have changed after unsubscribe.
165
167
  expect(updateCount).toBe(countAfterFirstAdd);
@@ -179,7 +181,7 @@ describe('AtomQuery', () => {
179
181
 
180
182
  test('multiple atoms from same query share underlying subscription', async () => {
181
183
  db.add(Obj.make(TestItem, { name: 'Object', value: 500 }));
182
- await db.flush({ indexes: true });
184
+ await db.flush();
183
185
 
184
186
  const queryResult: QueryResult.QueryResult<TestItem> = db.query(
185
187
  Query.select(Filter.type(TestItem, { value: 500 })),
@@ -260,3 +262,127 @@ describe('AtomQuery with queues', () => {
260
262
  expect(results[0].name).toEqual('jane');
261
263
  });
262
264
  });
265
+
266
+ const SchemaA = Schema.Struct({
267
+ name: Schema.String,
268
+ }).pipe(
269
+ Type.object({
270
+ typename: 'com.example.type.a',
271
+ version: '0.1.0',
272
+ }),
273
+ );
274
+
275
+ const SchemaB = Schema.Struct({
276
+ value: Schema.Number,
277
+ }).pipe(
278
+ Type.object({
279
+ typename: 'com.example.type.b',
280
+ version: '0.1.0',
281
+ }),
282
+ );
283
+
284
+ describe('AtomQuery.fromQuery with schema registry', () => {
285
+ let schemaRegistry: RuntimeSchemaRegistry;
286
+ let registry: Registry.Registry;
287
+
288
+ beforeEach(() => {
289
+ schemaRegistry = new RuntimeSchemaRegistry([]);
290
+ registry = Registry.make();
291
+ });
292
+
293
+ test('creates atom with initial results from schema query', async ({ expect }) => {
294
+ await schemaRegistry.register([SchemaA]);
295
+
296
+ const queryResult = schemaRegistry.query();
297
+ const atom = AtomQuery.fromQuery(queryResult);
298
+ const results = registry.get(atom);
299
+
300
+ expect(results).toHaveLength(1);
301
+ expect(Type.getTypename(results[0])).toBe('com.example.type.a');
302
+ });
303
+
304
+ test('atom updates when new schemas are registered', async ({ expect }) => {
305
+ await schemaRegistry.register([SchemaA]);
306
+
307
+ const queryResult = schemaRegistry.query();
308
+ const atom = AtomQuery.fromQuery(queryResult);
309
+
310
+ // Get initial results and subscribe.
311
+ const initialResults = registry.get(atom);
312
+ expect(initialResults).toHaveLength(1);
313
+
314
+ let updateCount = 0;
315
+ let latestResults: Type.AnyEntity[] = [];
316
+ registry.subscribe(atom, () => {
317
+ updateCount++;
318
+ latestResults = registry.get(atom);
319
+ });
320
+
321
+ // Allow reactive query to start (deferred via queueMicrotask).
322
+ await sleep(10);
323
+
324
+ // Register a new schema.
325
+ await schemaRegistry.register([SchemaB]);
326
+
327
+ expect(updateCount).toBeGreaterThan(0);
328
+ expect(latestResults).toHaveLength(2);
329
+ expect(latestResults.map(Type.getTypename).sort()).toEqual(['com.example.type.a', 'com.example.type.b']);
330
+ });
331
+
332
+ test('atom works with empty initial results', ({ expect }) => {
333
+ const queryResult = schemaRegistry.query();
334
+ const atom = AtomQuery.fromQuery(queryResult);
335
+ const results = registry.get(atom);
336
+
337
+ expect(results).toHaveLength(0);
338
+ });
339
+
340
+ test('atom with filtered query only reflects matching schemas', async ({ expect }) => {
341
+ const queryResult = schemaRegistry.query({ typename: 'com.example.type.a' });
342
+ const atom = AtomQuery.fromQuery(queryResult);
343
+
344
+ // Get initial (empty) results and subscribe.
345
+ const initialResults = registry.get(atom);
346
+ expect(initialResults).toHaveLength(0);
347
+
348
+ let latestResults: Type.AnyEntity[] = [];
349
+ registry.subscribe(atom, () => {
350
+ latestResults = registry.get(atom);
351
+ });
352
+
353
+ await sleep(10);
354
+
355
+ // Register non-matching schema.
356
+ await schemaRegistry.register([SchemaB]);
357
+ // Results updated but still empty for this filter.
358
+ expect(latestResults).toHaveLength(0);
359
+
360
+ // Register matching schema.
361
+ await schemaRegistry.register([SchemaA]);
362
+ expect(latestResults).toHaveLength(1);
363
+ expect(Type.getTypename(latestResults[0])).toBe('com.example.type.a');
364
+ });
365
+
366
+ test('unsubscribing from atom stops updates', async ({ expect }) => {
367
+ const queryResult = schemaRegistry.query();
368
+ const atom = AtomQuery.fromQuery(queryResult);
369
+
370
+ registry.get(atom);
371
+
372
+ let updateCount = 0;
373
+ const unsubscribe = registry.subscribe(atom, () => {
374
+ updateCount++;
375
+ });
376
+
377
+ await sleep(10);
378
+
379
+ await schemaRegistry.register([SchemaA]);
380
+ const countAfterFirst = updateCount;
381
+ expect(countAfterFirst).toBeGreaterThan(0);
382
+
383
+ unsubscribe();
384
+
385
+ await schemaRegistry.register([SchemaB]);
386
+ expect(updateCount).toBe(countAfterFirst);
387
+ });
388
+ });