@dxos/echo-atom 0.8.4-main.2244d791bb → 0.8.4-main.3fbcb4aa9b
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/README.md +28 -18
- package/dist/lib/neutral/index.mjs +18 -10
- package/dist/lib/neutral/index.mjs.map +3 -3
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/types/src/atom.d.ts +4 -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 +10 -14
- package/src/atom.test.ts +18 -18
- package/src/atom.ts +18 -14
- package/src/batching.test.ts +24 -24
- package/src/query-atom.test.ts +138 -12
- package/src/query-atom.ts +5 -10
- package/src/reactivity.test.ts +25 -25
- package/src/ref-atom.test.ts +45 -11
- package/src/ref-utils.ts +17 -2
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', () => {
|
|
@@ -33,7 +35,7 @@ describe('AtomRef - Basic Functionality', () => {
|
|
|
33
35
|
|
|
34
36
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
35
37
|
db.add(targetObj);
|
|
36
|
-
await db.flush(
|
|
38
|
+
await db.flush();
|
|
37
39
|
|
|
38
40
|
const ref = Ref.make(targetObj);
|
|
39
41
|
const atom = AtomRef.make(ref);
|
|
@@ -48,7 +50,7 @@ describe('AtomRef - Basic Functionality', () => {
|
|
|
48
50
|
|
|
49
51
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
50
52
|
db.add(targetObj);
|
|
51
|
-
await db.flush(
|
|
53
|
+
await db.flush();
|
|
52
54
|
|
|
53
55
|
const ref = Ref.make(targetObj);
|
|
54
56
|
const atom = AtomRef.make(ref);
|
|
@@ -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', () => {
|
|
@@ -90,7 +124,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
90
124
|
|
|
91
125
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
92
126
|
db.add(targetObj);
|
|
93
|
-
await db.flush(
|
|
127
|
+
await db.flush();
|
|
94
128
|
|
|
95
129
|
const ref = Ref.make(targetObj);
|
|
96
130
|
|
|
@@ -116,7 +150,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
116
150
|
});
|
|
117
151
|
db.add(targetObj1);
|
|
118
152
|
db.add(targetObj2);
|
|
119
|
-
await db.flush(
|
|
153
|
+
await db.flush();
|
|
120
154
|
|
|
121
155
|
const ref1 = Ref.make(targetObj1);
|
|
122
156
|
const ref2 = Ref.make(targetObj2);
|
|
@@ -133,7 +167,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
133
167
|
|
|
134
168
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
135
169
|
db.add(targetObj);
|
|
136
|
-
await db.flush(
|
|
170
|
+
await db.flush();
|
|
137
171
|
|
|
138
172
|
// Create two separate refs to the same target.
|
|
139
173
|
const ref1 = Ref.make(targetObj);
|
|
@@ -158,7 +192,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
158
192
|
|
|
159
193
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
160
194
|
db.add(targetObj);
|
|
161
|
-
await db.flush(
|
|
195
|
+
await db.flush();
|
|
162
196
|
|
|
163
197
|
const ref = Ref.make(targetObj);
|
|
164
198
|
|
|
@@ -197,7 +231,7 @@ describe('AtomRef - Expando Objects', () => {
|
|
|
197
231
|
test('works with Expando objects', async () => {
|
|
198
232
|
const targetObj = Obj.make(TestSchema.Expando, { name: 'Expando Target', value: 42 });
|
|
199
233
|
db.add(targetObj);
|
|
200
|
-
await db.flush(
|
|
234
|
+
await db.flush();
|
|
201
235
|
|
|
202
236
|
const ref = Ref.make(targetObj);
|
|
203
237
|
const atom = AtomRef.make(ref);
|
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;
|