@dxos/echo-atom 0.8.4-main.6fa680abb7 → 0.8.4-main.8360d9e660
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/lib/neutral/index.mjs +2 -3
- package/dist/lib/neutral/index.mjs.map +3 -3
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/types/src/atom.d.ts.map +1 -1
- package/dist/types/src/query-atom.d.ts +2 -2
- package/dist/types/src/query-atom.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/atom.test.ts +3 -3
- package/src/atom.ts +0 -1
- package/src/query-atom.test.ts +135 -10
- package/src/query-atom.ts +4 -9
- package/src/ref-atom.test.ts +7 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/echo-atom",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.8360d9e660",
|
|
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,15 +29,15 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@effect-atom/atom": "^0.5.1",
|
|
32
|
-
"@dxos/echo
|
|
33
|
-
"@dxos/echo": "0.8.4-main.
|
|
34
|
-
"@dxos/invariant": "0.8.4-main.
|
|
35
|
-
"@dxos/util": "0.8.4-main.
|
|
32
|
+
"@dxos/echo": "0.8.4-main.8360d9e660",
|
|
33
|
+
"@dxos/echo-db": "0.8.4-main.8360d9e660",
|
|
34
|
+
"@dxos/invariant": "0.8.4-main.8360d9e660",
|
|
35
|
+
"@dxos/util": "0.8.4-main.8360d9e660"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"effect": "3.19.16",
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/
|
|
39
|
+
"@dxos/random": "0.8.4-main.8360d9e660",
|
|
40
|
+
"@dxos/test-utils": "0.8.4-main.8360d9e660"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"effect": "3.19.16"
|
package/src/atom.test.ts
CHANGED
|
@@ -357,7 +357,7 @@ describe('AtomObj.makeWithReactive', () => {
|
|
|
357
357
|
tasks: [Ref.make(task)],
|
|
358
358
|
}),
|
|
359
359
|
);
|
|
360
|
-
await db.flush(
|
|
360
|
+
await db.flush();
|
|
361
361
|
|
|
362
362
|
const ref = person.tasks![0];
|
|
363
363
|
const registry = Registry.make();
|
|
@@ -381,7 +381,7 @@ describe('AtomObj.makeWithReactive', () => {
|
|
|
381
381
|
tasks: [Ref.make(task)],
|
|
382
382
|
}),
|
|
383
383
|
);
|
|
384
|
-
await db.flush(
|
|
384
|
+
await db.flush();
|
|
385
385
|
|
|
386
386
|
const ref = person.tasks![0];
|
|
387
387
|
const registry = Registry.make();
|
|
@@ -390,7 +390,7 @@ describe('AtomObj.makeWithReactive', () => {
|
|
|
390
390
|
expect(registry.get(atom)).toBe(task);
|
|
391
391
|
|
|
392
392
|
db.remove(task);
|
|
393
|
-
await db.flush(
|
|
393
|
+
await db.flush();
|
|
394
394
|
|
|
395
395
|
expect(registry.get(atom)).toBeUndefined();
|
|
396
396
|
});
|
package/src/atom.ts
CHANGED
|
@@ -161,7 +161,6 @@ export function makeProperty<T extends Obj.Unknown, K extends keyof T>(
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
assertArgument(Obj.isObject(obj), 'obj', 'Object must be a reactive object');
|
|
164
|
-
assertArgument(key in obj, 'key', 'Property must exist on object');
|
|
165
164
|
return propertyFamily(obj)(key);
|
|
166
165
|
}
|
|
167
166
|
|
package/src/query-atom.test.ts
CHANGED
|
@@ -6,10 +6,11 @@ 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
11
|
import { Filter, Query } from '@dxos/echo';
|
|
11
12
|
import { TestSchema } from '@dxos/echo/testing';
|
|
12
|
-
import { type EchoDatabase } from '@dxos/echo-db';
|
|
13
|
+
import { type EchoDatabase, RuntimeSchemaRegistry } from '@dxos/echo-db';
|
|
13
14
|
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
14
15
|
import { SpaceId } from '@dxos/keys';
|
|
15
16
|
|
|
@@ -48,7 +49,7 @@ describe('AtomQuery', () => {
|
|
|
48
49
|
test('creates atom with initial results', async () => {
|
|
49
50
|
db.add(Obj.make(TestItem, { name: 'Object 1', value: 100 }));
|
|
50
51
|
db.add(Obj.make(TestItem, { name: 'Object 2', value: 100 }));
|
|
51
|
-
await db.flush(
|
|
52
|
+
await db.flush();
|
|
52
53
|
|
|
53
54
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
54
55
|
Query.select(Filter.type(TestItem, { value: 100 })),
|
|
@@ -64,7 +65,7 @@ describe('AtomQuery', () => {
|
|
|
64
65
|
|
|
65
66
|
test('registry.subscribe fires on QueryResult changes', async () => {
|
|
66
67
|
db.add(Obj.make(TestItem, { name: 'Initial', value: 200 }));
|
|
67
|
-
await db.flush(
|
|
68
|
+
await db.flush();
|
|
68
69
|
|
|
69
70
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
70
71
|
Query.select(Filter.type(TestItem, { value: 200 })),
|
|
@@ -87,7 +88,7 @@ describe('AtomQuery', () => {
|
|
|
87
88
|
|
|
88
89
|
// Add a new object that matches the query.
|
|
89
90
|
db.add(Obj.make(TestItem, { name: 'New Object', value: 200 }));
|
|
90
|
-
await db.flush({
|
|
91
|
+
await db.flush({ updates: true });
|
|
91
92
|
|
|
92
93
|
// Subscription should have fired.
|
|
93
94
|
expect(updateCount).toBeGreaterThan(0);
|
|
@@ -97,7 +98,7 @@ describe('AtomQuery', () => {
|
|
|
97
98
|
test('registry.subscribe fires when objects are removed', async () => {
|
|
98
99
|
const obj1 = db.add(Obj.make(TestItem, { name: 'Object 1', value: 300 }));
|
|
99
100
|
db.add(Obj.make(TestItem, { name: 'Object 2', value: 300 }));
|
|
100
|
-
await db.flush(
|
|
101
|
+
await db.flush();
|
|
101
102
|
|
|
102
103
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
103
104
|
Query.select(Filter.type(TestItem, { value: 300 })),
|
|
@@ -120,7 +121,7 @@ describe('AtomQuery', () => {
|
|
|
120
121
|
|
|
121
122
|
// Remove an object.
|
|
122
123
|
db.remove(obj1);
|
|
123
|
-
await db.flush({
|
|
124
|
+
await db.flush({ updates: true });
|
|
124
125
|
|
|
125
126
|
// Subscription should have fired.
|
|
126
127
|
expect(updateCount).toBeGreaterThan(0);
|
|
@@ -130,7 +131,7 @@ describe('AtomQuery', () => {
|
|
|
130
131
|
|
|
131
132
|
test('unsubscribing from registry stops receiving updates', async () => {
|
|
132
133
|
db.add(Obj.make(TestItem, { name: 'Initial', value: 400 }));
|
|
133
|
-
await db.flush(
|
|
134
|
+
await db.flush();
|
|
134
135
|
|
|
135
136
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
136
137
|
Query.select(Filter.type(TestItem, { value: 400 })),
|
|
@@ -151,7 +152,7 @@ describe('AtomQuery', () => {
|
|
|
151
152
|
|
|
152
153
|
// Add object and verify subscription fires.
|
|
153
154
|
db.add(Obj.make(TestItem, { name: 'Object 2', value: 400 }));
|
|
154
|
-
await db.flush({
|
|
155
|
+
await db.flush({ updates: true });
|
|
155
156
|
const countAfterFirstAdd = updateCount;
|
|
156
157
|
expect(countAfterFirstAdd).toBeGreaterThan(0);
|
|
157
158
|
|
|
@@ -160,7 +161,7 @@ describe('AtomQuery', () => {
|
|
|
160
161
|
|
|
161
162
|
// Add another object.
|
|
162
163
|
db.add(Obj.make(TestItem, { name: 'Object 3', value: 400 }));
|
|
163
|
-
await db.flush({
|
|
164
|
+
await db.flush({ updates: true });
|
|
164
165
|
|
|
165
166
|
// Update count should not have changed after unsubscribe.
|
|
166
167
|
expect(updateCount).toBe(countAfterFirstAdd);
|
|
@@ -180,7 +181,7 @@ describe('AtomQuery', () => {
|
|
|
180
181
|
|
|
181
182
|
test('multiple atoms from same query share underlying subscription', async () => {
|
|
182
183
|
db.add(Obj.make(TestItem, { name: 'Object', value: 500 }));
|
|
183
|
-
await db.flush(
|
|
184
|
+
await db.flush();
|
|
184
185
|
|
|
185
186
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
186
187
|
Query.select(Filter.type(TestItem, { value: 500 })),
|
|
@@ -261,3 +262,127 @@ describe('AtomQuery with queues', () => {
|
|
|
261
262
|
expect(results[0].name).toEqual('jane');
|
|
262
263
|
});
|
|
263
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
|
+
});
|
package/src/query-atom.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { DXN, Database, type Entity, type Filter, Query, type QueryResult } from
|
|
|
8
8
|
import { WeakDictionary } from '@dxos/util';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Create a self-updating atom from
|
|
11
|
+
* Create a self-updating atom from any QueryResult (e.g. schema registry queries).
|
|
12
12
|
* Internally subscribes to queryResult and uses get.setSelf to update.
|
|
13
13
|
* Cleanup is handled via get.addFinalizer.
|
|
14
14
|
*
|
|
@@ -17,18 +17,13 @@ import { WeakDictionary } from '@dxos/util';
|
|
|
17
17
|
* @param queryResult - The QueryResult to wrap.
|
|
18
18
|
* @returns An atom that automatically updates when query results change.
|
|
19
19
|
*/
|
|
20
|
-
export const fromQuery = <T
|
|
20
|
+
export const fromQuery = <T>(queryResult: QueryResult.QueryResult<T>): Atom.Atom<T[]> =>
|
|
21
21
|
Atom.make((get) => {
|
|
22
|
-
// TODO(wittjosiah): Consider subscribing to individual objects here as well, and grabbing their snapshots.
|
|
23
|
-
// Subscribe to QueryResult changes.
|
|
24
22
|
const unsubscribe = queryResult.subscribe(() => {
|
|
25
|
-
get.setSelf(queryResult.
|
|
23
|
+
get.setSelf(queryResult.runSync());
|
|
26
24
|
});
|
|
27
|
-
|
|
28
|
-
// Register cleanup for when atom is no longer used.
|
|
29
25
|
get.addFinalizer(unsubscribe);
|
|
30
|
-
|
|
31
|
-
return queryResult.results;
|
|
26
|
+
return queryResult.runSync();
|
|
32
27
|
});
|
|
33
28
|
|
|
34
29
|
// Registry: key → Queryable (WeakRef with auto-cleanup when GC'd).
|
package/src/ref-atom.test.ts
CHANGED
|
@@ -33,7 +33,7 @@ describe('AtomRef - Basic Functionality', () => {
|
|
|
33
33
|
|
|
34
34
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
35
35
|
db.add(targetObj);
|
|
36
|
-
await db.flush(
|
|
36
|
+
await db.flush();
|
|
37
37
|
|
|
38
38
|
const ref = Ref.make(targetObj);
|
|
39
39
|
const atom = AtomRef.make(ref);
|
|
@@ -48,7 +48,7 @@ describe('AtomRef - Basic Functionality', () => {
|
|
|
48
48
|
|
|
49
49
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
50
50
|
db.add(targetObj);
|
|
51
|
-
await db.flush(
|
|
51
|
+
await db.flush();
|
|
52
52
|
|
|
53
53
|
const ref = Ref.make(targetObj);
|
|
54
54
|
const atom = AtomRef.make(ref);
|
|
@@ -90,7 +90,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
90
90
|
|
|
91
91
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
92
92
|
db.add(targetObj);
|
|
93
|
-
await db.flush(
|
|
93
|
+
await db.flush();
|
|
94
94
|
|
|
95
95
|
const ref = Ref.make(targetObj);
|
|
96
96
|
|
|
@@ -116,7 +116,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
116
116
|
});
|
|
117
117
|
db.add(targetObj1);
|
|
118
118
|
db.add(targetObj2);
|
|
119
|
-
await db.flush(
|
|
119
|
+
await db.flush();
|
|
120
120
|
|
|
121
121
|
const ref1 = Ref.make(targetObj1);
|
|
122
122
|
const ref2 = Ref.make(targetObj2);
|
|
@@ -133,7 +133,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
133
133
|
|
|
134
134
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
135
135
|
db.add(targetObj);
|
|
136
|
-
await db.flush(
|
|
136
|
+
await db.flush();
|
|
137
137
|
|
|
138
138
|
// Create two separate refs to the same target.
|
|
139
139
|
const ref1 = Ref.make(targetObj);
|
|
@@ -158,7 +158,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
158
158
|
|
|
159
159
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
160
160
|
db.add(targetObj);
|
|
161
|
-
await db.flush(
|
|
161
|
+
await db.flush();
|
|
162
162
|
|
|
163
163
|
const ref = Ref.make(targetObj);
|
|
164
164
|
|
|
@@ -197,7 +197,7 @@ describe('AtomRef - Expando Objects', () => {
|
|
|
197
197
|
test('works with Expando objects', async () => {
|
|
198
198
|
const targetObj = Obj.make(TestSchema.Expando, { name: 'Expando Target', value: 42 });
|
|
199
199
|
db.add(targetObj);
|
|
200
|
-
await db.flush(
|
|
200
|
+
await db.flush();
|
|
201
201
|
|
|
202
202
|
const ref = Ref.make(targetObj);
|
|
203
203
|
const atom = AtomRef.make(ref);
|