@dxos/echo-atom 0.8.4-main.3eb6e50203 → 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 +19 -12
- 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 +19 -16
- 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 +51 -24
- package/src/ref-atom.test.ts +45 -11
- 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.3fbcb4aa9b",
|
|
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,29 +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
28
|
"@effect-atom/atom": "^0.5.1",
|
|
32
|
-
"
|
|
33
|
-
"@dxos/echo": "0.8.4-main.
|
|
34
|
-
"@dxos/echo
|
|
35
|
-
"@dxos/invariant": "0.8.4-main.
|
|
36
|
-
"@dxos/util": "0.8.4-main.3eb6e50203"
|
|
29
|
+
"@dxos/util": "0.8.4-main.3fbcb4aa9b",
|
|
30
|
+
"@dxos/echo-db": "0.8.4-main.3fbcb4aa9b",
|
|
31
|
+
"@dxos/echo": "0.8.4-main.3fbcb4aa9b",
|
|
32
|
+
"@dxos/invariant": "0.8.4-main.3fbcb4aa9b"
|
|
37
33
|
},
|
|
38
34
|
"devDependencies": {
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"@dxos/test-utils": "0.8.4-main.
|
|
42
|
-
"@dxos/
|
|
35
|
+
"effect": "3.20.0",
|
|
36
|
+
"@dxos/random": "0.8.4-main.3fbcb4aa9b",
|
|
37
|
+
"@dxos/test-utils": "0.8.4-main.3fbcb4aa9b",
|
|
38
|
+
"@dxos/context": "0.8.4-main.3fbcb4aa9b"
|
|
43
39
|
},
|
|
44
40
|
"peerDependencies": {
|
|
45
|
-
"effect": "3.
|
|
41
|
+
"effect": "3.20.0"
|
|
46
42
|
},
|
|
47
43
|
"publishConfig": {
|
|
48
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.
|
|
@@ -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
|
/**
|
|
@@ -159,7 +161,6 @@ export function makeProperty<T extends Obj.Unknown, K extends keyof T>(
|
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
assertArgument(Obj.isObject(obj), 'obj', 'Object must be a reactive object');
|
|
162
|
-
assertArgument(key in obj, 'key', 'Property must exist on object');
|
|
163
164
|
return propertyFamily(obj)(key);
|
|
164
165
|
}
|
|
165
166
|
|
|
@@ -176,7 +177,7 @@ const objectWithReactiveFamily = Atom.family(<T extends Obj.Unknown>(obj: T): At
|
|
|
176
177
|
get.addFinalizer(() => unsubscribe());
|
|
177
178
|
|
|
178
179
|
return obj;
|
|
179
|
-
});
|
|
180
|
+
}).pipe(Atom.keepAlive);
|
|
180
181
|
});
|
|
181
182
|
|
|
182
183
|
/**
|
|
@@ -187,7 +188,9 @@ const refWithReactiveFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T
|
|
|
187
188
|
const effect = (get: Atom.Context) =>
|
|
188
189
|
Effect.gen(function* () {
|
|
189
190
|
const snapshot = get(make(ref));
|
|
190
|
-
if (snapshot == null)
|
|
191
|
+
if (snapshot == null) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
191
194
|
const option = yield* Obj.getReactiveOption(snapshot);
|
|
192
195
|
return Option.getOrElse(option, () => undefined);
|
|
193
196
|
});
|
|
@@ -201,7 +204,7 @@ const refWithReactiveFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T
|
|
|
201
204
|
/**
|
|
202
205
|
* Like {@link make} but returns the live reactive object instead of a snapshot.
|
|
203
206
|
* Same input: Obj or Ref.Ref. Same output shape: Atom that updates when the object mutates.
|
|
204
|
-
* 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).
|
|
205
208
|
*
|
|
206
209
|
* @param objOrRef - The reactive object or ref.
|
|
207
210
|
* @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,10 +6,12 @@ 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 { sleep } from '@dxos/async';
|
|
9
10
|
import { Obj, type QueryResult, Type } from '@dxos/echo';
|
|
10
|
-
import {
|
|
11
|
-
import { type EchoDatabase,
|
|
11
|
+
import { Filter, Query } from '@dxos/echo';
|
|
12
|
+
import { type EchoDatabase, RuntimeSchemaRegistry } from '@dxos/echo-db';
|
|
12
13
|
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
14
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
13
15
|
import { SpaceId } from '@dxos/keys';
|
|
14
16
|
|
|
15
17
|
import * as AtomQuery from './query-atom';
|
|
@@ -22,7 +24,7 @@ const TestItem = Schema.Struct({
|
|
|
22
24
|
value: Schema.Number,
|
|
23
25
|
}).pipe(
|
|
24
26
|
Type.object({
|
|
25
|
-
typename: 'example.
|
|
27
|
+
typename: 'com.example.type.test-item',
|
|
26
28
|
version: '0.1.0',
|
|
27
29
|
}),
|
|
28
30
|
);
|
|
@@ -47,7 +49,7 @@ describe('AtomQuery', () => {
|
|
|
47
49
|
test('creates atom with initial results', async () => {
|
|
48
50
|
db.add(Obj.make(TestItem, { name: 'Object 1', value: 100 }));
|
|
49
51
|
db.add(Obj.make(TestItem, { name: 'Object 2', value: 100 }));
|
|
50
|
-
await db.flush(
|
|
52
|
+
await db.flush();
|
|
51
53
|
|
|
52
54
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
53
55
|
Query.select(Filter.type(TestItem, { value: 100 })),
|
|
@@ -63,7 +65,7 @@ describe('AtomQuery', () => {
|
|
|
63
65
|
|
|
64
66
|
test('registry.subscribe fires on QueryResult changes', async () => {
|
|
65
67
|
db.add(Obj.make(TestItem, { name: 'Initial', value: 200 }));
|
|
66
|
-
await db.flush(
|
|
68
|
+
await db.flush();
|
|
67
69
|
|
|
68
70
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
69
71
|
Query.select(Filter.type(TestItem, { value: 200 })),
|
|
@@ -86,7 +88,7 @@ describe('AtomQuery', () => {
|
|
|
86
88
|
|
|
87
89
|
// Add a new object that matches the query.
|
|
88
90
|
db.add(Obj.make(TestItem, { name: 'New Object', value: 200 }));
|
|
89
|
-
await db.flush({
|
|
91
|
+
await db.flush({ updates: true });
|
|
90
92
|
|
|
91
93
|
// Subscription should have fired.
|
|
92
94
|
expect(updateCount).toBeGreaterThan(0);
|
|
@@ -96,7 +98,7 @@ describe('AtomQuery', () => {
|
|
|
96
98
|
test('registry.subscribe fires when objects are removed', async () => {
|
|
97
99
|
const obj1 = db.add(Obj.make(TestItem, { name: 'Object 1', value: 300 }));
|
|
98
100
|
db.add(Obj.make(TestItem, { name: 'Object 2', value: 300 }));
|
|
99
|
-
await db.flush(
|
|
101
|
+
await db.flush();
|
|
100
102
|
|
|
101
103
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
102
104
|
Query.select(Filter.type(TestItem, { value: 300 })),
|
|
@@ -119,7 +121,7 @@ describe('AtomQuery', () => {
|
|
|
119
121
|
|
|
120
122
|
// Remove an object.
|
|
121
123
|
db.remove(obj1);
|
|
122
|
-
await db.flush({
|
|
124
|
+
await db.flush({ updates: true });
|
|
123
125
|
|
|
124
126
|
// Subscription should have fired.
|
|
125
127
|
expect(updateCount).toBeGreaterThan(0);
|
|
@@ -129,7 +131,7 @@ describe('AtomQuery', () => {
|
|
|
129
131
|
|
|
130
132
|
test('unsubscribing from registry stops receiving updates', async () => {
|
|
131
133
|
db.add(Obj.make(TestItem, { name: 'Initial', value: 400 }));
|
|
132
|
-
await db.flush(
|
|
134
|
+
await db.flush();
|
|
133
135
|
|
|
134
136
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
135
137
|
Query.select(Filter.type(TestItem, { value: 400 })),
|
|
@@ -150,7 +152,7 @@ describe('AtomQuery', () => {
|
|
|
150
152
|
|
|
151
153
|
// Add object and verify subscription fires.
|
|
152
154
|
db.add(Obj.make(TestItem, { name: 'Object 2', value: 400 }));
|
|
153
|
-
await db.flush({
|
|
155
|
+
await db.flush({ updates: true });
|
|
154
156
|
const countAfterFirstAdd = updateCount;
|
|
155
157
|
expect(countAfterFirstAdd).toBeGreaterThan(0);
|
|
156
158
|
|
|
@@ -159,7 +161,7 @@ describe('AtomQuery', () => {
|
|
|
159
161
|
|
|
160
162
|
// Add another object.
|
|
161
163
|
db.add(Obj.make(TestItem, { name: 'Object 3', value: 400 }));
|
|
162
|
-
await db.flush({
|
|
164
|
+
await db.flush({ updates: true });
|
|
163
165
|
|
|
164
166
|
// Update count should not have changed after unsubscribe.
|
|
165
167
|
expect(updateCount).toBe(countAfterFirstAdd);
|
|
@@ -179,7 +181,7 @@ describe('AtomQuery', () => {
|
|
|
179
181
|
|
|
180
182
|
test('multiple atoms from same query share underlying subscription', async () => {
|
|
181
183
|
db.add(Obj.make(TestItem, { name: 'Object', value: 500 }));
|
|
182
|
-
await db.flush(
|
|
184
|
+
await db.flush();
|
|
183
185
|
|
|
184
186
|
const queryResult: QueryResult.QueryResult<TestItem> = db.query(
|
|
185
187
|
Query.select(Filter.type(TestItem, { value: 500 })),
|
|
@@ -260,3 +262,127 @@ describe('AtomQuery with queues', () => {
|
|
|
260
262
|
expect(results[0].name).toEqual('jane');
|
|
261
263
|
});
|
|
262
264
|
});
|
|
265
|
+
|
|
266
|
+
const SchemaA = Schema.Struct({
|
|
267
|
+
name: Schema.String,
|
|
268
|
+
}).pipe(
|
|
269
|
+
Type.object({
|
|
270
|
+
typename: 'com.example.type.a',
|
|
271
|
+
version: '0.1.0',
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const SchemaB = Schema.Struct({
|
|
276
|
+
value: Schema.Number,
|
|
277
|
+
}).pipe(
|
|
278
|
+
Type.object({
|
|
279
|
+
typename: 'com.example.type.b',
|
|
280
|
+
version: '0.1.0',
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
describe('AtomQuery.fromQuery with schema registry', () => {
|
|
285
|
+
let schemaRegistry: RuntimeSchemaRegistry;
|
|
286
|
+
let registry: Registry.Registry;
|
|
287
|
+
|
|
288
|
+
beforeEach(() => {
|
|
289
|
+
schemaRegistry = new RuntimeSchemaRegistry([]);
|
|
290
|
+
registry = Registry.make();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('creates atom with initial results from schema query', async ({ expect }) => {
|
|
294
|
+
await schemaRegistry.register([SchemaA]);
|
|
295
|
+
|
|
296
|
+
const queryResult = schemaRegistry.query();
|
|
297
|
+
const atom = AtomQuery.fromQuery(queryResult);
|
|
298
|
+
const results = registry.get(atom);
|
|
299
|
+
|
|
300
|
+
expect(results).toHaveLength(1);
|
|
301
|
+
expect(Type.getTypename(results[0])).toBe('com.example.type.a');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('atom updates when new schemas are registered', async ({ expect }) => {
|
|
305
|
+
await schemaRegistry.register([SchemaA]);
|
|
306
|
+
|
|
307
|
+
const queryResult = schemaRegistry.query();
|
|
308
|
+
const atom = AtomQuery.fromQuery(queryResult);
|
|
309
|
+
|
|
310
|
+
// Get initial results and subscribe.
|
|
311
|
+
const initialResults = registry.get(atom);
|
|
312
|
+
expect(initialResults).toHaveLength(1);
|
|
313
|
+
|
|
314
|
+
let updateCount = 0;
|
|
315
|
+
let latestResults: Type.AnyEntity[] = [];
|
|
316
|
+
registry.subscribe(atom, () => {
|
|
317
|
+
updateCount++;
|
|
318
|
+
latestResults = registry.get(atom);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Allow reactive query to start (deferred via queueMicrotask).
|
|
322
|
+
await sleep(10);
|
|
323
|
+
|
|
324
|
+
// Register a new schema.
|
|
325
|
+
await schemaRegistry.register([SchemaB]);
|
|
326
|
+
|
|
327
|
+
expect(updateCount).toBeGreaterThan(0);
|
|
328
|
+
expect(latestResults).toHaveLength(2);
|
|
329
|
+
expect(latestResults.map(Type.getTypename).sort()).toEqual(['com.example.type.a', 'com.example.type.b']);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('atom works with empty initial results', ({ expect }) => {
|
|
333
|
+
const queryResult = schemaRegistry.query();
|
|
334
|
+
const atom = AtomQuery.fromQuery(queryResult);
|
|
335
|
+
const results = registry.get(atom);
|
|
336
|
+
|
|
337
|
+
expect(results).toHaveLength(0);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('atom with filtered query only reflects matching schemas', async ({ expect }) => {
|
|
341
|
+
const queryResult = schemaRegistry.query({ typename: 'com.example.type.a' });
|
|
342
|
+
const atom = AtomQuery.fromQuery(queryResult);
|
|
343
|
+
|
|
344
|
+
// Get initial (empty) results and subscribe.
|
|
345
|
+
const initialResults = registry.get(atom);
|
|
346
|
+
expect(initialResults).toHaveLength(0);
|
|
347
|
+
|
|
348
|
+
let latestResults: Type.AnyEntity[] = [];
|
|
349
|
+
registry.subscribe(atom, () => {
|
|
350
|
+
latestResults = registry.get(atom);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await sleep(10);
|
|
354
|
+
|
|
355
|
+
// Register non-matching schema.
|
|
356
|
+
await schemaRegistry.register([SchemaB]);
|
|
357
|
+
// Results updated but still empty for this filter.
|
|
358
|
+
expect(latestResults).toHaveLength(0);
|
|
359
|
+
|
|
360
|
+
// Register matching schema.
|
|
361
|
+
await schemaRegistry.register([SchemaA]);
|
|
362
|
+
expect(latestResults).toHaveLength(1);
|
|
363
|
+
expect(Type.getTypename(latestResults[0])).toBe('com.example.type.a');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('unsubscribing from atom stops updates', async ({ expect }) => {
|
|
367
|
+
const queryResult = schemaRegistry.query();
|
|
368
|
+
const atom = AtomQuery.fromQuery(queryResult);
|
|
369
|
+
|
|
370
|
+
registry.get(atom);
|
|
371
|
+
|
|
372
|
+
let updateCount = 0;
|
|
373
|
+
const unsubscribe = registry.subscribe(atom, () => {
|
|
374
|
+
updateCount++;
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
await sleep(10);
|
|
378
|
+
|
|
379
|
+
await schemaRegistry.register([SchemaA]);
|
|
380
|
+
const countAfterFirst = updateCount;
|
|
381
|
+
expect(countAfterFirst).toBeGreaterThan(0);
|
|
382
|
+
|
|
383
|
+
unsubscribe();
|
|
384
|
+
|
|
385
|
+
await schemaRegistry.register([SchemaB]);
|
|
386
|
+
expect(updateCount).toBe(countAfterFirst);
|
|
387
|
+
});
|
|
388
|
+
});
|
package/src/query-atom.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { DXN, Database, type Entity, type Filter, Query, type QueryResult } from
|
|
|
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).
|
|
@@ -37,7 +32,7 @@ const queryableRegistry = new WeakDictionary<string, Database.Queryable>();
|
|
|
37
32
|
// Key separator that won't appear in identifiers (DXN strings use colons).
|
|
38
33
|
const KEY_SEPARATOR = '~';
|
|
39
34
|
|
|
40
|
-
// Atom.family keyed by "identifier
|
|
35
|
+
// Atom.family keyed by "identifier~serializedAST".
|
|
41
36
|
const queryFamily = Atom.family((key: string) => {
|
|
42
37
|
// Parse key outside Atom.make - runs once per key.
|
|
43
38
|
const separatorIndex = key.indexOf(KEY_SEPARATOR);
|