@dxos/app-graph 0.6.3-main.9e4e207 → 0.6.3-next.2f65b78

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,47 +2,17 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { batch, effect, untracked } from '@preact/signals-core';
5
+ import { untracked } from '@preact/signals-core';
6
6
 
7
- import { Trigger } from '@dxos/async';
8
- import { type ReactiveObject, create } from '@dxos/echo-schema';
7
+ import { create } from '@dxos/echo-schema';
9
8
  import { invariant } from '@dxos/invariant';
10
9
  import { nonNullable } from '@dxos/util';
11
10
 
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
- };
11
+ import { isActionLike, type EdgeDirection, type Node, type NodeArg, type NodeBase } from './node';
23
12
 
24
13
  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;
45
14
 
15
+ export type TraversalOptions = {
46
16
  /**
47
17
  * The node to start traversing from.
48
18
  *
@@ -51,54 +21,40 @@ export type GraphTraversalOptions = {
51
21
  node?: Node;
52
22
 
53
23
  /**
54
- * The relation to traverse graph edges.
24
+ * The direction to traverse graph edges.
55
25
  *
56
26
  * @default 'outbound'
57
27
  */
58
- relation?: Relation;
28
+ direction?: EdgeDirection;
59
29
 
60
30
  /**
61
- * Only traverse nodes that are already loaded.
31
+ * A predicate to filter nodes which are passed to the `visitor` callback.
62
32
  */
63
- onlyLoaded?: boolean;
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;
64
39
  };
65
40
 
66
41
  /**
67
42
  * The Graph represents the structure of the application constructed via plugins.
68
43
  */
69
44
  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
-
77
45
  /**
78
46
  * @internal
79
47
  */
80
- readonly _nodes: Record<string, ReactiveObject<NodeInternal>> = {};
48
+ readonly _nodes = create<Record<string, NodeBase>>({
49
+ [ROOT_ID]: { id: ROOT_ID, properties: {}, data: null },
50
+ });
81
51
 
82
52
  /**
83
53
  * @internal
84
54
  */
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
- }
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[]>>({});
102
58
 
103
59
  /**
104
60
  * Alias for `findNode('root')`.
@@ -110,16 +66,11 @@ export class Graph {
110
66
  /**
111
67
  * Convert the graph to a JSON object.
112
68
  */
113
- toJSON({
114
- id = ROOT_ID,
115
- maxLength = 32,
116
- onlyLoaded = true,
117
- }: { id?: string; maxLength?: number; onlyLoaded?: boolean } = {}) {
69
+ toJSON({ id = ROOT_ID, maxLength = 32 }: { id?: string; maxLength?: number } = {}) {
118
70
  const toJSON = (node: Node, seen: string[] = []): any => {
119
- const nodes = this.nodes(node, { onlyLoaded });
71
+ const nodes = node.nodes();
120
72
  const obj: Record<string, any> = {
121
73
  id: node.id.length > maxLength ? `${node.id.slice(0, maxLength - 3)}...` : node.id,
122
- type: node.type,
123
74
  };
124
75
  if (node.properties.label) {
125
76
  obj.label = node.properties.label;
@@ -143,148 +94,57 @@ export class Graph {
143
94
 
144
95
  /**
145
96
  * 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.
149
- */
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
97
  */
164
- waitForNode(id: string, timeout = NODE_TIMEOUT): Promise<Node> {
165
- if (this._nodes[id]) {
166
- return Promise.resolve(this._nodes[id]);
98
+ findNode(id: string): Node | undefined {
99
+ const nodeBase = this._nodes[id];
100
+ if (!nodeBase) {
101
+ return undefined;
167
102
  }
168
103
 
169
- const trigger = this._waitingForNodes[id] ?? (this._waitingForNodes[id] = new Trigger<Node>());
170
- return trigger.wait({ timeout });
171
- }
172
-
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
- }
181
-
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] ?? [];
104
+ return this._constructNode(nodeBase);
187
105
  }
188
106
 
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
- }
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
+ };
198
124
 
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
- }
125
+ return node;
126
+ };
214
127
 
215
- const shouldContinue = visitor(node, [...path, node.id]);
216
- if (shouldContinue === false) {
217
- return;
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 [];
218
132
  }
219
133
 
220
- Object.values(this._getNodes({ node, relation, onlyLoaded })).forEach((child) =>
221
- this.traverse({ node: child, relation, visitor, onlyLoaded }, [...path, node.id]),
222
- );
134
+ return edges.map((id) => this.findNode(id)).filter(nonNullable);
223
135
  }
224
136
 
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;
137
+ private getEdgeKey(id: string, direction: EdgeDirection) {
138
+ return `${id}-${direction}`;
277
139
  }
278
140
 
279
141
  /**
280
142
  * Add nodes to the graph.
281
- *
282
- * @internal
283
143
  */
