@dxos/app-graph 0.8.4-main.e8ec1fe → 0.8.4-main.ef1bc66f44
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/browser/index.mjs +1012 -551
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1011 -551
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/atoms.d.ts +8 -0
- package/dist/types/src/atoms.d.ts.map +1 -0
- package/dist/types/src/graph-builder.d.ts +107 -65
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +174 -204
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +6 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node-matcher.d.ts +218 -0
- package/dist/types/src/node-matcher.d.ts.map +1 -0
- package/dist/types/src/node-matcher.test.d.ts +2 -0
- package/dist/types/src/node-matcher.test.d.ts.map +1 -0
- package/dist/types/src/node.d.ts +32 -3
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +35 -33
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +494 -78
- package/src/graph-builder.ts +543 -250
- package/src/graph.test.ts +297 -104
- package/src/graph.ts +934 -404
- package/src/index.ts +9 -3
- package/src/node-matcher.test.ts +301 -0
- package/src/node-matcher.ts +284 -0
- package/src/node.ts +39 -6
- package/src/stories/EchoGraph.stories.tsx +94 -92
- package/src/stories/Tree.tsx +1 -1
- package/dist/types/src/experimental/graph-projections.test.d.ts +0 -25
- package/dist/types/src/experimental/graph-projections.test.d.ts.map +0 -1
- package/dist/types/src/signals-integration.test.d.ts +0 -2
- package/dist/types/src/signals-integration.test.d.ts.map +0 -1
- package/dist/types/src/testing.d.ts +0 -5
- package/dist/types/src/testing.d.ts.map +0 -1
- package/src/experimental/graph-projections.test.ts +0 -56
- package/src/signals-integration.test.ts +0 -219
- package/src/testing.ts +0 -20
|
@@ -6,33 +6,25 @@ import { Atom, type Registry, RegistryContext, useAtomValue } from '@effect-atom
|
|
|
6
6
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
7
7
|
import * as Function from 'effect/Function';
|
|
8
8
|
import * as Option from 'effect/Option';
|
|
9
|
-
import React, { type PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Query,
|
|
16
|
-
type QueryResult,
|
|
17
|
-
type Space,
|
|
18
|
-
SpaceState,
|
|
19
|
-
isSpace,
|
|
20
|
-
live,
|
|
21
|
-
} from '@dxos/client/echo';
|
|
22
|
-
import { Obj, Type } from '@dxos/echo';
|
|
9
|
+
import React, { type PropsWithChildren, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
10
|
+
|
|
11
|
+
import { Filter, type Space, SpaceState, isSpace } from '@dxos/client/echo';
|
|
12
|
+
import { Obj, Query } from '@dxos/echo';
|
|
13
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
14
|
+
import { AtomObj, AtomQuery } from '@dxos/echo-atom';
|
|
23
15
|
import { faker } from '@dxos/random';
|
|
24
16
|
import { type Client, useClient } from '@dxos/react-client';
|
|
25
17
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
26
18
|
import { Icon, IconButton, Input, Select } from '@dxos/react-ui';
|
|
27
19
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
28
20
|
import { Path, Tree } from '@dxos/react-ui-list';
|
|
29
|
-
import { getSize, mx } from '@dxos/
|
|
21
|
+
import { getSize, mx } from '@dxos/ui-theme';
|
|
30
22
|
import { byPosition, isNonNullable, safeParseInt } from '@dxos/util';
|
|
31
23
|
|
|
32
|
-
import
|
|
33
|
-
import
|
|
34
|
-
import
|
|
35
|
-
import
|
|
24
|
+
import * as CreateAtom from '../atoms';
|
|
25
|
+
import * as Graph from '../graph';
|
|
26
|
+
import * as GraphBuilder from '../graph-builder';
|
|
27
|
+
import * as Node from '../node';
|
|
36
28
|
|
|
37
29
|
import { JsonTree } from './Tree';
|
|
38
30
|
|
|
@@ -56,45 +48,44 @@ const actionWeights = {
|
|
|
56
48
|
[Action.RENAME_OBJECT]: 4,
|
|
57
49
|
};
|
|
58
50
|
|
|
59
|
-
const createGraph = (client: Client, registry: Registry.Registry): ExpandableGraph => {
|
|
60
|
-
const spaceBuilderExtension =
|
|
51
|
+
const createGraph = (client: Client, registry: Registry.Registry): Graph.ExpandableGraph => {
|
|
52
|
+
const spaceBuilderExtension = GraphBuilder.createExtensionRaw({
|
|
61
53
|
id: 'space',
|
|
62
54
|
connector: (node) =>
|
|
63
55
|
Atom.make((get) =>
|
|
64
56
|
Function.pipe(
|
|
65
57
|
get(node),
|
|
66
|
-
Option.flatMap((node) => (node.id ===
|
|
58
|
+
Option.flatMap((node) => (node.id === Node.RootId ? Option.some(node) : Option.none())),
|
|
67
59
|
Option.map(() => {
|
|
68
|
-
const spaces = get(
|
|
60
|
+
const spaces = get(CreateAtom.fromObservable(client.spaces)) ?? [];
|
|
69
61
|
return spaces
|
|
70
|
-
.filter((space) => get(
|
|
71
|
-
.map((space) =>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
62
|
+
.filter((space: any) => get(CreateAtom.fromObservable(space.state)) === SpaceState.SPACE_READY)
|
|
63
|
+
.map((space) => {
|
|
64
|
+
const propertiesSnapshot = get(AtomObj.make(space.properties));
|
|
65
|
+
return {
|
|
66
|
+
id: space.id,
|
|
67
|
+
type: 'dxos.org/type/Space',
|
|
68
|
+
properties: {
|
|
69
|
+
label: propertiesSnapshot.name,
|
|
70
|
+
},
|
|
71
|
+
data: space,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
79
74
|
}),
|
|
80
75
|
Option.getOrElse(() => []),
|
|
81
76
|
),
|
|
82
77
|
),
|
|
83
78
|
});
|
|
84
79
|
|
|
85
|
-
const objectBuilderExtension =
|
|
80
|
+
const objectBuilderExtension = GraphBuilder.createExtensionRaw({
|
|
86
81
|
id: 'object',
|
|
87
82
|
connector: (node) => {
|
|
88
|
-
let query: QueryResult<Live<Expando>> | undefined;
|
|
89
83
|
return Atom.make((get) =>
|
|
90
84
|
Function.pipe(
|
|
91
85
|
get(node),
|
|
92
86
|
Option.flatMap((node) => (isSpace(node.data) ? Option.some(node.data) : Option.none())),
|
|
93
87
|
Option.map((space) => {
|
|
94
|
-
|
|
95
|
-
query = space.db.query(Query.type(Expando, { type: 'test' }));
|
|
96
|
-
}
|
|
97
|
-
const objects = get(atomFromQuery(query));
|
|
88
|
+
const objects = get(AtomQuery.make(space.db, Query.type(TestSchema.Expando, { type: 'test' })));
|
|
98
89
|
return objects.map((object) => ({
|
|
99
90
|
id: object.id,
|
|
100
91
|
type: 'dxos.org/type/test',
|
|
@@ -108,15 +99,15 @@ const createGraph = (client: Client, registry: Registry.Registry): ExpandableGra
|
|
|
108
99
|
},
|
|
109
100
|
});
|
|
110
101
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
102
|
+
const builder = GraphBuilder.make({ registry });
|
|
103
|
+
GraphBuilder.addExtension(builder, spaceBuilderExtension);
|
|
104
|
+
GraphBuilder.addExtension(builder, objectBuilderExtension);
|
|
105
|
+
const graph = builder.graph;
|
|
114
106
|
graph.onNodeChanged.on(({ id }) => {
|
|
115
|
-
|
|
107
|
+
Graph.expand(graph, id);
|
|
116
108
|
});
|
|
117
|
-
|
|
109
|
+
Graph.expand(graph, Node.RootId);
|
|
118
110
|
(window as any).graph = graph;
|
|
119
|
-
|
|
120
111
|
return graph;
|
|
121
112
|
};
|
|
122
113
|
|
|
@@ -136,9 +127,9 @@ const getRandomSpace = (client: Client): Space | undefined => {
|
|
|
136
127
|
const getSpaceWithObjects = async (client: Client): Promise<Space | undefined> => {
|
|
137
128
|
const readySpaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY);
|
|
138
129
|
const spaceQueries = await Promise.all(
|
|
139
|
-
readySpaces.map((space) => space.db.query(Filter.type(Expando, { type: 'test' })).run()),
|
|
130
|
+
readySpaces.map((space) => space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run()),
|
|
140
131
|
);
|
|
141
|
-
const spaces = readySpaces.filter((space, index) => spaceQueries[index].
|
|
132
|
+
const spaces = readySpaces.filter((space, index) => spaceQueries[index].length > 0);
|
|
142
133
|
return spaces[Math.floor(Math.random() * spaces.length)];
|
|
143
134
|
};
|
|
144
135
|
|
|
@@ -155,14 +146,16 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
155
146
|
case Action.RENAME_SPACE: {
|
|
156
147
|
const space = getRandomSpace(client);
|
|
157
148
|
if (space) {
|
|
158
|
-
space.properties
|
|
149
|
+
Obj.change(space.properties, (p) => {
|
|
150
|
+
p.name = faker.commerce.productName();
|
|
151
|
+
});
|
|
159
152
|
}
|
|
160
153
|
break;
|
|
161
154
|
}
|
|
162
155
|
|
|
163
156
|
case Action.ADD_OBJECT:
|
|
164
157
|
getRandomSpace(client)?.db.add(
|
|
165
|
-
Obj.make(
|
|
158
|
+
Obj.make(TestSchema.Expando, {
|
|
166
159
|
type: 'test',
|
|
167
160
|
name: faker.commerce.productName(),
|
|
168
161
|
}),
|
|
@@ -172,7 +165,7 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
172
165
|
case Action.REMOVE_OBJECT: {
|
|
173
166
|
const space = await getSpaceWithObjects(client);
|
|
174
167
|
if (space) {
|
|
175
|
-
const
|
|
168
|
+
const objects = await space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run();
|
|
176
169
|
space.db.remove(objects[Math.floor(Math.random() * objects.length)]);
|
|
177
170
|
}
|
|
178
171
|
break;
|
|
@@ -181,8 +174,11 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
181
174
|
case Action.RENAME_OBJECT: {
|
|
182
175
|
const space = await getSpaceWithObjects(client);
|
|
183
176
|
if (space) {
|
|
184
|
-
const
|
|
185
|
-
objects[Math.floor(Math.random() * objects.length)]
|
|
177
|
+
const objects = await space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run();
|
|
178
|
+
const object = objects[Math.floor(Math.random() * objects.length)];
|
|
179
|
+
Obj.change(object, (o) => {
|
|
180
|
+
o.name = faker.commerce.productName();
|
|
181
|
+
});
|
|
186
182
|
}
|
|
187
183
|
break;
|
|
188
184
|
}
|
|
@@ -256,7 +252,7 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
256
252
|
const meta = {
|
|
257
253
|
title: 'sdk/app-graph/EchoGraph',
|
|
258
254
|
decorators: [
|
|
259
|
-
withTheme,
|
|
255
|
+
withTheme(),
|
|
260
256
|
withClientProvider({
|
|
261
257
|
createIdentity: true,
|
|
262
258
|
onCreateIdentity: async ({ client }) => {
|
|
@@ -292,26 +288,37 @@ export const TreeView: Story = {
|
|
|
292
288
|
const client = useClient();
|
|
293
289
|
const registry = useContext(RegistryContext);
|
|
294
290
|
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
295
|
-
const
|
|
291
|
+
const stateRef = useRef(new Map<string, Atom.Writable<{ open: boolean; current: boolean }>>());
|
|
292
|
+
|
|
293
|
+
const getOrCreateState = useMemo(
|
|
294
|
+
() => (path: string) => {
|
|
295
|
+
let atom = stateRef.current.get(path);
|
|
296
|
+
if (!atom) {
|
|
297
|
+
atom = Atom.make({ open: true, current: false }).pipe(Atom.keepAlive);
|
|
298
|
+
stateRef.current.set(path, atom);
|
|
299
|
+
}
|
|
300
|
+
return atom;
|
|
301
|
+
},
|
|
302
|
+
[],
|
|
303
|
+
);
|
|
296
304
|
|
|
297
305
|
const useItems = useCallback(
|
|
298
|
-
(node?: Node, options?: { disposition?: string; sort?: boolean }) => {
|
|
299
|
-
const connections = useAtomValue(graph.connections(node?.id ??
|
|
306
|
+
(node?: Node.Node, options?: { disposition?: string; sort?: boolean }) => {
|
|
307
|
+
const connections = useAtomValue(graph.connections(node?.id ?? Node.RootId));
|
|
300
308
|
return options?.sort ? connections.toSorted((a, b) => byPosition(a.properties, b.properties)) : connections;
|
|
301
309
|
},
|
|
302
310
|
[graph],
|
|
303
311
|
);
|
|
304
312
|
|
|
305
313
|
const getProps = useCallback(
|
|
306
|
-
(node: Node, path: string[]) => {
|
|
307
|
-
const children = graph
|
|
308
|
-
.getConnections(node.id, 'outbound')
|
|
314
|
+
(node: Node.Node, path: string[]) => {
|
|
315
|
+
const children = Graph.getConnections(graph, node.id, 'outbound')
|
|
309
316
|
.map((n) => {
|
|
310
317
|
// Break cycles.
|
|
311
318
|
const nextPath = [...path, node.id];
|
|
312
|
-
return nextPath.includes(n.id) ? undefined : (n as Node);
|
|
319
|
+
return nextPath.includes(n.id) ? undefined : (n as Node.Node);
|
|
313
320
|
})
|
|
314
|
-
.filter(isNonNullable) as Node[];
|
|
321
|
+
.filter(isNonNullable) as Node.Node[];
|
|
315
322
|
const parentOf =
|
|
316
323
|
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
317
324
|
return {
|
|
@@ -324,59 +331,54 @@ export const TreeView: Story = {
|
|
|
324
331
|
[graph],
|
|
325
332
|
);
|
|
326
333
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return object.open;
|
|
336
|
-
},
|
|
337
|
-
[state],
|
|
338
|
-
);
|
|
334
|
+
// Hook that subscribes to item state via Atom.
|
|
335
|
+
const useItemState = (_path: string[]) => {
|
|
336
|
+
const path = useMemo(() => Path.create(..._path), [_path.join('~')]);
|
|
337
|
+
const atom = getOrCreateState(path);
|
|
338
|
+
return useAtomValue(atom);
|
|
339
|
+
};
|
|
339
340
|
|
|
340
|
-
const
|
|
341
|
-
(_path
|
|
342
|
-
|
|
343
|
-
const object = state.get(path) ?? live({ open: false, current: false });
|
|
344
|
-
if (!state.has(path)) {
|
|
345
|
-
state.set(path, object);
|
|
346
|
-
}
|
|
341
|
+
const useIsOpen = (_path: string[]) => {
|
|
342
|
+
return useItemState(_path).open;
|
|
343
|
+
};
|
|
347
344
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
);
|
|
345
|
+
const useIsCurrent = (_path: string[]) => {
|
|
346
|
+
return useItemState(_path).current;
|
|
347
|
+
};
|
|
352
348
|
|
|
353
349
|
const onOpenChange = useCallback(
|
|
354
350
|
({ path: _path, open }: { path: string[]; open: boolean }) => {
|
|
355
351
|
const path = Path.create(..._path);
|
|
356
|
-
const
|
|
357
|
-
|
|
352
|
+
const atom = stateRef.current.get(path);
|
|
353
|
+
if (atom) {
|
|
354
|
+
const prev = registry.get(atom);
|
|
355
|
+
registry.set(atom, { ...prev, open });
|
|
356
|
+
}
|
|
358
357
|
},
|
|
359
|
-
[
|
|
358
|
+
[registry],
|
|
360
359
|
);
|
|
361
360
|
|
|
362
361
|
const onSelect = useCallback(
|
|
363
362
|
({ path: _path, current }: { path: string[]; current: boolean }) => {
|
|
364
363
|
const path = Path.create(..._path);
|
|
365
|
-
const
|
|
366
|
-
|
|
364
|
+
const atom = stateRef.current.get(path);
|
|
365
|
+
if (atom) {
|
|
366
|
+
const prev = registry.get(atom);
|
|
367
|
+
registry.set(atom, { ...prev, current });
|
|
368
|
+
}
|
|
367
369
|
},
|
|
368
|
-
[
|
|
370
|
+
[registry],
|
|
369
371
|
);
|
|
370
372
|
|
|
371
373
|
return (
|
|
372
374
|
<>
|
|
373
375
|
<Controls />
|
|
374
376
|
<Tree
|
|
375
|
-
id={
|
|
377
|
+
id={Node.RootId}
|
|
376
378
|
useItems={useItems}
|
|
377
379
|
getProps={getProps}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
+
useIsOpen={useIsOpen}
|
|
381
|
+
useIsCurrent={useIsCurrent}
|
|
380
382
|
onOpenChange={onOpenChange}
|
|
381
383
|
onSelect={onSelect}
|
|
382
384
|
/>
|
package/src/stories/Tree.tsx
CHANGED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
type Cb = () => void;
|
|
2
|
-
interface Query {
|
|
3
|
-
id?: string[];
|
|
4
|
-
typename?: string[];
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Lazy query result that can be executed.
|
|
8
|
-
* Does not run without the run function being called.
|
|
9
|
-
*/
|
|
10
|
-
interface QueryResult<T> {
|
|
11
|
-
/**
|
|
12
|
-
* Execute the query and subscribe to the result.
|
|
13
|
-
* @param next Called at least once with the first value (maybe synchronously) and then for every subsequent update.
|
|
14
|
-
* @param error Called on error. `next` is never called after that.
|
|
15
|
-
* @returns Function to dispose the query and unsubscribe.
|
|
16
|
-
*/
|
|
17
|
-
run(onData: (value?: T[]) => void, onError: (err: Error) => void): Cb;
|
|
18
|
-
}
|
|
19
|
-
declare const QueryResult: Readonly<{
|
|
20
|
-
fromPromise: <T>(run: (onDispose: (cb: Cb) => void) => Promise<T[]>) => QueryResult<T>;
|
|
21
|
-
}>;
|
|
22
|
-
interface _Resolver<T> {
|
|
23
|
-
query(query: Query): QueryResult<T>;
|
|
24
|
-
}
|
|
25
|
-
//# sourceMappingURL=graph-projections.test.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graph-projections.test.d.ts","sourceRoot":"","sources":["../../../../src/experimental/graph-projections.test.ts"],"names":[],"mappings":"AAIA,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC;AAErB,UAAU,KAAK;IACb,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;GAGG;AACH,UAAU,WAAW,CAAC,CAAC;IACrB;;;;;OAKG;IACH,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,KAAK,IAAI,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,EAAE,CAAC;CACvE;AAED,QAAA,MAAM,WAAW;kBACD,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,IAAI,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,KAAG,WAAW,CAAC,CAAC,CAAC;EAyBpF,CAAC;AAEH,UAAU,SAAS,CAAC,CAAC;IACnB,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;CACrC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"signals-integration.test.d.ts","sourceRoot":"","sources":["../../../src/signals-integration.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { Atom } from '@effect-atom/atom-react';
|
|
2
|
-
import { type AnyEchoObject } from '@dxos/echo/internal';
|
|
3
|
-
import { type QueryResult } from '@dxos/echo-db';
|
|
4
|
-
export declare const atomFromQuery: <T extends AnyEchoObject>(query: QueryResult<T>) => Atom.Atom<T[]>;
|
|
5
|
-
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../../src/testing.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAE/C,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,eAAO,MAAM,aAAa,GAAI,CAAC,SAAS,aAAa,EAAE,OAAO,WAAW,CAAC,CAAC,CAAC,KAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAU3F,CAAC"}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
type Cb = () => void;
|
|
6
|
-
|
|
7
|
-
interface Query {
|
|
8
|
-
id?: string[];
|
|
9
|
-
typename?: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Lazy query result that can be executed.
|
|
14
|
-
* Does not run without the run function being called.
|
|
15
|
-
*/
|
|
16
|
-
interface QueryResult<T> {
|
|
17
|
-
/**
|
|
18
|
-
* Execute the query and subscribe to the result.
|
|
19
|
-
* @param next Called at least once with the first value (maybe synchronously) and then for every subsequent update.
|
|
20
|
-
* @param error Called on error. `next` is never called after that.
|
|
21
|
-
* @returns Function to dispose the query and unsubscribe.
|
|
22
|
-
*/
|
|
23
|
-
run(onData: (value?: T[]) => void, onError: (err: Error) => void): Cb;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const QueryResult = Object.freeze({
|
|
27
|
-
fromPromise: <T>(run: (onDispose: (cb: Cb) => void) => Promise<T[]>): QueryResult<T> => {
|
|
28
|
-
return {
|
|
29
|
-
run: (onData, onError) => {
|
|
30
|
-
const cbs: Cb[] = [];
|
|
31
|
-
let disposed = false;
|
|
32
|
-
const dispose = () => {
|
|
33
|
-
cbs.forEach((cb) => cb());
|
|
34
|
-
disposed = true;
|
|
35
|
-
};
|
|
36
|
-
run((cb) => (disposed ? cb() : cbs.push(cb))).then(
|
|
37
|
-
(data) => {
|
|
38
|
-
if (disposed) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
onData(data);
|
|
42
|
-
},
|
|
43
|
-
(err) => {
|
|
44
|
-
dispose();
|
|
45
|
-
onError(err);
|
|
46
|
-
},
|
|
47
|
-
);
|
|
48
|
-
return dispose;
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
interface _Resolver<T> {
|
|
55
|
-
query(query: Query): QueryResult<T>;
|
|
56
|
-
}
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { Atom, Registry } from '@effect-atom/atom-react';
|
|
6
|
-
import { signal } from '@preact/signals-core';
|
|
7
|
-
import { afterEach, beforeEach, describe, expect, onTestFinished, test } from 'vitest';
|
|
8
|
-
|
|
9
|
-
import { Trigger } from '@dxos/async';
|
|
10
|
-
import { Obj, Type } from '@dxos/echo';
|
|
11
|
-
import { Ref } from '@dxos/echo/internal';
|
|
12
|
-
import { Filter } from '@dxos/echo-db';
|
|
13
|
-
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
14
|
-
import { registerSignalsRuntime } from '@dxos/echo-signals';
|
|
15
|
-
|
|
16
|
-
import { ROOT_ID } from './graph';
|
|
17
|
-
import { GraphBuilder, atomFromSignal, createExtension } from './graph-builder';
|
|
18
|
-
import { atomFromQuery } from './testing';
|
|
19
|
-
|
|
20
|
-
registerSignalsRuntime();
|
|
21
|
-
|
|
22
|
-
const EXAMPLE_TYPE = 'dxos.org/type/example';
|
|
23
|
-
|
|
24
|
-
describe('signals integration', () => {
|
|
25
|
-
test('creating atom from signal', () => {
|
|
26
|
-
const registry = Registry.make();
|
|
27
|
-
const state = signal<number>(0);
|
|
28
|
-
const value = atomFromSignal(() => state.value);
|
|
29
|
-
const inline = Atom.make((get) => {
|
|
30
|
-
// NOTE: This will create a new atom instance each time.
|
|
31
|
-
// This test is verifying that this behaves the same as using a stable atom instance.
|
|
32
|
-
// The parent will remain subscribed to one instance until the new one is created.
|
|
33
|
-
// The old one will then be garbage collected because it is no longer referenced.
|
|
34
|
-
const atom = atomFromSignal(() => get(value));
|
|
35
|
-
return get(atom);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
let count = 0;
|
|
39
|
-
const cancel = registry.subscribe(value, (value) => {
|
|
40
|
-
count = value;
|
|
41
|
-
});
|
|
42
|
-
onTestFinished(() => cancel());
|
|
43
|
-
|
|
44
|
-
let inlineCount = 0;
|
|
45
|
-
const inlineCancel = registry.subscribe(inline, (value) => {
|
|
46
|
-
inlineCount = value;
|
|
47
|
-
});
|
|
48
|
-
onTestFinished(() => inlineCancel());
|
|
49
|
-
|
|
50
|
-
registry.get(value);
|
|
51
|
-
registry.get(inline);
|
|
52
|
-
expect(count).to.eq(0);
|
|
53
|
-
expect(inlineCount).to.eq(0);
|
|
54
|
-
|
|
55
|
-
state.value = 1;
|
|
56
|
-
expect(count).to.eq(1);
|
|
57
|
-
expect(inlineCount).to.eq(1);
|
|
58
|
-
|
|
59
|
-
state.value = 2;
|
|
60
|
-
expect(count).to.eq(2);
|
|
61
|
-
expect(inlineCount).to.eq(2);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe('echo', () => {
|
|
65
|
-
let dbBuilder: EchoTestBuilder;
|
|
66
|
-
|
|
67
|
-
beforeEach(async () => {
|
|
68
|
-
dbBuilder = await new EchoTestBuilder().open();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
afterEach(async () => {
|
|
72
|
-
await dbBuilder.close();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('atom references are loaded lazily and receive signal notifications', async () => {
|
|
76
|
-
const registry = Registry.make();
|
|
77
|
-
await using peer = await dbBuilder.createPeer();
|
|
78
|
-
|
|
79
|
-
let outerId: string;
|
|
80
|
-
{
|
|
81
|
-
await using db = await peer.createDatabase();
|
|
82
|
-
const inner = db.add({ name: 'inner' });
|
|
83
|
-
const outer = db.add({ inner: Ref.make(inner) });
|
|
84
|
-
outerId = outer.id;
|
|
85
|
-
await db.flush();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
await peer.reload();
|
|
89
|
-
{
|
|
90
|
-
await using db = await peer.openLastDatabase();
|
|
91
|
-
const outer = (await db.query(Filter.ids(outerId)).first()) as any;
|
|
92
|
-
const innerAtom = atomFromSignal(() => outer.inner.target);
|
|
93
|
-
const loaded = new Trigger();
|
|
94
|
-
|
|
95
|
-
let count = 0;
|
|
96
|
-
const cancel = registry.subscribe(innerAtom, (inner) => {
|
|
97
|
-
count++;
|
|
98
|
-
if (inner) {
|
|
99
|
-
loaded.wake();
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
onTestFinished(() => cancel());
|
|
104
|
-
|
|
105
|
-
expect(registry.get(innerAtom)).to.eq(undefined);
|
|
106
|
-
expect(count).to.eq(1);
|
|
107
|
-
|
|
108
|
-
await loaded.wait();
|
|
109
|
-
expect(registry.get(innerAtom)).to.include({ name: 'inner' });
|
|
110
|
-
expect(count).to.eq(2);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('references graph builder', async () => {
|
|
115
|
-
const registry = Registry.make();
|
|
116
|
-
await using peer = await dbBuilder.createPeer();
|
|
117
|
-
|
|
118
|
-
let outerId, innerId: string;
|
|
119
|
-
{
|
|
120
|
-
await using db = await peer.createDatabase();
|
|
121
|
-
const inner = db.add({ name: 'inner' });
|
|
122
|
-
const outer = db.add({ inner: Ref.make(inner) });
|
|
123
|
-
innerId = inner.id;
|
|
124
|
-
outerId = outer.id;
|
|
125
|
-
await db.flush();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
await peer.reload();
|
|
129
|
-
|
|
130
|
-
{
|
|
131
|
-
await using db = await peer.openLastDatabase();
|
|
132
|
-
const outer = (await db.query(Filter.ids(outerId)).first()) as any;
|
|
133
|
-
const innerAtom = atomFromSignal(() => outer.inner.target);
|
|
134
|
-
const inner = registry.get(innerAtom);
|
|
135
|
-
expect(inner).to.eq(undefined);
|
|
136
|
-
|
|
137
|
-
const builder = new GraphBuilder({ registry });
|
|
138
|
-
builder.addExtension(
|
|
139
|
-
createExtension({
|
|
140
|
-
id: 'outbound-connector',
|
|
141
|
-
connector: () =>
|
|
142
|
-
Atom.make((get) => {
|
|
143
|
-
const inner = get(innerAtom) as any;
|
|
144
|
-
return inner ? [{ id: inner.id, type: EXAMPLE_TYPE, data: inner.name }] : [];
|
|
145
|
-
}),
|
|
146
|
-
}),
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
const graph = builder.graph;
|
|
150
|
-
|
|
151
|
-
const loaded = new Trigger();
|
|
152
|
-
let count = 0;
|
|
153
|
-
const cancel = registry.subscribe(graph.connections(ROOT_ID), (nodes) => {
|
|
154
|
-
count++;
|
|
155
|
-
if (nodes.length > 0) {
|
|
156
|
-
loaded.wake();
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
onTestFinished(() => cancel());
|
|
160
|
-
registry.get(graph.connections(ROOT_ID));
|
|
161
|
-
expect(count).to.eq(1);
|
|
162
|
-
|
|
163
|
-
graph.expand(ROOT_ID);
|
|
164
|
-
await loaded.wait();
|
|
165
|
-
expect(count).to.eq(2);
|
|
166
|
-
|
|
167
|
-
const nodes = registry.get(graph.connections(ROOT_ID));
|
|
168
|
-
expect(nodes).has.length(1);
|
|
169
|
-
expect(nodes[0].id).to.eq(innerId);
|
|
170
|
-
expect(nodes[0].data).to.eq('inner');
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test('query graph builder', async () => {
|
|
175
|
-
const registry = Registry.make();
|
|
176
|
-
await using peer = await dbBuilder.createPeer();
|
|
177
|
-
await using db = await peer.createDatabase();
|
|
178
|
-
db.add(Obj.make(Type.Expando, { name: 'a' }));
|
|
179
|
-
db.add(Obj.make(Type.Expando, { name: 'b' }));
|
|
180
|
-
|
|
181
|
-
const builder = new GraphBuilder({ registry });
|
|
182
|
-
builder.addExtension(
|
|
183
|
-
createExtension({
|
|
184
|
-
id: 'expando',
|
|
185
|
-
connector: () => {
|
|
186
|
-
const query = db.query(Filter.type(Type.Expando));
|
|
187
|
-
|
|
188
|
-
return Atom.make((get) => {
|
|
189
|
-
const objects = get(atomFromQuery(query));
|
|
190
|
-
return objects.map((object) => ({ id: object.id, type: EXAMPLE_TYPE, data: object.name }));
|
|
191
|
-
});
|
|
192
|
-
},
|
|
193
|
-
}),
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
const graph = builder.graph;
|
|
197
|
-
let count = 0;
|
|
198
|
-
const cancel = registry.subscribe(graph.connections(ROOT_ID), (nodes) => {
|
|
199
|
-
count = nodes.length;
|
|
200
|
-
});
|
|
201
|
-
onTestFinished(() => cancel());
|
|
202
|
-
|
|
203
|
-
registry.get(graph.connections(ROOT_ID));
|
|
204
|
-
expect(count).to.eq(0);
|
|
205
|
-
|
|
206
|
-
graph.expand(ROOT_ID);
|
|
207
|
-
expect(count).to.eq(2);
|
|
208
|
-
|
|
209
|
-
const object = db.add(Obj.make(Type.Expando, { name: 'c' }));
|
|
210
|
-
await db.flush();
|
|
211
|
-
expect(count).to.eq(3);
|
|
212
|
-
|
|
213
|
-
// NOTE: This graph builder is not reactive to the object update.
|
|
214
|
-
object.name = 'updated';
|
|
215
|
-
await db.flush();
|
|
216
|
-
expect(count).to.eq(3);
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
});
|
package/src/testing.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { Atom } from '@effect-atom/atom-react';
|
|
6
|
-
|
|
7
|
-
import { type AnyEchoObject } from '@dxos/echo/internal';
|
|
8
|
-
import { type QueryResult } from '@dxos/echo-db';
|
|
9
|
-
|
|
10
|
-
export const atomFromQuery = <T extends AnyEchoObject>(query: QueryResult<T>): Atom.Atom<T[]> => {
|
|
11
|
-
return Atom.make((get) => {
|
|
12
|
-
const unsubscribe = query.subscribe((result) => {
|
|
13
|
-
get.setSelf(result.objects);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
get.addFinalizer(() => unsubscribe());
|
|
17
|
-
|
|
18
|
-
return query.objects;
|
|
19
|
-
});
|
|
20
|
-
};
|