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