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

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.
@@ -1,5 +1,6 @@
1
1
  // packages/sdk/app-graph/src/graph.ts
2
- import { untracked } from "@preact/signals-core";
2
+ import { batch, effect, untracked } from "@preact/signals-core";
3
+ import { asyncTimeout, Trigger } from "@dxos/async";
3
4
  import { create } from "@dxos/echo-schema";
4
5
  import { invariant } from "@dxos/invariant";
5
6
  import { nonNullable } from "@dxos/util";
@@ -13,51 +14,56 @@ var isActionLike = (data) => isAction(data) || isActionGroup(data);
13
14
 
14
15
  // packages/sdk/app-graph/src/graph.ts
15
16
  var __dxlog_file = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph.ts";
17
+ var graphSymbol = Symbol("graph");
18
+ var getGraph = (node) => {
19
+ const graph = node[graphSymbol];
20
+ invariant(graph, "Node is not associated with a graph.", {
21
+ F: __dxlog_file,
22
+ L: 20,
23
+ S: void 0,
24
+ A: [
25
+ "graph",
26
+ "'Node is not associated with a graph.'"
27
+ ]
28
+ });
29
+ return graph;
30
+ };
16
31
  var ROOT_ID = "root";
32
+ var ROOT_TYPE = "dxos.org/type/GraphRoot";
33
+ var ACTION_TYPE = "dxos.org/type/GraphAction";
34
+ var ACTION_GROUP_TYPE = "dxos.org/type/GraphActionGroup";
35
+ var NODE_TIMEOUT = 5e3;
17
36
  var Graph = class {
18
- constructor() {
37
+ constructor({ onInitialNode, onInitialNodes, onRemoveNode } = {}) {
38
+ this._waitingForNodes = {};
39
+ this._initialized = {};
19
40
  /**
20
41
  * @internal
21
42
  */
22
- this._nodes = create({
23
- [ROOT_ID]: {
24
- id: ROOT_ID,
25
- properties: {},
26
- data: null
27
- }
28
- });
43
+ this._nodes = {};
29
44
  /**
30
45
  * @internal
31
46
  */
32
- // Key is the `${node.id}-${direction}` and value is an ordered list of node ids.
33
- // Explicit type required because TS says this is not portable.
34
- this._edges = create({});
35
- this._constructNode = (nodeBase) => {
36
- const node = {
37
- ...nodeBase,
38
- edges: ({ direction = "outbound" } = {}) => {
39
- return this._edges[this.getEdgeKey(node.id, direction)];
40
- },
41
- nodes: ({ direction, filter } = {}) => {
42
- const nodes = this._getNodes({
43
- id: node.id,
44
- direction
45
- }).filter((n) => !isActionLike(n));
46
- return filter ? nodes.filter((n) => filter(n, node)) : nodes;
47
- },
48
- node: (id) => {
49
- return this._getNodes({
50
- id
51
- }).find((node2) => node2.id === id);
52
- },
53
- actions: () => {
54
- return this._getNodes({
55
- id: node.id
56
- }).filter(isActionLike);
57
- }
58
- };
59
- return node;
47
+ this._edges = {};
48
+ this._constructNode = (node) => {
49
+ return create({
50
+ ...node,
51
+ [graphSymbol]: this
52
+ });
60
53
  };
54
+ this._onInitialNode = onInitialNode;
55
+ this._onInitialNodes = onInitialNodes;
56
+ this._onRemoveNode = onRemoveNode;
57
+ this._nodes[ROOT_ID] = this._constructNode({
58
+ id: ROOT_ID,
59
+ type: ROOT_TYPE,
60
+ properties: {},
61
+ data: null
62
+ });
63
+ this._edges[ROOT_ID] = create({
64
+ inbound: [],
65
+ outbound: []
66
+ });
61
67
  }
62
68
  /**
63
69
  * Alias for `findNode('root')`.
@@ -68,11 +74,14 @@ var Graph = class {
68
74
  /**
69
75
  * Convert the graph to a JSON object.
70
76
  */
71
- toJSON({ id = ROOT_ID, maxLength = 32 } = {}) {
77
+ toJSON({ id = ROOT_ID, maxLength = 32, onlyLoaded = true } = {}) {
72
78
  const toJSON = (node, seen = []) => {
73
- const nodes = node.nodes();
79
+ const nodes = this.nodes(node, {
80
+ onlyLoaded
81
+ });
74
82
  const obj = {
75
- id: node.id.length > maxLength ? `${node.id.slice(0, maxLength - 3)}...` : node.id
83
+ id: node.id.length > maxLength ? `${node.id.slice(0, maxLength - 3)}...` : node.id,
84
+ type: node.type
76
85
  };
77
86
  if (node.properties.label) {
78
87
  obj.label = node.properties.label;
@@ -91,7 +100,7 @@ var Graph = class {
91
100
  const root = this.findNode(id);
92
101
  invariant(root, `Node not found: ${id}`, {
93
102
  F: __dxlog_file,
94
- L: 91,
103
+ L: 140,
95
104
  S: this,
96
105
  A: [
97
106
  "root",
@@ -102,257 +111,638 @@ var Graph = class {
102
111
  }
103
112
  /**
104
113
  * Find the node with the given id in the graph.
114
+ *
115
+ * If a node is not found within the graph and an `onInitialNode` callback is provided,
116
+ * it is called with the id and type of the node, potentially initializing the node.
105
117
  */
106
- findNode(id) {
107
- const nodeBase = this._nodes[id];
108
- if (!nodeBase) {
109
- return void 0;
118
+ findNode(id, type) {
119
+ const existingNode = this._nodes[id];
120
+ const nodeArg = !existingNode && this._onInitialNode?.(id, type);
121
+ return existingNode ?? (nodeArg ? this._addNode(nodeArg) : void 0);
122
+ }
123
+ /**
124
+ * Wait for a node to be added to the graph.
125
+ *
126
+ * If the node is already present in the graph, the promise resolves immediately.
127
+ *
128
+ * @param id The id of the node to wait for.
129
+ * @param timeout The time in milliseconds to wait for the node to be added.
130
+ */
131
+ async waitForNode(id, timeout = NODE_TIMEOUT) {
132
+ const node = this.findNode(id);
133
+ if (node) {
134
+ return node;
110
135
  }
111
- return this._constructNode(nodeBase);
136
+ const trigger = this._waitingForNodes[id] ?? (this._waitingForNodes[id] = new Trigger());
137
+ return asyncTimeout(trigger.wait(), timeout, `Node not found: ${id}`);
112
138
  }
113
- _getNodes({ id, direction = "outbound" }) {
114
- const edges = this._edges[this.getEdgeKey(id, direction)];
115
- if (!edges) {
116
- return [];
139
+ /**
140
+ * Nodes that this node is connected to in default order.
141
+ */
142
+ nodes(node, options = {}) {
143
+ const { onlyLoaded, relation, filter, type } = options;
144
+ const nodes = this._getNodes({
145
+ node,
146
+ relation,
147
+ type,
148
+ onlyLoaded
149
+ });
150
+ return nodes.filter((n) => untracked(() => !isActionLike(n))).filter((n) => filter?.(n, node) ?? true);
151
+ }
152
+ /**
153
+ * Edges that this node is connected to in default order.
154
+ */
155
+ edges(node, { relation = "outbound" } = {}) {
156
+ return this._edges[node.id]?.[relation] ?? [];
157
+ }
158
+ /**
159
+ * Actions or action groups that this node is connected to in default order.
160
+ */
161
+ actions(node, { onlyLoaded } = {}) {
162
+ return [
163
+ ...this._getNodes({
164
+ node,
165
+ type: ACTION_GROUP_TYPE,
166
+ onlyLoaded
167
+ }),
168
+ ...this._getNodes({
169
+ node,
170
+ type: ACTION_TYPE,
171
+ onlyLoaded
172
+ })
173
+ ];
174
+ }
175
+ /**
176
+ * Recursive depth-first traversal of the graph.
177
+ *
178
+ * @param options.node The node to start traversing from.
179
+ * @param options.relation The relation to traverse graph edges.
180
+ * @param options.visitor A callback which is called for each node visited during traversal.
181
+ */
182
+ traverse({ visitor, node = this.root, relation = "outbound", onlyLoaded }, path = []) {
183
+ if (path.includes(node.id)) {
184
+ return;
117
185
  }
118
- return edges.map((id2) => this.findNode(id2)).filter(nonNullable);
186
+ const shouldContinue = visitor(node, [
187
+ ...path,
188
+ node.id
189
+ ]);
190
+ if (shouldContinue === false) {
191
+ return;
192
+ }
193
+ Object.values(this._getNodes({
194
+ node,
195
+ relation,
196
+ onlyLoaded
197
+ })).forEach((child) => this.traverse({
198
+ node: child,
199
+ relation,
200
+ visitor,
201
+ onlyLoaded
202
+ }, [
203
+ ...path,
204
+ node.id
205
+ ]));
119
206
  }
120
- getEdgeKey(id, direction) {
121
- return `${id}-${direction}`;
207
+ /**
208
+ * Recursive depth-first traversal of the graph wrapping each visitor call in an effect.
209
+ *
210
+ * @param options.node The node to start traversing from.
211
+ * @param options.relation The relation to traverse graph edges.
212
+ * @param options.visitor A callback which is called for each node visited during traversal.
213
+ */
214
+ subscribeTraverse({ visitor, node = this.root, relation = "outbound", onlyLoaded }, currentPath = []) {
215
+ return effect(() => {
216
+ const path = [
217
+ ...currentPath,
218
+ node.id
219
+ ];
220
+ const result = visitor(node, path);
221
+ if (result === false) {
222
+ return;
223
+ }
224
+ const nodes = this._getNodes({
225
+ node,
226
+ relation,
227
+ onlyLoaded
228
+ });
229
+ const nodeSubscriptions = nodes.map((n) => this.subscribeTraverse({
230
+ node: n,
231
+ visitor,
232
+ onlyLoaded
233
+ }, path));
234
+ return () => {
235
+ nodeSubscriptions.forEach((unsubscribe) => unsubscribe());
236
+ };
237
+ });
238
+ }
239
+ /**
240
+ * Get the path between two nodes in the graph.
241
+ */
242
+ getPath({ source = "root", target }) {
243
+ const start = this.findNode(source);
244
+ if (!start) {
245
+ return void 0;
246
+ }
247
+ let found;
248
+ this.traverse({
249
+ onlyLoaded: true,
250
+ node: start,
251
+ visitor: (node, path) => {
252
+ if (found) {
253
+ return false;
254
+ }
255
+ if (node.id === target) {
256
+ found = path;
257
+ }
258
+ }
259
+ });
260
+ return found;
122
261
  }
123
262
  /**
124
263
  * Add nodes to the graph.
264
+ *
265
+ * @internal
125
266
  */
126
- addNodes(...nodes) {
127
- return nodes.map((node) => this._addNode(node));
267
+ _addNodes(nodes) {
268
+ return batch(() => nodes.map((node) => this._addNode(node)));
128
269
  }
129
270
  _addNode({ nodes, edges, ..._node }) {
130
271
  return untracked(() => {
131
- const node = {
272
+ const existingNode = this._nodes[_node.id];
273
+ const node = existingNode ?? this._constructNode({
132
274
  data: null,
133
275
  properties: {},
134
276
  ..._node
135
- };
136
- this._nodes[node.id] = node;
277
+ });
278
+ if (existingNode) {
279
+ const { data, properties, type } = _node;
280
+ if (data && data !== node.data) {
281
+ node.data = data;
282
+ }
283
+ if (type !== node.type) {
284
+ node.type = type;
285
+ }
286
+ for (const key in properties) {
287
+ if (properties[key] !== node.properties[key]) {
288
+ node.properties[key] = properties[key];
289
+ }
290
+ }
291
+ } else {
292
+ this._nodes[node.id] = node;
293
+ this._edges[node.id] = create({
294
+ inbound: [],
295
+ outbound: []
296
+ });
297
+ }
298
+ const trigger = this._waitingForNodes[node.id];
299
+ if (trigger) {
300
+ trigger.wake(node);
301
+ delete this._waitingForNodes[node.id];
302
+ }
137
303
  if (nodes) {
138
304
  nodes.forEach((subNode) => {
139
305
  this._addNode(subNode);
140
- this.addEdge({
306
+ this._addEdge({
141
307
  source: node.id,
142
308
  target: subNode.id
143
309
  });
144
310
  });
145
311
  }
146
312
  if (edges) {
147
- edges.forEach(([id, direction]) => direction === "outbound" ? this.addEdge({
313
+ edges.forEach(([id, relation]) => relation === "outbound" ? this._addEdge({
148
314
  source: node.id,
149
315
  target: id
150
- }) : this.addEdge({
316
+ }) : this._addEdge({
151
317
  source: id,
152
318
  target: node.id
153
319
  }));
154
320
  }
155
- return this._constructNode(node);
321
+ return node;
156
322
  });
157
323
  }
158
324
  /**
159
325
  * Remove nodes from the graph.
160
326
  *
161
- * @param id The id of the node to remove.
327
+ * @param ids The id of the node to remove.
162
328
  * @param edges Whether to remove edges connected to the node from the graph as well.
329
+ * @internal
163
330
  */
164
- removeNode(id, edges = false) {
331
+ _removeNodes(ids, edges = false) {
332
+ batch(() => ids.forEach((id) => this._removeNode(id, edges)));
333
+ }
334
+ _removeNode(id, edges = false) {
165
335
  untracked(() => {
166
336
  const node = this.findNode(id);
167
337
  if (!node) {
168
338
  return;
169
339
  }
170
340
  if (edges) {
171
- delete this._edges[this.getEdgeKey(id, "outbound")];
172
- delete this._edges[this.getEdgeKey(id, "inbound")];
173
341
  this._getNodes({
174
- id
175
- }).forEach((node2) => this.removeEdge({
176
- source: id,
177
- target: node2.id
178
- }));
342
+ node,
343
+ onlyLoaded: true
344
+ }).forEach((node2) => {
345
+ this._removeEdge({
346
+ source: id,
347
+ target: node2.id
348
+ });
349
+ });
179
350
  this._getNodes({
180
- id,
181
- direction: "inbound"
182
- }).forEach((node2) => this.removeEdge({
183
- source: node2.id,
184
- target: id
185
- }));
351
+ node,
352
+ relation: "inbound",
353
+ onlyLoaded: true
354
+ }).forEach((node2) => {
355
+ this._removeEdge({
356
+ source: node2.id,
357
+ target: id
358
+ });
359
+ });
360
+ delete this._edges[id];
186
361
  }
187
362
  delete this._nodes[id];
363
+ this._onRemoveNode?.(id);
188
364
  });
189
365
  }
190
366
  /**
191
- * Add an edge to the graph.
367
+ * Add edges to the graph.
368
+ *
369
+ * @internal
192
370
  */
193
- addEdge({ source, target }) {
371
+ _addEdges(edges) {
372
+ batch(() => edges.forEach((edge) => this._addEdge(edge)));
373
+ }
374
+ _addEdge({ source, target }) {
194
375
  untracked(() => {
195
- const outbound = this._edges[this.getEdgeKey(source, "outbound")];
196
- if (!outbound) {
197
- this._edges[this.getEdgeKey(source, "outbound")] = [
198
- target
199
- ];
200
- } else if (!outbound.includes(target)) {
201
- outbound.push(target);
376
+ if (!this._edges[source]) {
377
+ this._edges[source] = create({
378
+ inbound: [],
379
+ outbound: []
380
+ });
381
+ }
382
+ if (!this._edges[target]) {
383
+ this._edges[target] = create({
384
+ inbound: [],
385
+ outbound: []
386
+ });
202
387
  }
203
- const inbound = this._edges[this.getEdgeKey(target, "inbound")];
204
- if (!inbound) {
205
- this._edges[this.getEdgeKey(target, "inbound")] = [
206
- source
207
- ];
208
- } else if (!inbound.includes(source)) {
209
- inbound.push(source);
388
+ const sourceEdges = this._edges[source];
389
+ if (!sourceEdges.outbound.includes(target)) {
390
+ sourceEdges.outbound.push(target);
391
+ }
392
+ const targetEdges = this._edges[target];
393
+ if (!targetEdges.inbound.includes(source)) {
394
+ targetEdges.inbound.push(source);
210
395
  }
211
396
  });
212
397
  }
213
398
  /**
399
+ * Remove edges from the graph.
400
+ * @internal
401
+ */
402
+ _removeEdges(edges) {
403
+ batch(() => edges.forEach((edge) => this._removeEdge(edge)));
404
+ }
405
+ _removeEdge({ source, target }) {
406
+ untracked(() => {
407
+ batch(() => {
408
+ const outboundIndex = this._edges[source]?.outbound.findIndex((id) => id === target);
409
+ if (outboundIndex !== void 0 && outboundIndex !== -1) {
410
+ this._edges[source].outbound.splice(outboundIndex, 1);
411
+ }
412
+ const inboundIndex = this._edges[target]?.inbound.findIndex((id) => id === source);
413
+ if (inboundIndex !== void 0 && inboundIndex !== -1) {
414
+ this._edges[target].inbound.splice(inboundIndex, 1);
415
+ }
416
+ });
417
+ });
418
+ }
419
+ /**
214
420
  * Sort edges for a node.
215
421
  *
216
422
  * Edges not included in the sorted list are appended to the end of the list.
217
423
  *
218
424
  * @param nodeId The id of the node to sort edges for.
219
- * @param direction The direction of the edges from the node to sort.
425
+ * @param relation The relation of the edges from the node to sort.
220
426
  * @param edges The ordered list of edges.
427
+ * @ignore
221
428
  */
222
- sortEdges(nodeId, direction, edges) {
429
+ _sortEdges(nodeId, relation, edges) {
223
430
  untracked(() => {
224
- const current = this._edges[this.getEdgeKey(nodeId, direction)];
225
- if (current) {
226
- const unsorted = current.filter((id) => !edges.includes(id)) ?? [];
227
- const sorted = edges.filter((id) => current.includes(id)) ?? [];
228
- current.splice(0, current.length, ...[
229
- ...sorted,
230
- ...unsorted
231
- ]);
232
- }
431
+ batch(() => {
432
+ const current = this._edges[nodeId];
433
+ if (current) {
434
+ const unsorted = current[relation].filter((id) => !edges.includes(id)) ?? [];
435
+ const sorted = edges.filter((id) => current[relation].includes(id)) ?? [];
436
+ current[relation].splice(0, current[relation].length, ...[
437
+ ...sorted,
438
+ ...unsorted
439
+ ]);
440
+ }
441
+ });
233
442
  });
234
443
  }
235
- /**
236
- * Remove an edge from the graph.
237
- */
238
- removeEdge({ source, target }) {
239
- untracked(() => {
240
- const outboundIndex = this._edges[this.getEdgeKey(source, "outbound")]?.findIndex((id) => id === target);
241
- if (outboundIndex !== -1) {
242
- this._edges[this.getEdgeKey(source, "outbound")].splice(outboundIndex, 1);
243
- }
244
- const inboundIndex = this._edges[this.getEdgeKey(target, "inbound")]?.findIndex((id) => id === source);
245
- if (inboundIndex !== -1) {
246
- this._edges[this.getEdgeKey(target, "inbound")].splice(inboundIndex, 1);
444
+ _getNodes({ node, relation = "outbound", type, onlyLoaded }) {
445
+ const key = `${node.id}-${relation}-${type}`;
446
+ const initialized = this._initialized[key];
447
+ if (!initialized && !onlyLoaded && this._onInitialNodes) {
448
+ const args = this._onInitialNodes(node, relation, type)?.filter((n) => !type || n.type === type);
449
+ this._initialized[key] = true;
450
+ if (args && args.length > 0) {
451
+ const nodes = this._addNodes(args);
452
+ this._addEdges(nodes.map(({ id }) => relation === "outbound" ? {
453
+ source: node.id,
454
+ target: id
455
+ } : {
456
+ source: id,
457
+ target: node.id
458
+ }));
247
459
  }
248
- });
249
- }
250
- /**
251
- * Recursive depth-first traversal.
252
- *
253
- * @param options.node The node to start traversing from.
254
- * @param options.direction The direction to traverse graph edges.
255
- * @param options.filter A predicate to filter nodes which are passed to the `visitor` callback.
256
- * @param options.visitor A callback which is called for each node visited during traversal.
257
- */
258
- traverse({ node = this.root, direction = "outbound", filter, visitor }, path = []) {
259
- if (path.includes(node.id)) {
260
- return;
261
460
  }
262
- if (!filter || filter(node)) {
263
- visitor?.(node, [
264
- ...path,
265
- node.id
266
- ]);
267
- }
268
- Object.values(this._getNodes({
269
- id: node.id,
270
- direction
271
- })).forEach((child) => this.traverse({
272
- node: child,
273
- direction,
274
- filter,
275
- visitor
276
- }, [
277
- ...path,
278
- node.id
279
- ]));
280
- }
281
- /**
282
- * Get the path between two nodes in the graph.
283
- */
284
- getPath({ source = "root", target }) {
285
- const start = this.findNode(source);
286
- if (!start) {
287
- return void 0;
461
+ const edges = this._edges[node.id];
462
+ if (!edges) {
463
+ return [];
464
+ } else {
465
+ return edges[relation].map((id) => this._nodes[id]).filter(nonNullable).filter((n) => !type || n.type === type);
288
466
  }
289
- let found;
290
- this.traverse({
291
- node: start,
292
- filter: () => !found,
293
- visitor: (node, path) => {
294
- if (node.id === target) {
295
- found = path;
296
- }
297
- }
298
- });
299
- return found;
300
467
  }
301
468
  };
302
469
 
303
470
  // packages/sdk/app-graph/src/graph-builder.ts
304
- import { EventSubscriptions } from "@dxos/async";
471
+ import { effect as effect2, signal } from "@preact/signals-core";
472
+ import { create as create2 } from "@dxos/echo-schema";
473
+ import { invariant as invariant2 } from "@dxos/invariant";
474
+ import { nonNullable as nonNullable2 } from "@dxos/util";
475
+ var __dxlog_file2 = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph-builder.ts";
476
+ var createExtension = (extension) => {
477
+ const { id, resolver, connector, actions, actionGroups, ...rest } = extension;
478
+ const getId = (key) => `${id}/${key}`;
479
+ return [
480
+ resolver ? {
481
+ id: getId("resolver"),
482
+ resolver
483
+ } : void 0,
484
+ connector ? {
485
+ ...rest,
486
+ id: getId("connector"),
487
+ connector
488
+ } : void 0,
489
+ actionGroups ? {
490
+ ...rest,
491
+ id: getId("actionGroups"),
492
+ type: ACTION_GROUP_TYPE,
493
+ relation: "outbound",
494
+ connector: ({ node }) => actionGroups({
495
+ node
496
+ })?.map((arg) => ({
497
+ ...arg,
498
+ data: actionGroupSymbol,
499
+ type: ACTION_GROUP_TYPE
500
+ }))
501
+ } : void 0,
502
+ actions ? {
503
+ ...rest,
504
+ id: getId("actions"),
505
+ type: ACTION_TYPE,
506
+ relation: "outbound",
507
+ connector: ({ node }) => actions({
508
+ node
509
+ })?.map((arg) => ({
510
+ ...arg,
511
+ type: ACTION_TYPE
512
+ }))
513
+ } : void 0
514
+ ].filter(nonNullable2);
515
+ };
516
+ var Dispatcher = class {
517
+ constructor() {
518
+ this.stateIndex = 0;
519
+ this.state = {};
520
+ this.cleanup = [];
521
+ }
522
+ };
523
+ var BuilderInternal = class {
524
+ };
525
+ var memoize = (fn, key = "result") => {
526
+ const dispatcher = BuilderInternal.currentDispatcher;
527
+ invariant2(dispatcher?.currentExtension, "memoize must be called within an extension", {
528
+ F: __dxlog_file2,
529
+ L: 129,
530
+ S: void 0,
531
+ A: [
532
+ "dispatcher?.currentExtension",
533
+ "'memoize must be called within an extension'"
534
+ ]
535
+ });
536
+ const all = dispatcher.state[dispatcher.currentExtension][dispatcher.stateIndex] ?? {};
537
+ const current = all[key];
538
+ const result = current ? current.result : fn();
539
+ dispatcher.state[dispatcher.currentExtension][dispatcher.stateIndex] = {
540
+ ...all,
541
+ [key]: {
542
+ result
543
+ }
544
+ };
545
+ dispatcher.stateIndex++;
546
+ return result;
547
+ };
548
+ var cleanup = (fn) => {
549
+ memoize(() => {
550
+ const dispatcher = BuilderInternal.currentDispatcher;
551
+ invariant2(dispatcher, "cleanup must be called within an extension", {
552
+ F: __dxlog_file2,
553
+ L: 144,
554
+ S: void 0,
555
+ A: [
556
+ "dispatcher",
557
+ "'cleanup must be called within an extension'"
558
+ ]
559
+ });
560
+ dispatcher.cleanup.push(fn);
561
+ });
562
+ };
563
+ var toSignal = (subscribe, get, key) => {
564
+ const thisSignal = memoize(() => {
565
+ return signal(get());
566
+ }, key);
567
+ const unsubscribe = memoize(() => {
568
+ return subscribe(() => thisSignal.value = get());
569
+ }, key);
570
+ cleanup(() => {
571
+ unsubscribe();
572
+ });
573
+ return thisSignal.value;
574
+ };
305
575
  var GraphBuilder = class {
306
576
  constructor() {
307
- this._extensions = /* @__PURE__ */ new Map();
308
- this._unsubscribe = new EventSubscriptions();
577
+ this._dispatcher = new Dispatcher();
578
+ this._extensions = create2({});
579
+ this._resolverSubscriptions = /* @__PURE__ */ new Map();
580
+ this._connectorSubscriptions = /* @__PURE__ */ new Map();
581
+ this._nodeChanged = {};
582
+ this._graph = new Graph({
583
+ onInitialNode: (id, type) => this._onInitialNode(id, type),
584
+ onInitialNodes: (node, relation, type) => this._onInitialNodes(node, relation, type),
585
+ onRemoveNode: (id) => this._onRemoveNode(id)
586
+ });
587
+ }
588
+ get graph() {
589
+ return this._graph;
309
590
  }
310
591
  /**
311
592
  * Register a node builder which will be called in order to construct the graph.
312
593
  */
313
- addExtension(id, extension) {
314
- this._extensions.set(id, extension);
594
+ addExtension(extension) {
595
+ if (Array.isArray(extension)) {
596
+ extension.forEach((ext) => this.addExtension(ext));
597
+ return this;
598
+ }
599
+ this._dispatcher.state[extension.id] = [];
600
+ this._extensions[extension.id] = extension;
315
601
  return this;
316
602
  }
317
603
  /**
318
604
  * Remove a node builder from the graph builder.
319
605
  */
320
606
  removeExtension(id) {
321
- this._extensions.delete(id);
607
+ delete this._extensions[id];
322
608
  return this;
323
609
  }
610
+ destroy() {
611
+ this._dispatcher.cleanup.forEach((fn) => fn());
612
+ this._resolverSubscriptions.forEach((unsubscribe) => unsubscribe());
613
+ this._connectorSubscriptions.forEach((unsubscribe) => unsubscribe());
614
+ this._resolverSubscriptions.clear();
615
+ this._connectorSubscriptions.clear();
616
+ }
324
617
  /**
325
- * Construct the graph, starting by calling all registered extensions.
326
- * @param previousGraph If provided, the graph will be updated in place.
618
+ * Traverse a graph using just the connector extensions, without subscribing to any signals or persisting any nodes.
327
619
  */
328
- build(previousGraph) {
329
- this._unsubscribe.clear();
330
- const graph = previousGraph ?? new Graph();
331
- Array.from(this._extensions.values()).forEach((builder) => {
332
- const unsubscribe = builder(graph);
333
- unsubscribe && this._unsubscribe.add(unsubscribe);
334
- });
335
- return graph;
620
+ // TODO(wittjosiah): Rename? This is not traversing the graph proper.
621
+ async traverse({ node, relation = "outbound", visitor }, path = []) {
622
+ if (path.includes(node.id)) {
623
+ return;
624
+ }
625
+ visitor(node, [
626
+ ...path,
627
+ node.id
628
+ ]);
629
+ const nodes = Object.values(this._extensions).filter((extension) => relation === (extension.relation ?? "outbound")).flatMap((extension) => extension.connector?.({
630
+ node
631
+ }) ?? []).map((arg) => ({
632
+ id: arg.id,
633
+ type: arg.type,
634
+ data: arg.data ?? null,
635
+ properties: arg.properties ?? {}
636
+ }));
637
+ await Promise.all(nodes.map((n) => this.traverse({
638
+ node: n,
639
+ relation,
640
+ visitor
641
+ }, [
642
+ ...path,
643
+ node.id
644
+ ])));
336
645
  }
337
- };
338
-
339
- // packages/sdk/app-graph/src/helpers.ts
340
- var manageNodes = ({ graph, condition, nodes, removeEdges }) => {
341
- if (condition) {
342
- return graph.addNodes(...nodes);
343
- } else {
344
- nodes.forEach(({ id }) => graph.removeNode(id, removeEdges));
646
+ _onInitialNode(nodeId, nodeType) {
647
+ this._nodeChanged[nodeId] = this._nodeChanged[nodeId] ?? signal({});
648
+ let initialized;
649
+ for (const { id, type, resolver } of Object.values(this._extensions)) {
650
+ if (!resolver || nodeType && type !== nodeType) {
651
+ continue;
652
+ }
653
+ const unsubscribe = effect2(() => {
654
+ this._dispatcher.currentExtension = id;
655
+ this._dispatcher.stateIndex = 0;
656
+ BuilderInternal.currentDispatcher = this._dispatcher;
657
+ const node = resolver({
658
+ id: nodeId
659
+ });
660
+ BuilderInternal.currentDispatcher = void 0;
661
+ if (node && initialized) {
662
+ this.graph._addNodes([
663
+ node
664
+ ]);
665
+ if (this._nodeChanged[initialized.id]) {
666
+ this._nodeChanged[initialized.id].value = {};
667
+ }
668
+ } else if (node) {
669
+ initialized = node;
670
+ }
671
+ });
672
+ if (initialized) {
673
+ this._resolverSubscriptions.set(nodeId, unsubscribe);
674
+ break;
675
+ } else {
676
+ unsubscribe();
677
+ }
678
+ }
679
+ return initialized;
680
+ }
681
+ _onInitialNodes(node, nodesRelation, nodesType) {
682
+ this._nodeChanged[node.id] = this._nodeChanged[node.id] ?? signal({});
683
+ let initialized;
684
+ let previous = [];
685
+ this._connectorSubscriptions.set(node.id, effect2(() => {
686
+ Object.keys(this._extensions);
687
+ this._nodeChanged[node.id].value;
688
+ const nodes = [];
689
+ for (const { id, connector, filter, type, relation = "outbound" } of Object.values(this._extensions)) {
690
+ if (!connector || relation !== nodesRelation || nodesType && type !== nodesType || filter && !filter(node)) {
691
+ continue;
692
+ }
693
+ this._dispatcher.currentExtension = id;
694
+ this._dispatcher.stateIndex = 0;
695
+ BuilderInternal.currentDispatcher = this._dispatcher;
696
+ nodes.push(...connector({
697
+ node
698
+ }) ?? []);
699
+ BuilderInternal.currentDispatcher = void 0;
700
+ }
701
+ const ids = nodes.map((n) => n.id);
702
+ const removed = previous.filter((id) => !ids.includes(id));
703
+ previous = ids;
704
+ if (initialized) {
705
+ this.graph._removeNodes(removed, true);
706
+ this.graph._addNodes(nodes);
707
+ this.graph._addEdges(nodes.map(({ id }) => ({
708
+ source: node.id,
709
+ target: id
710
+ })));
711
+ this.graph._sortEdges(node.id, "outbound", nodes.map(({ id }) => id));
712
+ nodes.forEach((n) => {
713
+ if (this._nodeChanged[n.id]) {
714
+ this._nodeChanged[n.id].value = {};
715
+ }
716
+ });
717
+ } else {
718
+ initialized = nodes;
719
+ }
720
+ }));
721
+ return initialized;
722
+ }
723
+ _onRemoveNode(nodeId) {
724
+ this._resolverSubscriptions.get(nodeId)?.();
725
+ this._connectorSubscriptions.get(nodeId)?.();
726
+ this._resolverSubscriptions.delete(nodeId);
727
+ this._connectorSubscriptions.delete(nodeId);
345
728
  }
346
729
  };
347
730
  export {
731
+ ACTION_GROUP_TYPE,
732
+ ACTION_TYPE,
348
733
  Graph,
349
734
  GraphBuilder,
350
735
  ROOT_ID,
736
+ ROOT_TYPE,
351
737
  actionGroupSymbol,
738
+ cleanup,
739
+ createExtension,
740
+ getGraph,
352
741
  isAction,
353
742
  isActionGroup,
354
743
  isActionLike,
355
744
  isGraphNode,
356
- manageNodes
745
+ memoize,
746
+ toSignal
357
747
  };
358
748
  //# sourceMappingURL=index.mjs.map