284
- _addNodes<TData = null, TProperties extends Record<string, any> = Record<string, any>>(
285
- nodes: NodeArg<TData, TProperties>[],
144
+ addNodes<TData = null, TProperties extends Record<string, any> = Record<string, any>>(
145
+ ...nodes: NodeArg<TData, TProperties>[]
286
146
  ): Node<TData, TProperties>[] {
287
- return batch(() => nodes.map((node) => this._addNode(node)));
147
+ return nodes.map((node) => this._addNode(node));
288
148
  }
289
149
 
290
150
  private _addNode<TData, TProperties extends Record<string, any> = Record<string, any>>({
@@ -293,65 +153,35 @@ export class Graph {
293
153
  ..._node
294
154
  }: NodeArg<TData, TProperties>): Node<TData, TProperties> {
295
155
  return untracked(() => {
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
- }
156
+ const node = { data: null, properties: {}, ..._node };
157
+ this._nodes[node.id] = node;
323
158
 
324
159
  if (nodes) {
325
160
  nodes.forEach((subNode) => {
326
161
  this._addNode(subNode);
327
- this._addEdge({ source: node.id, target: subNode.id });
162
+ this.addEdge({ source: node.id, target: subNode.id });
328
163
  });
329
164
  }
330
165
 
331
166
  if (edges) {
332
- edges.forEach(([id, relation]) =>
333
- relation === 'outbound'
334
- ? this._addEdge({ source: node.id, target: id })
335
- : this._addEdge({ source: id, target: node.id }),
167
+ edges.forEach(([id, direction]) =>
168
+ direction === 'outbound'
169
+ ? this.addEdge({ source: node.id, target: id })
170
+ : this.addEdge({ source: id, target: node.id }),
336
171
  );
337
172
  }
338
173
 
339
- return node as unknown as Node<TData, TProperties>;
174
+ return this._constructNode(node) as Node<TData, TProperties>;
340
175
  });
341
176
  }
342
177
 
343
178
  /**
344
179
  * Remove nodes from the graph.
345
180
  *
346
- * @param ids The id of the node to remove.
181
+ * @param id The id of the node to remove.
347
182
  * @param edges Whether to remove edges connected to the node from the graph as well.
348
- * @internal
349
183
  */
350
- _removeNodes(ids: string[], edges = false) {
351
- batch(() => ids.forEach((id) => this._removeNode(id, edges)));
352
- }
353
-
354
- private _removeNode(id: string, edges = false) {
184
+ removeNode(id: string, edges = false) {
355
185
  untracked(() => {
356
186
  const node = this.findNode(id);
357
187
  if (!node) {
@@ -359,75 +189,40 @@ export class Graph {
359
189
  }
360
190
 
361
191
  if (edges) {
362
- // Remove edges from connected nodes.
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
192
  // Remove edges from node.
371
- delete this._edges[id];
193
+ delete this._edges[this.getEdgeKey(id, 'outbound')];
194
+ delete this._edges[this.getEdgeKey(id, 'inbound')];
195
+
196
+ // 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
+ );
372
201
  }
373
202
 
374
203
  // Remove node.
375
204
  delete this._nodes[id];
376
- this._onRemoveNode?.(id);
377
205
  });
378
206
  }
379
207
 
380
208
  /**
381
- * Add edges to the graph.
382
- *
383
- * @internal
209
+ * Add an edge to the graph.
384
210
  */
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 }) {
211
+ addEdge({ source, target }: { source: string; target: string }) {
390
212
  untracked(() => {
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: [] });
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);
396
218
  }
397
219
 
398
- const sourceEdges = this._edges[source];
399
- if (!sourceEdges.outbound.includes(target)) {
400
- sourceEdges.outbound.push(target);
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);
401
225
  }
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
- });
431
226
  });
432
227
  }
433
228
 
@@ -437,62 +232,80 @@ export class Graph {
437
232
  * Edges not included in the sorted list are appended to the end of the list.
438
233
  *
439
234
  * @param nodeId The id of the node to sort edges for.
440
- * @param relation The relation of the edges from the node to sort.
235
+ * @param direction The direction of the edges from the node to sort.
441
236
  * @param edges The ordered list of edges.
442
- * @ignore
443
237
  */
444
- _sortEdges(nodeId: string, relation: Relation, edges: string[]) {
238
+ sortEdges(nodeId: string, direction: EdgeDirection, edges: string[]) {
445
239
  untracked(() => {
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
- });
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
+ }
454
246
  });
455
247
  }
456
248
 
457
- private _constructNode = (node: Omit<Node, typeof graphSymbol>) => {
458
- return create<NodeInternal>({ ...node, [graphSymbol]: this });
459
- };
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
+ }
460
258
 
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
- );
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);
485
262
  }
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;
486
278
  }
487
279
 
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);
280
+ if (!filter || filter(node)) {
281
+ visitor?.(node, [...path, node.id]);
496
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;
296
+ }
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;
497
310
  }
498
311
  }
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
@@ -4,4 +4,5 @@
4
4
 
5
5
  export * from './graph';
6
6
  export * from './graph-builder';
7
+ export * from './helpers';
7
8
  export * from './node';