@dxos/app-graph 0.4.6 → 0.4.7-main.06facb4
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 +289 -229
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +282 -239
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/graph-builder.d.ts +8 -16
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +71 -14
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/helpers.d.ts +14 -0
- package/dist/types/src/helpers.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +57 -47
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/package.json +10 -29
- package/src/graph-builder.ts +18 -206
- package/src/graph.test.ts +242 -146
- package/src/graph.ts +241 -48
- package/src/helpers.ts +27 -0
- package/src/index.ts +1 -1
- package/src/node.ts +78 -66
- package/src/stories/EchoGraph.stories.tsx +59 -27
- package/dist/lib/browser/testing.mjs +0 -98
- package/dist/lib/browser/testing.mjs.map +0 -7
- package/dist/lib/node/testing.cjs +0 -122
- package/dist/lib/node/testing.cjs.map +0 -7
- package/dist/types/src/action.d.ts +0 -68
- package/dist/types/src/action.d.ts.map +0 -1
- package/dist/types/src/testing.d.ts +0 -52
- package/dist/types/src/testing.d.ts.map +0 -1
- package/src/action.ts +0 -92
- package/src/testing.ts +0 -152
package/src/graph.ts
CHANGED
|
@@ -2,25 +2,30 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import get from 'lodash.get';
|
|
5
|
+
import { untracked } from '@preact/signals-core';
|
|
6
|
+
import { type DeepSignal, deepSignal } from 'deepsignal/react';
|
|
8
7
|
|
|
9
8
|
import { invariant } from '@dxos/invariant';
|
|
9
|
+
import { nonNullable } from '@dxos/util';
|
|
10
10
|
|
|
11
|
-
import { type
|
|
12
|
-
|
|
11
|
+
import { isActionLike, type EdgeDirection, type Node, type NodeArg, type NodeBase } from './node';
|
|
12
|
+
|
|
13
|
+
export const ROOT_ID = 'root';
|
|
13
14
|
|
|
14
15
|
export type TraversalOptions = {
|
|
15
16
|
/**
|
|
16
|
-
* The node to start traversing from.
|
|
17
|
+
* The node to start traversing from.
|
|
18
|
+
*
|
|
19
|
+
* @default root
|
|
17
20
|
*/
|
|
18
21
|
node?: Node;
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
|
-
* The direction to traverse
|
|
24
|
+
* The direction to traverse graph edges.
|
|
25
|
+
*
|
|
26
|
+
* @default 'outbound'
|
|
22
27
|
*/
|
|
23
|
-
direction?:
|
|
28
|
+
direction?: EdgeDirection;
|
|
24
29
|
|
|
25
30
|
/**
|
|
26
31
|
* A predicate to filter nodes which are passed to the `visitor` callback.
|
|
@@ -37,76 +42,264 @@ export type TraversalOptions = {
|
|
|
37
42
|
* The Graph represents the structure of the application constructed via plugins.
|
|
38
43
|
*/
|
|
39
44
|
export class Graph {
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
/**
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
readonly _nodes = deepSignal<Record<string, NodeBase>>({
|
|
49
|
+
[ROOT_ID]: { id: ROOT_ID, properties: {}, data: null },
|
|
50
|
+
});
|
|
42
51
|
|
|
43
|
-
|
|
52
|
+
/**
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
// Key is the `${node.id}-${direction}` and value is an ordered list of node ids.
|
|
56
|
+
// Explicit type required because TS says this is not portable.
|
|
57
|
+
readonly _edges: DeepSignal<Record<string, string[]>> = deepSignal({});
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Alias for `findNode('root')`.
|
|
61
|
+
*/
|
|
62
|
+
get root() {
|
|
63
|
+
return this.findNode(ROOT_ID)!;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Convert the graph to a JSON object.
|
|
68
|
+
*/
|
|
69
|
+
toJSON({ id = ROOT_ID, maxLength = 32 }: { id?: string; maxLength?: number } = {}) {
|
|
47
70
|
const toJSON = (node: Node): any => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
children: node.children.length ? node.children.map((node) => toJSON(node)) : undefined,
|
|
52
|
-
actions: node.actions.length
|
|
53
|
-
? node.actions.map(({ id, label }) => ({
|
|
54
|
-
id,
|
|
55
|
-
label: toLabel(label),
|
|
56
|
-
}))
|
|
57
|
-
: undefined,
|
|
71
|
+
const nodes = node.nodes();
|
|
72
|
+
const obj: Record<string, any> = {
|
|
73
|
+
id: node.id.length > maxLength ? `${node.id.slice(0, maxLength - 3)}...` : node.id,
|
|
58
74
|
};
|
|
75
|
+
if (node.properties.label) {
|
|
76
|
+
obj.label = node.properties.label;
|
|
77
|
+
}
|
|
78
|
+
if (nodes.length) {
|
|
79
|
+
obj.nodes = nodes.map((node) => toJSON(node));
|
|
80
|
+
}
|
|
81
|
+
return obj;
|
|
59
82
|
};
|
|
60
83
|
|
|
61
|
-
|
|
84
|
+
const root = this.findNode(id);
|
|
85
|
+
invariant(root, `Node not found: ${id}`);
|
|
86
|
+
return toJSON(root);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Find the node with the given id in the graph.
|
|
91
|
+
*/
|
|
92
|
+
findNode(id: string): Node | undefined {
|
|
93
|
+
const nodeBase = this._nodes[id];
|
|
94
|
+
if (!nodeBase) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return this._constructNode(nodeBase);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private _constructNode = (nodeBase: NodeBase): Node => {
|
|
102
|
+
const node: Node = {
|
|
103
|
+
...nodeBase,
|
|
104
|
+
edges: ({ direction = 'outbound' } = {}) => {
|
|
105
|
+
return this._edges[this.getEdgeKey(node.id, direction)];
|
|
106
|
+
},
|
|
107
|
+
nodes: ({ direction, filter } = {}) => {
|
|
108
|
+
const nodes = this._getNodes({ id: node.id, direction }).filter((n) => !isActionLike(n));
|
|
109
|
+
return filter ? nodes.filter((n) => filter(n, node)) : nodes;
|
|
110
|
+
},
|
|
111
|
+
node: (id: string) => {
|
|
112
|
+
return this._getNodes({ id }).find((node) => node.id === id);
|
|
113
|
+
},
|
|
114
|
+
actions: () => {
|
|
115
|
+
return this._getNodes({ id: node.id }).filter(isActionLike);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return node;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
private _getNodes({ id, direction = 'outbound' }: { id: string; direction?: EdgeDirection }): Node[] {
|
|
123
|
+
const edges = this._edges[this.getEdgeKey(id, direction)];
|
|
124
|
+
if (!edges) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return edges.map((id) => this.findNode(id)).filter(nonNullable);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private getEdgeKey(id: string, direction: EdgeDirection) {
|
|
132
|
+
return `${id}-${direction}`;
|
|
62
133
|
}
|
|
63
134
|
|
|
64
135
|
/**
|
|
65
|
-
*
|
|
136
|
+
* Add nodes to the graph.
|
|
66
137
|
*/
|
|
67
|
-
|
|
68
|
-
|
|
138
|
+
addNodes<TData = null, TProperties extends Record<string, any> = Record<string, any>>(
|
|
139
|
+
...nodes: NodeArg<TData, TProperties>[]
|
|
140
|
+
): Node<TData, TProperties>[] {
|
|
141
|
+
return nodes.map((node) => this._addNode(node));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private _addNode<TData, TProperties extends Record<string, any> = Record<string, any>>({
|
|
145
|
+
nodes,
|
|
146
|
+
edges,
|
|
147
|
+
..._node
|
|
148
|
+
}: NodeArg<TData, TProperties>): Node<TData, TProperties> {
|
|
149
|
+
return untracked(() => {
|
|
150
|
+
const node = { data: null, properties: {}, ..._node };
|
|
151
|
+
this._nodes[node.id] = node;
|
|
152
|
+
|
|
153
|
+
if (nodes) {
|
|
154
|
+
nodes.forEach((subNode) => {
|
|
155
|
+
this._addNode(subNode);
|
|
156
|
+
this.addEdge({ source: node.id, target: subNode.id });
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (edges) {
|
|
161
|
+
edges.forEach(([id, direction]) =>
|
|
162
|
+
direction === 'outbound'
|
|
163
|
+
? this.addEdge({ source: node.id, target: id })
|
|
164
|
+
: this.addEdge({ source: id, target: node.id }),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return this._constructNode(node) as Node<TData, TProperties>;
|
|
169
|
+
});
|
|
69
170
|
}
|
|
70
171
|
|
|
71
172
|
/**
|
|
72
|
-
*
|
|
173
|
+
* Remove nodes from the graph.
|
|
174
|
+
*
|
|
175
|
+
* @param id The id of the node to remove.
|
|
176
|
+
* @param edges Whether to remove edges connected to the node from the graph as well.
|
|
73
177
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
178
|
+
removeNode(id: string, edges = false) {
|
|
179
|
+
untracked(() => {
|
|
180
|
+
const node = this.findNode(id);
|
|
181
|
+
if (!node) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (edges) {
|
|
186
|
+
// Remove edges from node.
|
|
187
|
+
delete this._edges[this.getEdgeKey(id, 'outbound')];
|
|
188
|
+
delete this._edges[this.getEdgeKey(id, 'inbound')];
|
|
189
|
+
|
|
190
|
+
// Remove edges from connected nodes.
|
|
191
|
+
this._getNodes({ id }).forEach((node) => this.removeEdge({ source: id, target: node.id }));
|
|
192
|
+
this._getNodes({ id, direction: 'inbound' }).forEach((node) =>
|
|
193
|
+
this.removeEdge({ source: node.id, target: id }),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Remove node.
|
|
198
|
+
delete this._nodes[id];
|
|
199
|
+
});
|
|
76
200
|
}
|
|
77
201
|
|
|
78
202
|
/**
|
|
79
|
-
*
|
|
203
|
+
* Add an edge to the graph.
|
|
80
204
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
205
|
+
addEdge({ source, target }: { source: string; target: string }) {
|
|
206
|
+
untracked(() => {
|
|
207
|
+
const outbound = this._edges[this.getEdgeKey(source, 'outbound')];
|
|
208
|
+
if (!outbound) {
|
|
209
|
+
this._edges[this.getEdgeKey(source, 'outbound')] = [target];
|
|
210
|
+
} else if (!outbound.includes(target)) {
|
|
211
|
+
outbound.push(target);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const inbound = this._edges[this.getEdgeKey(target, 'inbound')];
|
|
215
|
+
if (!inbound) {
|
|
216
|
+
this._edges[this.getEdgeKey(target, 'inbound')] = [source];
|
|
217
|
+
} else if (!inbound.includes(source)) {
|
|
218
|
+
inbound.push(source);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
84
221
|
}
|
|
85
222
|
|
|
86
223
|
/**
|
|
87
|
-
*
|
|
224
|
+
* Sort edges for a node.
|
|
225
|
+
*
|
|
226
|
+
* Edges not included in the sorted list are appended to the end of the list.
|
|
227
|
+
*
|
|
228
|
+
* @param nodeId The id of the node to sort edges for.
|
|
229
|
+
* @param direction The direction of the edges from the node to sort.
|
|
230
|
+
* @param edges The ordered list of edges.
|
|
88
231
|
*/
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
232
|
+
sortEdges(nodeId: string, direction: EdgeDirection, edges: string[]) {
|
|
233
|
+
untracked(() => {
|
|
234
|
+
const current = this._edges[this.getEdgeKey(nodeId, direction)];
|
|
235
|
+
if (current) {
|
|
236
|
+
const unsorted = current.filter((id) => !edges.includes(id)) ?? [];
|
|
237
|
+
const sorted = edges.filter((id) => current.includes(id)) ?? [];
|
|
238
|
+
current.splice(0, current.length, ...[...sorted, ...unsorted]);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
94
242
|
|
|
95
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Remove an edge from the graph.
|
|
245
|
+
*/
|
|
246
|
+
removeEdge({ source, target }: { source: string; target: string }) {
|
|
247
|
+
untracked(() => {
|
|
248
|
+
const outboundIndex = this._edges[this.getEdgeKey(source, 'outbound')]?.findIndex((id) => id === target);
|
|
249
|
+
if (outboundIndex !== -1) {
|
|
250
|
+
this._edges[this.getEdgeKey(source, 'outbound')].splice(outboundIndex, 1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const inboundIndex = this._edges[this.getEdgeKey(target, 'inbound')]?.findIndex((id) => id === source);
|
|
254
|
+
if (inboundIndex !== -1) {
|
|
255
|
+
this._edges[this.getEdgeKey(target, 'inbound')].splice(inboundIndex, 1);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
96
258
|
}
|
|
97
259
|
|
|
98
260
|
/**
|
|
99
|
-
* Recursive
|
|
261
|
+
* Recursive depth-first traversal.
|
|
262
|
+
*
|
|
263
|
+
* @param options.node The node to start traversing from.
|
|
264
|
+
* @param options.direction The direction to traverse graph edges.
|
|
265
|
+
* @param options.filter A predicate to filter nodes which are passed to the `visitor` callback.
|
|
266
|
+
* @param options.visitor A callback which is called for each node visited during traversal.
|
|
100
267
|
*/
|
|
101
|
-
traverse({ node = this.
|
|
268
|
+
traverse({ node = this.root, direction = 'outbound', filter, visitor }: TraversalOptions, path: string[] = []): void {
|
|
269
|
+
// Break cycles.
|
|
270
|
+
if (path.includes(node.id)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
102
274
|
if (!filter || filter(node)) {
|
|
103
|
-
visitor?.(node,
|
|
275
|
+
visitor?.(node, [...path, node.id]);
|
|
104
276
|
}
|
|
105
277
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
278
|
+
Object.values(this._getNodes({ id: node.id, direction })).forEach((child) =>
|
|
279
|
+
this.traverse({ node: child, direction, filter, visitor }, [...path, node.id]),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get the path between two nodes in the graph.
|
|
285
|
+
*/
|
|
286
|
+
getPath({ source = 'root', target }: { source?: string; target: string }): string[] | undefined {
|
|
287
|
+
const start = this.findNode(source);
|
|
288
|
+
if (!start) {
|
|
289
|
+
return undefined;
|
|
110
290
|
}
|
|
291
|
+
|
|
292
|
+
let found: string[] | undefined;
|
|
293
|
+
this.traverse({
|
|
294
|
+
node: start,
|
|
295
|
+
filter: () => !found,
|
|
296
|
+
visitor: (node, path) => {
|
|
297
|
+
if (node.id === target) {
|
|
298
|
+
found = path;
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return found;
|
|
111
304
|
}
|
|
112
305
|
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Graph } from './graph';
|
|
6
|
+
import { type Node, type NodeArg } from './node';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* If the condition is true, adds the nodes to the graph, otherwise removes the nodes from the graph.
|
|
10
|
+
*/
|
|
11
|
+
export const manageNodes = <TData = null, TProperties extends Record<string, any> = Record<string, any>>({
|
|
12
|
+
graph,
|
|
13
|
+
condition,
|
|
14
|
+
nodes,
|
|
15
|
+
removeEdges,
|
|
16
|
+
}: {
|
|
17
|
+
graph: Graph;
|
|
18
|
+
condition: boolean;
|
|
19
|
+
nodes: NodeArg<TData, TProperties>[];
|
|
20
|
+
removeEdges?: boolean;
|
|
21
|
+
}): Node<TData, TProperties>[] | void => {
|
|
22
|
+
if (condition) {
|
|
23
|
+
return graph.addNodes(...nodes);
|
|
24
|
+
} else {
|
|
25
|
+
nodes.forEach(({ id }) => graph.removeNode(id, removeEdges));
|
|
26
|
+
}
|
|
27
|
+
};
|
package/src/index.ts
CHANGED
package/src/node.ts
CHANGED
|
@@ -2,56 +2,21 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import type
|
|
6
|
-
import { type FC } from 'react';
|
|
7
|
-
|
|
8
|
-
import type { UnsubscribeCallback } from '@dxos/async';
|
|
9
|
-
|
|
10
|
-
import { type ActionArg, type Action, type Label } from './action';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Called when a node is added to the graph, allowing other node builders to add children, actions or properties.
|
|
14
|
-
*/
|
|
15
|
-
export type NodeBuilder = (parent: Node) => UnsubscribeCallback | void;
|
|
5
|
+
import { type MaybePromise, type MakeOptional } from '@dxos/util';
|
|
16
6
|
|
|
17
7
|
/**
|
|
18
8
|
* Represents a node in the graph.
|
|
19
9
|
*/
|
|
20
|
-
|
|
10
|
+
// TODO(wittjosiah): Use Effect Schema.
|
|
11
|
+
export type NodeBase<TData = any, TProperties extends Record<string, any> = Record<string, any>> = {
|
|
21
12
|
/**
|
|
22
13
|
* Globally unique ID.
|
|
23
14
|
*/
|
|
24
15
|
id: string;
|
|
25
16
|
|
|
26
|
-
/**
|
|
27
|
-
* Parent node in the graph.
|
|
28
|
-
*/
|
|
29
|
-
parent: Node | null;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Label to be used when displaying the node.
|
|
33
|
-
* For default labels, use a translated string.
|
|
34
|
-
*
|
|
35
|
-
* @example 'My Node'
|
|
36
|
-
* @example ['unknown node label, { ns: 'example-plugin' }]
|
|
37
|
-
*/
|
|
38
|
-
label: Label;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Description to be used when displaying a detailed view of the node.
|
|
42
|
-
* For default descriptions, use a translated string.
|
|
43
|
-
*/
|
|
44
|
-
description?: Label;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Icon to be used when displaying the node.
|
|
48
|
-
*/
|
|
49
|
-
icon?: FC<IconProps>;
|
|
50
|
-
|
|
51
17
|
/**
|
|
52
18
|
* Properties of the node relevant to displaying the node.
|
|
53
19
|
*/
|
|
54
|
-
// TODO(burdon): Make this extensible and move label, description, and icon into here?
|
|
55
20
|
properties: TProperties;
|
|
56
21
|
|
|
57
22
|
/**
|
|
@@ -59,50 +24,97 @@ export type Node<TData = any, TProperties extends Record<string, any> = Record<s
|
|
|
59
24
|
*/
|
|
60
25
|
// TODO(burdon): Type system (e.g., minimally provide identifier string vs. TypedObject vs. Graph mixin type system)?
|
|
61
26
|
// type field would prevent convoluted sniffing of object properties. And allow direct pass-through for ECHO TypedObjects.
|
|
62
|
-
// TODO(burdon): In some places `null` is cast to TData so make optional?
|
|
63
27
|
data: TData;
|
|
28
|
+
};
|
|
64
29
|
|
|
30
|
+
export type NodeFilter<T = any, U extends Record<string, any> = Record<string, any>> = (
|
|
31
|
+
node: Node<unknown, Record<string, any>>,
|
|
32
|
+
connectedNode: Node,
|
|
33
|
+
) => node is Node<T, U>;
|
|
34
|
+
|
|
35
|
+
export type EdgeDirection = 'outbound' | 'inbound';
|
|
36
|
+
|
|
37
|
+
export type ConnectedNodes = {
|
|
65
38
|
/**
|
|
66
|
-
*
|
|
39
|
+
* Edges that this node is connected to in default order.
|
|
67
40
|
*/
|
|
68
|
-
|
|
69
|
-
childrenMap: Record<string, Node>;
|
|
41
|
+
edges(params?: { direction?: EdgeDirection }): Readonly<string[]>;
|
|
70
42
|
|
|
71
43
|
/**
|
|
72
|
-
*
|
|
44
|
+
* Nodes that this node is connected to in default order.
|
|
73
45
|
*/
|
|
74
|
-
|
|
46
|
+
nodes<T = any, U extends Record<string, any> = Record<string, any>>(params?: {
|
|
47
|
+
direction?: EdgeDirection;
|
|
48
|
+
filter?: NodeFilter<T, U>;
|
|
49
|
+
}): Node<T>[];
|
|
75
50
|
|
|
76
51
|
/**
|
|
77
|
-
*
|
|
52
|
+
* Get a specific connected node by id.
|
|
78
53
|
*/
|
|
79
|
-
|
|
54
|
+
node(id: string): Node | undefined;
|
|
55
|
+
};
|
|
80
56
|
|
|
57
|
+
export type ConnectedActions = {
|
|
81
58
|
/**
|
|
82
|
-
* Actions
|
|
59
|
+
* Actions or action groups that this node is connected to in default order.
|
|
83
60
|
*/
|
|
84
|
-
|
|
61
|
+
actions(): ActionLike[];
|
|
62
|
+
};
|
|
85
63
|
|
|
86
|
-
|
|
87
|
-
|
|
64
|
+
export type Node<TData = any, TProperties extends Record<string, any> = Record<string, any>> = Readonly<
|
|
65
|
+
Omit<NodeBase<TData, TProperties>, 'properties'> & { properties: Readonly<TProperties> } & ConnectedNodes &
|
|
66
|
+
ConnectedActions
|
|
67
|
+
>;
|
|
88
68
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
69
|
+
export const isGraphNode = (data: unknown): data is Node =>
|
|
70
|
+
data && typeof data === 'object' && 'id' in data && 'properties' in data && data.properties
|
|
71
|
+
? typeof data.properties === 'object' && 'data' in data
|
|
72
|
+
: false;
|
|
73
|
+
|
|
74
|
+
export type NodeArg<TData, TProperties extends Record<string, any> = Record<string, any>> = MakeOptional<
|
|
75
|
+
NodeBase<TData, TProperties>,
|
|
76
|
+
'data' | 'properties'
|
|
77
|
+
> & {
|
|
78
|
+
/** Will automatically add nodes with an edge from this node to each. */
|
|
79
|
+
nodes?: NodeArg<unknown>[];
|
|
80
|
+
|
|
81
|
+
/** Will automatically add specified edges. */
|
|
82
|
+
edges?: [string, EdgeDirection][];
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
//
|
|
86
|
+
// Actions
|
|
87
|
+
//
|
|
94
88
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
export type InvokeParams = {
|
|
90
|
+
/** Node the invoked action is connected to. */
|
|
91
|
+
node: Node;
|
|
92
|
+
|
|
93
|
+
caller?: string;
|
|
99
94
|
};
|
|
100
95
|
|
|
101
|
-
export type
|
|
102
|
-
Node,
|
|
103
|
-
'id' | 'label'
|
|
104
|
-
> &
|
|
105
|
-
Partial<Omit<Node<TData, TProperties>, 'id' | 'label' | 'actions'>> & { actions?: ActionArg[] };
|
|
96
|
+
export type ActionData = (params: InvokeParams) => MaybePromise<void>;
|
|
106
97
|
|
|
107
|
-
export
|
|
108
|
-
|
|
98
|
+
export type Action<TProperties extends Record<string, any> = Record<string, any>> = Readonly<
|
|
99
|
+
Omit<NodeBase<ActionData, TProperties>, 'properties'> & {
|
|
100
|
+
properties: Readonly<TProperties>;
|
|
101
|
+
} & ConnectedNodes
|
|
102
|
+
>;
|
|
103
|
+
|
|
104
|
+
export const isAction = (data: unknown): data is Action =>
|
|
105
|
+
isGraphNode(data) ? typeof data.data === 'function' : false;
|
|
106
|
+
|
|
107
|
+
export const actionGroupSymbol = Symbol('ActionGroup');
|
|
108
|
+
|
|
109
|
+
export type ActionGroup = Readonly<
|
|
110
|
+
Omit<NodeBase<typeof actionGroupSymbol, Record<string, any>>, 'properties'> & {
|
|
111
|
+
properties: Readonly<Record<string, any>>;
|
|
112
|
+
} & ConnectedActions
|
|
113
|
+
>;
|
|
114
|
+
|
|
115
|
+
export const isActionGroup = (data: unknown): data is ActionGroup =>
|
|
116
|
+
isGraphNode(data) ? data.data === actionGroupSymbol : false;
|
|
117
|
+
|
|
118
|
+
export type ActionLike = Action | ActionGroup;
|
|
119
|
+
|
|
120
|
+
export const isActionLike = (data: unknown): data is Action | ActionGroup => isAction(data) || isActionGroup(data);
|