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