@dxos/echo-atom 0.0.0 → 0.8.4-main.1068cf700f
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 +214 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/types/src/atom.d.ts +41 -0
- package/dist/types/src/atom.d.ts.map +1 -0
- package/dist/types/src/atom.test.d.ts +2 -0
- package/dist/types/src/atom.test.d.ts.map +1 -0
- package/dist/types/src/batching.test.d.ts +2 -0
- package/dist/types/src/batching.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/query-atom.d.ts +24 -0
- package/dist/types/src/query-atom.d.ts.map +1 -0
- package/dist/types/src/query-atom.test.d.ts +2 -0
- package/dist/types/src/query-atom.test.d.ts.map +1 -0
- package/dist/types/src/reactivity.test.d.ts +2 -0
- package/dist/types/src/reactivity.test.d.ts.map +1 -0
- package/dist/types/src/ref-atom.d.ts +13 -0
- package/dist/types/src/ref-atom.d.ts.map +1 -0
- package/dist/types/src/ref-atom.test.d.ts +2 -0
- package/dist/types/src/ref-atom.test.d.ts.map +1 -0
- package/dist/types/src/ref-utils.d.ts +14 -0
- package/dist/types/src/ref-utils.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +17 -11
- package/src/atom.test.ts +249 -3
- package/src/atom.ts +163 -51
- package/src/query-atom.test.ts +63 -1
- package/src/query-atom.ts +7 -4
- package/src/reactivity.test.ts +48 -0
- package/src/ref-atom.test.ts +209 -0
- package/src/ref-atom.ts +17 -9
package/src/query-atom.ts
CHANGED
|
@@ -34,10 +34,13 @@ export const fromQuery = <T extends Entity.Unknown>(queryResult: QueryResult.Que
|
|
|
34
34
|
// Registry: key → Queryable (WeakRef with auto-cleanup when GC'd).
|
|
35
35
|
const queryableRegistry = new WeakDictionary<string, Database.Queryable>();
|
|
36
36
|
|
|
37
|
-
//
|
|
37
|
+
// Key separator that won't appear in identifiers (DXN strings use colons).
|
|
38
|
+
const KEY_SEPARATOR = '~';
|
|
39
|
+
|
|
40
|
+
// Atom.family keyed by "identifier\0serializedAST".
|
|
38
41
|
const queryFamily = Atom.family((key: string) => {
|
|
39
42
|
// Parse key outside Atom.make - runs once per key.
|
|
40
|
-
const separatorIndex = key.indexOf(
|
|
43
|
+
const separatorIndex = key.indexOf(KEY_SEPARATOR);
|
|
41
44
|
const identifier = key.slice(0, separatorIndex);
|
|
42
45
|
const serializedAst = key.slice(separatorIndex + 1);
|
|
43
46
|
|
|
@@ -114,8 +117,8 @@ const fromQueryable = <T extends Entity.Unknown>(
|
|
|
114
117
|
? queryOrFilter
|
|
115
118
|
: Query.select(queryOrFilter as Filter.Filter<T>);
|
|
116
119
|
|
|
117
|
-
// Build key: identifier
|
|
118
|
-
const key = `${identifier}
|
|
120
|
+
// Build key: identifier\0serializedAST (using null char as separator to avoid DXN colon conflicts).
|
|
121
|
+
const key = `${identifier}${KEY_SEPARATOR}${JSON.stringify(normalizedQuery.ast)}`;
|
|
119
122
|
|
|
120
123
|
return queryFamily(key) as Atom.Atom<T[]>;
|
|
121
124
|
};
|
package/src/reactivity.test.ts
CHANGED
|
@@ -155,4 +155,52 @@ describe('Echo Atom - Reactivity', () => {
|
|
|
155
155
|
expect(finalSnapshot.name).toBe('Updated');
|
|
156
156
|
expect(finalSnapshot.email).toBe('updated@example.com');
|
|
157
157
|
});
|
|
158
|
+
|
|
159
|
+
test('property mutation on standalone Obj.make object is synchronous', () => {
|
|
160
|
+
// Test objects created with just Obj.make() - no createObject/database.
|
|
161
|
+
const obj = Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' });
|
|
162
|
+
|
|
163
|
+
const actions: string[] = [];
|
|
164
|
+
const unsubscribe = Obj.subscribe(obj, () => {
|
|
165
|
+
actions.push('update');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
actions.push('before');
|
|
169
|
+
Obj.change(obj, (o) => {
|
|
170
|
+
o.name = 'Updated';
|
|
171
|
+
});
|
|
172
|
+
actions.push('after');
|
|
173
|
+
|
|
174
|
+
// Updates must be synchronous: before -> update -> after.
|
|
175
|
+
expect(actions).toEqual(['before', 'update', 'after']);
|
|
176
|
+
|
|
177
|
+
// Verify the property was modified.
|
|
178
|
+
expect(obj.name).toBe('Updated');
|
|
179
|
+
|
|
180
|
+
unsubscribe();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('array splice on standalone Obj.make object is synchronous', () => {
|
|
184
|
+
// Test objects created with just Obj.make() - no createObject/database.
|
|
185
|
+
const obj = Obj.make(TestSchema.Example, { stringArray: ['a', 'b', 'c', 'd'] });
|
|
186
|
+
|
|
187
|
+
const actions: string[] = [];
|
|
188
|
+
const unsubscribe = Obj.subscribe(obj, () => {
|
|
189
|
+
actions.push('update');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
actions.push('before');
|
|
193
|
+
Obj.change(obj, (o) => {
|
|
194
|
+
o.stringArray!.splice(1, 1);
|
|
195
|
+
});
|
|
196
|
+
actions.push('after');
|
|
197
|
+
|
|
198
|
+
// Updates must be synchronous: before -> update -> after.
|
|
199
|
+
expect(actions).toEqual(['before', 'update', 'after']);
|
|
200
|
+
|
|
201
|
+
// Verify the array was modified.
|
|
202
|
+
expect(obj.stringArray).toEqual(['a', 'c', 'd']);
|
|
203
|
+
|
|
204
|
+
unsubscribe();
|
|
205
|
+
});
|
|
158
206
|
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Registry from '@effect-atom/atom/Registry';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { Obj, Ref } from '@dxos/echo';
|
|
9
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
10
|
+
import { type EchoDatabase } from '@dxos/echo-db';
|
|
11
|
+
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
12
|
+
|
|
13
|
+
import * as AtomRef from './ref-atom';
|
|
14
|
+
|
|
15
|
+
describe('AtomRef - Basic Functionality', () => {
|
|
16
|
+
let testBuilder: EchoTestBuilder;
|
|
17
|
+
let db: EchoDatabase;
|
|
18
|
+
let registry: Registry.Registry;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
testBuilder = await new EchoTestBuilder().open();
|
|
22
|
+
const { db: database } = await testBuilder.createDatabase();
|
|
23
|
+
db = database;
|
|
24
|
+
registry = Registry.make();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await testBuilder.close();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('AtomRef.make returns target when ref is loaded', async () => {
|
|
32
|
+
await db.graph.schemaRegistry.register([TestSchema.Person]);
|
|
33
|
+
|
|
34
|
+
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
35
|
+
db.add(targetObj);
|
|
36
|
+
await db.flush({ indexes: true });
|
|
37
|
+
|
|
38
|
+
const ref = Ref.make(targetObj);
|
|
39
|
+
const atom = AtomRef.make(ref);
|
|
40
|
+
|
|
41
|
+
// Should return the target object (as a snapshot).
|
|
42
|
+
const result = registry.get(atom);
|
|
43
|
+
expect(result?.name).toBe('Target');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('AtomRef.make does not subscribe to target changes (use AtomObj for reactive snapshots)', async () => {
|
|
47
|
+
await db.graph.schemaRegistry.register([TestSchema.Person]);
|
|
48
|
+
|
|
49
|
+
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
50
|
+
db.add(targetObj);
|
|
51
|
+
await db.flush({ indexes: true });
|
|
52
|
+
|
|
53
|
+
const ref = Ref.make(targetObj);
|
|
54
|
+
const atom = AtomRef.make(ref);
|
|
55
|
+
|
|
56
|
+
// Subscribe to updates.
|
|
57
|
+
let updateCount = 0;
|
|
58
|
+
registry.subscribe(atom, () => updateCount++, { immediate: true });
|
|
59
|
+
|
|
60
|
+
expect(updateCount).toBe(1);
|
|
61
|
+
|
|
62
|
+
// Mutate target - ref atom does NOT react to this.
|
|
63
|
+
Obj.change(targetObj, (o) => {
|
|
64
|
+
o.name = 'Updated';
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Update count should still be 1 - ref atom doesn't subscribe to target changes.
|
|
68
|
+
expect(updateCount).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('AtomRef - Referential Equality', () => {
|
|
73
|
+
let testBuilder: EchoTestBuilder;
|
|
74
|
+
let db: EchoDatabase;
|
|
75
|
+
let registry: Registry.Registry;
|
|
76
|
+
|
|
77
|
+
beforeEach(async () => {
|
|
78
|
+
testBuilder = await new EchoTestBuilder().open();
|
|
79
|
+
const { db: database } = await testBuilder.createDatabase();
|
|
80
|
+
db = database;
|
|
81
|
+
registry = Registry.make();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(async () => {
|
|
85
|
+
await testBuilder.close();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('AtomRef.make returns same atom instance for same ref', async () => {
|
|
89
|
+
await db.graph.schemaRegistry.register([TestSchema.Person]);
|
|
90
|
+
|
|
91
|
+
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
92
|
+
db.add(targetObj);
|
|
93
|
+
await db.flush({ indexes: true });
|
|
94
|
+
|
|
95
|
+
const ref = Ref.make(targetObj);
|
|
96
|
+
|
|
97
|
+
const atom1 = AtomRef.make(ref);
|
|
98
|
+
const atom2 = AtomRef.make(ref);
|
|
99
|
+
|
|
100
|
+
// Same ref should return the exact same atom instance.
|
|
101
|
+
expect(atom1).toBe(atom2);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('AtomRef.make returns different atom instances for different refs', async () => {
|
|
105
|
+
await db.graph.schemaRegistry.register([TestSchema.Person]);
|
|
106
|
+
|
|
107
|
+
const targetObj1 = Obj.make(TestSchema.Person, {
|
|
108
|
+
name: 'Target1',
|
|
109
|
+
username: 'target1',
|
|
110
|
+
email: 'target1@example.com',
|
|
111
|
+
});
|
|
112
|
+
const targetObj2 = Obj.make(TestSchema.Person, {
|
|
113
|
+
name: 'Target2',
|
|
114
|
+
username: 'target2',
|
|
115
|
+
email: 'target2@example.com',
|
|
116
|
+
});
|
|
117
|
+
db.add(targetObj1);
|
|
118
|
+
db.add(targetObj2);
|
|
119
|
+
await db.flush({ indexes: true });
|
|
120
|
+
|
|
121
|
+
const ref1 = Ref.make(targetObj1);
|
|
122
|
+
const ref2 = Ref.make(targetObj2);
|
|
123
|
+
|
|
124
|
+
const atom1 = AtomRef.make(ref1);
|
|
125
|
+
const atom2 = AtomRef.make(ref2);
|
|
126
|
+
|
|
127
|
+
// Different refs should return different atom instances.
|
|
128
|
+
expect(atom1).not.toBe(atom2);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('AtomRef.make returns same atom for refs created separately to same target', async () => {
|
|
132
|
+
await db.graph.schemaRegistry.register([TestSchema.Person]);
|
|
133
|
+
|
|
134
|
+
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
135
|
+
db.add(targetObj);
|
|
136
|
+
await db.flush({ indexes: true });
|
|
137
|
+
|
|
138
|
+
// Create two separate refs to the same target.
|
|
139
|
+
const ref1 = Ref.make(targetObj);
|
|
140
|
+
const ref2 = Ref.make(targetObj);
|
|
141
|
+
|
|
142
|
+
// Refs are different objects (not referentially equal).
|
|
143
|
+
expect(ref1).not.toBe(ref2);
|
|
144
|
+
|
|
145
|
+
const atom1 = AtomRef.make(ref1);
|
|
146
|
+
const atom2 = AtomRef.make(ref2);
|
|
147
|
+
|
|
148
|
+
// Refs with the same DXN resolve to the same atom via Hash/Equal traits.
|
|
149
|
+
expect(atom1).toBe(atom2);
|
|
150
|
+
|
|
151
|
+
// Both atoms should return the same target data.
|
|
152
|
+
expect(registry.get(atom1)?.name).toBe('Target');
|
|
153
|
+
expect(registry.get(atom2)?.name).toBe('Target');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('cached ref atoms return same instance after multiple retrievals', async () => {
|
|
157
|
+
await db.graph.schemaRegistry.register([TestSchema.Person]);
|
|
158
|
+
|
|
159
|
+
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
160
|
+
db.add(targetObj);
|
|
161
|
+
await db.flush({ indexes: true });
|
|
162
|
+
|
|
163
|
+
const ref = Ref.make(targetObj);
|
|
164
|
+
|
|
165
|
+
// Get the same atom multiple times.
|
|
166
|
+
const atom1 = AtomRef.make(ref);
|
|
167
|
+
const atom2 = AtomRef.make(ref);
|
|
168
|
+
const atom3 = AtomRef.make(ref);
|
|
169
|
+
|
|
170
|
+
// All should be the same instance.
|
|
171
|
+
expect(atom1).toBe(atom2);
|
|
172
|
+
expect(atom2).toBe(atom3);
|
|
173
|
+
|
|
174
|
+
// All should return the same target value.
|
|
175
|
+
expect(registry.get(atom1)?.name).toBe('Target');
|
|
176
|
+
expect(registry.get(atom2)?.name).toBe('Target');
|
|
177
|
+
expect(registry.get(atom3)?.name).toBe('Target');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('AtomRef - Expando Objects', () => {
|
|
182
|
+
let testBuilder: EchoTestBuilder;
|
|
183
|
+
let db: EchoDatabase;
|
|
184
|
+
let registry: Registry.Registry;
|
|
185
|
+
|
|
186
|
+
beforeEach(async () => {
|
|
187
|
+
testBuilder = await new EchoTestBuilder().open();
|
|
188
|
+
const { db: database } = await testBuilder.createDatabase();
|
|
189
|
+
db = database;
|
|
190
|
+
registry = Registry.make();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
afterEach(async () => {
|
|
194
|
+
await testBuilder.close();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('works with Expando objects', async () => {
|
|
198
|
+
const targetObj = Obj.make(TestSchema.Expando, { name: 'Expando Target', value: 42 });
|
|
199
|
+
db.add(targetObj);
|
|
200
|
+
await db.flush({ indexes: true });
|
|
201
|
+
|
|
202
|
+
const ref = Ref.make(targetObj);
|
|
203
|
+
const atom = AtomRef.make(ref);
|
|
204
|
+
|
|
205
|
+
const result = registry.get(atom);
|
|
206
|
+
expect(result?.name).toBe('Expando Target');
|
|
207
|
+
expect(result?.value).toBe(42);
|
|
208
|
+
});
|
|
209
|
+
});
|
package/src/ref-atom.ts
CHANGED
|
@@ -4,21 +4,29 @@
|
|
|
4
4
|
|
|
5
5
|
import * as Atom from '@effect-atom/atom/Atom';
|
|
6
6
|
|
|
7
|
-
import { type
|
|
7
|
+
import { type Ref } from '@dxos/echo';
|
|
8
8
|
|
|
9
9
|
import { loadRefTarget } from './ref-utils';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* Atom family for ECHO refs.
|
|
13
|
+
* Uses ref reference as key - same ref returns same atom.
|
|
13
14
|
* This atom only updates once when the ref loads - it does not subscribe to object changes.
|
|
14
|
-
* Use AtomObj.make with a ref if you need reactive snapshots.
|
|
15
|
+
* Use AtomObj.make with a ref if you need reactive snapshots of ECHO objects.
|
|
15
16
|
*/
|
|
16
|
-
|
|
17
|
-
if (!ref) {
|
|
18
|
-
return Atom.make<T | undefined>(() => undefined);
|
|
19
|
-
}
|
|
20
|
-
|
|
17
|
+
const refFamily = Atom.family(<T>(ref: Ref.Ref<T>): Atom.Atom<T | undefined> => {
|
|
21
18
|
return Atom.make<T | undefined>((get) => {
|
|
22
19
|
return loadRefTarget(ref, get, (target) => target);
|
|
23
20
|
});
|
|
24
|
-
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a read-only atom for a reference target.
|
|
25
|
+
* Returns undefined if the target hasn't loaded yet.
|
|
26
|
+
* Updates when the ref loads but does NOT subscribe to target object changes.
|
|
27
|
+
* Use AtomObj.make with a ref if you need reactive snapshots of ECHO objects.
|
|
28
|
+
* Uses Atom.family internally - same ref reference returns same atom instance.
|
|
29
|
+
*
|
|
30
|
+
* Supports refs to any target type including ECHO objects and Queues.
|
|
31
|
+
*/
|
|
32
|
+
export const make = refFamily;
|