@dxos/echo-atom 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef
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 +10 -1
- 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/src/ref-utils.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -11
- package/src/atom.test.ts +9 -9
- package/src/atom.ts +4 -2
- package/src/batching.test.ts +14 -14
- package/src/reactivity.test.ts +15 -15
- package/src/ref-atom.test.ts +36 -2
- 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.abd8ff62ef",
|
|
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",
|
|
@@ -20,25 +20,22 @@
|
|
|
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
28
|
"@effect-atom/atom": "^0.5.1",
|
|
32
|
-
"@dxos/echo-db": "0.8.4-main.
|
|
33
|
-
"@dxos/
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/util": "0.8.4-main.
|
|
29
|
+
"@dxos/echo-db": "0.8.4-main.abd8ff62ef",
|
|
30
|
+
"@dxos/invariant": "0.8.4-main.abd8ff62ef",
|
|
31
|
+
"@dxos/echo": "0.8.4-main.abd8ff62ef",
|
|
32
|
+
"@dxos/util": "0.8.4-main.abd8ff62ef"
|
|
36
33
|
},
|
|
37
34
|
"devDependencies": {
|
|
38
35
|
"effect": "3.20.0",
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/
|
|
36
|
+
"@dxos/context": "0.8.4-main.abd8ff62ef",
|
|
37
|
+
"@dxos/random": "0.8.4-main.abd8ff62ef",
|
|
38
|
+
"@dxos/test-utils": "0.8.4-main.abd8ff62ef"
|
|
42
39
|
},
|
|
43
40
|
"peerDependencies": {
|
|
44
41
|
"effect": "3.20.0"
|
package/src/atom.test.ts
CHANGED
|
@@ -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,8 +72,8 @@ describe('Echo Atom - Basic Functionality', () => {
|
|
|
72
72
|
{ immediate: true },
|
|
73
73
|
);
|
|
74
74
|
|
|
75
|
-
// Mutate object via Obj.
|
|
76
|
-
Obj.
|
|
75
|
+
// Mutate object via Obj.update.
|
|
76
|
+
Obj.update(obj, (obj) => {
|
|
77
77
|
obj.name = 'Updated';
|
|
78
78
|
});
|
|
79
79
|
|
|
@@ -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,8 +104,8 @@ describe('Echo Atom - Basic Functionality', () => {
|
|
|
104
104
|
{ immediate: true },
|
|
105
105
|
);
|
|
106
106
|
|
|
107
|
-
// Update through Obj.
|
|
108
|
-
Obj.
|
|
107
|
+
// Update through Obj.update.
|
|
108
|
+
Obj.update(obj, (obj) => {
|
|
109
109
|
obj.title = (obj.title ?? '') + ' Updated';
|
|
110
110
|
});
|
|
111
111
|
|
|
@@ -140,7 +140,7 @@ describe('Echo Atom - Basic Functionality', () => {
|
|
|
140
140
|
expect(propertyUpdateCount).toBe(1);
|
|
141
141
|
|
|
142
142
|
// Mutate the standalone object.
|
|
143
|
-
Obj.
|
|
143
|
+
Obj.update(obj, (obj) => {
|
|
144
144
|
obj.name = 'Updated Standalone';
|
|
145
145
|
});
|
|
146
146
|
|
|
@@ -244,7 +244,7 @@ describe('Echo Atom - Referential Equality', () => {
|
|
|
244
244
|
expect(updateCount).toBe(1);
|
|
245
245
|
|
|
246
246
|
// Mutate the object.
|
|
247
|
-
Obj.
|
|
247
|
+
Obj.update(obj, (obj) => {
|
|
248
248
|
obj.name = 'Updated';
|
|
249
249
|
});
|
|
250
250
|
|
|
@@ -276,7 +276,7 @@ describe('Echo Atom - Referential Equality', () => {
|
|
|
276
276
|
expect(updateCount).toBe(1);
|
|
277
277
|
|
|
278
278
|
// Mutate the specific property.
|
|
279
|
-
Obj.
|
|
279
|
+
Obj.update(obj, (obj) => {
|
|
280
280
|
obj.name = 'Updated';
|
|
281
281
|
});
|
|
282
282
|
|
package/src/atom.ts
CHANGED
|
@@ -188,7 +188,9 @@ const refWithReactiveFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T
|
|
|
188
188
|
const effect = (get: Atom.Context) =>
|
|
189
189
|
Effect.gen(function* () {
|
|
190
190
|
const snapshot = get(make(ref));
|
|
191
|
-
if (snapshot == null)
|
|
191
|
+
if (snapshot == null) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
192
194
|
const option = yield* Obj.getReactiveOption(snapshot);
|
|
193
195
|
return Option.getOrElse(option, () => undefined);
|
|
194
196
|
});
|
|
@@ -202,7 +204,7 @@ const refWithReactiveFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T
|
|
|
202
204
|
/**
|
|
203
205
|
* Like {@link make} but returns the live reactive object instead of a snapshot.
|
|
204
206
|
* 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.
|
|
207
|
+
* Prefer {@link make} (snapshot) unless you need the live Obj.Obj for generic mutations (e.g. Obj.update).
|
|
206
208
|
*
|
|
207
209
|
* @param objOrRef - The reactive object or ref.
|
|
208
210
|
* @returns An atom that returns the live object. Returns undefined for refs (async loading) or undefined input.
|
package/src/batching.test.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { TestSchema } from '@dxos/echo/testing';
|
|
|
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.
|
|
36
|
+
// Make multiple updates to the same object in a single Obj.update call.
|
|
37
|
+
Obj.update(obj, (obj) => {
|
|
38
38
|
obj.name = 'Updated1';
|
|
39
39
|
obj.email = 'updated@example.com';
|
|
40
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.
|
|
74
|
+
// Make multiple separate Obj.update calls.
|
|
75
|
+
Obj.update(obj, (obj) => {
|
|
76
76
|
obj.name = 'Updated1';
|
|
77
77
|
});
|
|
78
|
-
Obj.
|
|
78
|
+
Obj.update(obj, (obj) => {
|
|
79
79
|
obj.email = 'updated@example.com';
|
|
80
80
|
});
|
|
81
|
-
Obj.
|
|
81
|
+
Obj.update(obj, (obj) => {
|
|
82
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.
|
|
116
|
+
// Make multiple updates to the same property in a single Obj.update call.
|
|
117
|
+
Obj.update(obj, (obj) => {
|
|
118
118
|
obj.name = 'Updated1';
|
|
119
119
|
obj.name = 'Updated2';
|
|
120
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/reactivity.test.ts
CHANGED
|
@@ -28,8 +28,8 @@ 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.
|
|
31
|
+
// Update the object via Obj.update.
|
|
32
|
+
Obj.update(obj, (obj) => {
|
|
33
33
|
obj.name = 'Updated';
|
|
34
34
|
});
|
|
35
35
|
|
|
@@ -50,8 +50,8 @@ 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.
|
|
53
|
+
// Update the property via Obj.update.
|
|
54
|
+
Obj.update(obj, (obj) => {
|
|
55
55
|
obj.name = 'Updated';
|
|
56
56
|
});
|
|
57
57
|
|
|
@@ -82,8 +82,8 @@ describe('Echo Atom - Reactivity', () => {
|
|
|
82
82
|
emailUpdateCount++;
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
// Update only email property via Obj.
|
|
86
|
-
Obj.
|
|
85
|
+
// Update only email property via Obj.update.
|
|
86
|
+
Obj.update(obj, (obj) => {
|
|
87
87
|
obj.email = 'updated@example.com';
|
|
88
88
|
});
|
|
89
89
|
|
|
@@ -109,8 +109,8 @@ describe('Echo Atom - Reactivity', () => {
|
|
|
109
109
|
registry.subscribe(nameAtom, () => {});
|
|
110
110
|
registry.subscribe(emailAtom, () => {});
|
|
111
111
|
|
|
112
|
-
// Update multiple properties via Obj.
|
|
113
|
-
Obj.
|
|
112
|
+
// Update multiple properties via Obj.update.
|
|
113
|
+
Obj.update(obj, (obj) => {
|
|
114
114
|
obj.name = 'Updated';
|
|
115
115
|
obj.email = 'updated@example.com';
|
|
116
116
|
});
|
|
@@ -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.
|
|
143
|
+
// Update object via Obj.update.
|
|
144
|
+
Obj.update(obj, (obj) => {
|
|
145
145
|
obj.name = 'Updated';
|
|
146
146
|
});
|
|
147
|
-
Obj.
|
|
147
|
+
Obj.update(obj, (obj) => {
|
|
148
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,7 +167,7 @@ describe('Echo Atom - Reactivity', () => {
|
|
|
167
167
|
});
|
|
168
168
|
|
|
169
169
|
actions.push('before');
|
|
170
|
-
Obj.
|
|
170
|
+
Obj.update(obj, (obj) => {
|
|
171
171
|
obj.name = 'Updated';
|
|
172
172
|
});
|
|
173
173
|
actions.push('after');
|
|
@@ -191,7 +191,7 @@ describe('Echo Atom - Reactivity', () => {
|
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
actions.push('before');
|
|
194
|
-
Obj.
|
|
194
|
+
Obj.update(obj, (obj) => {
|
|
195
195
|
obj.stringArray!.splice(1, 1);
|
|
196
196
|
});
|
|
197
197
|
actions.push('after');
|
|
@@ -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';
|
|
8
|
+
import { Filter, Obj, Ref } from '@dxos/echo';
|
|
9
9
|
import { type EchoDatabase } from '@dxos/echo-db';
|
|
10
10
|
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
11
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', () => {
|
|
@@ -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.
|
|
65
|
+
Obj.update(targetObj, (targetObj) => {
|
|
64
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', () => {
|
package/src/ref-utils.ts
CHANGED
|
@@ -21,19 +21,34 @@ export const loadRefTarget = <T, R>(
|
|
|
21
21
|
get: Atom.Context,
|
|
22
22
|
onTargetAvailable: (target: T) => R,
|
|
23
23
|
): R | undefined => {
|
|
24
|
+
// Accessing `ref.target` registers a resolution callback when the target is
|
|
25
|
+
// not yet loaded, so resolution can be observed via `ref.onResolved` below.
|
|
24
26
|
const currentTarget = ref.target;
|
|
25
27
|
if (currentTarget) {
|
|
26
28
|
return onTargetAvailable(currentTarget);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
//
|
|
31
|
+
// Subscribe to the ref's resolution event in case the target loads later
|
|
32
|
+
// (e.g. when a sibling client creates the linked object). Without this,
|
|
33
|
+
// a one-shot async load that fails because the document hasn't propagated
|
|
34
|
+
// would leave the atom permanently undefined.
|
|
35
|
+
const unsubscribe = ref.onResolved(() => {
|
|
36
|
+
const target = ref.target;
|
|
37
|
+
if (target) {
|
|
38
|
+
get.setSelf(onTargetAvailable(target));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
get.addFinalizer(unsubscribe);
|
|
42
|
+
|
|
43
|
+
// Also try async load (e.g. for objects that need disk loading).
|
|
30
44
|
void ref
|
|
31
45
|
.load()
|
|
32
46
|
.then((loadedTarget) => {
|
|
33
47
|
get.setSelf(onTargetAvailable(loadedTarget));
|
|
34
48
|
})
|
|
35
49
|
.catch(() => {
|
|
36
|
-
// Loading failed
|
|
50
|
+
// Loading failed; the resolution subscription above will pick up
|
|
51
|
+
// cross-client updates when they arrive.
|
|
37
52
|
});
|
|
38
53
|
|
|
39
54
|
return undefined;
|