@dxos/app-graph 0.6.3-main.0ca5117 → 0.6.3-main.28bccc4

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