@dxos/app-graph 0.6.3-main.9e4e207 → 0.6.3-next.2f65b78

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