@dxos/echo-atom 0.8.4-main.3eb6e50203 → 0.8.4-main.40e3dcdf1b
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 +7 -8
- package/dist/lib/neutral/index.mjs.map +3 -3
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/types/src/atom.d.ts +3 -1
- package/dist/types/src/atom.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -9
- package/src/atom.test.ts +3 -3
- package/src/atom.ts +15 -13
- package/src/query-atom.test.ts +12 -11
- package/src/query-atom.ts +1 -1
- package/src/reactivity.test.ts +27 -0
- package/src/ref-atom.test.ts +7 -7
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.40e3dcdf1b",
|
|
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",
|
|
@@ -29,17 +29,15 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@effect-atom/atom": "^0.5.1",
|
|
32
|
-
"
|
|
33
|
-
"@dxos/echo": "0.8.4-main.
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/
|
|
36
|
-
"@dxos/util": "0.8.4-main.3eb6e50203"
|
|
32
|
+
"@dxos/echo": "0.8.4-main.40e3dcdf1b",
|
|
33
|
+
"@dxos/echo-db": "0.8.4-main.40e3dcdf1b",
|
|
34
|
+
"@dxos/invariant": "0.8.4-main.40e3dcdf1b",
|
|
35
|
+
"@dxos/util": "0.8.4-main.40e3dcdf1b"
|
|
37
36
|
},
|
|
38
37
|
"devDependencies": {
|
|
39
|
-
"@types/lodash.isequal": "^4.5.0",
|
|
40
38
|
"effect": "3.19.16",
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
39
|
+
"@dxos/random": "0.8.4-main.40e3dcdf1b",
|
|
40
|
+
"@dxos/test-utils": "0.8.4-main.40e3dcdf1b"
|
|
43
41
|
},
|
|
44
42
|
"peerDependencies": {
|
|
45
43
|
"effect": "3.19.16"
|
package/src/atom.test.ts
CHANGED
|
@@ -357,7 +357,7 @@ describe('AtomObj.makeWithReactive', () => {
|
|
|
357
357
|
tasks: [Ref.make(task)],
|
|
358
358
|
}),
|
|
359
359
|
);
|
|
360
|
-
await db.flush(
|
|
360
|
+
await db.flush();
|
|
361
361
|
|
|
362
362
|
const ref = person.tasks![0];
|
|
363
363
|
const registry = Registry.make();
|
|
@@ -381,7 +381,7 @@ describe('AtomObj.makeWithReactive', () => {
|
|
|
381
381
|
tasks: [Ref.make(task)],
|
|
382
382
|
}),
|
|
383
383
|
);
|
|
384
|
-
await db.flush(
|
|
384
|
+
await db.flush();
|
|
385
385
|
|
|
386
386
|
const ref = person.tasks![0];
|
|
387
387
|
const registry = Registry.make();
|
|
@@ -390,7 +390,7 @@ describe('AtomObj.makeWithReactive', () => {
|
|
|
390
390
|
expect(registry.get(atom)).toBe(task);
|
|
391
391
|
|
|
392
392
|
db.remove(task);
|
|
393
|
-
await db.flush(
|
|
393
|
+
await db.flush();
|
|
394
394
|
|
|
395
395
|
expect(registry.get(atom)).toBeUndefined();
|
|
396
396
|
});
|
package/src/atom.ts
CHANGED
|
@@ -7,9 +7,8 @@ import * as Result from '@effect-atom/atom/Result';
|
|
|
7
7
|
import * as Effect from 'effect/Effect';
|
|
8
8
|
import * as Function from 'effect/Function';
|
|
9
9
|
import * as Option from 'effect/Option';
|
|
10
|
-
import isEqual from 'lodash.isequal';
|
|
11
10
|
|
|
12
|
-
import { Obj, Ref } from '@dxos/echo';
|
|
11
|
+
import { type Entity, Obj, Ref, Relation } from '@dxos/echo';
|
|
13
12
|
import { assertArgument } from '@dxos/invariant';
|
|
14
13
|
|
|
15
14
|
import { loadRefTarget } from './ref-utils';
|
|
@@ -27,7 +26,7 @@ const objectFamily = Atom.family(<T extends Obj.Unknown>(obj: T): Atom.Atom<Obj.
|
|
|
27
26
|
get.addFinalizer(() => unsubscribe());
|
|
28
27
|
|
|
29
28
|
return Obj.getSnapshot(obj);
|
|
30
|
-
});
|
|
29
|
+
}).pipe(Atom.keepAlive);
|
|
31
30
|
});
|
|
32
31
|
|
|
33
32
|
/**
|
|
@@ -52,7 +51,7 @@ const refFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T>): Atom.Ato
|
|
|
52
51
|
});
|
|
53
52
|
|
|
54
53
|
return loadRefTarget(ref, get, setupTargetSubscription);
|
|
55
|
-
});
|
|
54
|
+
}).pipe(Atom.keepAlive);
|
|
56
55
|
});
|
|
57
56
|
|
|
58
57
|
/**
|
|
@@ -84,7 +83,7 @@ const propertyFamily = Atom.family(<T extends Obj.Unknown>(obj: T) =>
|
|
|
84
83
|
|
|
85
84
|
const unsubscribe = Obj.subscribe(obj, () => {
|
|
86
85
|
const newValue = obj[key];
|
|
87
|
-
if (
|
|
86
|
+
if (previousSnapshot !== newValue) {
|
|
88
87
|
previousSnapshot = snapshotForComparison(newValue);
|
|
89
88
|
// Return a snapshot copy so React sees a new reference.
|
|
90
89
|
get.setSelf(snapshotForComparison(newValue));
|
|
@@ -95,7 +94,7 @@ const propertyFamily = Atom.family(<T extends Obj.Unknown>(obj: T) =>
|
|
|
95
94
|
|
|
96
95
|
// Return a snapshot copy so React sees a new reference.
|
|
97
96
|
return snapshotForComparison(obj[key]);
|
|
98
|
-
});
|
|
97
|
+
}).pipe(Atom.keepAlive);
|
|
99
98
|
}),
|
|
100
99
|
);
|
|
101
100
|
|
|
@@ -111,27 +110,30 @@ const propertyFamily = Atom.family(<T extends Obj.Unknown>(obj: T) =>
|
|
|
111
110
|
* @returns An atom that returns the object snapshot (plain data). Returns undefined only for refs (async loading) or undefined input.
|
|
112
111
|
*/
|
|
113
112
|
export function make<T extends Obj.Unknown>(obj: T): Atom.Atom<Obj.Snapshot<T>>;
|
|
113
|
+
export function make<T extends Relation.Unknown>(relation: T): Atom.Atom<Relation.Snapshot<T>>;
|
|
114
|
+
export function make<T extends Entity.Unknown>(entity: T): Atom.Atom<Entity.Snapshot>;
|
|
114
115
|
export function make<T extends Obj.Unknown>(ref: Ref.Ref<T>): Atom.Atom<Obj.Snapshot<T> | undefined>;
|
|
115
116
|
export function make<T extends Obj.Unknown>(
|
|
116
117
|
objOrRef: T | Ref.Ref<T> | undefined,
|
|
117
118
|
): Atom.Atom<Obj.Snapshot<T> | undefined>;
|
|
118
|
-
export function make<T extends
|
|
119
|
+
export function make<T extends Entity.Unknown>(
|
|
119
120
|
objOrRef: T | Ref.Ref<T> | undefined,
|
|
120
|
-
): Atom.Atom<
|
|
121
|
+
): Atom.Atom<Entity.Snapshot | undefined> {
|
|
121
122
|
if (objOrRef === undefined) {
|
|
122
|
-
return Atom.make<
|
|
123
|
+
return Atom.make<Entity.Snapshot | undefined>(() => undefined);
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
// Handle Ref inputs.
|
|
126
127
|
if (Ref.isRef(objOrRef)) {
|
|
127
|
-
return refFamily(objOrRef as
|
|
128
|
+
return refFamily(objOrRef as any);
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
// At this point, objOrRef is definitely T (not a Ref).
|
|
131
132
|
const obj = objOrRef as T;
|
|
132
|
-
assertArgument(Obj.isObject(obj), 'obj', 'Object must be a reactive object');
|
|
133
|
+
assertArgument(Obj.isObject(obj) || Relation.isRelation(obj), 'obj', 'Object must be a reactive object');
|
|
133
134
|
|
|
134
|
-
|
|
135
|
+
// TODO(dmaretskyi): Fix echo types during review.
|
|
136
|
+
return objectFamily(obj as any);
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
/**
|
|
@@ -176,7 +178,7 @@ const objectWithReactiveFamily = Atom.family(<T extends Obj.Unknown>(obj: T): At
|
|
|
176
178
|
get.addFinalizer(() => unsubscribe());
|
|
177
179
|
|
|
178
180
|
return obj;
|
|
179
|
-
});
|
|
181
|
+
}).pipe(Atom.keepAlive);
|
|
180
182
|
});
|
|
181
183
|
|
|
182
184
|
/**
|
package/src/query-atom.test.ts
CHANGED
|
@@ -7,8 +7,9 @@ import * as Schema from 'effect/Schema';
|
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
8
8
|
|
|
9
9
|
import { Obj, type QueryResult, Type } from '@dxos/echo';
|
|
10
|
+
import { Filter, Query } from '@dxos/echo';
|
|
10
11
|
import { TestSchema } from '@dxos/echo/testing';
|
|
11
|
-
import { type EchoDatabase
|
|
12
|
+
import { type EchoDatabase } from '@dxos/echo-db';
|
|
12
13
|
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
13
14
|
import { SpaceId } from '@dxos/keys';
|
|
14
15
|
|
|
@@ -22,7 +23,7 @@ const TestItem = Schema.Struct({
|
|
|
22
23
|
value: Schema.Number,
|
|
23
24
|
}).pipe(
|
|
24
25
|
Type.object({
|
|
25
|
-
typename: 'example.
|
|
26
|
+
typename: 'com.example.type.test-item',
|
|
26
27
|
version: '0.1.0',
|
|
27
28
|
}),
|
|
28
29
|
);
|
|
@@ -47,7 +48,7 @@ describe('AtomQuery', () => {
|
|
|
47
48
|
test('creates atom with initial results', async () => {
|
|
48
49
|
db.add(Obj.make(TestItem, { name: 'Object 1', value: 100 }));
|
|
49
50
|
db.add(Obj.make(TestItem, { name: 'Object 2', value: 100 }));
|
|
50
|
-
await db.flush(
|
|
51
|
+
await db.flush();
|
|
51
52
|
|
|
52
53
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
53
54
|
Query.select(Filter.type(TestItem, { value: 100 })),
|
|
@@ -63,7 +64,7 @@ describe('AtomQuery', () => {
|
|
|
63
64
|
|
|
64
65
|
test('registry.subscribe fires on QueryResult changes', async () => {
|
|
65
66
|
db.add(Obj.make(TestItem, { name: 'Initial', value: 200 }));
|
|
66
|
-
await db.flush(
|
|
67
|
+
await db.flush();
|
|
67
68
|
|
|
68
69
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
69
70
|
Query.select(Filter.type(TestItem, { value: 200 })),
|
|
@@ -86,7 +87,7 @@ describe('AtomQuery', () => {
|
|
|
86
87
|
|
|
87
88
|
// Add a new object that matches the query.
|
|
88
89
|
db.add(Obj.make(TestItem, { name: 'New Object', value: 200 }));
|
|
89
|
-
await db.flush({
|
|
90
|
+
await db.flush({ updates: true });
|
|
90
91
|
|
|
91
92
|
// Subscription should have fired.
|
|
92
93
|
expect(updateCount).toBeGreaterThan(0);
|
|
@@ -96,7 +97,7 @@ describe('AtomQuery', () => {
|
|
|
96
97
|
test('registry.subscribe fires when objects are removed', async () => {
|
|
97
98
|
const obj1 = db.add(Obj.make(TestItem, { name: 'Object 1', value: 300 }));
|
|
98
99
|
db.add(Obj.make(TestItem, { name: 'Object 2', value: 300 }));
|
|
99
|
-
await db.flush(
|
|
100
|
+
await db.flush();
|
|
100
101
|
|
|
101
102
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
102
103
|
Query.select(Filter.type(TestItem, { value: 300 })),
|
|
@@ -119,7 +120,7 @@ describe('AtomQuery', () => {
|
|
|
119
120
|
|
|
120
121
|
// Remove an object.
|
|
121
122
|
db.remove(obj1);
|
|
122
|
-
await db.flush({
|
|
123
|
+
await db.flush({ updates: true });
|
|
123
124
|
|
|
124
125
|
// Subscription should have fired.
|
|
125
126
|
expect(updateCount).toBeGreaterThan(0);
|
|
@@ -129,7 +130,7 @@ describe('AtomQuery', () => {
|
|
|
129
130
|
|
|
130
131
|
test('unsubscribing from registry stops receiving updates', async () => {
|
|
131
132
|
db.add(Obj.make(TestItem, { name: 'Initial', value: 400 }));
|
|
132
|
-
await db.flush(
|
|
133
|
+
await db.flush();
|
|
133
134
|
|
|
134
135
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
135
136
|
Query.select(Filter.type(TestItem, { value: 400 })),
|
|
@@ -150,7 +151,7 @@ describe('AtomQuery', () => {
|
|
|
150
151
|
|
|
151
152
|
// Add object and verify subscription fires.
|
|
152
153
|
db.add(Obj.make(TestItem, { name: 'Object 2', value: 400 }));
|
|
153
|
-
await db.flush({
|
|
154
|
+
await db.flush({ updates: true });
|
|
154
155
|
const countAfterFirstAdd = updateCount;
|
|
155
156
|
expect(countAfterFirstAdd).toBeGreaterThan(0);
|
|
156
157
|
|
|
@@ -159,7 +160,7 @@ describe('AtomQuery', () => {
|
|
|
159
160
|
|
|
160
161
|
// Add another object.
|
|
161
162
|
db.add(Obj.make(TestItem, { name: 'Object 3', value: 400 }));
|
|
162
|
-
await db.flush({
|
|
163
|
+
await db.flush({ updates: true });
|
|
163
164
|
|
|
164
165
|
// Update count should not have changed after unsubscribe.
|
|
165
166
|
expect(updateCount).toBe(countAfterFirstAdd);
|
|
@@ -179,7 +180,7 @@ describe('AtomQuery', () => {
|
|
|
179
180
|
|
|
180
181
|
test('multiple atoms from same query share underlying subscription', async () => {
|
|
181
182
|
db.add(Obj.make(TestItem, { name: 'Object', value: 500 }));
|
|
182
|
-
await db.flush(
|
|
183
|
+
await db.flush();
|
|
183
184
|
|
|
184
185
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
185
186
|
Query.select(Filter.type(TestItem, { value: 500 })),
|
package/src/query-atom.ts
CHANGED
|
@@ -37,7 +37,7 @@ const queryableRegistry = new WeakDictionary<string, Database.Queryable>();
|
|
|
37
37
|
// Key separator that won't appear in identifiers (DXN strings use colons).
|
|
38
38
|
const KEY_SEPARATOR = '~';
|
|
39
39
|
|
|
40
|
-
// Atom.family keyed by "identifier
|
|
40
|
+
// Atom.family keyed by "identifier~serializedAST".
|
|
41
41
|
const queryFamily = Atom.family((key: string) => {
|
|
42
42
|
// Parse key outside Atom.make - runs once per key.
|
|
43
43
|
const separatorIndex = key.indexOf(KEY_SEPARATOR);
|
package/src/reactivity.test.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { describe, expect, test } from 'vitest';
|
|
|
8
8
|
import { Obj } from '@dxos/echo';
|
|
9
9
|
import { TestSchema } from '@dxos/echo/testing';
|
|
10
10
|
import { createObject } from '@dxos/echo-db';
|
|
11
|
+
import { arrayMove } from '@dxos/util';
|
|
11
12
|
|
|
12
13
|
import * as AtomObj from './atom';
|
|
13
14
|
|
|
@@ -203,4 +204,30 @@ describe('Echo Atom - Reactivity', () => {
|
|
|
203
204
|
|
|
204
205
|
unsubscribe();
|
|
205
206
|
});
|
|
207
|
+
|
|
208
|
+
test('property atom for array property updates when array is reordered in place', () => {
|
|
209
|
+
// Verifies that makeProperty(obj, 'columns')-style atoms subscribe to in-place
|
|
210
|
+
// array mutations (e.g. arrayMove), so UI stays in sync after column reorder.
|
|
211
|
+
const obj = createObject(Obj.make(TestSchema.Example, { stringArray: ['a', 'b', 'c'] }));
|
|
212
|
+
|
|
213
|
+
const registry = Registry.make();
|
|
214
|
+
const atom = AtomObj.makeProperty(obj, 'stringArray');
|
|
215
|
+
|
|
216
|
+
const initial = registry.get(atom);
|
|
217
|
+
expect(initial).toEqual(['a', 'b', 'c']);
|
|
218
|
+
|
|
219
|
+
let updateCount = 0;
|
|
220
|
+
registry.subscribe(atom, () => {
|
|
221
|
+
updateCount++;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Reorder in place (e.g. move first to last).
|
|
225
|
+
Obj.change(obj, (obj) => {
|
|
226
|
+
arrayMove(obj.stringArray!, 0, 2);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(updateCount).toBe(1);
|
|
230
|
+
const afterReorder = registry.get(atom);
|
|
231
|
+
expect(afterReorder).toEqual(['b', 'c', 'a']);
|
|
232
|
+
});
|
|
206
233
|
});
|
package/src/ref-atom.test.ts
CHANGED
|
@@ -33,7 +33,7 @@ describe('AtomRef - Basic Functionality', () => {
|
|
|
33
33
|
|
|
34
34
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
35
35
|
db.add(targetObj);
|
|
36
|
-
await db.flush(
|
|
36
|
+
await db.flush();
|
|
37
37
|
|
|
38
38
|
const ref = Ref.make(targetObj);
|
|
39
39
|
const atom = AtomRef.make(ref);
|
|
@@ -48,7 +48,7 @@ describe('AtomRef - Basic Functionality', () => {
|
|
|
48
48
|
|
|
49
49
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
50
50
|
db.add(targetObj);
|
|
51
|
-
await db.flush(
|
|
51
|
+
await db.flush();
|
|
52
52
|
|
|
53
53
|
const ref = Ref.make(targetObj);
|
|
54
54
|
const atom = AtomRef.make(ref);
|
|
@@ -90,7 +90,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
90
90
|
|
|
91
91
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
92
92
|
db.add(targetObj);
|
|
93
|
-
await db.flush(
|
|
93
|
+
await db.flush();
|
|
94
94
|
|
|
95
95
|
const ref = Ref.make(targetObj);
|
|
96
96
|
|
|
@@ -116,7 +116,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
116
116
|
});
|
|
117
117
|
db.add(targetObj1);
|
|
118
118
|
db.add(targetObj2);
|
|
119
|
-
await db.flush(
|
|
119
|
+
await db.flush();
|
|
120
120
|
|
|
121
121
|
const ref1 = Ref.make(targetObj1);
|
|
122
122
|
const ref2 = Ref.make(targetObj2);
|
|
@@ -133,7 +133,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
133
133
|
|
|
134
134
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
135
135
|
db.add(targetObj);
|
|
136
|
-
await db.flush(
|
|
136
|
+
await db.flush();
|
|
137
137
|
|
|
138
138
|
// Create two separate refs to the same target.
|
|
139
139
|
const ref1 = Ref.make(targetObj);
|
|
@@ -158,7 +158,7 @@ describe('AtomRef - Referential Equality', () => {
|
|
|
158
158
|
|
|
159
159
|
const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
|
|
160
160
|
db.add(targetObj);
|
|
161
|
-
await db.flush(
|
|
161
|
+
await db.flush();
|
|
162
162
|
|
|
163
163
|
const ref = Ref.make(targetObj);
|
|
164
164
|
|
|
@@ -197,7 +197,7 @@ describe('AtomRef - Expando Objects', () => {
|
|
|
197
197
|
test('works with Expando objects', async () => {
|
|
198
198
|
const targetObj = Obj.make(TestSchema.Expando, { name: 'Expando Target', value: 42 });
|
|
199
199
|
db.add(targetObj);
|
|
200
|
-
await db.flush(
|
|
200
|
+
await db.flush();
|
|
201
201
|
|
|
202
202
|
const ref = Ref.make(targetObj);
|
|
203
203
|
const atom = AtomRef.make(ref);
|