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