@dxos/app-graph 0.8.4-main.84f28bd → 0.8.4-main.ae835ea
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 +207 -149
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +207 -149
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/graph-builder.d.ts +15 -4
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +7 -3
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +1 -1
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts +8 -10
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/src/testing.d.ts +1 -1
- package/dist/types/src/testing.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +31 -33
- package/src/graph-builder.test.ts +67 -9
- package/src/graph-builder.ts +69 -22
- package/src/graph.test.ts +2 -2
- package/src/graph.ts +20 -19
- package/src/node.ts +5 -3
- package/src/signals-integration.test.ts +7 -7
- package/src/stories/EchoGraph.stories.tsx +29 -26
- package/src/testing.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/app-graph",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.ae835ea",
|
|
4
4
|
"description": "Constructs knowledge graphs for the purpose of building applications on top of",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"type": "module",
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
13
|
+
"source": "./src/index.ts",
|
|
13
14
|
"types": "./dist/types/src/index.d.ts",
|
|
14
15
|
"browser": "./dist/lib/browser/index.mjs",
|
|
15
16
|
"node": "./dist/lib/node-esm/index.mjs"
|
|
@@ -24,46 +25,43 @@
|
|
|
24
25
|
"src"
|
|
25
26
|
],
|
|
26
27
|
"dependencies": {
|
|
27
|
-
"@preact/signals-core": "^1.
|
|
28
|
+
"@preact/signals-core": "^1.12.1",
|
|
28
29
|
"main-thread-scheduling": "^14.1.1",
|
|
29
|
-
"@dxos/async": "0.8.4-main.
|
|
30
|
-
"@dxos/debug": "0.8.4-main.
|
|
31
|
-
"@dxos/
|
|
32
|
-
"@dxos/echo-signals": "0.8.4-main.
|
|
33
|
-
"@dxos/
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/util": "0.8.4-main.84f28bd"
|
|
30
|
+
"@dxos/async": "0.8.4-main.ae835ea",
|
|
31
|
+
"@dxos/debug": "0.8.4-main.ae835ea",
|
|
32
|
+
"@dxos/invariant": "0.8.4-main.ae835ea",
|
|
33
|
+
"@dxos/echo-signals": "0.8.4-main.ae835ea",
|
|
34
|
+
"@dxos/live-object": "0.8.4-main.ae835ea",
|
|
35
|
+
"@dxos/log": "0.8.4-main.ae835ea",
|
|
36
|
+
"@dxos/util": "0.8.4-main.ae835ea",
|
|
37
|
+
"@dxos/echo": "0.8.4-main.ae835ea"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@effect-rx/rx-react": "0.
|
|
41
|
-
"@effect/platform": "0.
|
|
42
|
-
"@
|
|
43
|
-
"@types/react": "~
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"react": "~
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"@dxos/
|
|
50
|
-
"@dxos/
|
|
51
|
-
"@dxos/react-
|
|
52
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
53
|
-
"@dxos/react-ui-
|
|
54
|
-
"@dxos/react-ui-tabs": "0.8.4-main.
|
|
55
|
-
"@dxos/storybook-utils": "0.8.4-main.
|
|
56
|
-
"@dxos/react-ui-theme": "0.8.4-main.84f28bd"
|
|
40
|
+
"@effect-rx/rx-react": "0.42.4",
|
|
41
|
+
"@effect/platform": "0.92.1",
|
|
42
|
+
"@types/react": "~19.2.2",
|
|
43
|
+
"@types/react-dom": "~19.2.2",
|
|
44
|
+
"effect": "3.18.3",
|
|
45
|
+
"react": "~19.2.0",
|
|
46
|
+
"react-dom": "~19.2.0",
|
|
47
|
+
"vite": "7.1.9",
|
|
48
|
+
"@dxos/echo-db": "0.8.4-main.ae835ea",
|
|
49
|
+
"@dxos/random": "0.8.4-main.ae835ea",
|
|
50
|
+
"@dxos/react-client": "0.8.4-main.ae835ea",
|
|
51
|
+
"@dxos/react-ui": "0.8.4-main.ae835ea",
|
|
52
|
+
"@dxos/react-ui-list": "0.8.4-main.ae835ea",
|
|
53
|
+
"@dxos/react-ui-theme": "0.8.4-main.ae835ea",
|
|
54
|
+
"@dxos/react-ui-tabs": "0.8.4-main.ae835ea",
|
|
55
|
+
"@dxos/storybook-utils": "0.8.4-main.ae835ea"
|
|
57
56
|
},
|
|
58
57
|
"peerDependencies": {
|
|
59
58
|
"@effect-rx/rx-react": "^0.34.1",
|
|
60
59
|
"@effect/platform": "^0.80.12",
|
|
61
|
-
"@phosphor-icons/react": "^2.1.5",
|
|
62
60
|
"effect": "3.14.21",
|
|
63
|
-
"react": "
|
|
64
|
-
"react-dom": "
|
|
65
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
66
|
-
"@dxos/react-ui-theme": "0.8.4-main.
|
|
61
|
+
"react": "^19.0.0",
|
|
62
|
+
"react-dom": "^19.0.0",
|
|
63
|
+
"@dxos/react-ui": "0.8.4-main.ae835ea",
|
|
64
|
+
"@dxos/react-ui-theme": "0.8.4-main.ae835ea"
|
|
67
65
|
},
|
|
68
66
|
"publishConfig": {
|
|
69
67
|
"access": "public"
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { Registry, Rx } from '@effect-rx/rx-react';
|
|
6
|
-
import
|
|
6
|
+
import * as Function from 'effect/Function';
|
|
7
|
+
import * as Option from 'effect/Option';
|
|
7
8
|
import { describe, expect, onTestFinished, test } from 'vitest';
|
|
8
9
|
|
|
9
|
-
import {
|
|
10
|
+
import { Trigger, sleep } from '@dxos/async';
|
|
10
11
|
|
|
11
12
|
import { ROOT_ID } from './graph';
|
|
12
|
-
import {
|
|
13
|
+
import { GraphBuilder, createExtension } from './graph-builder';
|
|
13
14
|
import { type Node } from './node';
|
|
14
15
|
|
|
15
16
|
const exampleId = (id: number) => `dx:test:${id}`;
|
|
@@ -17,6 +18,63 @@ const EXAMPLE_ID = exampleId(1);
|
|
|
17
18
|
const EXAMPLE_TYPE = 'dxos.org/type/example';
|
|
18
19
|
|
|
19
20
|
describe('GraphBuilder', () => {
|
|
21
|
+
describe('resolver', () => {
|
|
22
|
+
test('works', async () => {
|
|
23
|
+
const registry = Registry.make();
|
|
24
|
+
const builder = new GraphBuilder({ registry });
|
|
25
|
+
const graph = builder.graph;
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
const node = graph.getNode(EXAMPLE_ID).pipe(Option.getOrNull);
|
|
29
|
+
expect(node).to.be.null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
builder.addExtension(
|
|
33
|
+
createExtension({
|
|
34
|
+
id: 'resolver',
|
|
35
|
+
resolver: () => {
|
|
36
|
+
console.log('resolver');
|
|
37
|
+
return Rx.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 });
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
await graph.initialize(EXAMPLE_ID);
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
const node = graph.getNode(EXAMPLE_ID).pipe(Option.getOrNull);
|
|
45
|
+
expect(node?.id).to.equal(EXAMPLE_ID);
|
|
46
|
+
expect(node?.type).to.equal(EXAMPLE_TYPE);
|
|
47
|
+
expect(node?.data).to.equal(1);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('updates', async () => {
|
|
52
|
+
const registry = Registry.make();
|
|
53
|
+
const builder = new GraphBuilder({ registry });
|
|
54
|
+
const name = Rx.make('default');
|
|
55
|
+
builder.addExtension(
|
|
56
|
+
createExtension({
|
|
57
|
+
id: 'resolver',
|
|
58
|
+
resolver: () => Rx.make((get) => ({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(name) })),
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
const graph = builder.graph;
|
|
62
|
+
await graph.initialize(EXAMPLE_ID);
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
const node = graph.getNode(EXAMPLE_ID).pipe(Option.getOrNull);
|
|
66
|
+
expect(node?.data).to.equal('default');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
registry.set(name, 'updated');
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
const node = graph.getNode(EXAMPLE_ID).pipe(Option.getOrNull);
|
|
73
|
+
expect(node?.data).to.equal('updated');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
20
78
|
describe('connector', () => {
|
|
21
79
|
test('works', () => {
|
|
22
80
|
const registry = Registry.make();
|
|
@@ -181,7 +239,7 @@ describe('GraphBuilder', () => {
|
|
|
181
239
|
id: 'root',
|
|
182
240
|
connector: (node) =>
|
|
183
241
|
Rx.make((get) =>
|
|
184
|
-
pipe(
|
|
242
|
+
Function.pipe(
|
|
185
243
|
get(node),
|
|
186
244
|
Option.flatMap((node) => (node.id === 'root' ? Option.some(get(name)) : Option.none())),
|
|
187
245
|
Option.filter((name) => name !== 'removed'),
|
|
@@ -273,7 +331,7 @@ describe('GraphBuilder', () => {
|
|
|
273
331
|
id: 'root',
|
|
274
332
|
connector: (node) =>
|
|
275
333
|
Rx.make((get) =>
|
|
276
|
-
pipe(
|
|
334
|
+
Function.pipe(
|
|
277
335
|
get(node),
|
|
278
336
|
Option.flatMap((node) => (node.id === 'root' ? Option.some(get(name)) : Option.none())),
|
|
279
337
|
Option.filter((name) => name !== 'removed'),
|
|
@@ -286,7 +344,7 @@ describe('GraphBuilder', () => {
|
|
|
286
344
|
id: 'connector1',
|
|
287
345
|
connector: (node) =>
|
|
288
346
|
Rx.make((get) =>
|
|
289
|
-
pipe(
|
|
347
|
+
Function.pipe(
|
|
290
348
|
get(node),
|
|
291
349
|
Option.flatMap((node) => (node.id === EXAMPLE_ID ? Option.some(get(sub)) : Option.none())),
|
|
292
350
|
Option.map((sub) => [{ id: exampleId(2), type: EXAMPLE_TYPE, data: sub }]),
|
|
@@ -298,7 +356,7 @@ describe('GraphBuilder', () => {
|
|
|
298
356
|
id: 'connector2',
|
|
299
357
|
connector: (node) =>
|
|
300
358
|
Rx.make((get) =>
|
|
301
|
-
pipe(
|
|
359
|
+
Function.pipe(
|
|
302
360
|
get(node),
|
|
303
361
|
Option.flatMap((node) => (node.id === EXAMPLE_ID ? Option.some(node.data) : Option.none())),
|
|
304
362
|
Option.map((data) => [{ id: exampleId(3), type: EXAMPLE_TYPE, data }]),
|
|
@@ -382,7 +440,7 @@ describe('GraphBuilder', () => {
|
|
|
382
440
|
id: 'connector',
|
|
383
441
|
connector: (node) => {
|
|
384
442
|
return Rx.make((get) =>
|
|
385
|
-
pipe(
|
|
443
|
+
Function.pipe(
|
|
386
444
|
get(node),
|
|
387
445
|
Option.map((node) => (node.data ? node.data + 1 : 1)),
|
|
388
446
|
Option.filter((data) => data <= 5),
|
|
@@ -418,7 +476,7 @@ describe('GraphBuilder', () => {
|
|
|
418
476
|
id: 'connector',
|
|
419
477
|
connector: (node) =>
|
|
420
478
|
Rx.make((get) =>
|
|
421
|
-
pipe(
|
|
479
|
+
Function.pipe(
|
|
422
480
|
get(node),
|
|
423
481
|
Option.map((node) => (node.data ? node.data + 1 : 1)),
|
|
424
482
|
Option.filter((data) => data <= 5),
|
package/src/graph-builder.ts
CHANGED
|
@@ -4,14 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
import { Registry, Rx } from '@effect-rx/rx-react';
|
|
6
6
|
import { effect } from '@preact/signals-core';
|
|
7
|
-
import
|
|
7
|
+
import * as Array from 'effect/Array';
|
|
8
|
+
import * as Function from 'effect/Function';
|
|
9
|
+
import * as Option from 'effect/Option';
|
|
10
|
+
import * as Record from 'effect/Record';
|
|
8
11
|
|
|
9
|
-
import { type MulticastObservable, type
|
|
12
|
+
import { type CleanupFn, type MulticastObservable, type Trigger } from '@dxos/async';
|
|
10
13
|
import { log } from '@dxos/log';
|
|
11
|
-
import { byPosition, getDebugName, isNode, isNonNullable
|
|
14
|
+
import { type MaybePromise, type Position, byPosition, getDebugName, isNode, isNonNullable } from '@dxos/util';
|
|
12
15
|
|
|
13
|
-
import { ACTION_GROUP_TYPE, ACTION_TYPE,
|
|
14
|
-
import {
|
|
16
|
+
import { ACTION_GROUP_TYPE, ACTION_TYPE, type ExpandableGraph, Graph, type GraphParams, ROOT_ID } from './graph';
|
|
17
|
+
import { type ActionData, type Node, type NodeArg, type Relation, actionGroupSymbol } from './node';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Graph builder extension for adding nodes to the graph based on a node id.
|
|
21
|
+
*/
|
|
22
|
+
export type ResolverExtension = (id: string) => Rx.Rx<NodeArg<any> | null>;
|
|
15
23
|
|
|
16
24
|
/**
|
|
17
25
|
* Graph builder extension for adding nodes to the graph based on a connection to an existing node.
|
|
@@ -49,8 +57,7 @@ export type CreateExtensionOptions = {
|
|
|
49
57
|
id: string;
|
|
50
58
|
relation?: Relation;
|
|
51
59
|
position?: Position;
|
|
52
|
-
|
|
53
|
-
// resolver?: ResolverExtension;
|
|
60
|
+
resolver?: ResolverExtension;
|
|
54
61
|
connector?: ConnectorExtension;
|
|
55
62
|
actions?: ActionsExtension;
|
|
56
63
|
actionGroups?: ActionGroupsExtension;
|
|
@@ -64,12 +71,16 @@ export const createExtension = (extension: CreateExtensionOptions): BuilderExten
|
|
|
64
71
|
id,
|
|
65
72
|
position = 'static',
|
|
66
73
|
relation = 'outbound',
|
|
74
|
+
resolver: _resolver,
|
|
67
75
|
connector: _connector,
|
|
68
76
|
actions: _actions,
|
|
69
77
|
actionGroups: _actionGroups,
|
|
70
78
|
} = extension;
|
|
71
79
|
const getId = (key: string) => `${id}/${key}`;
|
|
72
80
|
|
|
81
|
+
const resolver =
|
|
82
|
+
_resolver && Rx.family((id: string) => _resolver(id).pipe(Rx.withLabel(`graph-builder:_resolver:${id}`)));
|
|
83
|
+
|
|
73
84
|
const connector =
|
|
74
85
|
_connector &&
|
|
75
86
|
Rx.family((node: Rx.Rx<Option.Option<Node>>) =>
|
|
@@ -87,7 +98,7 @@ export const createExtension = (extension: CreateExtensionOptions): BuilderExten
|
|
|
87
98
|
Rx.family((node: Rx.Rx<Option.Option<Node>>) => _actions(node).pipe(Rx.withLabel(`graph-builder:_actions:${id}`)));
|
|
88
99
|
|
|
89
100
|
return [
|
|
90
|
-
|
|
101
|
+
resolver ? { id: getId('resolver'), position, resolver } : undefined,
|
|
91
102
|
connector
|
|
92
103
|
? ({
|
|
93
104
|
id: getId('connector'),
|
|
@@ -157,7 +168,7 @@ export type BuilderExtension = Readonly<{
|
|
|
157
168
|
id: string;
|
|
158
169
|
position: Position;
|
|
159
170
|
relation?: Relation; // Only for connector.
|
|
160
|
-
|
|
171
|
+
resolver?: ResolverExtension;
|
|
161
172
|
connector?: (node: Rx.Rx<Option.Option<Node>>) => Rx.Rx<NodeArg<any>[]>;
|
|
162
173
|
}>;
|
|
163
174
|
|
|
@@ -179,12 +190,12 @@ export const flattenExtensions = (extension: BuilderExtensions, acc: BuilderExte
|
|
|
179
190
|
// Should track LRU nodes that are not in the set/radius and remove them beyond a certain threshold.
|
|
180
191
|
export class GraphBuilder {
|
|
181
192
|
// TODO(wittjosiah): Use Context.
|
|
182
|
-
private readonly
|
|
193
|
+
private readonly _subscriptions = new Map<string, CleanupFn>();
|
|
183
194
|
private readonly _extensions = Rx.make(Record.empty<string, BuilderExtension>()).pipe(
|
|
184
195
|
Rx.keepAlive,
|
|
185
196
|
Rx.withLabel('graph-builder:extensions'),
|
|
186
197
|
);
|
|
187
|
-
|
|
198
|
+
private readonly _initialized: Record<string, Trigger> = {};
|
|
188
199
|
private readonly _registry: Registry.Registry;
|
|
189
200
|
private readonly _graph: Graph;
|
|
190
201
|
|
|
@@ -194,7 +205,7 @@ export class GraphBuilder {
|
|
|
194
205
|
...params,
|
|
195
206
|
registry: this._registry,
|
|
196
207
|
onExpand: (id, relation) => this._onExpand(id, relation),
|
|
197
|
-
|
|
208
|
+
onInitialize: (id) => this._onInitialize(id),
|
|
198
209
|
onRemoveNode: (id) => this._onRemoveNode(id),
|
|
199
210
|
});
|
|
200
211
|
}
|
|
@@ -275,16 +286,31 @@ export class GraphBuilder {
|
|
|
275
286
|
}
|
|
276
287
|
|
|
277
288
|
destroy(): void {
|
|
278
|
-
this.
|
|
279
|
-
this.
|
|
289
|
+
this._subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
290
|
+
this._subscriptions.clear();
|
|
280
291
|
}
|
|
281
292
|
|
|
293
|
+
private readonly _resolvers = Rx.family<string, Rx.Rx<Option.Option<NodeArg<any>>>>((id) => {
|
|
294
|
+
return Rx.make((get) => {
|
|
295
|
+
return Function.pipe(
|
|
296
|
+
get(this._extensions),
|
|
297
|
+
Record.values,
|
|
298
|
+
Array.sortBy(byPosition),
|
|
299
|
+
Array.map(({ resolver }) => resolver),
|
|
300
|
+
Array.filter(isNonNullable),
|
|
301
|
+
Array.map((resolver) => get(resolver(id))),
|
|
302
|
+
Array.filter(isNonNullable),
|
|
303
|
+
Array.head,
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
282
308
|
private readonly _connectors = Rx.family<string, Rx.Rx<NodeArg<any>[]>>((key) => {
|
|
283
309
|
return Rx.make((get) => {
|
|
284
310
|
const [id, relation] = key.split('+');
|
|
285
311
|
const node = this._graph.node(id);
|
|
286
312
|
|
|
287
|
-
return pipe(
|
|
313
|
+
return Function.pipe(
|
|
288
314
|
get(this._extensions),
|
|
289
315
|
Record.values,
|
|
290
316
|
// TODO(wittjosiah): Sort on write rather than read.
|
|
@@ -341,17 +367,38 @@ export class GraphBuilder {
|
|
|
341
367
|
{ immediate: true },
|
|
342
368
|
);
|
|
343
369
|
|
|
344
|
-
this.
|
|
370
|
+
this._subscriptions.set(id, cancel);
|
|
345
371
|
}
|
|
346
372
|
|
|
347
|
-
// TODO(wittjosiah):
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
373
|
+
// TODO(wittjosiah): If the same node is added by a connector, the resolver should probably cancel itself?
|
|
374
|
+
private async _onInitialize(id: string) {
|
|
375
|
+
log('onInitialize', { id });
|
|
376
|
+
const resolver = this._resolvers(id);
|
|
377
|
+
|
|
378
|
+
const cancel = this._registry.subscribe(
|
|
379
|
+
resolver,
|
|
380
|
+
(node) => {
|
|
381
|
+
const trigger = this._initialized[id];
|
|
382
|
+
Option.match(node, {
|
|
383
|
+
onSome: (node) => {
|
|
384
|
+
this._graph.addNodes([node]);
|
|
385
|
+
trigger?.wake();
|
|
386
|
+
},
|
|
387
|
+
onNone: () => {
|
|
388
|
+
trigger?.wake();
|
|
389
|
+
this._graph.removeNodes([id]);
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
},
|
|
393
|
+
{ immediate: true },
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
this._subscriptions.set(id, cancel);
|
|
397
|
+
}
|
|
351
398
|
|
|
352
399
|
private _onRemoveNode(id: string): void {
|
|
353
|
-
this.
|
|
354
|
-
this.
|
|
400
|
+
this._subscriptions.get(id)?.();
|
|
401
|
+
this._subscriptions.delete(id);
|
|
355
402
|
}
|
|
356
403
|
}
|
|
357
404
|
|
package/src/graph.test.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { Registry, Rx } from '@effect-rx/rx-react';
|
|
6
|
-
import
|
|
6
|
+
import * as Option from 'effect/Option';
|
|
7
7
|
import { assert, describe, expect, onTestFinished, test } from 'vitest';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { Graph, ROOT_ID, ROOT_TYPE, getGraph } from './graph';
|
|
10
10
|
import { type Node } from './node';
|
|
11
11
|
|
|
12
12
|
const exampleId = (id: number) => `dx:test:${id}`;
|
package/src/graph.ts
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { Registry, Rx } from '@effect-rx/rx-react';
|
|
6
|
-
import
|
|
6
|
+
import * as Function from 'effect/Function';
|
|
7
|
+
import * as Option from 'effect/Option';
|
|
8
|
+
import * as Record from 'effect/Record';
|
|
7
9
|
|
|
8
10
|
import { Event, Trigger } from '@dxos/async';
|
|
9
11
|
import { todo } from '@dxos/debug';
|
|
10
12
|
import { invariant } from '@dxos/invariant';
|
|
11
13
|
import { log } from '@dxos/log';
|
|
12
|
-
import {
|
|
14
|
+
import { type MakeOptional, isNonNullable } from '@dxos/util';
|
|
13
15
|
|
|
14
|
-
import { type
|
|
16
|
+
import { type Action, type ActionGroup, type Node, type NodeArg, type Relation } from './node';
|
|
15
17
|
|
|
16
18
|
const graphSymbol = Symbol('graph');
|
|
17
19
|
type DeepWriteable<T> = { -readonly [K in keyof T]: T[K] extends object ? DeepWriteable<T[K]> : T[K] };
|
|
@@ -59,8 +61,7 @@ export type GraphParams = {
|
|
|
59
61
|
nodes?: MakeOptional<Node, 'data' | 'cacheable'>[];
|
|
60
62
|
edges?: Record<string, Edges>;
|
|
61
63
|
onExpand?: Graph['_onExpand'];
|
|
62
|
-
|
|
63
|
-
// onInitialize?: Graph['_onInitialize'];
|
|
64
|
+
onInitialize?: Graph['_onInitialize'];
|
|
64
65
|
onRemoveNode?: Graph['_onRemoveNode'];
|
|
65
66
|
};
|
|
66
67
|
|
|
@@ -166,7 +167,7 @@ export interface ExpandableGraph extends ReadableGraph {
|
|
|
166
167
|
*
|
|
167
168
|
* Fires the `onInitialize` callback to provide initial data for a node.
|
|
168
169
|
*/
|
|
169
|
-
|
|
170
|
+
initialize(id: string): Promise<void>;
|
|
170
171
|
|
|
171
172
|
/**
|
|
172
173
|
* Expand a node in the graph.
|
|
@@ -230,7 +231,7 @@ export class Graph implements WritableGraph {
|
|
|
230
231
|
readonly onNodeChanged = new Event<{ id: string; node: Option.Option<Node> }>();
|
|
231
232
|
|
|
232
233
|
private readonly _onExpand?: (id: string, relation: Relation) => void;
|
|
233
|
-
|
|
234
|
+
private readonly _onInitialize?: (id: string) => Promise<void>;
|
|
234
235
|
private readonly _onRemoveNode?: (id: string) => void;
|
|
235
236
|
|
|
236
237
|
private readonly _registry: Registry.Registry;
|
|
@@ -286,7 +287,7 @@ export class Graph implements WritableGraph {
|
|
|
286
287
|
const toJSON = (node: Node, seen: string[] = []): any => {
|
|
287
288
|
const nodes = get(this.connections(node.id));
|
|
288
289
|
const obj: Record<string, any> = {
|
|
289
|
-
id: node.id
|
|
290
|
+
id: node.id,
|
|
290
291
|
type: node.type,
|
|
291
292
|
};
|
|
292
293
|
if (node.properties.label) {
|
|
@@ -309,8 +310,9 @@ export class Graph implements WritableGraph {
|
|
|
309
310
|
}).pipe(Rx.withLabel(`graph:json:${id}`));
|
|
310
311
|
});
|
|
311
312
|
|
|
312
|
-
constructor({ registry, nodes, edges, onExpand, onRemoveNode }: GraphParams = {}) {
|
|
313
|
+
constructor({ registry, nodes, edges, onInitialize, onExpand, onRemoveNode }: GraphParams = {}) {
|
|
313
314
|
this._registry = registry ?? Registry.make();
|
|
315
|
+
this._onInitialize = onInitialize;
|
|
314
316
|
this._onExpand = onExpand;
|
|
315
317
|
this._onRemoveNode = onRemoveNode;
|
|
316
318
|
|
|
@@ -379,15 +381,14 @@ export class Graph implements WritableGraph {
|
|
|
379
381
|
return this._registry.get(this.edges(id));
|
|
380
382
|
}
|
|
381
383
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// }
|
|
384
|
+
async initialize(id: string) {
|
|
385
|
+
const initialized = Record.get(this._initialized, id).pipe(Option.getOrElse(() => false));
|
|
386
|
+
log('initialize', { id, initialized });
|
|
387
|
+
if (!initialized) {
|
|
388
|
+
await this._onInitialize?.(id);
|
|
389
|
+
Record.set(this._initialized, id, true);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
391
392
|
|
|
392
393
|
expand(id: string, relation: Relation = 'outbound'): void {
|
|
393
394
|
const key = `${id}$${relation}`;
|
|
@@ -554,7 +555,7 @@ export class Graph implements WritableGraph {
|
|
|
554
555
|
}
|
|
555
556
|
|
|
556
557
|
getPath({ source = 'root', target }: { source?: string; target: string }): Option.Option<string[]> {
|
|
557
|
-
return pipe(
|
|
558
|
+
return Function.pipe(
|
|
558
559
|
this.getNode(source),
|
|
559
560
|
Option.flatMap((node) => {
|
|
560
561
|
let found: Option.Option<string[]> = Option.none();
|
package/src/node.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type
|
|
5
|
+
import { type MakeOptional, type MaybePromise } from '@dxos/util';
|
|
6
|
+
|
|
7
|
+
import { ACTION_GROUP_TYPE, ACTION_TYPE } from './graph';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Represents a node in the graph.
|
|
@@ -84,7 +86,7 @@ export type Action<TProperties extends Record<string, any> = Record<string, any>
|
|
|
84
86
|
>;
|
|
85
87
|
|
|
86
88
|
export const isAction = (data: unknown): data is Action =>
|
|
87
|
-
isGraphNode(data) ? typeof data.data === 'function' : false;
|
|
89
|
+
isGraphNode(data) ? typeof data.data === 'function' && data.type === ACTION_TYPE : false;
|
|
88
90
|
|
|
89
91
|
export const actionGroupSymbol = Symbol('ActionGroup');
|
|
90
92
|
|
|
@@ -95,7 +97,7 @@ export type ActionGroup<TProperties extends Record<string, any> = Record<string,
|
|
|
95
97
|
>;
|
|
96
98
|
|
|
97
99
|
export const isActionGroup = (data: unknown): data is ActionGroup =>
|
|
98
|
-
isGraphNode(data) ? data.data === actionGroupSymbol : false;
|
|
100
|
+
isGraphNode(data) ? data.data === actionGroupSymbol && data.type === ACTION_GROUP_TYPE : false;
|
|
99
101
|
|
|
100
102
|
export type ActionLike = Action | ActionGroup;
|
|
101
103
|
|
|
@@ -7,14 +7,14 @@ import { signal } from '@preact/signals-core';
|
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, onTestFinished, test } from 'vitest';
|
|
8
8
|
|
|
9
9
|
import { Trigger } from '@dxos/async';
|
|
10
|
+
import { Obj, Type } from '@dxos/echo';
|
|
11
|
+
import { Ref } from '@dxos/echo/internal';
|
|
10
12
|
import { Filter } from '@dxos/echo-db';
|
|
11
13
|
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
12
|
-
import { Expando, Ref } from '@dxos/echo-schema';
|
|
13
14
|
import { registerSignalsRuntime } from '@dxos/echo-signals';
|
|
14
|
-
import { live } from '@dxos/live-object';
|
|
15
15
|
|
|
16
16
|
import { ROOT_ID } from './graph';
|
|
17
|
-
import {
|
|
17
|
+
import { GraphBuilder, createExtension, rxFromSignal } from './graph-builder';
|
|
18
18
|
import { rxFromQuery } from './testing';
|
|
19
19
|
|
|
20
20
|
registerSignalsRuntime();
|
|
@@ -174,15 +174,15 @@ describe('signals integration', () => {
|
|
|
174
174
|
const registry = Registry.make();
|
|
175
175
|
await using peer = await dbBuilder.createPeer();
|
|
176
176
|
await using db = await peer.createDatabase();
|
|
177
|
-
db.add(
|
|
178
|
-
db.add(
|
|
177
|
+
db.add(Obj.make(Type.Expando, { name: 'a' }));
|
|
178
|
+
db.add(Obj.make(Type.Expando, { name: 'b' }));
|
|
179
179
|
|
|
180
180
|
const builder = new GraphBuilder({ registry });
|
|
181
181
|
builder.addExtension(
|
|
182
182
|
createExtension({
|
|
183
183
|
id: 'expando',
|
|
184
184
|
connector: () => {
|
|
185
|
-
const query = db.query(Filter.type(Expando));
|
|
185
|
+
const query = db.query(Filter.type(Type.Expando));
|
|
186
186
|
|
|
187
187
|
return Rx.make((get) => {
|
|
188
188
|
const objects = get(rxFromQuery(query));
|
|
@@ -205,7 +205,7 @@ describe('signals integration', () => {
|
|
|
205
205
|
graph.expand(ROOT_ID);
|
|
206
206
|
expect(count).to.eq(2);
|
|
207
207
|
|
|
208
|
-
const object = db.add(
|
|
208
|
+
const object = db.add(Obj.make(Type.Expando, { name: 'c' }));
|
|
209
209
|
await db.flush();
|
|
210
210
|
expect(count).to.eq(3);
|
|
211
211
|
|