@dxos/app-graph 0.8.3 → 0.8.4-main.28f8d3d
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 +194 -141
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +194 -141
- 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 +6 -2
- 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 +3 -8
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +25 -26
- package/src/graph-builder.test.ts +59 -2
- package/src/graph-builder.ts +65 -21
- package/src/graph.test.ts +1 -1
- package/src/graph.ts +16 -17
- package/src/node.ts +5 -3
- package/src/signals-integration.test.ts +1 -1
- package/src/stories/EchoGraph.stories.tsx +20 -135
- package/dist/lib/node/index.cjs +0 -816
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/app-graph",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4-main.28f8d3d",
|
|
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"
|
|
@@ -26,44 +27,42 @@
|
|
|
26
27
|
"dependencies": {
|
|
27
28
|
"@preact/signals-core": "^1.9.0",
|
|
28
29
|
"main-thread-scheduling": "^14.1.1",
|
|
29
|
-
"@dxos/async": "0.8.
|
|
30
|
-
"@dxos/debug": "0.8.
|
|
31
|
-
"@dxos/echo": "0.8.
|
|
32
|
-
"@dxos/
|
|
33
|
-
"@dxos/echo-signals": "0.8.
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/
|
|
30
|
+
"@dxos/async": "0.8.4-main.28f8d3d",
|
|
31
|
+
"@dxos/debug": "0.8.4-main.28f8d3d",
|
|
32
|
+
"@dxos/echo": "0.8.4-main.28f8d3d",
|
|
33
|
+
"@dxos/echo-schema": "0.8.4-main.28f8d3d",
|
|
34
|
+
"@dxos/echo-signals": "0.8.4-main.28f8d3d",
|
|
35
|
+
"@dxos/invariant": "0.8.4-main.28f8d3d",
|
|
36
|
+
"@dxos/log": "0.8.4-main.28f8d3d",
|
|
37
|
+
"@dxos/live-object": "0.8.4-main.28f8d3d",
|
|
38
|
+
"@dxos/util": "0.8.4-main.28f8d3d"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
|
-
"@effect-rx/rx-react": "
|
|
41
|
-
"@effect/platform": "0.
|
|
42
|
-
"@phosphor-icons/react": "^2.1.5",
|
|
41
|
+
"@effect-rx/rx-react": "0.38.0",
|
|
42
|
+
"@effect/platform": "0.90.2",
|
|
43
43
|
"@types/react": "~18.2.0",
|
|
44
44
|
"@types/react-dom": "~18.2.0",
|
|
45
|
-
"effect": "3.
|
|
45
|
+
"effect": "3.17.7",
|
|
46
46
|
"react": "~18.2.0",
|
|
47
47
|
"react-dom": "~18.2.0",
|
|
48
48
|
"vite": "5.4.7",
|
|
49
|
-
"@dxos/
|
|
50
|
-
"@dxos/
|
|
51
|
-
"@dxos/react-
|
|
52
|
-
"@dxos/react-ui-list": "0.8.
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/
|
|
55
|
-
"@dxos/react-ui-
|
|
56
|
-
"@dxos/
|
|
49
|
+
"@dxos/echo-db": "0.8.4-main.28f8d3d",
|
|
50
|
+
"@dxos/random": "0.8.4-main.28f8d3d",
|
|
51
|
+
"@dxos/react-client": "0.8.4-main.28f8d3d",
|
|
52
|
+
"@dxos/react-ui-list": "0.8.4-main.28f8d3d",
|
|
53
|
+
"@dxos/react-ui-theme": "0.8.4-main.28f8d3d",
|
|
54
|
+
"@dxos/storybook-utils": "0.8.4-main.28f8d3d",
|
|
55
|
+
"@dxos/react-ui-tabs": "0.8.4-main.28f8d3d",
|
|
56
|
+
"@dxos/react-ui": "0.8.4-main.28f8d3d"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@effect-rx/rx-react": "^0.34.1",
|
|
60
|
-
"@effect/platform": "0.80.12",
|
|
61
|
-
"@phosphor-icons/react": "^2.1.5",
|
|
60
|
+
"@effect/platform": "^0.80.12",
|
|
62
61
|
"effect": "3.14.21",
|
|
63
62
|
"react": "~18.2.0",
|
|
64
63
|
"react-dom": "~18.2.0",
|
|
65
|
-
"@dxos/react-ui": "0.8.
|
|
66
|
-
"@dxos/react-ui-theme": "0.8.
|
|
64
|
+
"@dxos/react-ui": "0.8.4-main.28f8d3d",
|
|
65
|
+
"@dxos/react-ui-theme": "0.8.4-main.28f8d3d"
|
|
67
66
|
},
|
|
68
67
|
"publishConfig": {
|
|
69
68
|
"access": "public"
|
|
@@ -6,10 +6,10 @@ import { Registry, Rx } from '@effect-rx/rx-react';
|
|
|
6
6
|
import { Option, pipe } from 'effect';
|
|
7
7
|
import { describe, expect, onTestFinished, test } from 'vitest';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { Trigger, sleep } from '@dxos/async';
|
|
10
10
|
|
|
11
11
|
import { ROOT_ID } from './graph';
|
|
12
|
-
import {
|
|
12
|
+
import { GraphBuilder, createExtension } from './graph-builder';
|
|
13
13
|
import { type Node } from './node';
|
|
14
14
|
|
|
15
15
|
const exampleId = (id: number) => `dx:test:${id}`;
|
|
@@ -17,6 +17,63 @@ const EXAMPLE_ID = exampleId(1);
|
|
|
17
17
|
const EXAMPLE_TYPE = 'dxos.org/type/example';
|
|
18
18
|
|
|
19
19
|
describe('GraphBuilder', () => {
|
|
20
|
+
describe('resolver', () => {
|
|
21
|
+
test('works', async () => {
|
|
22
|
+
const registry = Registry.make();
|
|
23
|
+
const builder = new GraphBuilder({ registry });
|
|
24
|
+
const graph = builder.graph;
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
const node = graph.getNode(EXAMPLE_ID).pipe(Option.getOrNull);
|
|
28
|
+
expect(node).to.be.null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
builder.addExtension(
|
|
32
|
+
createExtension({
|
|
33
|
+
id: 'resolver',
|
|
34
|
+
resolver: () => {
|
|
35
|
+
console.log('resolver');
|
|
36
|
+
return Rx.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 });
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
await graph.initialize(EXAMPLE_ID);
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
const node = graph.getNode(EXAMPLE_ID).pipe(Option.getOrNull);
|
|
44
|
+
expect(node?.id).to.equal(EXAMPLE_ID);
|
|
45
|
+
expect(node?.type).to.equal(EXAMPLE_TYPE);
|
|
46
|
+
expect(node?.data).to.equal(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('updates', async () => {
|
|
51
|
+
const registry = Registry.make();
|
|
52
|
+
const builder = new GraphBuilder({ registry });
|
|
53
|
+
const name = Rx.make('default');
|
|
54
|
+
builder.addExtension(
|
|
55
|
+
createExtension({
|
|
56
|
+
id: 'resolver',
|
|
57
|
+
resolver: () => Rx.make((get) => ({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(name) })),
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
const graph = builder.graph;
|
|
61
|
+
await graph.initialize(EXAMPLE_ID);
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
const node = graph.getNode(EXAMPLE_ID).pipe(Option.getOrNull);
|
|
65
|
+
expect(node?.data).to.equal('default');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
registry.set(name, 'updated');
|
|
69
|
+
|
|
70
|
+
{
|
|
71
|
+
const node = graph.getNode(EXAMPLE_ID).pipe(Option.getOrNull);
|
|
72
|
+
expect(node?.data).to.equal('updated');
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
20
77
|
describe('connector', () => {
|
|
21
78
|
test('works', () => {
|
|
22
79
|
const registry = Registry.make();
|
package/src/graph-builder.ts
CHANGED
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
import { Registry, Rx } from '@effect-rx/rx-react';
|
|
6
6
|
import { effect } from '@preact/signals-core';
|
|
7
|
-
import { Array,
|
|
7
|
+
import { Array, Option, Record, pipe } from 'effect';
|
|
8
8
|
|
|
9
|
-
import { type MulticastObservable, type
|
|
9
|
+
import { type CleanupFn, type MulticastObservable, type Trigger } from '@dxos/async';
|
|
10
10
|
import { log } from '@dxos/log';
|
|
11
|
-
import { byPosition, getDebugName, isNode, isNonNullable
|
|
11
|
+
import { type MaybePromise, type Position, byPosition, getDebugName, isNode, isNonNullable } from '@dxos/util';
|
|
12
12
|
|
|
13
|
-
import { ACTION_GROUP_TYPE, ACTION_TYPE,
|
|
14
|
-
import {
|
|
13
|
+
import { ACTION_GROUP_TYPE, ACTION_TYPE, type ExpandableGraph, Graph, type GraphParams, ROOT_ID } from './graph';
|
|
14
|
+
import { type ActionData, type Node, type NodeArg, type Relation, actionGroupSymbol } from './node';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Graph builder extension for adding nodes to the graph based on a node id.
|
|
18
|
+
*/
|
|
19
|
+
export type ResolverExtension = (id: string) => Rx.Rx<NodeArg<any> | null>;
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* Graph builder extension for adding nodes to the graph based on a connection to an existing node.
|
|
@@ -49,8 +54,7 @@ export type CreateExtensionOptions = {
|
|
|
49
54
|
id: string;
|
|
50
55
|
relation?: Relation;
|
|
51
56
|
position?: Position;
|
|
52
|
-
|
|
53
|
-
// resolver?: ResolverExtension;
|
|
57
|
+
resolver?: ResolverExtension;
|
|
54
58
|
connector?: ConnectorExtension;
|
|
55
59
|
actions?: ActionsExtension;
|
|
56
60
|
actionGroups?: ActionGroupsExtension;
|
|
@@ -64,12 +68,16 @@ export const createExtension = (extension: CreateExtensionOptions): BuilderExten
|
|
|
64
68
|
id,
|
|
65
69
|
position = 'static',
|
|
66
70
|
relation = 'outbound',
|
|
71
|
+
resolver: _resolver,
|
|
67
72
|
connector: _connector,
|
|
68
73
|
actions: _actions,
|
|
69
74
|
actionGroups: _actionGroups,
|
|
70
75
|
} = extension;
|
|
71
76
|
const getId = (key: string) => `${id}/${key}`;
|
|
72
77
|
|
|
78
|
+
const resolver =
|
|
79
|
+
_resolver && Rx.family((id: string) => _resolver(id).pipe(Rx.withLabel(`graph-builder:_resolver:${id}`)));
|
|
80
|
+
|
|
73
81
|
const connector =
|
|
74
82
|
_connector &&
|
|
75
83
|
Rx.family((node: Rx.Rx<Option.Option<Node>>) =>
|
|
@@ -87,7 +95,7 @@ export const createExtension = (extension: CreateExtensionOptions): BuilderExten
|
|
|
87
95
|
Rx.family((node: Rx.Rx<Option.Option<Node>>) => _actions(node).pipe(Rx.withLabel(`graph-builder:_actions:${id}`)));
|
|
88
96
|
|
|
89
97
|
return [
|
|
90
|
-
|
|
98
|
+
resolver ? { id: getId('resolver'), position, resolver } : undefined,
|
|
91
99
|
connector
|
|
92
100
|
? ({
|
|
93
101
|
id: getId('connector'),
|
|
@@ -157,7 +165,7 @@ export type BuilderExtension = Readonly<{
|
|
|
157
165
|
id: string;
|
|
158
166
|
position: Position;
|
|
159
167
|
relation?: Relation; // Only for connector.
|
|
160
|
-
|
|
168
|
+
resolver?: ResolverExtension;
|
|
161
169
|
connector?: (node: Rx.Rx<Option.Option<Node>>) => Rx.Rx<NodeArg<any>[]>;
|
|
162
170
|
}>;
|
|
163
171
|
|
|
@@ -179,12 +187,12 @@ export const flattenExtensions = (extension: BuilderExtensions, acc: BuilderExte
|
|
|
179
187
|
// Should track LRU nodes that are not in the set/radius and remove them beyond a certain threshold.
|
|
180
188
|
export class GraphBuilder {
|
|
181
189
|
// TODO(wittjosiah): Use Context.
|
|
182
|
-
private readonly
|
|
190
|
+
private readonly _subscriptions = new Map<string, CleanupFn>();
|
|
183
191
|
private readonly _extensions = Rx.make(Record.empty<string, BuilderExtension>()).pipe(
|
|
184
192
|
Rx.keepAlive,
|
|
185
193
|
Rx.withLabel('graph-builder:extensions'),
|
|
186
194
|
);
|
|
187
|
-
|
|
195
|
+
private readonly _initialized: Record<string, Trigger> = {};
|
|
188
196
|
private readonly _registry: Registry.Registry;
|
|
189
197
|
private readonly _graph: Graph;
|
|
190
198
|
|
|
@@ -194,7 +202,7 @@ export class GraphBuilder {
|
|
|
194
202
|
...params,
|
|
195
203
|
registry: this._registry,
|
|
196
204
|
onExpand: (id, relation) => this._onExpand(id, relation),
|
|
197
|
-
|
|
205
|
+
onInitialize: (id) => this._onInitialize(id),
|
|
198
206
|
onRemoveNode: (id) => this._onRemoveNode(id),
|
|
199
207
|
});
|
|
200
208
|
}
|
|
@@ -275,10 +283,25 @@ export class GraphBuilder {
|
|
|
275
283
|
}
|
|
276
284
|
|
|
277
285
|
destroy(): void {
|
|
278
|
-
this.
|
|
279
|
-
this.
|
|
286
|
+
this._subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
287
|
+
this._subscriptions.clear();
|
|
280
288
|
}
|
|
281
289
|
|
|
290
|
+
private readonly _resolvers = Rx.family<string, Rx.Rx<Option.Option<NodeArg<any>>>>((id) => {
|
|
291
|
+
return Rx.make((get) => {
|
|
292
|
+
return pipe(
|
|
293
|
+
get(this._extensions),
|
|
294
|
+
Record.values,
|
|
295
|
+
Array.sortBy(byPosition),
|
|
296
|
+
Array.map(({ resolver }) => resolver),
|
|
297
|
+
Array.filter(isNonNullable),
|
|
298
|
+
Array.map((resolver) => get(resolver(id))),
|
|
299
|
+
Array.filter(isNonNullable),
|
|
300
|
+
Array.head,
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
282
305
|
private readonly _connectors = Rx.family<string, Rx.Rx<NodeArg<any>[]>>((key) => {
|
|
283
306
|
return Rx.make((get) => {
|
|
284
307
|
const [id, relation] = key.split('+');
|
|
@@ -341,17 +364,38 @@ export class GraphBuilder {
|
|
|
341
364
|
{ immediate: true },
|
|
342
365
|
);
|
|
343
366
|
|
|
344
|
-
this.
|
|
367
|
+
this._subscriptions.set(id, cancel);
|
|
345
368
|
}
|
|
346
369
|
|
|
347
|
-
// TODO(wittjosiah):
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
370
|
+
// TODO(wittjosiah): If the same node is added by a connector, the resolver should probably cancel itself?
|
|
371
|
+
private async _onInitialize(id: string) {
|
|
372
|
+
log('onInitialize', { id });
|
|
373
|
+
const resolver = this._resolvers(id);
|
|
374
|
+
|
|
375
|
+
const cancel = this._registry.subscribe(
|
|
376
|
+
resolver,
|
|
377
|
+
(node) => {
|
|
378
|
+
const trigger = this._initialized[id];
|
|
379
|
+
Option.match(node, {
|
|
380
|
+
onSome: (node) => {
|
|
381
|
+
this._graph.addNodes([node]);
|
|
382
|
+
trigger?.wake();
|
|
383
|
+
},
|
|
384
|
+
onNone: () => {
|
|
385
|
+
trigger?.wake();
|
|
386
|
+
this._graph.removeNodes([id]);
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
},
|
|
390
|
+
{ immediate: true },
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
this._subscriptions.set(id, cancel);
|
|
394
|
+
}
|
|
351
395
|
|
|
352
396
|
private _onRemoveNode(id: string): void {
|
|
353
|
-
this.
|
|
354
|
-
this.
|
|
397
|
+
this._subscriptions.get(id)?.();
|
|
398
|
+
this._subscriptions.delete(id);
|
|
355
399
|
}
|
|
356
400
|
}
|
|
357
401
|
|
package/src/graph.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Registry, Rx } from '@effect-rx/rx-react';
|
|
|
6
6
|
import { Option } from 'effect';
|
|
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,15 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { Registry, Rx } from '@effect-rx/rx-react';
|
|
6
|
-
import { Option,
|
|
6
|
+
import { Option, Record, pipe } from 'effect';
|
|
7
7
|
|
|
8
8
|
import { Event, Trigger } from '@dxos/async';
|
|
9
9
|
import { todo } from '@dxos/debug';
|
|
10
10
|
import { invariant } from '@dxos/invariant';
|
|
11
11
|
import { log } from '@dxos/log';
|
|
12
|
-
import {
|
|
12
|
+
import { type MakeOptional, isNonNullable } from '@dxos/util';
|
|
13
13
|
|
|
14
|
-
import { type
|
|
14
|
+
import { type Action, type ActionGroup, type Node, type NodeArg, type Relation } from './node';
|
|
15
15
|
|
|
16
16
|
const graphSymbol = Symbol('graph');
|
|
17
17
|
type DeepWriteable<T> = { -readonly [K in keyof T]: T[K] extends object ? DeepWriteable<T[K]> : T[K] };
|
|
@@ -59,8 +59,7 @@ export type GraphParams = {
|
|
|
59
59
|
nodes?: MakeOptional<Node, 'data' | 'cacheable'>[];
|
|
60
60
|
edges?: Record<string, Edges>;
|
|
61
61
|
onExpand?: Graph['_onExpand'];
|
|
62
|
-
|
|
63
|
-
// onInitialize?: Graph['_onInitialize'];
|
|
62
|
+
onInitialize?: Graph['_onInitialize'];
|
|
64
63
|
onRemoveNode?: Graph['_onRemoveNode'];
|
|
65
64
|
};
|
|
66
65
|
|
|
@@ -166,7 +165,7 @@ export interface ExpandableGraph extends ReadableGraph {
|
|
|
166
165
|
*
|
|
167
166
|
* Fires the `onInitialize` callback to provide initial data for a node.
|
|
168
167
|
*/
|
|
169
|
-
|
|
168
|
+
initialize(id: string): Promise<void>;
|
|
170
169
|
|
|
171
170
|
/**
|
|
172
171
|
* Expand a node in the graph.
|
|
@@ -230,7 +229,7 @@ export class Graph implements WritableGraph {
|
|
|
230
229
|
readonly onNodeChanged = new Event<{ id: string; node: Option.Option<Node> }>();
|
|
231
230
|
|
|
232
231
|
private readonly _onExpand?: (id: string, relation: Relation) => void;
|
|
233
|
-
|
|
232
|
+
private readonly _onInitialize?: (id: string) => Promise<void>;
|
|
234
233
|
private readonly _onRemoveNode?: (id: string) => void;
|
|
235
234
|
|
|
236
235
|
private readonly _registry: Registry.Registry;
|
|
@@ -309,8 +308,9 @@ export class Graph implements WritableGraph {
|
|
|
309
308
|
}).pipe(Rx.withLabel(`graph:json:${id}`));
|
|
310
309
|
});
|
|
311
310
|
|
|
312
|
-
constructor({ registry, nodes, edges, onExpand, onRemoveNode }: GraphParams = {}) {
|
|
311
|
+
constructor({ registry, nodes, edges, onInitialize, onExpand, onRemoveNode }: GraphParams = {}) {
|
|
313
312
|
this._registry = registry ?? Registry.make();
|
|
313
|
+
this._onInitialize = onInitialize;
|
|
314
314
|
this._onExpand = onExpand;
|
|
315
315
|
this._onRemoveNode = onRemoveNode;
|
|
316
316
|
|
|
@@ -379,15 +379,14 @@ export class Graph implements WritableGraph {
|
|
|
379
379
|
return this._registry.get(this.edges(id));
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// }
|
|
382
|
+
async initialize(id: string) {
|
|
383
|
+
const initialized = Record.get(this._initialized, id).pipe(Option.getOrElse(() => false));
|
|
384
|
+
log('initialize', { id, initialized });
|
|
385
|
+
if (!initialized) {
|
|
386
|
+
await this._onInitialize?.(id);
|
|
387
|
+
Record.set(this._initialized, id, true);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
391
390
|
|
|
392
391
|
expand(id: string, relation: Relation = 'outbound'): void {
|
|
393
392
|
const key = `${id}$${relation}`;
|
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
|
|
|
@@ -14,7 +14,7 @@ import { registerSignalsRuntime } from '@dxos/echo-signals';
|
|
|
14
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();
|
|
@@ -5,38 +5,38 @@
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
7
|
import { type Registry, RegistryContext, Rx, useRxValue } from '@effect-rx/rx-react';
|
|
8
|
-
import {
|
|
8
|
+
import { type Meta } from '@storybook/react-vite';
|
|
9
9
|
import { Option, pipe } from 'effect';
|
|
10
10
|
import React, { type PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
Expando,
|
|
14
|
+
Filter,
|
|
15
|
+
type Live,
|
|
15
16
|
Query,
|
|
16
17
|
type QueryResult,
|
|
17
18
|
type Space,
|
|
18
19
|
SpaceState,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Filter,
|
|
20
|
+
isSpace,
|
|
21
|
+
live,
|
|
22
22
|
} from '@dxos/client/echo';
|
|
23
23
|
import { Obj, Type } from '@dxos/echo';
|
|
24
24
|
import { faker } from '@dxos/random';
|
|
25
25
|
import { type Client, useClient } from '@dxos/react-client';
|
|
26
26
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
27
|
-
import {
|
|
27
|
+
import { Icon, IconButton, Input, Select } from '@dxos/react-ui';
|
|
28
28
|
import { Path, Tree } from '@dxos/react-ui-list';
|
|
29
|
-
import { Tabs } from '@dxos/react-ui-tabs';
|
|
30
29
|
import { getSize, mx } from '@dxos/react-ui-theme';
|
|
31
30
|
import { withTheme } from '@dxos/storybook-utils';
|
|
32
31
|
import { byPosition, isNonNullable, safeParseInt } from '@dxos/util';
|
|
33
32
|
|
|
34
|
-
import { JsonTree } from './Tree';
|
|
35
33
|
import { type ExpandableGraph, ROOT_ID } from '../graph';
|
|
36
34
|
import { GraphBuilder, createExtension, rxFromObservable, rxFromSignal } from '../graph-builder';
|
|
37
35
|
import { type Node } from '../node';
|
|
38
36
|
import { rxFromQuery } from '../testing';
|
|
39
37
|
|
|
38
|
+
import { JsonTree } from './Tree';
|
|
39
|
+
|
|
40
40
|
const DEFAULT_PERIOD = 500;
|
|
41
41
|
|
|
42
42
|
enum Action {
|
|
@@ -205,7 +205,11 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
205
205
|
return (
|
|
206
206
|
<>
|
|
207
207
|
<div className='flex shrink-0 p-2 space-x-2'>
|
|
208
|
-
<
|
|
208
|
+
<IconButton
|
|
209
|
+
icon={generating ? 'ph--pause--regular' : 'ph--play--regular'}
|
|
210
|
+
label={generating ? 'Pause' : 'Play'}
|
|
211
|
+
onClick={() => setGenerating((generating) => !generating)}
|
|
212
|
+
/>
|
|
209
213
|
<div className='relative' title='mutation period'>
|
|
210
214
|
<Input.Root>
|
|
211
215
|
<Input.TextInput
|
|
@@ -217,11 +221,9 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
217
221
|
onChange={({ target: { value } }) => setActionInterval(value)}
|
|
218
222
|
/>
|
|
219
223
|
</Input.Root>
|
|
220
|
-
<
|
|
224
|
+
<Icon icon='ph--timer--regular' classNames={mx('absolute inline-end-1 block-start-1 mt-[6px]', getSize(3))} />
|
|
221
225
|
</div>
|
|
222
|
-
<
|
|
223
|
-
<Plus />
|
|
224
|
-
</Button>
|
|
226
|
+
<IconButton icon='ph--plus--regular' label='Add' onClick={() => action && runAction(client, action)} />
|
|
225
227
|
<Select.Root value={action?.toString()} onValueChange={(action) => setAction(action as unknown as Action)}>
|
|
226
228
|
<Select.TriggerButton placeholder='Select value' />
|
|
227
229
|
<Select.Portal>
|
|
@@ -245,10 +247,9 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
245
247
|
);
|
|
246
248
|
};
|
|
247
249
|
|
|
248
|
-
|
|
250
|
+
const meta: Meta = {
|
|
249
251
|
title: 'sdk/app-graph/EchoGraph',
|
|
250
252
|
decorators: [
|
|
251
|
-
withTheme,
|
|
252
253
|
withClientProvider({
|
|
253
254
|
createIdentity: true,
|
|
254
255
|
onIdentityCreated: async ({ client }) => {
|
|
@@ -256,9 +257,12 @@ export default {
|
|
|
256
257
|
await client.spaces.create();
|
|
257
258
|
},
|
|
258
259
|
}),
|
|
260
|
+
withTheme,
|
|
259
261
|
],
|
|
260
262
|
};
|
|
261
263
|
|
|
264
|
+
export default meta;
|
|
265
|
+
|
|
262
266
|
export const JsonView = {
|
|
263
267
|
render: () => {
|
|
264
268
|
const client = useClient();
|
|
@@ -372,122 +376,3 @@ export const TreeView = {
|
|
|
372
376
|
);
|
|
373
377
|
},
|
|
374
378
|
};
|
|
375
|
-
|
|
376
|
-
// TODO(wittjosiah): Remove.
|
|
377
|
-
export const TabTreeView = {
|
|
378
|
-
render: () => {
|
|
379
|
-
const client = useClient();
|
|
380
|
-
const registry = useContext(RegistryContext);
|
|
381
|
-
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
382
|
-
const state = useMemo(() => new Map<string, Live<{ open: boolean; current: boolean }>>(), []);
|
|
383
|
-
|
|
384
|
-
const useItems = useCallback(
|
|
385
|
-
(node?: Node, options?: { disposition?: string; sort?: boolean }) => {
|
|
386
|
-
const connections = useRxValue(graph.connections(node?.id ?? ROOT_ID));
|
|
387
|
-
return options?.sort ? connections.toSorted((a, b) => byPosition(a.properties, b.properties)) : connections;
|
|
388
|
-
},
|
|
389
|
-
[graph],
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
const getProps = useCallback(
|
|
393
|
-
(node: Node, path: string[]) => {
|
|
394
|
-
const children = graph
|
|
395
|
-
.getConnections(node.id, 'outbound')
|
|
396
|
-
.map((n) => {
|
|
397
|
-
// Break cycles.
|
|
398
|
-
const nextPath = [...path, node.id];
|
|
399
|
-
return nextPath.includes(n.id) ? undefined : (n as Node);
|
|
400
|
-
})
|
|
401
|
-
.filter(isNonNullable) as Node[];
|
|
402
|
-
const parentOf =
|
|
403
|
-
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
404
|
-
return {
|
|
405
|
-
id: node.id,
|
|
406
|
-
label: node.id,
|
|
407
|
-
icon: node.type === 'dxos.org/type/Space' ? 'ph--planet--regular' : 'ph--placeholder--regular',
|
|
408
|
-
parentOf,
|
|
409
|
-
};
|
|
410
|
-
},
|
|
411
|
-
[graph],
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
const isOpen = useCallback(
|
|
415
|
-
(_path: string[]) => {
|
|
416
|
-
const path = Path.create(..._path);
|
|
417
|
-
const object = state.get(path) ?? live({ open: true, current: false });
|
|
418
|
-
if (!state.has(path)) {
|
|
419
|
-
state.set(path, object);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
return object.open;
|
|
423
|
-
},
|
|
424
|
-
[state],
|
|
425
|
-
);
|
|
426
|
-
|
|
427
|
-
const isCurrent = useCallback(
|
|
428
|
-
(_path: string[]) => {
|
|
429
|
-
const path = Path.create(..._path);
|
|
430
|
-
const object = state.get(path) ?? live({ open: false, current: false });
|
|
431
|
-
if (!state.has(path)) {
|
|
432
|
-
state.set(path, object);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return object.current;
|
|
436
|
-
},
|
|
437
|
-
[state],
|
|
438
|
-
);
|
|
439
|
-
|
|
440
|
-
const onOpenChange = useCallback(
|
|
441
|
-
({ path: _path, open }: { path: string[]; open: boolean }) => {
|
|
442
|
-
const path = Path.create(..._path);
|
|
443
|
-
const object = state.get(path);
|
|
444
|
-
object!.open = open;
|
|
445
|
-
},
|
|
446
|
-
[state],
|
|
447
|
-
);
|
|
448
|
-
|
|
449
|
-
const onSelect = useCallback(
|
|
450
|
-
({ path: _path, current }: { path: string[]; current: boolean }) => {
|
|
451
|
-
const path = Path.create(..._path);
|
|
452
|
-
const object = state.get(path);
|
|
453
|
-
object!.current = current;
|
|
454
|
-
},
|
|
455
|
-
[state],
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
const spaces = useItems(graph.root);
|
|
459
|
-
|
|
460
|
-
return (
|
|
461
|
-
<>
|
|
462
|
-
<Controls />
|
|
463
|
-
<Tabs.Root defaultValue={spaces[0].id}>
|
|
464
|
-
<Tabs.Tablist>
|
|
465
|
-
{spaces.map((space) => {
|
|
466
|
-
return (
|
|
467
|
-
<Tabs.Tab key={space.id} value={space.id}>
|
|
468
|
-
{space.id}
|
|
469
|
-
</Tabs.Tab>
|
|
470
|
-
);
|
|
471
|
-
})}
|
|
472
|
-
</Tabs.Tablist>
|
|
473
|
-
{spaces.map((space) => {
|
|
474
|
-
return (
|
|
475
|
-
<Tabs.Tabpanel key={space.id} value={space.id}>
|
|
476
|
-
<Tree
|
|
477
|
-
id={space.id}
|
|
478
|
-
root={space}
|
|
479
|
-
useItems={useItems}
|
|
480
|
-
getProps={getProps}
|
|
481
|
-
isOpen={isOpen}
|
|
482
|
-
isCurrent={isCurrent}
|
|
483
|
-
onOpenChange={onOpenChange}
|
|
484
|
-
onSelect={onSelect}
|
|
485
|
-
/>
|
|
486
|
-
</Tabs.Tabpanel>
|
|
487
|
-
);
|
|
488
|
-
})}
|
|
489
|
-
</Tabs.Root>
|
|
490
|
-
</>
|
|
491
|
-
);
|
|
492
|
-
},
|
|
493
|
-
};
|