@dxos/app-graph 0.6.3-main.40d1cec → 0.6.3-main.9e4e207

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