@dxos/echo-atom 0.8.4-main.d05539e30a → 0.8.4-main.d9fc60f731
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 +8 -5
- package/dist/lib/neutral/index.mjs.map +3 -3
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/types/src/atom.d.ts +1 -1
- package/dist/types/src/atom.d.ts.map +1 -1
- package/dist/types/src/query-atom.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -11
- package/src/atom.ts +7 -2
- package/src/query-atom.test.ts +57 -134
- package/src/query-atom.ts +8 -4
- package/src/ref-atom.test.ts +6 -6
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.d9fc60f731",
|
|
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",
|
|
@@ -25,20 +25,20 @@
|
|
|
25
25
|
"src"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@effect-atom/atom": "^0.5.
|
|
29
|
-
"@dxos/
|
|
30
|
-
"@dxos/util": "0.8.4-main.
|
|
31
|
-
"@dxos/
|
|
32
|
-
"@dxos/echo": "0.8.4-main.
|
|
28
|
+
"@effect-atom/atom": "^0.5.3",
|
|
29
|
+
"@dxos/echo-db": "0.8.4-main.d9fc60f731",
|
|
30
|
+
"@dxos/util": "0.8.4-main.d9fc60f731",
|
|
31
|
+
"@dxos/invariant": "0.8.4-main.d9fc60f731",
|
|
32
|
+
"@dxos/echo": "0.8.4-main.d9fc60f731"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"effect": "3.
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/context": "0.8.4-main.
|
|
38
|
-
"@dxos/
|
|
35
|
+
"effect": "3.21.2",
|
|
36
|
+
"@dxos/test-utils": "0.8.4-main.d9fc60f731",
|
|
37
|
+
"@dxos/context": "0.8.4-main.d9fc60f731",
|
|
38
|
+
"@dxos/random": "0.8.4-main.d9fc60f731"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"effect": "3.
|
|
41
|
+
"effect": "3.21.2"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|
package/src/atom.ts
CHANGED
|
@@ -8,7 +8,7 @@ import * as Effect from 'effect/Effect';
|
|
|
8
8
|
import * as Function from 'effect/Function';
|
|
9
9
|
import * as Option from 'effect/Option';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { Entity, Obj, Ref, Relation } from '@dxos/echo';
|
|
12
12
|
import { assertArgument } from '@dxos/invariant';
|
|
13
13
|
|
|
14
14
|
import { loadRefTarget } from './ref-utils';
|
|
@@ -130,7 +130,12 @@ export function make<T extends Entity.Unknown>(
|
|
|
130
130
|
|
|
131
131
|
// At this point, objOrRef is definitely T (not a Ref).
|
|
132
132
|
const obj = objOrRef as T;
|
|
133
|
-
|
|
133
|
+
// Accept any kind of entity (Obj, Relation, or persisted Type) — the
|
|
134
|
+
// reactive subscription only needs `[KindId]` to be set; the proxy machinery
|
|
135
|
+
// is kind-agnostic. Narrowing to Obj/Relation here used to throw for type
|
|
136
|
+
// entities returned by the schema registry (e.g. persisted RoastLog), which
|
|
137
|
+
// broke the schema-node connector in plugin-space.
|
|
138
|
+
assertArgument(Entity.isEntity(obj), 'obj', 'Object must be a reactive object');
|
|
134
139
|
|
|
135
140
|
// TODO(dmaretskyi): Fix echo types during review.
|
|
136
141
|
return objectFamily(obj as any);
|
package/src/query-atom.test.ts
CHANGED
|
@@ -6,9 +6,8 @@ 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 {
|
|
10
|
-
import {
|
|
11
|
-
import { type EchoDatabase, RuntimeSchemaRegistry } from '@dxos/echo-db';
|
|
9
|
+
import { DXN, Filter, Obj, Query, type QueryResult, Type } from '@dxos/echo';
|
|
10
|
+
import { type EchoDatabase, makeRegistry } from '@dxos/echo-db';
|
|
12
11
|
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
13
12
|
import { TestSchema } from '@dxos/echo/testing';
|
|
14
13
|
import { SpaceId } from '@dxos/keys';
|
|
@@ -21,13 +20,12 @@ import * as AtomQuery from './query-atom';
|
|
|
21
20
|
const TestItem = Schema.Struct({
|
|
22
21
|
name: Schema.String,
|
|
23
22
|
value: Schema.Number,
|
|
24
|
-
}).pipe(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
);
|
|
30
|
-
type TestItem = Schema.Schema.Type<typeof TestItem>;
|
|
23
|
+
}).pipe(Type.makeObject(DXN.make('com.example.type.testItem', '0.1.0')));
|
|
24
|
+
type TestItem = Type.InstanceType<typeof TestItem>;
|
|
25
|
+
|
|
26
|
+
const TestItem2 = Schema.Struct({
|
|
27
|
+
title: Schema.String,
|
|
28
|
+
}).pipe(Type.makeObject(DXN.make('com.example.type.testItem2', '0.1.0')));
|
|
31
29
|
|
|
32
30
|
describe('AtomQuery', () => {
|
|
33
31
|
let testBuilder: EchoTestBuilder;
|
|
@@ -202,6 +200,55 @@ describe('AtomQuery', () => {
|
|
|
202
200
|
});
|
|
203
201
|
});
|
|
204
202
|
|
|
203
|
+
describe('AtomQuery with registry', () => {
|
|
204
|
+
let atomRegistry: Registry.Registry;
|
|
205
|
+
|
|
206
|
+
beforeEach(() => {
|
|
207
|
+
atomRegistry = Registry.make();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('AtomQuery.make memoizes per registry instance', () => {
|
|
211
|
+
const registry = makeRegistry({ initial: [TestItem] });
|
|
212
|
+
|
|
213
|
+
// Same registry + filter must yield the same atom instance. Otherwise reactive
|
|
214
|
+
// connectors re-create the atom on every recompute, which destabilizes the
|
|
215
|
+
// effect-atom dependency graph and loops synchronously (the type-create freeze).
|
|
216
|
+
const atom1 = AtomQuery.make(registry, Filter.type(Type.Type));
|
|
217
|
+
const atom2 = AtomQuery.make(registry, Filter.type(Type.Type));
|
|
218
|
+
expect(atom1).toBe(atom2);
|
|
219
|
+
|
|
220
|
+
// A different registry instance must yield a different atom.
|
|
221
|
+
const otherRegistry = makeRegistry({ initial: [TestItem] });
|
|
222
|
+
expect(AtomQuery.make(otherRegistry, Filter.type(Type.Type))).not.toBe(atom1);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('AtomQuery.make queries registry type entities', () => {
|
|
226
|
+
const registry = makeRegistry({ initial: [TestItem] });
|
|
227
|
+
|
|
228
|
+
const atom = AtomQuery.make(registry, Filter.type(Type.Type));
|
|
229
|
+
const results = atomRegistry.get(atom);
|
|
230
|
+
|
|
231
|
+
expect(results.map((type) => Type.getTypename(type))).toContain('com.example.type.testItem');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('AtomQuery.make updates when registry contents change', () => {
|
|
235
|
+
const registry = makeRegistry({ initial: [TestItem] });
|
|
236
|
+
|
|
237
|
+
const atom = AtomQuery.make(registry, Filter.type(Type.Type));
|
|
238
|
+
expect(atomRegistry.get(atom)).toHaveLength(1);
|
|
239
|
+
|
|
240
|
+
let updateCount = 0;
|
|
241
|
+
atomRegistry.subscribe(atom, () => {
|
|
242
|
+
updateCount++;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
registry.add([TestItem2]);
|
|
246
|
+
|
|
247
|
+
expect(updateCount).toBeGreaterThan(0);
|
|
248
|
+
expect(atomRegistry.get(atom)).toHaveLength(2);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
205
252
|
describe('AtomQuery with queues', () => {
|
|
206
253
|
let testBuilder: EchoTestBuilder;
|
|
207
254
|
let registry: Registry.Registry;
|
|
@@ -261,127 +308,3 @@ describe('AtomQuery with queues', () => {
|
|
|
261
308
|
expect(results[0].name).toEqual('jane');
|
|
262
309
|
});
|
|
263
310
|
});
|
|
264
|
-
|
|
265
|
-
const SchemaA = Schema.Struct({
|
|
266
|
-
name: Schema.String,
|
|
267
|
-
}).pipe(
|
|
268
|
-
Type.object({
|
|
269
|
-
typename: 'com.example.type.a',
|
|
270
|
-
version: '0.1.0',
|
|
271
|
-
}),
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
const SchemaB = Schema.Struct({
|
|
275
|
-
value: Schema.Number,
|
|
276
|
-
}).pipe(
|
|
277
|
-
Type.object({
|
|
278
|
-
typename: 'com.example.type.b',
|
|
279
|
-
version: '0.1.0',
|
|
280
|
-
}),
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
describe('AtomQuery.fromQuery with schema registry', () => {
|
|
284
|
-
let schemaRegistry: RuntimeSchemaRegistry;
|
|
285
|
-
let registry: Registry.Registry;
|
|
286
|
-
|
|
287
|
-
beforeEach(() => {
|
|
288
|
-
schemaRegistry = new RuntimeSchemaRegistry([]);
|
|
289
|
-
registry = Registry.make();
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
test('creates atom with initial results from schema query', async ({ expect }) => {
|
|
293
|
-
await schemaRegistry.register([SchemaA]);
|
|
294
|
-
|
|
295
|
-
const queryResult = schemaRegistry.query();
|
|
296
|
-
const atom = AtomQuery.fromQuery(queryResult);
|
|
297
|
-
const results = registry.get(atom);
|
|
298
|
-
|
|
299
|
-
expect(results).toHaveLength(1);
|
|
300
|
-
expect(Type.getTypename(results[0])).toBe('com.example.type.a');
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
test('atom updates when new schemas are registered', async ({ expect }) => {
|
|
304
|
-
await schemaRegistry.register([SchemaA]);
|
|
305
|
-
|
|
306
|
-
const queryResult = schemaRegistry.query();
|
|
307
|
-
const atom = AtomQuery.fromQuery(queryResult);
|
|
308
|
-
|
|
309
|
-
// Get initial results and subscribe.
|
|
310
|
-
const initialResults = registry.get(atom);
|
|
311
|
-
expect(initialResults).toHaveLength(1);
|
|
312
|
-
|
|
313
|
-
let updateCount = 0;
|
|
314
|
-
let latestResults: Type.AnyEntity[] = [];
|
|
315
|
-
registry.subscribe(atom, () => {
|
|
316
|
-
updateCount++;
|
|
317
|
-
latestResults = registry.get(atom);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// Allow reactive query to start (deferred via queueMicrotask).
|
|
321
|
-
await sleep(10);
|
|
322
|
-
|
|
323
|
-
// Register a new schema.
|
|
324
|
-
await schemaRegistry.register([SchemaB]);
|
|
325
|
-
|
|
326
|
-
expect(updateCount).toBeGreaterThan(0);
|
|
327
|
-
expect(latestResults).toHaveLength(2);
|
|
328
|
-
expect(latestResults.map(Type.getTypename).sort()).toEqual(['com.example.type.a', 'com.example.type.b']);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
test('atom works with empty initial results', ({ expect }) => {
|
|
332
|
-
const queryResult = schemaRegistry.query();
|
|
333
|
-
const atom = AtomQuery.fromQuery(queryResult);
|
|
334
|
-
const results = registry.get(atom);
|
|
335
|
-
|
|
336
|
-
expect(results).toHaveLength(0);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
test('atom with filtered query only reflects matching schemas', async ({ expect }) => {
|
|
340
|
-
const queryResult = schemaRegistry.query({ typename: 'com.example.type.a' });
|
|
341
|
-
const atom = AtomQuery.fromQuery(queryResult);
|
|
342
|
-
|
|
343
|
-
// Get initial (empty) results and subscribe.
|
|
344
|
-
const initialResults = registry.get(atom);
|
|
345
|
-
expect(initialResults).toHaveLength(0);
|
|
346
|
-
|
|
347
|
-
let latestResults: Type.AnyEntity[] = [];
|
|
348
|
-
registry.subscribe(atom, () => {
|
|
349
|
-
latestResults = registry.get(atom);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
await sleep(10);
|
|
353
|
-
|
|
354
|
-
// Register non-matching schema.
|
|
355
|
-
await schemaRegistry.register([SchemaB]);
|
|
356
|
-
// Results updated but still empty for this filter.
|
|
357
|
-
expect(latestResults).toHaveLength(0);
|
|
358
|
-
|
|
359
|
-
// Register matching schema.
|
|
360
|
-
await schemaRegistry.register([SchemaA]);
|
|
361
|
-
expect(latestResults).toHaveLength(1);
|
|
362
|
-
expect(Type.getTypename(latestResults[0])).toBe('com.example.type.a');
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
test('unsubscribing from atom stops updates', async ({ expect }) => {
|
|
366
|
-
const queryResult = schemaRegistry.query();
|
|
367
|
-
const atom = AtomQuery.fromQuery(queryResult);
|
|
368
|
-
|
|
369
|
-
registry.get(atom);
|
|
370
|
-
|
|
371
|
-
let updateCount = 0;
|
|
372
|
-
const unsubscribe = registry.subscribe(atom, () => {
|
|
373
|
-
updateCount++;
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
await sleep(10);
|
|
377
|
-
|
|
378
|
-
await schemaRegistry.register([SchemaA]);
|
|
379
|
-
const countAfterFirst = updateCount;
|
|
380
|
-
expect(countAfterFirst).toBeGreaterThan(0);
|
|
381
|
-
|
|
382
|
-
unsubscribe();
|
|
383
|
-
|
|
384
|
-
await schemaRegistry.register([SchemaB]);
|
|
385
|
-
expect(updateCount).toBe(countAfterFirst);
|
|
386
|
-
});
|
|
387
|
-
});
|
package/src/query-atom.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { Atom } from '@effect-atom/atom';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { Database, type Entity, type Filter, Query, type QueryResult, Registry, URI } from '@dxos/echo';
|
|
8
8
|
import { WeakDictionary } from '@dxos/util';
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -68,9 +68,13 @@ const getQueryableIdentifier = (queryable: Database.Queryable): string => {
|
|
|
68
68
|
if (Database.isDatabase(queryable)) {
|
|
69
69
|
return queryable.spaceId;
|
|
70
70
|
}
|
|
71
|
-
//
|
|
72
|
-
if (
|
|
73
|
-
return queryable.
|
|
71
|
+
// Registry: use its stable per-instance id.
|
|
72
|
+
if (Registry.isRegistry(queryable)) {
|
|
73
|
+
return queryable.id;
|
|
74
|
+
}
|
|
75
|
+
// Queue or similar: use uri if it's a URI (EID or DXN).
|
|
76
|
+
if ('uri' in queryable && URI.isURI(queryable.uri)) {
|
|
77
|
+
return queryable.uri;
|
|
74
78
|
}
|
|
75
79
|
// Fallback: use id if it's a string.
|
|
76
80
|
if ('id' in queryable && typeof queryable.id === 'string') {
|
package/src/ref-atom.test.ts
CHANGED
|
@@ -31,7 +31,7 @@ describe('AtomRef - Basic Functionality', () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
test('AtomRef.make returns target when ref is loaded', async () => {
|
|
34
|
-
|
|
34
|
+
db.graph.registry.add([TestSchema.Person]);
|
|
35
35
|
|
|
36
36
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
37
37
|
db.add(targetObj);
|
|
@@ -46,7 +46,7 @@ describe('AtomRef - Basic Functionality', () => {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
test('AtomRef.make does not subscribe to target changes (use AtomObj for reactive snapshots)', async () => {
|
|
49
|
-
|
|
49
|
+
db.graph.registry.add([TestSchema.Person]);
|
|
50
50
|
|
|
51
51
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
52
52
|
db.add(targetObj);
|
|
@@ -120,7 +120,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
test('AtomRef.make returns same atom instance for same ref', async () => {
|
|
123
|
-
|
|
123
|
+
db.graph.registry.add([TestSchema.Person]);
|
|
124
124
|
|
|
125
125
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
126
126
|
db.add(targetObj);
|
|
@@ -136,7 +136,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
136
136
|
});
|
|
137
137
|
|
|
138
138
|
test('AtomRef.make returns different atom instances for different refs', async () => {
|
|
139
|
-
|
|
139
|
+
db.graph.registry.add([TestSchema.Person]);
|
|
140
140
|
|
|
141
141
|
const targetObj1 = Obj.make(TestSchema.Person, {
|
|
142
142
|
name: 'Target1',
|
|
@@ -163,7 +163,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
test('AtomRef.make returns same atom for refs created separately to same target', async () => {
|
|
166
|
-
|
|
166
|
+
db.graph.registry.add([TestSchema.Person]);
|
|
167
167
|
|
|
168
168
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
169
169
|
db.add(targetObj);
|
|
@@ -188,7 +188,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
188
188
|
});
|
|
189
189
|
|
|
190
190
|
test('cached ref atoms return same instance after multiple retrievals', async () => {
|
|
191
|
-
|
|
191
|
+
db.graph.registry.add([TestSchema.Person]);
|
|
192
192
|
|
|
193
193
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
194
194
|
db.add(targetObj);
|