@dxos/app-graph 0.6.3-main.d007b87 → 0.6.3-main.d76104f
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 +572 -190
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +577 -184
- 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 +99 -7
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph-builder.test.d.ts +2 -0
- package/dist/types/src/graph-builder.test.d.ts.map +1 -0
- package/dist/types/src/graph.d.ts +94 -45
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +99 -40
- 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 +14 -12
- package/src/graph-builder.test.ts +322 -0
- package/src/graph-builder.ts +327 -19
- package/src/graph.test.ts +431 -179
- package/src/graph.ts +335 -148
- package/src/index.ts +0 -1
- package/src/node.ts +15 -42
- package/src/stories/EchoGraph.stories.tsx +91 -103
- package/dist/types/src/helpers.d.ts +0 -12
- package/dist/types/src/helpers.d.ts.map +0 -1
- package/src/helpers.ts +0 -27
package/src/graph.ts
CHANGED
|
@@ -2,17 +2,45 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { untracked } from '@preact/signals-core';
|
|
5
|
+
import { batch, effect, untracked } from '@preact/signals-core';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { asyncTimeout, Trigger } from '@dxos/async';
|
|
8
|
+
import { type ReactiveObject, create } from '@dxos/echo-schema';
|
|
8
9
|
import { invariant } from '@dxos/invariant';
|
|
9
10
|
import { nonNullable } from '@dxos/util';
|
|
10
11
|
|
|
11
|
-
import {
|
|
12
|
+
import { type Relation, type Node, type NodeArg, type NodeFilter, isActionLike } from './node';
|
|
13
|
+
|
|
14
|
+
const graphSymbol = Symbol('graph');
|
|
15
|
+
type DeepWriteable<T> = { -readonly [K in keyof T]: DeepWriteable<T[K]> };
|
|
16
|
+
type NodeInternal = DeepWriteable<Node> & { [graphSymbol]: Graph };
|
|
17
|
+
|
|
18
|
+
export const getGraph = (node: Node): Graph => {
|
|
19
|
+
const graph = (node as NodeInternal)[graphSymbol];
|
|
20
|
+
invariant(graph, 'Node is not associated with a graph.');
|
|
21
|
+
return graph;
|
|
22
|
+
};
|
|
12
23
|
|
|
13
24
|
export const ROOT_ID = 'root';
|
|
25
|
+
export const ROOT_TYPE = 'dxos.org/type/GraphRoot';
|
|
26
|
+
export const ACTION_TYPE = 'dxos.org/type/GraphAction';
|
|
27
|
+
export const ACTION_GROUP_TYPE = 'dxos.org/type/GraphActionGroup';
|
|
28
|
+
|
|
29
|
+
export type NodesOptions<T = any, U extends Record<string, any> = Record<string, any>> = {
|
|
30
|
+
relation?: Relation;
|
|
31
|
+
filter?: NodeFilter<T, U>;
|
|
32
|
+
expansion?: boolean;
|
|
33
|
+
type?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type GraphTraversalOptions = {
|
|
37
|
+
/**
|
|
38
|
+
* A callback which is called for each node visited during traversal.
|
|
39
|
+
*
|
|
40
|
+
* If the callback returns `false`, traversal is stops recursing.
|
|
41
|
+
*/
|
|
42
|
+
visitor: (node: Node, path: string[]) => boolean | void;
|
|
14
43
|
|
|
15
|
-
export type TraversalOptions = {
|
|
16
44
|
/**
|
|
17
45
|
* The node to start traversing from.
|
|
18
46
|
*
|
|
@@ -21,40 +49,54 @@ export type TraversalOptions = {
|
|
|
21
49
|
node?: Node;
|
|
22
50
|
|
|
23
51
|
/**
|
|
24
|
-
* The
|
|
52
|
+
* The relation to traverse graph edges.
|
|
25
53
|
*
|
|
26
54
|
* @default 'outbound'
|
|
27
55
|
*/
|
|
28
|
-
|
|
56
|
+
relation?: Relation;
|
|
29
57
|
|
|
30
58
|
/**
|
|
31
|
-
*
|
|
59
|
+
* Allow traversal to trigger expansion of the graph via `onInitialNodes`.
|
|
32
60
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* A callback which is called for each node visited during traversal.
|
|
37
|
-
*/
|
|
38
|
-
visitor?: (node: Node, path: string[]) => void;
|
|
61
|
+
expansion?: boolean;
|
|
39
62
|
};
|
|
40
63
|
|
|
41
64
|
/**
|
|
42
65
|
* The Graph represents the structure of the application constructed via plugins.
|
|
43
66
|
*/
|
|
44
67
|
export class Graph {
|
|
68
|
+
private readonly _onInitialNode?: (id: string) => Promise<void>;
|
|
69
|
+
private readonly _onInitialNodes?: (node: Node, relation: Relation, type?: string) => Promise<void>;
|
|
70
|
+
private readonly _onRemoveNode?: (id: string) => Promise<void>;
|
|
71
|
+
|
|
72
|
+
private readonly _waitingForNodes: Record<string, Trigger<Node>> = {};
|
|
73
|
+
private readonly _initialized: Record<string, boolean> = {};
|
|
74
|
+
|
|
45
75
|
/**
|
|
46
76
|
* @internal
|
|
47
77
|
*/
|
|
48
|
-
readonly _nodes
|
|
49
|
-
[ROOT_ID]: { id: ROOT_ID, properties: {}, data: null },
|
|
50
|
-
});
|
|
78
|
+
readonly _nodes: Record<string, ReactiveObject<NodeInternal>> = {};
|
|
51
79
|
|
|
52
80
|
/**
|
|
53
81
|
* @internal
|
|
54
82
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
83
|
+
readonly _edges: Record<string, ReactiveObject<{ inbound: string[]; outbound: string[] }>> = {};
|
|
84
|
+
|
|
85
|
+
constructor({
|
|
86
|
+
onInitialNode,
|
|
87
|
+
onInitialNodes,
|
|
88
|
+
onRemoveNode,
|
|
89
|
+
}: {
|
|
90
|
+
onInitialNode?: Graph['_onInitialNode'];
|
|
91
|
+
onInitialNodes?: Graph['_onInitialNodes'];
|
|
92
|
+
onRemoveNode?: Graph['_onRemoveNode'];
|
|
93
|
+
} = {}) {
|
|
94
|
+
this._onInitialNode = onInitialNode;
|
|
95
|
+
this._onInitialNodes = onInitialNodes;
|
|
96
|
+
this._onRemoveNode = onRemoveNode;
|
|
97
|
+
this._nodes[ROOT_ID] = this._constructNode({ id: ROOT_ID, type: ROOT_TYPE, properties: {}, data: null });
|
|
98
|
+
this._edges[ROOT_ID] = create({ inbound: [], outbound: [] });
|
|
99
|
+
}
|
|
58
100
|
|
|
59
101
|
/**
|
|
60
102
|
* Alias for `findNode('root')`.
|
|
@@ -68,9 +110,10 @@ export class Graph {
|
|
|
68
110
|
*/
|
|
69
111
|
toJSON({ id = ROOT_ID, maxLength = 32 }: { id?: string; maxLength?: number } = {}) {
|
|
70
112
|
const toJSON = (node: Node, seen: string[] = []): any => {
|
|
71
|
-
const nodes =
|
|
113
|
+
const nodes = this.nodes(node);
|
|
72
114
|
const obj: Record<string, any> = {
|
|
73
115
|
id: node.id.length > maxLength ? `${node.id.slice(0, maxLength - 3)}...` : node.id,
|
|
116
|
+
type: node.type,
|
|
74
117
|
};
|
|
75
118
|
if (node.properties.label) {
|
|
76
119
|
obj.label = node.properties.label;
|
|
@@ -94,57 +137,166 @@ export class Graph {
|
|
|
94
137
|
|
|
95
138
|
/**
|
|
96
139
|
* Find the node with the given id in the graph.
|
|
140
|
+
*
|
|
141
|
+
* If a node is not found within the graph and an `onInitialNode` callback is provided,
|
|
142
|
+
* it is called with the id and type of the node, potentially initializing the node.
|
|
97
143
|
*/
|
|
98
144
|
findNode(id: string): Node | undefined {
|
|
99
|
-
const
|
|
100
|
-
if (!
|
|
101
|
-
|
|
145
|
+
const existingNode = this._nodes[id];
|
|
146
|
+
if (!existingNode) {
|
|
147
|
+
void this._onInitialNode?.(id);
|
|
102
148
|
}
|
|
103
149
|
|
|
104
|
-
return
|
|
150
|
+
return existingNode;
|
|
105
151
|
}
|
|
106
152
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
123
|
-
};
|
|
153
|
+
/**
|
|
154
|
+
* Wait for a node to be added to the graph.
|
|
155
|
+
*
|
|
156
|
+
* If the node is already present in the graph, the promise resolves immediately.
|
|
157
|
+
*
|
|
158
|
+
* @param id The id of the node to wait for.
|
|
159
|
+
* @param timeout The time in milliseconds to wait for the node to be added.
|
|
160
|
+
*/
|
|
161
|
+
async waitForNode(id: string, timeout?: number): Promise<Node> {
|
|
162
|
+
const trigger = this._waitingForNodes[id] ?? (this._waitingForNodes[id] = new Trigger<Node>());
|
|
163
|
+
const node = this.findNode(id);
|
|
164
|
+
if (node) {
|
|
165
|
+
delete this._waitingForNodes[id];
|
|
166
|
+
return node;
|
|
167
|
+
}
|
|
124
168
|
|
|
125
|
-
|
|
126
|
-
|
|
169
|
+
if (timeout === undefined) {
|
|
170
|
+
return trigger.wait();
|
|
171
|
+
} else {
|
|
172
|
+
return asyncTimeout(trigger.wait(), timeout, `Node not found: ${id}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
127
175
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Nodes that this node is connected to in default order.
|
|
178
|
+
*/
|
|
179
|
+
nodes<T = any, U extends Record<string, any> = Record<string, any>>(node: Node, options: NodesOptions<T, U> = {}) {
|
|
180
|
+
const { relation, expansion, filter, type } = options;
|
|
181
|
+
const nodes = this._getNodes({ node, relation, expansion, type });
|
|
182
|
+
return nodes.filter((n) => untracked(() => !isActionLike(n))).filter((n) => filter?.(n, node) ?? true);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Edges that this node is connected to in default order.
|
|
187
|
+
*/
|
|
188
|
+
edges(node: Node, { relation = 'outbound' }: { relation?: Relation } = {}) {
|
|
189
|
+
return this._edges[node.id]?.[relation] ?? [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Actions or action groups that this node is connected to in default order.
|
|
194
|
+
*/
|
|
195
|
+
actions(node: Node, { expansion }: { expansion?: boolean } = {}) {
|
|
196
|
+
return [
|
|
197
|
+
...this._getNodes({ node, expansion, type: ACTION_GROUP_TYPE }),
|
|
198
|
+
...this._getNodes({ node, expansion, type: ACTION_TYPE }),
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async expand(node: Node, relation: Relation = 'outbound', type?: string) {
|
|
203
|
+
// TODO(wittjosiah): Factor out helper.
|
|
204
|
+
const key = `${node.id}-${relation}-${type}`;
|
|
205
|
+
const initialized = this._initialized[key];
|
|
206
|
+
if (!initialized && this._onInitialNodes) {
|
|
207
|
+
await this._onInitialNodes(node, relation, type);
|
|
208
|
+
this._initialized[key] = true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Recursive depth-first traversal of the graph.
|
|
214
|
+
*
|
|
215
|
+
* @param options.node The node to start traversing from.
|
|
216
|
+
* @param options.relation The relation to traverse graph edges.
|
|
217
|
+
* @param options.visitor A callback which is called for each node visited during traversal.
|
|
218
|
+
*/
|
|
219
|
+
traverse(
|
|
220
|
+
{ visitor, node = this.root, relation = 'outbound', expansion }: GraphTraversalOptions,
|
|
221
|
+
path: string[] = [],
|
|
222
|
+
): void {
|
|
223
|
+
// Break cycles.
|
|
224
|
+
if (path.includes(node.id)) {
|
|
225
|
+
return;
|
|
132
226
|
}
|
|
133
227
|
|
|
134
|
-
|
|
228
|
+
const shouldContinue = visitor(node, [...path, node.id]);
|
|
229
|
+
if (shouldContinue === false) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
Object.values(this._getNodes({ node, relation, expansion })).forEach((child) =>
|
|
234
|
+
this.traverse({ node: child, relation, visitor, expansion }, [...path, node.id]),
|
|
235
|
+
);
|
|
135
236
|
}
|
|
136
237
|
|
|
137
|
-
|
|
138
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Recursive depth-first traversal of the graph wrapping each visitor call in an effect.
|
|
240
|
+
*
|
|
241
|
+
* @param options.node The node to start traversing from.
|
|
242
|
+
* @param options.relation The relation to traverse graph edges.
|
|
243
|
+
* @param options.visitor A callback which is called for each node visited during traversal.
|
|
244
|
+
*/
|
|
245
|
+
subscribeTraverse(
|
|
246
|
+
{ visitor, node = this.root, relation = 'outbound', expansion }: GraphTraversalOptions,
|
|
247
|
+
currentPath: string[] = [],
|
|
248
|
+
) {
|
|
249
|
+
return effect(() => {
|
|
250
|
+
const path = [...currentPath, node.id];
|
|
251
|
+
const result = visitor(node, path);
|
|
252
|
+
if (result === false) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const nodes = this._getNodes({ node, relation, expansion });
|
|
257
|
+
const nodeSubscriptions = nodes.map((n) => this.subscribeTraverse({ node: n, visitor, expansion }, path));
|
|
258
|
+
|
|
259
|
+
return () => {
|
|
260
|
+
nodeSubscriptions.forEach((unsubscribe) => unsubscribe());
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get the path between two nodes in the graph.
|
|
267
|
+
*/
|
|
268
|
+
getPath({ source = 'root', target }: { source?: string; target: string }): string[] | undefined {
|
|
269
|
+
const start = this.findNode(source);
|
|
270
|
+
if (!start) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let found: string[] | undefined;
|
|
275
|
+
this.traverse({
|
|
276
|
+
node: start,
|
|
277
|
+
visitor: (node, path) => {
|
|
278
|
+
if (found) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (node.id === target) {
|
|
283
|
+
found = path;
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
return found;
|
|
139
289
|
}
|
|
140
290
|
|
|
141
291
|
/**
|
|
142
292
|
* Add nodes to the graph.
|
|
293
|
+
*
|
|
294
|
+
* @internal
|
|
143
295
|
*/
|
|
144
|
-
|
|
145
|
-
|
|
296
|
+
_addNodes<TData = null, TProperties extends Record<string, any> = Record<string, any>>(
|
|
297
|
+
nodes: NodeArg<TData, TProperties>[],
|
|
146
298
|
): Node<TData, TProperties>[] {
|
|
147
|
-
return nodes.map((node) => this._addNode(node));
|
|
299
|
+
return batch(() => nodes.map((node) => this._addNode(node)));
|
|
148
300
|
}
|
|
149
301
|
|
|
150
302
|
private _addNode<TData, TProperties extends Record<string, any> = Record<string, any>>({
|
|
@@ -153,35 +305,65 @@ export class Graph {
|
|
|
153
305
|
..._node
|
|
154
306
|
}: NodeArg<TData, TProperties>): Node<TData, TProperties> {
|
|
155
307
|
return untracked(() => {
|
|
156
|
-
const
|
|
157
|
-
|
|
308
|
+
const existingNode = this._nodes[_node.id];
|
|
309
|
+
const node = existingNode ?? this._constructNode({ data: null, properties: {}, ..._node });
|
|
310
|
+
if (existingNode) {
|
|
311
|
+
const { data, properties, type } = _node;
|
|
312
|
+
if (data && data !== node.data) {
|
|
313
|
+
node.data = data;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (type !== node.type) {
|
|
317
|
+
node.type = type;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
for (const key in properties) {
|
|
321
|
+
if (properties[key] !== node.properties[key]) {
|
|
322
|
+
node.properties[key] = properties[key];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
this._nodes[node.id] = node;
|
|
327
|
+
this._edges[node.id] = create({ inbound: [], outbound: [] });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const trigger = this._waitingForNodes[node.id];
|
|
331
|
+
if (trigger) {
|
|
332
|
+
trigger.wake(node);
|
|
333
|
+
delete this._waitingForNodes[node.id];
|
|
334
|
+
}
|
|
158
335
|
|
|
159
336
|
if (nodes) {
|
|
160
337
|
nodes.forEach((subNode) => {
|
|
161
338
|
this._addNode(subNode);
|
|
162
|
-
this.
|
|
339
|
+
this._addEdge({ source: node.id, target: subNode.id });
|
|
163
340
|
});
|
|
164
341
|
}
|
|
165
342
|
|
|
166
343
|
if (edges) {
|
|
167
|
-
edges.forEach(([id,
|
|
168
|
-
|
|
169
|
-
? this.
|
|
170
|
-
: this.
|
|
344
|
+
edges.forEach(([id, relation]) =>
|
|
345
|
+
relation === 'outbound'
|
|
346
|
+
? this._addEdge({ source: node.id, target: id })
|
|
347
|
+
: this._addEdge({ source: id, target: node.id }),
|
|
171
348
|
);
|
|
172
349
|
}
|
|
173
350
|
|
|
174
|
-
return
|
|
351
|
+
return node as unknown as Node<TData, TProperties>;
|
|
175
352
|
});
|
|
176
353
|
}
|
|
177
354
|
|
|
178
355
|
/**
|
|
179
356
|
* Remove nodes from the graph.
|
|
180
357
|
*
|
|
181
|
-
* @param
|
|
358
|
+
* @param ids The id of the node to remove.
|
|
182
359
|
* @param edges Whether to remove edges connected to the node from the graph as well.
|
|
360
|
+
* @internal
|
|
183
361
|
*/
|
|
184
|
-
|
|
362
|
+
_removeNodes(ids: string[], edges = false) {
|
|
363
|
+
batch(() => ids.forEach((id) => this._removeNode(id, edges)));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private _removeNode(id: string, edges = false) {
|
|
185
367
|
untracked(() => {
|
|
186
368
|
const node = this.findNode(id);
|
|
187
369
|
if (!node) {
|
|
@@ -189,123 +371,128 @@ export class Graph {
|
|
|
189
371
|
}
|
|
190
372
|
|
|
191
373
|
if (edges) {
|
|
192
|
-
// Remove edges from node.
|
|
193
|
-
delete this._edges[this.getEdgeKey(id, 'outbound')];
|
|
194
|
-
delete this._edges[this.getEdgeKey(id, 'inbound')];
|
|
195
|
-
|
|
196
374
|
// Remove edges from connected nodes.
|
|
197
|
-
this._getNodes({
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
)
|
|
375
|
+
this._getNodes({ node }).forEach((node) => {
|
|
376
|
+
this._removeEdge({ source: id, target: node.id });
|
|
377
|
+
});
|
|
378
|
+
this._getNodes({ node, relation: 'inbound' }).forEach((node) => {
|
|
379
|
+
this._removeEdge({ source: node.id, target: id });
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Remove edges from node.
|
|
383
|
+
delete this._edges[id];
|
|
201
384
|
}
|
|
202
385
|
|
|
203
386
|
// Remove node.
|
|
204
387
|
delete this._nodes[id];
|
|
388
|
+
void this._onRemoveNode?.(id);
|
|
205
389
|
});
|
|
206
390
|
}
|
|
207
391
|
|
|
208
392
|
/**
|
|
209
|
-
* Add
|
|
393
|
+
* Add edges to the graph.
|
|
394
|
+
*
|
|
395
|
+
* @internal
|
|
210
396
|
*/
|
|
211
|
-
|
|
397
|
+
_addEdges(edges: { source: string; target: string }[]) {
|
|
398
|
+
batch(() => edges.forEach((edge) => this._addEdge(edge)));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private _addEdge({ source, target }: { source: string; target: string }) {
|
|
212
402
|
untracked(() => {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
403
|
+
if (!this._edges[source]) {
|
|
404
|
+
this._edges[source] = create({ inbound: [], outbound: [] });
|
|
405
|
+
}
|
|
406
|
+
if (!this._edges[target]) {
|
|
407
|
+
this._edges[target] = create({ inbound: [], outbound: [] });
|
|
218
408
|
}
|
|
219
409
|
|
|
220
|
-
const
|
|
221
|
-
if (!
|
|
222
|
-
|
|
223
|
-
} else if (!inbound.includes(source)) {
|
|
224
|
-
inbound.push(source);
|
|
410
|
+
const sourceEdges = this._edges[source];
|
|
411
|
+
if (!sourceEdges.outbound.includes(target)) {
|
|
412
|
+
sourceEdges.outbound.push(target);
|
|
225
413
|
}
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
414
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
* Edges not included in the sorted list are appended to the end of the list.
|
|
233
|
-
*
|
|
234
|
-
* @param nodeId The id of the node to sort edges for.
|
|
235
|
-
* @param direction The direction of the edges from the node to sort.
|
|
236
|
-
* @param edges The ordered list of edges.
|
|
237
|
-
*/
|
|
238
|
-
sortEdges(nodeId: string, direction: EdgeDirection, edges: string[]) {
|
|
239
|
-
untracked(() => {
|
|
240
|
-
const current = this._edges[this.getEdgeKey(nodeId, direction)];
|
|
241
|
-
if (current) {
|
|
242
|
-
const unsorted = current.filter((id) => !edges.includes(id)) ?? [];
|
|
243
|
-
const sorted = edges.filter((id) => current.includes(id)) ?? [];
|
|
244
|
-
current.splice(0, current.length, ...[...sorted, ...unsorted]);
|
|
415
|
+
const targetEdges = this._edges[target];
|
|
416
|
+
if (!targetEdges.inbound.includes(source)) {
|
|
417
|
+
targetEdges.inbound.push(source);
|
|
245
418
|
}
|
|
246
419
|
});
|
|
247
420
|
}
|
|
248
421
|
|
|
249
422
|
/**
|
|
250
|
-
* Remove
|
|
423
|
+
* Remove edges from the graph.
|
|
424
|
+
* @internal
|
|
251
425
|
*/
|
|
252
|
-
|
|
426
|
+
_removeEdges(edges: { source: string; target: string }[]) {
|
|
427
|
+
batch(() => edges.forEach((edge) => this._removeEdge(edge)));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private _removeEdge({ source, target }: { source: string; target: string }) {
|
|
253
431
|
untracked(() => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
432
|
+
batch(() => {
|
|
433
|
+
const outboundIndex = this._edges[source]?.outbound.findIndex((id) => id === target);
|
|
434
|
+
if (outboundIndex !== undefined && outboundIndex !== -1) {
|
|
435
|
+
this._edges[source].outbound.splice(outboundIndex, 1);
|
|
436
|
+
}
|
|
258
437
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
438
|
+
const inboundIndex = this._edges[target]?.inbound.findIndex((id) => id === source);
|
|
439
|
+
if (inboundIndex !== undefined && inboundIndex !== -1) {
|
|
440
|
+
this._edges[target].inbound.splice(inboundIndex, 1);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
263
443
|
});
|
|
264
444
|
}
|
|
265
445
|
|
|
266
446
|
/**
|
|
267
|
-
*
|
|
447
|
+
* Sort edges for a node.
|
|
268
448
|
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* @param
|
|
272
|
-
* @param
|
|
449
|
+
* Edges not included in the sorted list are appended to the end of the list.
|
|
450
|
+
*
|
|
451
|
+
* @param nodeId The id of the node to sort edges for.
|
|
452
|
+
* @param relation The relation of the edges from the node to sort.
|
|
453
|
+
* @param edges The ordered list of edges.
|
|
454
|
+
* @ignore
|
|
273
455
|
*/
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
this.traverse({ node: child, direction, filter, visitor }, [...path, node.id]),
|
|
286
|
-
);
|
|
456
|
+
_sortEdges(nodeId: string, relation: Relation, edges: string[]) {
|
|
457
|
+
untracked(() => {
|
|
458
|
+
batch(() => {
|
|
459
|
+
const current = this._edges[nodeId];
|
|
460
|
+
if (current) {
|
|
461
|
+
const unsorted = current[relation].filter((id) => !edges.includes(id)) ?? [];
|
|
462
|
+
const sorted = edges.filter((id) => current[relation].includes(id)) ?? [];
|
|
463
|
+
current[relation].splice(0, current[relation].length, ...[...sorted, ...unsorted]);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
});
|
|
287
467
|
}
|
|
288
468
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
getPath({ source = 'root', target }: { source?: string; target: string }): string[] | undefined {
|
|
293
|
-
const start = this.findNode(source);
|
|
294
|
-
if (!start) {
|
|
295
|
-
return undefined;
|
|
296
|
-
}
|
|
469
|
+
private _constructNode = (node: Omit<Node, typeof graphSymbol>) => {
|
|
470
|
+
return create<NodeInternal>({ ...node, [graphSymbol]: this });
|
|
471
|
+
};
|
|
297
472
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
473
|
+
private _getNodes({
|
|
474
|
+
node,
|
|
475
|
+
relation = 'outbound',
|
|
476
|
+
type,
|
|
477
|
+
expansion,
|
|
478
|
+
}: {
|
|
479
|
+
node: Node;
|
|
480
|
+
relation?: Relation;
|
|
481
|
+
type?: string;
|
|
482
|
+
expansion?: boolean;
|
|
483
|
+
}): Node[] {
|
|
484
|
+
if (expansion) {
|
|
485
|
+
void this.expand(node, relation, type);
|
|
486
|
+
}
|
|
308
487
|
|
|
309
|
-
|
|
488
|
+
const edges = this._edges[node.id];
|
|
489
|
+
if (!edges) {
|
|
490
|
+
return [];
|
|
491
|
+
} else {
|
|
492
|
+
return edges[relation]
|
|
493
|
+
.map((id) => this._nodes[id])
|
|
494
|
+
.filter(nonNullable)
|
|
495
|
+
.filter((n) => !type || n.type === type);
|
|
496
|
+
}
|
|
310
497
|
}
|
|
311
498
|
}
|