@dxos/echo-atom 0.8.4-main.d05673bc65 → 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/LICENSE +102 -5
- package/dist/lib/neutral/index.mjs +20 -9
- package/dist/lib/neutral/index.mjs.map +3 -3
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/types/src/atom.d.ts +2 -2
- 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/src/ref-utils.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -14
- package/src/atom.test.ts +15 -15
- package/src/atom.ts +11 -5
- package/src/batching.test.ts +24 -24
- package/src/query-atom.test.ts +58 -11
- package/src/query-atom.ts +12 -13
- package/src/reactivity.test.ts +25 -25
- package/src/ref-atom.test.ts +44 -10
- package/src/ref-utils.ts +17 -2
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",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/dxos/dxos"
|
|
10
10
|
},
|
|
11
|
-
"license": "
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
12
12
|
"author": "DXOS.org",
|
|
13
13
|
"sideEffects": false,
|
|
14
14
|
"type": "module",
|
|
@@ -20,27 +20,25 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"types": "dist/types/src/index.d.ts",
|
|
23
|
-
"typesVersions": {
|
|
24
|
-
"*": {}
|
|
25
|
-
},
|
|
26
23
|
"files": [
|
|
27
24
|
"dist",
|
|
28
25
|
"src"
|
|
29
26
|
],
|
|
30
27
|
"dependencies": {
|
|
31
|
-
"@effect-atom/atom": "^0.5.
|
|
32
|
-
"@dxos/echo": "0.8.4-main.
|
|
33
|
-
"@dxos/util": "0.8.4-main.
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/
|
|
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"
|
|
36
33
|
},
|
|
37
34
|
"devDependencies": {
|
|
38
|
-
"effect": "3.
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@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"
|
|
41
39
|
},
|
|
42
40
|
"peerDependencies": {
|
|
43
|
-
"effect": "3.
|
|
41
|
+
"effect": "3.21.2"
|
|
44
42
|
},
|
|
45
43
|
"publishConfig": {
|
|
46
44
|
"access": "public"
|
package/src/atom.test.ts
CHANGED
|
@@ -6,9 +6,9 @@ import * as Registry from '@effect-atom/atom/Registry';
|
|
|
6
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';
|
|
11
10
|
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
11
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
12
12
|
|
|
13
13
|
import * as AtomObj from './atom';
|
|
14
14
|
|
|
@@ -55,7 +55,7 @@ describe('Echo Atom - Basic Functionality', () => {
|
|
|
55
55
|
expect(registry.get(emailAtom)).toBe('test@example.com');
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
test('atom updates when object is mutated via Obj.
|
|
58
|
+
test('atom updates when object is mutated via Obj.update', () => {
|
|
59
59
|
const obj = createObject(
|
|
60
60
|
Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
|
|
61
61
|
);
|
|
@@ -72,9 +72,9 @@ describe('Echo Atom - Basic Functionality', () => {
|
|
|
72
72
|
{ immediate: true },
|
|
73
73
|
);
|
|
74
74
|
|
|
75
|
-
// Mutate object via Obj.
|
|
76
|
-
Obj.
|
|
77
|
-
|
|
75
|
+
// Mutate object via Obj.update.
|
|
76
|
+
Obj.update(obj, (obj) => {
|
|
77
|
+
obj.name = 'Updated';
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
// Subscription should have fired: immediate + update.
|
|
@@ -85,7 +85,7 @@ describe('Echo Atom - Basic Functionality', () => {
|
|
|
85
85
|
expect(obj.name).toBe('Updated');
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
test('property atom supports updater pattern via Obj.
|
|
88
|
+
test('property atom supports updater pattern via Obj.update', () => {
|
|
89
89
|
const obj = createObject(
|
|
90
90
|
Obj.make(TestSchema.Task, {
|
|
91
91
|
title: 'Task',
|
|
@@ -104,9 +104,9 @@ describe('Echo Atom - Basic Functionality', () => {
|
|
|
104
104
|
{ immediate: true },
|
|
105
105
|
);
|
|
106
106
|
|
|
107
|
-
// Update through Obj.
|
|
108
|
-
Obj.
|
|
109
|
-
|
|
107
|
+
// Update through Obj.update.
|
|
108
|
+
Obj.update(obj, (obj) => {
|
|
109
|
+
obj.title = (obj.title ?? '') + ' Updated';
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
// Subscription should have fired: immediate + update.
|
|
@@ -140,8 +140,8 @@ describe('Echo Atom - Basic Functionality', () => {
|
|
|
140
140
|
expect(propertyUpdateCount).toBe(1);
|
|
141
141
|
|
|
142
142
|
// Mutate the standalone object.
|
|
143
|
-
Obj.
|
|
144
|
-
|
|
143
|
+
Obj.update(obj, (obj) => {
|
|
144
|
+
obj.name = 'Updated Standalone';
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
// Both atoms should have received updates.
|
|
@@ -244,8 +244,8 @@ describe('Echo Atom - Referential Equality', () => {
|
|
|
244
244
|
expect(updateCount).toBe(1);
|
|
245
245
|
|
|
246
246
|
// Mutate the object.
|
|
247
|
-
Obj.
|
|
248
|
-
|
|
247
|
+
Obj.update(obj, (obj) => {
|
|
248
|
+
obj.name = 'Updated';
|
|
249
249
|
});
|
|
250
250
|
|
|
251
251
|
// The subscription should still work.
|
|
@@ -276,8 +276,8 @@ describe('Echo Atom - Referential Equality', () => {
|
|
|
276
276
|
expect(updateCount).toBe(1);
|
|
277
277
|
|
|
278
278
|
// Mutate the specific property.
|
|
279
|
-
Obj.
|
|
280
|
-
|
|
279
|
+
Obj.update(obj, (obj) => {
|
|
280
|
+
obj.name = 'Updated';
|
|
281
281
|
});
|
|
282
282
|
|
|
283
283
|
// The subscription should still work.
|
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);
|
|
@@ -161,7 +166,6 @@ export function makeProperty<T extends Obj.Unknown, K extends keyof T>(
|
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
assertArgument(Obj.isObject(obj), 'obj', 'Object must be a reactive object');
|
|
164
|
-
assertArgument(key in obj, 'key', 'Property must exist on object');
|
|
165
169
|
return propertyFamily(obj)(key);
|
|
166
170
|
}
|
|
167
171
|
|
|
@@ -189,7 +193,9 @@ const refWithReactiveFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T
|
|
|
189
193
|
const effect = (get: Atom.Context) =>
|
|
190
194
|
Effect.gen(function* () {
|
|
191
195
|
const snapshot = get(make(ref));
|
|
192
|
-
if (snapshot == null)
|
|
196
|
+
if (snapshot == null) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
193
199
|
const option = yield* Obj.getReactiveOption(snapshot);
|
|
194
200
|
return Option.getOrElse(option, () => undefined);
|
|
195
201
|
});
|
|
@@ -203,7 +209,7 @@ const refWithReactiveFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T
|
|
|
203
209
|
/**
|
|
204
210
|
* Like {@link make} but returns the live reactive object instead of a snapshot.
|
|
205
211
|
* Same input: Obj or Ref.Ref. Same output shape: Atom that updates when the object mutates.
|
|
206
|
-
* Prefer {@link make} (snapshot) unless you need the live Obj.Obj for generic mutations (e.g. Obj.
|
|
212
|
+
* Prefer {@link make} (snapshot) unless you need the live Obj.Obj for generic mutations (e.g. Obj.update).
|
|
207
213
|
*
|
|
208
214
|
* @param objOrRef - The reactive object or ref.
|
|
209
215
|
* @returns An atom that returns the live object. Returns undefined for refs (async loading) or undefined input.
|
package/src/batching.test.ts
CHANGED
|
@@ -6,13 +6,13 @@ 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
|
|
|
14
14
|
describe('Echo Atom - Batch Updates', () => {
|
|
15
|
-
test('multiple updates to same object atom in single Obj.
|
|
15
|
+
test('multiple updates to same object atom in single Obj.update fire single update', () => {
|
|
16
16
|
const obj = createObject(
|
|
17
17
|
Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
|
|
18
18
|
);
|
|
@@ -33,14 +33,14 @@ describe('Echo Atom - Batch Updates', () => {
|
|
|
33
33
|
const initialCount = updateCount;
|
|
34
34
|
expect(initialCount).toBe(1); // Verify immediate update fired.
|
|
35
35
|
|
|
36
|
-
// Make multiple updates to the same object in a single Obj.
|
|
37
|
-
Obj.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
// Make multiple updates to the same object in a single Obj.update call.
|
|
37
|
+
Obj.update(obj, (obj) => {
|
|
38
|
+
obj.name = 'Updated1';
|
|
39
|
+
obj.email = 'updated@example.com';
|
|
40
|
+
obj.username = 'updated';
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
// Should have fired once for initial + once for the Obj.
|
|
43
|
+
// Should have fired once for initial + once for the Obj.update (not once per property update).
|
|
44
44
|
expect(updateCount).toBe(2);
|
|
45
45
|
|
|
46
46
|
// Verify final state.
|
|
@@ -50,7 +50,7 @@ describe('Echo Atom - Batch Updates', () => {
|
|
|
50
50
|
expect(finalValue.username).toBe('updated');
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
test('multiple separate Obj.
|
|
53
|
+
test('multiple separate Obj.update calls fire separate updates', () => {
|
|
54
54
|
const obj = createObject(
|
|
55
55
|
Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
|
|
56
56
|
);
|
|
@@ -71,18 +71,18 @@ describe('Echo Atom - Batch Updates', () => {
|
|
|
71
71
|
const initialCount = updateCount;
|
|
72
72
|
expect(initialCount).toBe(1);
|
|
73
73
|
|
|
74
|
-
// Make multiple separate Obj.
|
|
75
|
-
Obj.
|
|
76
|
-
|
|
74
|
+
// Make multiple separate Obj.update calls.
|
|
75
|
+
Obj.update(obj, (obj) => {
|
|
76
|
+
obj.name = 'Updated1';
|
|
77
77
|
});
|
|
78
|
-
Obj.
|
|
79
|
-
|
|
78
|
+
Obj.update(obj, (obj) => {
|
|
79
|
+
obj.email = 'updated@example.com';
|
|
80
80
|
});
|
|
81
|
-
Obj.
|
|
82
|
-
|
|
81
|
+
Obj.update(obj, (obj) => {
|
|
82
|
+
obj.username = 'updated';
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
// Should have fired once for initial + once per Obj.
|
|
85
|
+
// Should have fired once for initial + once per Obj.update call.
|
|
86
86
|
expect(updateCount).toBe(4);
|
|
87
87
|
|
|
88
88
|
// Verify final state.
|
|
@@ -92,7 +92,7 @@ describe('Echo Atom - Batch Updates', () => {
|
|
|
92
92
|
expect(finalValue.username).toBe('updated');
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
test('multiple updates to same property atom in single Obj.
|
|
95
|
+
test('multiple updates to same property atom in single Obj.update fire single update', () => {
|
|
96
96
|
const obj = createObject(
|
|
97
97
|
Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
|
|
98
98
|
);
|
|
@@ -113,14 +113,14 @@ describe('Echo Atom - Batch Updates', () => {
|
|
|
113
113
|
const initialCount = updateCount;
|
|
114
114
|
expect(initialCount).toBe(1);
|
|
115
115
|
|
|
116
|
-
// Make multiple updates to the same property in a single Obj.
|
|
117
|
-
Obj.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
// Make multiple updates to the same property in a single Obj.update call.
|
|
117
|
+
Obj.update(obj, (obj) => {
|
|
118
|
+
obj.name = 'Updated1';
|
|
119
|
+
obj.name = 'Updated2';
|
|
120
|
+
obj.name = 'Updated3';
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
-
// Should have fired once for initial + once for the Obj.
|
|
123
|
+
// Should have fired once for initial + once for the Obj.update (not once per assignment).
|
|
124
124
|
expect(updateCount).toBe(2);
|
|
125
125
|
|
|
126
126
|
// Verify final state.
|
package/src/query-atom.test.ts
CHANGED
|
@@ -6,11 +6,10 @@ 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 { Obj, type QueryResult, Type } from '@dxos/echo';
|
|
10
|
-
import {
|
|
11
|
-
import { TestSchema } from '@dxos/echo/testing';
|
|
12
|
-
import { type EchoDatabase } 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';
|
|
13
11
|
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
12
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
14
13
|
import { SpaceId } from '@dxos/keys';
|
|
15
14
|
|
|
16
15
|
import * as AtomQuery from './query-atom';
|
|
@@ -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;
|
package/src/query-atom.ts
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
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
|
/**
|
|
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).
|
|
@@ -73,9 +68,13 @@ const getQueryableIdentifier = (queryable: Database.Queryable): string => {
|
|
|
73
68
|
if (Database.isDatabase(queryable)) {
|
|
74
69
|
return queryable.spaceId;
|
|
75
70
|
}
|
|
76
|
-
//
|
|
77
|
-
if (
|
|
78
|
-
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;
|
|
79
78
|
}
|
|
80
79
|
// Fallback: use id if it's a string.
|
|
81
80
|
if ('id' in queryable && typeof queryable.id === 'string') {
|
package/src/reactivity.test.ts
CHANGED
|
@@ -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
|
import { arrayMove } from '@dxos/util';
|
|
12
12
|
|
|
13
13
|
import * as AtomObj from './atom';
|
|
@@ -28,9 +28,9 @@ describe('Echo Atom - Reactivity', () => {
|
|
|
28
28
|
// Subscribe to enable reactivity.
|
|
29
29
|
registry.subscribe(atom, () => {});
|
|
30
30
|
|
|
31
|
-
// Update the object via Obj.
|
|
32
|
-
Obj.
|
|
33
|
-
|
|
31
|
+
// Update the object via Obj.update.
|
|
32
|
+
Obj.update(obj, (obj) => {
|
|
33
|
+
obj.name = 'Updated';
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
const updatedSnapshot = registry.get(atom);
|
|
@@ -50,9 +50,9 @@ describe('Echo Atom - Reactivity', () => {
|
|
|
50
50
|
// Subscribe to enable reactivity.
|
|
51
51
|
registry.subscribe(atom, () => {});
|
|
52
52
|
|
|
53
|
-
// Update the property via Obj.
|
|
54
|
-
Obj.
|
|
55
|
-
|
|
53
|
+
// Update the property via Obj.update.
|
|
54
|
+
Obj.update(obj, (obj) => {
|
|
55
|
+
obj.name = 'Updated';
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
expect(registry.get(atom)).toBe('Updated');
|
|
@@ -82,9 +82,9 @@ describe('Echo Atom - Reactivity', () => {
|
|
|
82
82
|
emailUpdateCount++;
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
// Update only email property via Obj.
|
|
86
|
-
Obj.
|
|
87
|
-
|
|
85
|
+
// Update only email property via Obj.update.
|
|
86
|
+
Obj.update(obj, (obj) => {
|
|
87
|
+
obj.email = 'updated@example.com';
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
// Name atom should NOT have changed.
|
|
@@ -109,10 +109,10 @@ describe('Echo Atom - Reactivity', () => {
|
|
|
109
109
|
registry.subscribe(nameAtom, () => {});
|
|
110
110
|
registry.subscribe(emailAtom, () => {});
|
|
111
111
|
|
|
112
|
-
// Update multiple properties via Obj.
|
|
113
|
-
Obj.
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
// Update multiple properties via Obj.update.
|
|
113
|
+
Obj.update(obj, (obj) => {
|
|
114
|
+
obj.name = 'Updated';
|
|
115
|
+
obj.email = 'updated@example.com';
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
expect(registry.get(nameAtom)).toBe('Updated');
|
|
@@ -140,15 +140,15 @@ describe('Echo Atom - Reactivity', () => {
|
|
|
140
140
|
const initialCount = updateCount;
|
|
141
141
|
expect(initialCount).toBe(1);
|
|
142
142
|
|
|
143
|
-
// Update object via Obj.
|
|
144
|
-
Obj.
|
|
145
|
-
|
|
143
|
+
// Update object via Obj.update.
|
|
144
|
+
Obj.update(obj, (obj) => {
|
|
145
|
+
obj.name = 'Updated';
|
|
146
146
|
});
|
|
147
|
-
Obj.
|
|
148
|
-
|
|
147
|
+
Obj.update(obj, (obj) => {
|
|
148
|
+
obj.email = 'updated@example.com';
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
-
// Updates fire through Obj.subscribe (one per Obj.
|
|
151
|
+
// Updates fire through Obj.subscribe (one per Obj.update call).
|
|
152
152
|
expect(updateCount).toBe(initialCount + 2);
|
|
153
153
|
|
|
154
154
|
// Verify final state - returns snapshot (plain object).
|
|
@@ -167,8 +167,8 @@ describe('Echo Atom - Reactivity', () => {
|
|
|
167
167
|
});
|
|
168
168
|
|
|
169
169
|
actions.push('before');
|
|
170
|
-
Obj.
|
|
171
|
-
|
|
170
|
+
Obj.update(obj, (obj) => {
|
|
171
|
+
obj.name = 'Updated';
|
|
172
172
|
});
|
|
173
173
|
actions.push('after');
|
|
174
174
|
|
|
@@ -191,8 +191,8 @@ describe('Echo Atom - Reactivity', () => {
|
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
actions.push('before');
|
|
194
|
-
Obj.
|
|
195
|
-
|
|
194
|
+
Obj.update(obj, (obj) => {
|
|
195
|
+
obj.stringArray!.splice(1, 1);
|
|
196
196
|
});
|
|
197
197
|
actions.push('after');
|
|
198
198
|
|
|
@@ -222,7 +222,7 @@ describe('Echo Atom - Reactivity', () => {
|
|
|
222
222
|
});
|
|
223
223
|
|
|
224
224
|
// Reorder in place (e.g. move first to last).
|
|
225
|
-
Obj.
|
|
225
|
+
Obj.update(obj, (obj) => {
|
|
226
226
|
arrayMove(obj.stringArray!, 0, 2);
|
|
227
227
|
});
|
|
228
228
|
|
package/src/ref-atom.test.ts
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
import * as Registry from '@effect-atom/atom/Registry';
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
7
7
|
|
|
8
|
-
import { Obj, Ref } from '@dxos/echo';
|
|
9
|
-
import { TestSchema } from '@dxos/echo/testing';
|
|
8
|
+
import { Filter, Obj, Ref } from '@dxos/echo';
|
|
10
9
|
import { type EchoDatabase } from '@dxos/echo-db';
|
|
11
10
|
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
11
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
12
|
+
import { PublicKey } from '@dxos/keys';
|
|
12
13
|
|
|
14
|
+
import * as AtomObj from './atom';
|
|
13
15
|
import * as AtomRef from './ref-atom';
|
|
14
16
|
|
|
15
17
|
describe('AtomRef - Basic Functionality', () => {
|
|
@@ -29,7 +31,7 @@ describe('AtomRef - Basic Functionality', () => {
|
|
|
29
31
|
});
|
|
30
32
|
|
|
31
33
|
test('AtomRef.make returns target when ref is loaded', async () => {
|
|
32
|
-
|
|
34
|
+
db.graph.registry.add([TestSchema.Person]);
|
|
33
35
|
|
|
34
36
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
35
37
|
db.add(targetObj);
|
|
@@ -44,7 +46,7 @@ describe('AtomRef - Basic Functionality', () => {
|
|
|
44
46
|
});
|
|
45
47
|
|
|
46
48
|
test('AtomRef.make does not subscribe to target changes (use AtomObj for reactive snapshots)', async () => {
|
|
47
|
-
|
|
49
|
+
db.graph.registry.add([TestSchema.Person]);
|
|
48
50
|
|
|
49
51
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
50
52
|
db.add(targetObj);
|
|
@@ -60,13 +62,45 @@ describe('AtomRef - Basic Functionality', () => {
|
|
|
60
62
|
expect(updateCount).toBe(1);
|
|
61
63
|
|
|
62
64
|
// Mutate target - ref atom does NOT react to this.
|
|
63
|
-
Obj.
|
|
64
|
-
|
|
65
|
+
Obj.update(targetObj, (targetObj) => {
|
|
66
|
+
targetObj.name = 'Updated';
|
|
65
67
|
});
|
|
66
68
|
|
|
67
69
|
// Update count should still be 1 - ref atom doesn't subscribe to target changes.
|
|
68
70
|
expect(updateCount).toBe(1);
|
|
69
71
|
});
|
|
72
|
+
|
|
73
|
+
// Sibling client (sharing services) creates a brand-new ref target. The atom
|
|
74
|
+
// must update once the target's document propagates and resolves.
|
|
75
|
+
test('atom resolves target created by sibling client', async () => {
|
|
76
|
+
const [spaceKey] = PublicKey.randomSequence();
|
|
77
|
+
await using peer = await testBuilder.createPeer({
|
|
78
|
+
types: [TestSchema.Person, TestSchema.Container],
|
|
79
|
+
});
|
|
80
|
+
await using db1 = await peer.createDatabase(spaceKey);
|
|
81
|
+
const parent1 = db1.add(Obj.make(TestSchema.Container, { objects: [] }));
|
|
82
|
+
await db1.flush();
|
|
83
|
+
|
|
84
|
+
await using client2 = await peer.createClient();
|
|
85
|
+
await using db2 = await peer.openDatabase(spaceKey, db1.rootUrl!, { client: client2 });
|
|
86
|
+
const [parent2] = await db2.query(Filter.id(parent1.id)).run();
|
|
87
|
+
|
|
88
|
+
const newPerson = db2.add(
|
|
89
|
+
Obj.make(TestSchema.Person, { name: 'Alice', username: 'alice', email: 'alice@example.com' }),
|
|
90
|
+
);
|
|
91
|
+
Obj.update(parent2, (parent2) => {
|
|
92
|
+
parent2.objects = [...(parent2.objects ?? []), Ref.make(newPerson)];
|
|
93
|
+
});
|
|
94
|
+
await db2.flush();
|
|
95
|
+
|
|
96
|
+
await expect.poll(() => (parent1.objects ?? []).length).toBeGreaterThan(0);
|
|
97
|
+
const atom = AtomObj.make(parent1.objects![0]);
|
|
98
|
+
|
|
99
|
+
let lastValue: any;
|
|
100
|
+
registry.subscribe(atom, (value) => (lastValue = value), { immediate: true });
|
|
101
|
+
|
|
102
|
+
await expect.poll(() => lastValue?.name).toBe('Alice');
|
|
103
|
+
});
|
|
70
104
|
});
|
|
71
105
|
|
|
72
106
|
describe('AtomRef - Referential Equality', () => {
|
|
@@ -86,7 +120,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
86
120
|
});
|
|
87
121
|
|
|
88
122
|
test('AtomRef.make returns same atom instance for same ref', async () => {
|
|
89
|
-
|
|
123
|
+
db.graph.registry.add([TestSchema.Person]);
|
|
90
124
|
|
|
91
125
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
92
126
|
db.add(targetObj);
|
|
@@ -102,7 +136,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
102
136
|
});
|
|
103
137
|
|
|
104
138
|
test('AtomRef.make returns different atom instances for different refs', async () => {
|
|
105
|
-
|
|
139
|
+
db.graph.registry.add([TestSchema.Person]);
|
|
106
140
|
|
|
107
141
|
const targetObj1 = Obj.make(TestSchema.Person, {
|
|
108
142
|
name: 'Target1',
|
|
@@ -129,7 +163,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
129
163
|
});
|
|
130
164
|
|
|
131
165
|
test('AtomRef.make returns same atom for refs created separately to same target', async () => {
|
|
132
|
-
|
|
166
|
+
db.graph.registry.add([TestSchema.Person]);
|
|
133
167
|
|
|
134
168
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
135
169
|
db.add(targetObj);
|
|
@@ -154,7 +188,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
154
188
|
});
|
|
155
189
|
|
|
156
190
|
test('cached ref atoms return same instance after multiple retrievals', async () => {
|
|
157
|
-
|
|
191
|
+
db.graph.registry.add([TestSchema.Person]);
|
|
158
192
|
|
|
159
193
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
160
194
|
db.add(targetObj);
|