@dxos/app-graph 0.6.3-main.d007b87 → 0.6.3-main.d76104f

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