@dxos/app-graph 0.8.4-main.69d29f4 → 0.8.4-main.6fa680abb7

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.
@@ -42,6 +42,8 @@ __export(graph_exports, {
42
42
  getRoot: () => getRoot,
43
43
  initialize: () => initialize,
44
44
  make: () => make,
45
+ relationFromKey: () => relationFromKey,
46
+ relationKey: () => relationKey,
45
47
  removeEdge: () => removeEdge,
46
48
  removeEdges: () => removeEdges,
47
49
  removeNode: () => removeNode,
@@ -58,7 +60,7 @@ import * as Pipeable from "effect/Pipeable";
58
60
  import * as Record from "effect/Record";
59
61
  import { Event, Trigger } from "@dxos/async";
60
62
  import { todo } from "@dxos/debug";
61
- import { invariant } from "@dxos/invariant";
63
+ import { invariant as invariant2 } from "@dxos/invariant";
62
64
  import { log } from "@dxos/log";
63
65
  import { isNonNullable } from "@dxos/util";
64
66
 
@@ -70,29 +72,79 @@ __export(node_exports, {
70
72
  RootId: () => RootId,
71
73
  RootType: () => RootType,
72
74
  actionGroupSymbol: () => actionGroupSymbol,
75
+ actionRelation: () => actionRelation,
76
+ childRelation: () => childRelation,
73
77
  isAction: () => isAction,
74
78
  isActionGroup: () => isActionGroup,
75
79
  isActionLike: () => isActionLike,
76
- isGraphNode: () => isGraphNode
80
+ isGraphNode: () => isGraphNode,
81
+ relation: () => relation
77
82
  });
78
83
  var RootId = "root";
79
- var RootType = "dxos.org/type/GraphRoot";
80
- var ActionType = "dxos.org/type/GraphAction";
81
- var ActionGroupType = "dxos.org/type/GraphActionGroup";
84
+ var RootType = "org.dxos.type.graph-root";
85
+ var ActionType = "org.dxos.type.graph-action";
86
+ var ActionGroupType = "org.dxos.type.graph-action-group";
87
+ var relation = (kind, direction = "outbound") => ({
88
+ kind,
89
+ direction
90
+ });
91
+ var childRelation = (direction = "outbound") => relation("child", direction);
92
+ var actionRelation = (direction = "outbound") => relation("action", direction);
82
93
  var isGraphNode = (data) => data && typeof data === "object" && "id" in data && "properties" in data && data.properties ? typeof data.properties === "object" && "data" in data : false;
83
94
  var isAction = (data) => isGraphNode(data) ? typeof data.data === "function" && data.type === ActionType : false;
84
- var actionGroupSymbol = Symbol("ActionGroup");
95
+ var actionGroupSymbol = /* @__PURE__ */ Symbol("ActionGroup");
85
96
  var isActionGroup = (data) => isGraphNode(data) ? data.data === actionGroupSymbol && data.type === ActionGroupType : false;
86
97
  var isActionLike = (data) => isAction(data) || isActionGroup(data);
87
98
 
99
+ // src/util.ts
100
+ import { invariant } from "@dxos/invariant";
101
+ var __dxlog_file = "/__w/dxos/dxos/packages/sdk/app-graph/src/util.ts";
102
+ var Separators = {
103
+ primary: "",
104
+ secondary: "",
105
+ path: "/"
106
+ };
107
+ var normalizeRelation = (relation2) => relation2 == null ? childRelation() : typeof relation2 === "string" ? relation(relation2) : relation2;
108
+ var shallowEqual = (a, b) => {
109
+ if (a === b) return true;
110
+ if (a == null || b == null || typeof a !== "object" || typeof b !== "object") return false;
111
+ const keysA = Object.keys(a);
112
+ const keysB = Object.keys(b);
113
+ if (keysA.length !== keysB.length) {
114
+ return false;
115
+ }
116
+ return keysA.every((k) => a[k] === b[k]);
117
+ };
118
+ var nodeArgsUnchanged = (prev, next) => {
119
+ if (prev.length !== next.length) {
120
+ return false;
121
+ }
122
+ return prev.every((prevNode, idx) => {
123
+ const nextNode = next[idx];
124
+ return prevNode.id === nextNode.id && prevNode.type === nextNode.type && shallowEqual(prevNode.data, nextNode.data) && shallowEqual(prevNode.properties, nextNode.properties);
125
+ });
126
+ };
127
+ var qualifyId = (parentId, segmentId) => `${parentId}${Separators.path}${segmentId}`;
128
+ var validateSegmentId = (id) => {
129
+ invariant(!id.includes(Separators.path), `Node segment ID must not contain '${Separators.path}': ${id}`, {
130
+ F: __dxlog_file,
131
+ L: 70,
132
+ S: void 0,
133
+ A: [
134
+ "!id.includes(Separators.path)",
135
+ "`Node segment ID must not contain '${Separators.path}': ${id}`"
136
+ ]
137
+ });
138
+ };
139
+
88
140
  // src/graph.ts
89
- var __dxlog_file = "/__w/dxos/dxos/packages/sdk/app-graph/src/graph.ts";
90
- var graphSymbol = Symbol("graph");
141
+ var __dxlog_file2 = "/__w/dxos/dxos/packages/sdk/app-graph/src/graph.ts";
142
+ var graphSymbol = /* @__PURE__ */ Symbol("graph");
91
143
  var getGraph = (node) => {
92
144
  const graph = node[graphSymbol];
93
- invariant(graph, "Node is not associated with a graph.", {
94
- F: __dxlog_file,
95
- L: 32,
145
+ invariant2(graph, "Node is not associated with a graph.", {
146
+ F: __dxlog_file2,
147
+ L: 33,
96
148
  S: void 0,
97
149
  A: [
98
150
  "graph",
@@ -101,8 +153,8 @@ var getGraph = (node) => {
101
153
  });
102
154
  return graph;
103
155
  };
104
- var GraphTypeId = Symbol.for("@dxos/app-graph/Graph");
105
- var GraphKind = Symbol.for("@dxos/app-graph/GraphKind");
156
+ var GraphTypeId = /* @__PURE__ */ Symbol.for("@dxos/app-graph/Graph");
157
+ var GraphKind = /* @__PURE__ */ Symbol.for("@dxos/app-graph/GraphKind");
106
158
  var GraphImpl = class {
107
159
  [GraphTypeId] = GraphTypeId;
108
160
  [GraphKind] = "writable";
@@ -115,6 +167,7 @@ var GraphImpl = class {
115
167
  _onRemoveNode;
116
168
  _registry;
117
169
  _expanded = Record.empty();
170
+ _pendingExpands = /* @__PURE__ */ new Set();
118
171
  _initialized = Record.empty();
119
172
  _initialEdges = Record.empty();
120
173
  _initialNodes = Record.fromEntries([
@@ -136,9 +189,9 @@ var GraphImpl = class {
136
189
  _nodeOrThrow = Atom2.family((id) => {
137
190
  return Atom2.make((get2) => {
138
191
  const node = get2(this._node(id));
139
- invariant(Option.isSome(node), `Node not available: ${id}`, {
140
- F: __dxlog_file,
141
- L: 174,
192
+ invariant2(Option.isSome(node), `Node not available: ${id}`, {
193
+ F: __dxlog_file2,
194
+ L: 172,
142
195
  S: this,
143
196
  A: [
144
197
  "Option.isSome(node)",
@@ -149,30 +202,33 @@ var GraphImpl = class {
149
202
  });
150
203
  });
151
204
  _edges = Atom2.family((id) => {
152
- const initial = Record.get(this._initialEdges, id).pipe(Option.getOrElse(() => ({
153
- inbound: [],
154
- outbound: []
155
- })));
205
+ const initial = Record.get(this._initialEdges, id).pipe(Option.getOrElse(() => ({})));
156
206
  return Atom2.make(initial).pipe(Atom2.keepAlive, Atom2.withLabel(`graph:edges:${id}`));
157
207
  });
158
208
  // NOTE: Currently the argument to the family needs to be referentially stable for the atom to be referentially stable.
159
209
  // TODO(wittjosiah): Atom feature request, support for something akin to `ComplexMap` to allow for complex arguments.
160
210
  _connections = Atom2.family((key) => {
161
211
  return Atom2.make((get2) => {
162
- const [id, relation] = key.split("$");
212
+ if (!key || key.indexOf(Separators.primary) <= 0) {
213
+ return [];
214
+ }
215
+ const { id, relation: relation2 } = relationFromConnectionKey(key);
163
216
  const edges = get2(this._edges(id));
164
- return edges[relation].map((id2) => get2(this._node(id2))).filter(Option.isSome).map((o) => o.value);
217
+ return (edges[relationKey(relation2)] ?? []).map((id2) => get2(this._node(id2))).filter(Option.isSome).map((o) => o.value);
165
218
  }).pipe(Atom2.withLabel(`graph:connections:${key}`));
166
219
  });
167
220
  _actions = Atom2.family((id) => {
168
221
  return Atom2.make((get2) => {
169
- return get2(this._connections(`${id}$outbound`)).filter((node) => node.type === ActionType || node.type === ActionGroupType);
222
+ if (!id) {
223
+ return [];
224
+ }
225
+ return get2(this._connections(connectionKey(id, actionRelation())));
170
226
  }).pipe(Atom2.withLabel(`graph:actions:${id}`));
171
227
  });
172
228
  _json = Atom2.family((id) => {
173
229
  return Atom2.make((get2) => {
174
230
  const toJSON2 = (node, seen = []) => {
175
- const nodes = get2(this._connections(`${node.id}$outbound`));
231
+ const nodes = get2(this._connections(connectionKey(node.id, "child")));
176
232
  const obj = {
177
233
  id: node.id,
178
234
  type: node.type
@@ -220,8 +276,8 @@ var GraphImpl = class {
220
276
  nodeOrThrow(id) {
221
277
  return nodeOrThrowImpl(this, id);
222
278
  }
223
- connections(id, relation = "outbound") {
224
- return connectionsImpl(this, id, relation);
279
+ connections(id, relation2) {
280
+ return connectionsImpl(this, id, relation2);
225
281
  }
226
282
  actions(id) {
227
283
  return actionsImpl(this, id);
@@ -258,9 +314,9 @@ var nodeOrThrowImpl = (graph, id) => {
258
314
  const internal = getInternal(graph);
259
315
  return internal._nodeOrThrow(id);
260
316
  };
261
- var connectionsImpl = (graph, id, relation = "outbound") => {
317
+ var connectionsImpl = (graph, id, relation2) => {
262
318
  const internal = getInternal(graph);
263
- return internal._connections(`${id}$${relation}`);
319
+ return internal._connections(connectionKey(id, relation2));
264
320
  };
265
321
  var actionsImpl = (graph, id) => {
266
322
  const internal = getInternal(graph);
@@ -299,19 +355,28 @@ function getNodeOrThrow(graphOrId, id) {
299
355
  function getRoot(graph) {
300
356
  return getNodeOrThrowImpl(graph, RootId);
301
357
  }
302
- var getConnectionsImpl = (graph, id, relation = "outbound") => {
358
+ var getConnectionsImpl = (graph, id, relation2) => {
303
359
  const internal = getInternal(graph);
304
- return internal._registry.get(connectionsImpl(graph, id, relation));
360
+ return internal._registry.get(connectionsImpl(graph, id, relation2));
305
361
  };
306
- function getConnections(graphOrId, idOrRelation, relation) {
362
+ function getConnections(graphOrId, idOrRelation, relation2) {
307
363
  if (typeof graphOrId === "string") {
308
364
  const id = graphOrId;
309
- const rel = (typeof idOrRelation === "string" ? "outbound" : idOrRelation) ?? "outbound";
365
+ const rel = idOrRelation;
310
366
  return (graph) => getConnectionsImpl(graph, id, rel);
311
367
  } else {
312
368
  const graph = graphOrId;
313
369
  const id = idOrRelation;
314
- const rel = relation ?? "outbound";
370
+ invariant2(relation2 !== void 0, "Relation is required.", {
371
+ F: __dxlog_file2,
372
+ L: 446,
373
+ S: this,
374
+ A: [
375
+ "relation !== undefined",
376
+ "'Relation is required.'"
377
+ ]
378
+ });
379
+ const rel = relation2;
315
380
  return getConnectionsImpl(graph, id, rel);
316
381
  }
317
382
  }
@@ -342,7 +407,7 @@ function getEdges(graphOrId, id) {
342
407
  }
343
408
  }
344
409
  var traverseImpl = (graph, options, path = []) => {
345
- const { visitor, source = RootId, relation = "outbound" } = options;
410
+ const { visitor, source = RootId, relation: relation2 } = options;
346
411
  if (path.includes(source)) {
347
412
  return;
348
413
  }
@@ -354,14 +419,25 @@ var traverseImpl = (graph, options, path = []) => {
354
419
  if (shouldContinue === false) {
355
420
  return;
356
421
  }
357
- Object.values(getConnections(graph, source, relation)).forEach((child) => traverseImpl(graph, {
358
- source: child.id,
359
- relation,
360
- visitor
361
- }, [
362
- ...path,
363
- source
364
- ]));
422
+ const relations = Array.isArray(relation2) ? relation2 : [
423
+ relation2
424
+ ];
425
+ const seen = /* @__PURE__ */ new Set();
426
+ for (const rel of relations) {
427
+ for (const connected of getConnections(graph, source, rel)) {
428
+ if (!seen.has(connected.id)) {
429
+ seen.add(connected.id);
430
+ traverseImpl(graph, {
431
+ source: connected.id,
432
+ relation: relation2,
433
+ visitor
434
+ }, [
435
+ ...path,
436
+ source
437
+ ]);
438
+ }
439
+ }
440
+ }
365
441
  };
366
442
  function traverse(graphOrOptions, optionsOrPath, path) {
367
443
  if (typeof graphOrOptions === "object" && "visitor" in graphOrOptions) {
@@ -380,6 +456,7 @@ var getPathImpl = (graph, params) => {
380
456
  let found = Option.none();
381
457
  traverseImpl(graph, {
382
458
  source: node.id,
459
+ relation: "child",
383
460
  visitor: (node2, path) => {
384
461
  if (Option.isSome(found)) {
385
462
  return false;
@@ -436,14 +513,14 @@ var initializeImpl = async (graph, id) => {
436
513
  id,
437
514
  initialized
438
515
  }, {
439
- F: __dxlog_file,
440
- L: 661,
516
+ F: __dxlog_file2,
517
+ L: 668,
441
518
  S: void 0,
442
519
  C: (f, a) => f(...a)
443
520
  });
444
521
  if (!initialized) {
445
- await internal._onInitialize?.(id);
446
522
  Record.set(internal._initialized, id, true);
523
+ await internal._onInitialize?.(id);
447
524
  }
448
525
  return graph;
449
526
  };
@@ -456,61 +533,93 @@ function initialize(graphOrId, id) {
456
533
  return initializeImpl(graph, id);
457
534
  }
458
535
  }
459
- var expandImpl = (graph, id, relation = "outbound") => {
536
+ var expandImpl = (graph, id, relation2) => {
460
537
  const internal = getInternal(graph);
461
- const key = `${id}$${relation}`;
538
+ const normalizedRelation = normalizeRelation(relation2);
539
+ const key = `${id}${Separators.primary}${relationKey(normalizedRelation)}`;
540
+ const nodeOpt = internal._registry.get(internal._node(id));
541
+ if (Option.isNone(nodeOpt)) {
542
+ internal._pendingExpands.add(key);
543
+ log("expand", {
544
+ key,
545
+ deferred: true
546
+ }, {
547
+ F: __dxlog_file2,
548
+ L: 714,
549
+ S: void 0,
550
+ C: (f, a) => f(...a)
551
+ });
552
+ return graph;
553
+ }
462
554
  const expanded = Record.get(internal._expanded, key).pipe(Option.getOrElse(() => false));
463
555
  log("expand", {
464
556
  key,
465
557
  expanded
466
558
  }, {
467
- F: __dxlog_file,
468
- L: 702,
559
+ F: __dxlog_file2,
560
+ L: 719,
469
561
  S: void 0,
470
562
  C: (f, a) => f(...a)
471
563
  });
472
564
  if (!expanded) {
473
- internal._onExpand?.(id, relation);
474
565
  Record.set(internal._expanded, key, true);
566
+ internal._onExpand?.(id, normalizedRelation);
475
567
  }
476
568
  return graph;
477
569
  };
478
- function expand(graphOrId, idOrRelation, relation) {
570
+ function expand(graphOrId, idOrRelation, relation2) {
479
571
  if (typeof graphOrId === "string") {
480
572
  const id = graphOrId;
481
- const rel = (typeof idOrRelation === "string" ? "outbound" : idOrRelation) ?? "outbound";
573
+ const rel = idOrRelation;
482
574
  return (graph) => expandImpl(graph, id, rel);
483
575
  } else {
484
576
  const graph = graphOrId;
485
577
  const id = idOrRelation;
486
- const rel = relation ?? "outbound";
578
+ invariant2(relation2 !== void 0, "Relation is required.", {
579
+ F: __dxlog_file2,
580
+ L: 755,
581
+ S: this,
582
+ A: [
583
+ "relation !== undefined",
584
+ "'Relation is required.'"
585
+ ]
586
+ });
587
+ const rel = relation2;
487
588
  return expandImpl(graph, id, rel);
488
589
  }
489
590
  }
490
- var sortEdgesImpl = (graph, id, relation, order) => {
591
+ var sortEdgesImpl = (graph, id, relation2, order) => {
491
592
  const internal = getInternal(graph);
492
593
  const edgesAtom = internal._edges(id);
493
594
  const edges = internal._registry.get(edgesAtom);
494
- const unsorted = edges[relation].filter((id2) => !order.includes(id2)) ?? [];
495
- const sorted = order.filter((id2) => edges[relation].includes(id2)) ?? [];
496
- edges[relation].splice(0, edges[relation].length, ...[
595
+ const relationId = relationKey(relation2);
596
+ const current = edges[relationId] ?? [];
597
+ const unsorted = current.filter((id2) => !order.includes(id2));
598
+ const sorted = order.filter((id2) => current.includes(id2));
599
+ const newOrder = [
497
600
  ...sorted,
498
601
  ...unsorted
499
- ]);
500
- internal._registry.set(edgesAtom, edges);
602
+ ];
603
+ if (newOrder.length === current.length && newOrder.every((id2, i) => id2 === current[i])) {
604
+ return graph;
605
+ }
606
+ internal._registry.set(edgesAtom, {
607
+ ...edges,
608
+ [relationId]: newOrder
609
+ });
501
610
  return graph;
502
611
  };
503
612
  function sortEdges(graphOrId, idOrRelation, relationOrOrder, order) {
504
613
  if (typeof graphOrId === "string") {
505
614
  const id = graphOrId;
506
- const relation = idOrRelation;
615
+ const relation2 = idOrRelation;
507
616
  const order2 = relationOrOrder;
508
- return (graph) => sortEdgesImpl(graph, id, relation, order2);
617
+ return (graph) => sortEdgesImpl(graph, id, relation2, order2);
509
618
  } else {
510
619
  const graph = graphOrId;
511
620
  const id = idOrRelation;
512
- const relation = relationOrOrder;
513
- return sortEdgesImpl(graph, id, relation, order);
621
+ const relation2 = relationOrOrder;
622
+ return sortEdgesImpl(graph, id, relation2, order);
514
623
  }
515
624
  }
516
625
  var addNodesImpl = (graph, nodes) => {
@@ -536,7 +645,7 @@ var addNodeImpl = (graph, nodeArg) => {
536
645
  Option.match(existingNode, {
537
646
  onSome: (existing) => {
538
647
  const typeChanged = existing.type !== type;
539
- const dataChanged = existing.data !== data;
648
+ const dataChanged = !shallowEqual(existing.data, data);
540
649
  const propertiesChanged = Object.keys(properties).some((key) => existing.properties[key] !== properties[key]);
541
650
  log("existing node", {
542
651
  id,
@@ -544,8 +653,8 @@ var addNodeImpl = (graph, nodeArg) => {
544
653
  dataChanged,
545
654
  propertiesChanged
546
655
  }, {
547
- F: __dxlog_file,
548
- L: 847,
656
+ F: __dxlog_file2,
657
+ L: 877,
549
658
  S: void 0,
550
659
  C: (f, a) => f(...a)
551
660
  });
@@ -556,8 +665,8 @@ var addNodeImpl = (graph, nodeArg) => {
556
665
  data,
557
666
  properties
558
667
  }, {
559
- F: __dxlog_file,
560
- L: 854,
668
+ F: __dxlog_file2,
669
+ L: 884,
561
670
  S: void 0,
562
671
  C: (f, a) => f(...a)
563
672
  });
@@ -585,8 +694,8 @@ var addNodeImpl = (graph, nodeArg) => {
585
694
  data,
586
695
  properties
587
696
  }, {
588
- F: __dxlog_file,
589
- L: 867,
697
+ F: __dxlog_file2,
698
+ L: 897,
590
699
  S: void 0,
591
700
  C: (f, a) => f(...a)
592
701
  });
@@ -602,13 +711,24 @@ var addNodeImpl = (graph, nodeArg) => {
602
711
  id,
603
712
  node: newNode
604
713
  });
714
+ const prefix = `${id}${Separators.primary}`;
715
+ const toApply = [
716
+ ...internal._pendingExpands
717
+ ].filter((k) => k.startsWith(prefix));
718
+ for (const pendingKey of toApply) {
719
+ internal._pendingExpands.delete(pendingKey);
720
+ const relation2 = relationFromKey(pendingKey.slice(prefix.length));
721
+ Record.set(internal._expanded, pendingKey, true);
722
+ internal._onExpand?.(id, relation2);
723
+ }
605
724
  }
606
725
  });
607
726
  if (nodes) {
608
727
  addNodesImpl(graph, nodes);
609
728
  const _edges = nodes.map((node) => ({
610
729
  source: id,
611
- target: node.id
730
+ target: node.id,
731
+ relation: "child"
612
732
  }));
613
733
  addEdgesImpl(graph, _edges);
614
734
  }
@@ -653,17 +773,27 @@ var removeNodeImpl = (graph, id, edges = false) => {
653
773
  node: Option.none()
654
774
  });
655
775
  if (edges) {
656
- const { inbound, outbound } = internal._registry.get(internal._edges(id));
657
- const edgesToRemove = [
658
- ...inbound.map((source) => ({
659
- source,
660
- target: id
661
- })),
662
- ...outbound.map((target) => ({
663
- source: id,
664
- target
665
- }))
666
- ];
776
+ const nodeEdges = internal._registry.get(internal._edges(id));
777
+ const edgesToRemove = [];
778
+ for (const [relationKeyValue, relatedIds] of Object.entries(nodeEdges)) {
779
+ const relation2 = relationFromKey(relationKeyValue);
780
+ const isInboundRelation = relation2.direction === "inbound";
781
+ for (const relatedId of relatedIds) {
782
+ if (isInboundRelation) {
783
+ edgesToRemove.push({
784
+ source: relatedId,
785
+ target: id,
786
+ relation: inverseRelation(relation2)
787
+ });
788
+ } else {
789
+ edgesToRemove.push({
790
+ source: id,
791
+ target: relatedId,
792
+ relation: relation2
793
+ });
794
+ }
795
+ }
796
+ }
667
797
  removeEdgesImpl(graph, edgesToRemove);
668
798
  }
669
799
  internal._onRemoveNode?.(id);
@@ -697,45 +827,53 @@ function addEdges(graphOrEdges, edges) {
697
827
  }
698
828
  }
699
829
  var addEdgeImpl = (graph, edgeArg) => {
830
+ const relation2 = normalizeRelation(edgeArg.relation);
831
+ const relationId = relationKey(relation2);
832
+ const inverse = inverseRelation(relation2);
833
+ const inverseId = relationKey(inverse);
700
834
  const internal = getInternal(graph);
701
835
  const sourceAtom = internal._edges(edgeArg.source);
702
836
  const source = internal._registry.get(sourceAtom);
703
- if (!source.outbound.includes(edgeArg.target)) {
704
- log("add outbound edge", {
837
+ const sourceList = source[relationId] ?? [];
838
+ if (!sourceList.includes(edgeArg.target)) {
839
+ log("add edge", {
705
840
  source: edgeArg.source,
706
- target: edgeArg.target
841
+ target: edgeArg.target,
842
+ relation: relationId
707
843
  }, {
708
- F: __dxlog_file,
709
- L: 1026,
844
+ F: __dxlog_file2,
845
+ L: 1081,
710
846
  S: void 0,
711
847
  C: (f, a) => f(...a)
712
848
  });
713
849
  internal._registry.set(sourceAtom, {
714
- inbound: source.inbound,
715
- outbound: [
716
- ...source.outbound,
850
+ ...source,
851
+ [relationId]: [
852
+ ...sourceList,
717
853
  edgeArg.target
718
854
  ]
719
855
  });
720
856
  }
721
857
  const targetAtom = internal._edges(edgeArg.target);
722
858
  const target = internal._registry.get(targetAtom);
723
- if (!target.inbound.includes(edgeArg.source)) {
724
- log("add inbound edge", {
859
+ const targetList = target[inverseId] ?? [];
860
+ if (!targetList.includes(edgeArg.source)) {
861
+ log("add inverse edge", {
725
862
  source: edgeArg.source,
726
- target: edgeArg.target
863
+ target: edgeArg.target,
864
+ relation: inverseId
727
865
  }, {
728
- F: __dxlog_file,
729
- L: 1039,
866
+ F: __dxlog_file2,
867
+ L: 1089,
730
868
  S: void 0,
731
869
  C: (f, a) => f(...a)
732
870
  });
733
871
  internal._registry.set(targetAtom, {
734
- inbound: [
735
- ...target.inbound,
872
+ ...target,
873
+ [inverseId]: [
874
+ ...targetList,
736
875
  edgeArg.source
737
- ],
738
- outbound: target.outbound
876
+ ]
739
877
  });
740
878
  }
741
879
  return graph;
@@ -768,32 +906,39 @@ function removeEdges(graphOrEdges, edgesOrRemoveOrphans, removeOrphans) {
768
906
  }
769
907
  }
770
908
  var removeEdgeImpl = (graph, edgeArg, removeOrphans = false) => {
909
+ const relation2 = normalizeRelation(edgeArg.relation);
910
+ const relationId = relationKey(relation2);
911
+ const inverse = inverseRelation(relation2);
912
+ const inverseId = relationKey(inverse);
771
913
  const internal = getInternal(graph);
772
914
  const sourceAtom = internal._edges(edgeArg.source);
773
915
  const source = internal._registry.get(sourceAtom);
774
- if (source.outbound.includes(edgeArg.target)) {
916
+ const sourceList = source[relationId] ?? [];
917
+ if (sourceList.includes(edgeArg.target)) {
775
918
  internal._registry.set(sourceAtom, {
776
- inbound: source.inbound,
777
- outbound: source.outbound.filter((id) => id !== edgeArg.target)
919
+ ...source,
920
+ [relationId]: sourceList.filter((id) => id !== edgeArg.target)
778
921
  });
779
922
  }
780
923
  const targetAtom = internal._edges(edgeArg.target);
781
924
  const target = internal._registry.get(targetAtom);
782
- if (target.inbound.includes(edgeArg.source)) {
925
+ const targetList = target[inverseId] ?? [];
926
+ if (targetList.includes(edgeArg.source)) {
783
927
  internal._registry.set(targetAtom, {
784
- inbound: target.inbound.filter((id) => id !== edgeArg.source),
785
- outbound: target.outbound
928
+ ...target,
929
+ [inverseId]: targetList.filter((id) => id !== edgeArg.source)
786
930
  });
787
931
  }
788
932
  if (removeOrphans) {
789
- const source2 = internal._registry.get(sourceAtom);
790
- const target2 = internal._registry.get(targetAtom);
791
- if (source2.outbound.length === 0 && source2.inbound.length === 0 && edgeArg.source !== RootId) {
933
+ const sourceAfter = internal._registry.get(sourceAtom);
934
+ const targetAfter = internal._registry.get(targetAtom);
935
+ const isEmpty = (edges) => Object.values(edges).every((ids) => ids.length === 0);
936
+ if (isEmpty(sourceAfter) && edgeArg.source !== RootId) {
792
937
  removeNodesImpl(graph, [
793
938
  edgeArg.source
794
939
  ]);
795
940
  }
796
- if (target2.outbound.length === 0 && target2.inbound.length === 0 && edgeArg.target !== RootId) {
941
+ if (isEmpty(targetAfter) && edgeArg.target !== RootId) {
797
942
  removeNodesImpl(graph, [
798
943
  edgeArg.target
799
944
  ]);
@@ -816,6 +961,57 @@ function removeEdge(graphOrEdgeArg, edgeArgOrRemoveOrphans, removeOrphans) {
816
961
  var make = (params) => {
817
962
  return new GraphImpl(params);
818
963
  };
964
+ var relationKey = (relation2) => {
965
+ const normalized = normalizeRelation(relation2);
966
+ return `${normalized.kind}${Separators.secondary}${normalized.direction}`;
967
+ };
968
+ var relationFromKey = (encoded) => {
969
+ const separatorIndex = encoded.lastIndexOf(Separators.secondary);
970
+ invariant2(separatorIndex > 0 && separatorIndex < encoded.length - 1, `Invalid relation key: ${encoded}`, {
971
+ F: __dxlog_file2,
972
+ L: 1234,
973
+ S: void 0,
974
+ A: [
975
+ "separatorIndex > 0 && separatorIndex < encoded.length - 1",
976
+ "`Invalid relation key: ${encoded}`"
977
+ ]
978
+ });
979
+ const kind = encoded.slice(0, separatorIndex);
980
+ const directionRaw = encoded.slice(separatorIndex + 1);
981
+ invariant2(directionRaw === "outbound" || directionRaw === "inbound", `Invalid relation direction: ${directionRaw}`, {
982
+ F: __dxlog_file2,
983
+ L: 1237,
984
+ S: void 0,
985
+ A: [
986
+ "directionRaw === 'outbound' || directionRaw === 'inbound'",
987
+ "`Invalid relation direction: ${directionRaw}`"
988
+ ]
989
+ });
990
+ return relation(kind, directionRaw);
991
+ };
992
+ var connectionKey = (id, relation2) => `${id}${Separators.primary}${relationKey(relation2)}`;
993
+ var relationFromConnectionKey = (key) => {
994
+ const separatorIndex = key.indexOf(Separators.primary);
995
+ invariant2(separatorIndex > 0 && separatorIndex < key.length - 1, `Invalid connection key: ${key}`, {
996
+ F: __dxlog_file2,
997
+ L: 1246,
998
+ S: void 0,
999
+ A: [
1000
+ "separatorIndex > 0 && separatorIndex < key.length - 1",
1001
+ "`Invalid connection key: ${key}`"
1002
+ ]
1003
+ });
1004
+ const id = key.slice(0, separatorIndex);
1005
+ const encodedRelation = key.slice(separatorIndex + 1);
1006
+ return {
1007
+ id,
1008
+ relation: relationFromKey(encodedRelation)
1009
+ };
1010
+ };
1011
+ var inverseRelation = (relation2) => {
1012
+ const normalized = normalizeRelation(relation2);
1013
+ return relation(normalized.kind, normalized.direction === "outbound" ? "inbound" : "outbound");
1014
+ };
819
1015
 
820
1016
  // src/graph-builder.ts
821
1017
  var graph_builder_exports = {};
@@ -829,6 +1025,7 @@ __export(graph_builder_exports, {
829
1025
  destroy: () => destroy,
830
1026
  explore: () => explore,
831
1027
  flattenExtensions: () => flattenExtensions,
1028
+ flush: () => flush,
832
1029
  from: () => from,
833
1030
  make: () => make2,
834
1031
  removeExtension: () => removeExtension
@@ -840,8 +1037,9 @@ import * as Function2 from "effect/Function";
840
1037
  import * as Option3 from "effect/Option";
841
1038
  import * as Pipeable2 from "effect/Pipeable";
842
1039
  import * as Record2 from "effect/Record";
1040
+ import { scheduleTask, yieldOrContinue } from "main-thread-scheduling";
843
1041
  import { log as log2 } from "@dxos/log";
844
- import { byPosition, getDebugName, isNode, isNonNullable as isNonNullable2 } from "@dxos/util";
1042
+ import { byPosition, getDebugName, isNonNullable as isNonNullable2 } from "@dxos/util";
845
1043
 
846
1044
  // src/node-matcher.ts
847
1045
  var node_matcher_exports = {};
@@ -865,18 +1063,16 @@ var whenNodeType = (type) => (node) => node.type === type ? Option2.some(node) :
865
1063
  var whenEchoType = (type) => (node) => Obj.instanceOf(type, node.data) ? Option2.some(node.data) : Option2.none();
866
1064
  var whenEchoObject = (node) => Obj.isObject(node.data) ? Option2.some(node.data) : Option2.none();
867
1065
  var whenAll = (...matchers) => (node) => {
868
- for (const matcher of matchers) {
869
- const result = matcher(node);
870
- if (Option2.isNone(result)) {
1066
+ for (const candidate of matchers) {
1067
+ if (Option2.isNone(candidate(node))) {
871
1068
  return Option2.none();
872
1069
  }
873
1070
  }
874
1071
  return Option2.some(node);
875
1072
  };
876
1073
  var whenAny = (...matchers) => (node) => {
877
- for (const matcher of matchers) {
878
- const result = matcher(node);
879
- if (Option2.isSome(result)) {
1074
+ for (const candidate of matchers) {
1075
+ if (Option2.isSome(candidate(node))) {
880
1076
  return Option2.some(node);
881
1077
  }
882
1078
  }
@@ -887,25 +1083,40 @@ var whenEchoObjectMatches = (node) => Obj.isObject(node.data) ? Option2.some(nod
887
1083
  var whenNot = (matcher) => (node) => Option2.isNone(matcher(node)) ? Option2.some(node) : Option2.none();
888
1084
 
889
1085
  // src/graph-builder.ts
890
- var __dxlog_file2 = "/__w/dxos/dxos/packages/sdk/app-graph/src/graph-builder.ts";
891
- var GraphBuilderTypeId = Symbol.for("@dxos/app-graph/GraphBuilder");
1086
+ var __dxlog_file3 = "/__w/dxos/dxos/packages/sdk/app-graph/src/graph-builder.ts";
1087
+ var GraphBuilderTypeId = /* @__PURE__ */ Symbol.for("@dxos/app-graph/GraphBuilder");
892
1088
  var GraphBuilderImpl = class {
893
1089
  [GraphBuilderTypeId] = GraphBuilderTypeId;
894
1090
  pipe() {
895
1091
  return Pipeable2.pipeArguments(this, arguments);
896
1092
  }
897
1093
  // TODO(wittjosiah): Use Context.
1094
+ /** Active subscriptions keyed by composite ID, cleaned up on node removal. */
898
1095
  _subscriptions = /* @__PURE__ */ new Map();
1096
+ /** Connector updates pending flush, keyed by connector key. */
1097
+ _dirtyConnectors = /* @__PURE__ */ new Map();
1098
+ /** Last-flushed node IDs per connector key, used for edge removal on update. */
1099
+ _connectorPrevious = /* @__PURE__ */ new Map();
1100
+ /** Last-flushed node args per connector key, used for change detection. */
1101
+ _connectorPreviousArgs = /* @__PURE__ */ new Map();
1102
+ /** Whether a dirty-flush task is already scheduled. */
1103
+ _flushScheduled = false;
1104
+ /** Resolves when the current flush completes. */
1105
+ _flushPromise = Promise.resolve();
1106
+ /** Registered builder extensions keyed by extension ID. */
899
1107
  _extensions = Atom3.make(Record2.empty()).pipe(Atom3.keepAlive, Atom3.withLabel("graph-builder:extensions"));
1108
+ /** Triggers signalling that a node's resolver has fired at least once. */
900
1109
  _initialized = {};
1110
+ /** Shared atom registry for reactive subscriptions. */
901
1111
  _registry;
1112
+ /** Backing graph with internal accessors for node atoms and construction. */
902
1113
  _graph;
903
1114
  constructor({ registry, ...params } = {}) {
904
1115
  this._registry = registry ?? Registry2.make();
905
1116
  const graph = make({
906
1117
  ...params,
907
1118
  registry: this._registry,
908
- onExpand: (id, relation) => this._onExpand(id, relation),
1119
+ onExpand: (id, relation2) => this._onExpand(id, relation2),
909
1120
  onInitialize: (id) => this._onInitialize(id),
910
1121
  onRemoveNode: (id) => this._onRemoveNode(id)
911
1122
  });
@@ -917,6 +1128,52 @@ var GraphBuilderImpl = class {
917
1128
  get extensions() {
918
1129
  return this._extensions;
919
1130
  }
1131
+ /** Apply a set of node changes for a single connector key. */
1132
+ _applyConnectorUpdate(key, nodes, previous) {
1133
+ const { id, relation: relation2 } = relationFromConnectorKey(key);
1134
+ const ids = nodes.map((node) => node.id);
1135
+ const removed = previous.filter((pid) => !ids.includes(pid));
1136
+ this._connectorPrevious.set(key, ids);
1137
+ this._connectorPreviousArgs.set(key, nodes);
1138
+ removeEdges(this._graph, removed.map((target) => ({
1139
+ source: id,
1140
+ target,
1141
+ relation: relation2
1142
+ })), true);
1143
+ addNodes(this._graph, nodes);
1144
+ addEdges(this._graph, nodes.map((node) => ({
1145
+ source: id,
1146
+ target: node.id,
1147
+ relation: relation2
1148
+ })));
1149
+ if (ids.length > 0) {
1150
+ const sortedIds = [
1151
+ ...nodes
1152
+ ].sort((a, b) => byPosition(a.properties ?? {}, b.properties ?? {})).map((n) => n.id);
1153
+ sortEdges(this._graph, id, relation2, sortedIds);
1154
+ }
1155
+ }
1156
+ _scheduleDirtyFlush() {
1157
+ if (!this._flushScheduled) {
1158
+ this._flushScheduled = true;
1159
+ this._flushPromise = scheduleTask(() => {
1160
+ this._flushScheduled = false;
1161
+ while (this._dirtyConnectors.size > 0) {
1162
+ const entries = [
1163
+ ...this._dirtyConnectors.entries()
1164
+ ];
1165
+ this._dirtyConnectors.clear();
1166
+ Atom3.batch(() => {
1167
+ for (const [key, { nodes, previous }] of entries) {
1168
+ this._applyConnectorUpdate(key, nodes, previous);
1169
+ }
1170
+ });
1171
+ }
1172
+ }, {
1173
+ strategy: "smooth"
1174
+ });
1175
+ }
1176
+ }
920
1177
  _resolvers = Atom3.family((id) => {
921
1178
  return Atom3.make((get2) => {
922
1179
  return Function2.pipe(get2(this._extensions), Record2.values, Array2.sortBy(byPosition), Array2.map(({ resolver }) => resolver), Array2.filter(isNonNullable2), Array2.map((resolver) => get2(resolver(id))), Array2.filter(isNonNullable2), Array2.head);
@@ -924,82 +1181,77 @@ var GraphBuilderImpl = class {
924
1181
  });
925
1182
  _connectors = Atom3.family((key) => {
926
1183
  return Atom3.make((get2) => {
927
- const [id, relation] = key.split("+");
1184
+ const { id, relation: relation2 } = relationFromConnectorKey(key);
928
1185
  const node = this._graph.node(id);
929
- return Function2.pipe(
930
- get2(this._extensions),
931
- Record2.values,
932
- // TODO(wittjosiah): Sort on write rather than read.
933
- Array2.sortBy(byPosition),
934
- Array2.filter(({ relation: _relation = "outbound" }) => _relation === relation),
935
- Array2.map(({ connector }) => connector?.(node)),
936
- Array2.filter(isNonNullable2),
937
- Array2.flatMap((result) => get2(result))
938
- );
1186
+ const sourceNode = Option3.getOrElse(get2(node), () => void 0);
1187
+ if (!sourceNode) {
1188
+ return [];
1189
+ }
1190
+ const extensions = Function2.pipe(get2(this._extensions), Record2.values, Array2.sortBy(byPosition), Array2.filter((ext) => relationKey(ext.relation ?? "child") === relationKey(relation2) && ext.connector != null));
1191
+ const nodes = [];
1192
+ for (const ext of extensions) {
1193
+ const result = get2(ext.connector(node));
1194
+ nodes.push(...result);
1195
+ }
1196
+ return nodes;
939
1197
  }).pipe(Atom3.withLabel(`graph-builder:connectors:${key}`));
940
1198
  });
941
- _onExpand(id, relation) {
1199
+ _onExpand(id, relation2) {
942
1200
  log2("onExpand", {
943
1201
  id,
944
- relation,
1202
+ relation: relation2,
945
1203
  registry: getDebugName(this._registry)
946
1204
  }, {
947
- F: __dxlog_file2,
948
- L: 176,
1205
+ F: __dxlog_file3,
1206
+ L: 261,
949
1207
  S: this,
950
1208
  C: (f, a) => f(...a)
951
1209
  });
952
- const connectors = this._connectors(`${id}+${relation}`);
953
- let previous = [];
954
- const cancel = this._registry.subscribe(connectors, (nodes) => {
1210
+ this._expandRelation(id, relation2);
1211
+ if (relation2.kind === "child" && relation2.direction === "outbound") {
1212
+ expand(this._graph, id, "action");
1213
+ }
1214
+ }
1215
+ _expandRelation(id, relation2) {
1216
+ const key = connectorKey(id, relation2);
1217
+ const connectors = this._connectors(key);
1218
+ const cancel = this._registry.subscribe(connectors, (rawNodes) => {
1219
+ const nodes = qualifyNodeArgs(id)(rawNodes);
1220
+ const previous = this._connectorPrevious.get(key) ?? [];
955
1221
  const ids = nodes.map((n) => n.id);
956
- const removed = previous.filter((id2) => !ids.includes(id2));
957
- previous = ids;
1222
+ if (ids.length === previous.length && ids.every((nodeId, idx) => nodeId === previous[idx])) {
1223
+ const prevArgs = this._connectorPreviousArgs.get(key);
1224
+ if (prevArgs && nodeArgsUnchanged(prevArgs, nodes)) {
1225
+ return;
1226
+ }
1227
+ }
958
1228
  log2("update", {
959
1229
  id,
960
- relation,
961
- ids,
962
- removed
1230
+ relation: relation2,
1231
+ ids
963
1232
  }, {
964
- F: __dxlog_file2,
965
- L: 187,
1233
+ F: __dxlog_file3,
1234
+ L: 288,
966
1235
  S: this,
967
1236
  C: (f, a) => f(...a)
968
1237
  });
969
- const update = () => {
970
- Atom3.batch(() => {
971
- removeEdges(this._graph, removed.map((target) => ({
972
- source: id,
973
- target
974
- })), true);
975
- addNodes(this._graph, nodes);
976
- addEdges(this._graph, nodes.map((node) => relation === "outbound" ? {
977
- source: id,
978
- target: node.id
979
- } : {
980
- source: node.id,
981
- target: id
982
- }));
983
- sortEdges(this._graph, id, relation, nodes.map(({ id: id2 }) => id2));
984
- });
985
- };
986
- if (typeof requestAnimationFrame === "function") {
987
- requestAnimationFrame(update);
988
- } else {
989
- update();
990
- }
1238
+ this._dirtyConnectors.set(key, {
1239
+ nodes,
1240
+ previous
1241
+ });
1242
+ this._scheduleDirtyFlush();
991
1243
  }, {
992
1244
  immediate: true
993
1245
  });
994
- this._subscriptions.set(id, cancel);
1246
+ this._subscriptions.set(subscriptionKey(id, "expand", key), cancel);
995
1247
  }
996
1248
  // TODO(wittjosiah): If the same node is added by a connector, the resolver should probably cancel itself?
997
1249
  async _onInitialize(id) {
998
1250
  log2("onInitialize", {
999
1251
  id
1000
1252
  }, {
1001
- F: __dxlog_file2,
1002
- L: 227,
1253
+ F: __dxlog_file3,
1254
+ L: 300,
1003
1255
  S: this,
1004
1256
  C: (f, a) => f(...a)
1005
1257
  });
@@ -1008,9 +1260,14 @@ var GraphBuilderImpl = class {
1008
1260
  const trigger = this._initialized[id];
1009
1261
  Option3.match(node, {
1010
1262
  onSome: (node2) => {
1011
- addNodes(this._graph, [
1012
- node2
1013
- ]);
1263
+ const connectorOwned = [
1264
+ ...this._connectorPrevious.values()
1265
+ ].some((ids) => ids.includes(id));
1266
+ if (!connectorOwned) {
1267
+ addNodes(this._graph, [
1268
+ node2
1269
+ ]);
1270
+ }
1014
1271
  trigger?.wake();
1015
1272
  },
1016
1273
  onNone: () => {
@@ -1023,11 +1280,16 @@ var GraphBuilderImpl = class {
1023
1280
  }, {
1024
1281
  immediate: true
1025
1282
  });
1026
- this._subscriptions.set(id, cancel);
1283
+ this._subscriptions.set(subscriptionKey(id, "init"), cancel);
1027
1284
  }
1028
1285
  _onRemoveNode(id) {
1029
- this._subscriptions.get(id)?.();
1030
- this._subscriptions.delete(id);
1286
+ const prefix = `${id}${Separators.primary}`;
1287
+ for (const [key, cleanup] of this._subscriptions) {
1288
+ if (key.startsWith(prefix)) {
1289
+ cleanup();
1290
+ this._subscriptions.delete(key);
1291
+ }
1292
+ }
1031
1293
  }
1032
1294
  };
1033
1295
  var make2 = (params) => {
@@ -1080,14 +1342,11 @@ function removeExtension(builderOrId, id) {
1080
1342
  }
1081
1343
  var exploreImpl = async (builder, options, path = []) => {
1082
1344
  const internal = builder;
1083
- const { registry = Registry2.make(), source = RootId, relation = "outbound", visitor } = options;
1345
+ const { registry = Registry2.make(), source = RootId, relation: relation2, visitor } = options;
1084
1346
  if (path.includes(source)) {
1085
1347
  return;
1086
1348
  }
1087
- if (!isNode()) {
1088
- const { yieldOrContinue } = await import("main-thread-scheduling");
1089
- await yieldOrContinue("idle");
1090
- }
1349
+ await yieldOrContinue("idle");
1091
1350
  const node = registry.get(internal._graph.nodeOrThrow(source));
1092
1351
  const shouldContinue = await visitor(node, [
1093
1352
  ...path,
@@ -1096,13 +1355,13 @@ var exploreImpl = async (builder, options, path = []) => {
1096
1355
  if (shouldContinue === false) {
1097
1356
  return;
1098
1357
  }
1099
- const nodes = Object.values(internal._registry.get(internal._extensions)).filter((extension) => relation === (extension.relation ?? "outbound")).map((extension) => extension.connector).filter(isNonNullable2).flatMap((connector) => registry.get(connector(internal._graph.node(source))));
1358
+ const nodes = Function2.pipe(internal._registry.get(internal._extensions), Record2.values, Array2.map((extension) => extension.connector), Array2.filter(isNonNullable2), Array2.flatMap((connector) => registry.get(connector(internal._graph.node(source)))), qualifyNodeArgs(source));
1100
1359
  await Promise.all(nodes.map((nodeArg) => {
1101
1360
  registry.set(internal._graph._node(nodeArg.id), internal._graph._constructNode(nodeArg));
1102
1361
  return exploreImpl(builder, {
1103
1362
  registry,
1104
1363
  source: nodeArg.id,
1105
- relation,
1364
+ relation: relation2,
1106
1365
  visitor
1107
1366
  }, [
1108
1367
  ...path,
@@ -1138,8 +1397,12 @@ function destroy(builder) {
1138
1397
  return destroyImpl(builder);
1139
1398
  }
1140
1399
  }
1400
+ var flush = (builder) => {
1401
+ return builder._flushPromise;
1402
+ };
1141
1403
  var createExtensionRaw = (extension) => {
1142
- const { id, position = "static", relation = "outbound", resolver: _resolver, connector: _connector, actions: _actions, actionGroups: _actionGroups } = extension;
1404
+ const { id, position = "static", relation: relation2 = "child", resolver: _resolver, connector: _connector, actions: _actions, actionGroups: _actionGroups } = extension;
1405
+ const normalizedRelation = normalizeRelation(relation2);
1143
1406
  const getId = (key) => `${id}/${key}`;
1144
1407
  const resolver = _resolver && Atom3.family((id2) => _resolver(id2).pipe(Atom3.withLabel(`graph-builder:_resolver:${id2}`)));
1145
1408
  const connector = _connector && Atom3.family((node) => _connector(node).pipe(Atom3.withLabel(`graph-builder:_connector:${id}`)));
@@ -1154,7 +1417,7 @@ var createExtensionRaw = (extension) => {
1154
1417
  connector ? {
1155
1418
  id: getId("connector"),
1156
1419
  position,
1157
- relation,
1420
+ relation: normalizedRelation,
1158
1421
  connector: Atom3.family((node) => Atom3.make((get2) => {
1159
1422
  try {
1160
1423
  return get2(connector(node));
@@ -1164,8 +1427,8 @@ var createExtensionRaw = (extension) => {
1164
1427
  node,
1165
1428
  error
1166
1429
  }, {
1167
- F: __dxlog_file2,
1168
- L: 509,
1430
+ F: __dxlog_file3,
1431
+ L: 596,
1169
1432
  S: void 0,
1170
1433
  C: (f, a) => f(...a)
1171
1434
  });
@@ -1176,7 +1439,7 @@ var createExtensionRaw = (extension) => {
1176
1439
  actionGroups ? {
1177
1440
  id: getId("actionGroups"),
1178
1441
  position,
1179
- relation: "outbound",
1442
+ relation: actionRelation(),
1180
1443
  connector: Atom3.family((node) => Atom3.make((get2) => {
1181
1444
  try {
1182
1445
  return get2(actionGroups(node)).map((arg) => ({
@@ -1190,8 +1453,8 @@ var createExtensionRaw = (extension) => {
1190
1453
  node,
1191
1454
  error
1192
1455
  }, {
1193
- F: __dxlog_file2,
1194
- L: 530,
1456
+ F: __dxlog_file3,
1457
+ L: 617,
1195
1458
  S: void 0,
1196
1459
  C: (f, a) => f(...a)
1197
1460
  });
@@ -1202,7 +1465,7 @@ var createExtensionRaw = (extension) => {
1202
1465
  actions ? {
1203
1466
  id: getId("actions"),
1204
1467
  position,
1205
- relation: "outbound",
1468
+ relation: actionRelation(),
1206
1469
  connector: Atom3.family((node) => Atom3.make((get2) => {
1207
1470
  try {
1208
1471
  return get2(actions(node)).map((arg) => ({
@@ -1215,8 +1478,8 @@ var createExtensionRaw = (extension) => {
1215
1478
  node,
1216
1479
  error
1217
1480
  }, {
1218
- F: __dxlog_file2,
1219
- L: 547,
1481
+ F: __dxlog_file3,
1482
+ L: 634,
1220
1483
  S: void 0,
1221
1484
  C: (f, a) => f(...a)
1222
1485
  });
@@ -1232,8 +1495,8 @@ var runEffectSyncWithFallback = (effect, context2, extensionId, fallback) => {
1232
1495
  extension: extensionId,
1233
1496
  error
1234
1497
  }, {
1235
- F: __dxlog_file2,
1236
- L: 590,
1498
+ F: __dxlog_file3,
1499
+ L: 677,
1237
1500
  S: void 0,
1238
1501
  C: (f, a) => f(...a)
1239
1502
  });
@@ -1241,7 +1504,7 @@ var runEffectSyncWithFallback = (effect, context2, extensionId, fallback) => {
1241
1504
  })));
1242
1505
  };
1243
1506
  var createExtension = (options) => Effect.map(Effect.context(), (context2) => {
1244
- const { id, match: match3, actions, connector, resolver, relation, position } = options;
1507
+ const { id, match: match3, actions, connector, resolver, relation: relation2, position } = options;
1245
1508
  const connectorExtension = connector ? createConnectorWithRuntime(id, match3, connector, context2) : void 0;
1246
1509
  const actionsExtension = actions ? (node) => Atom3.make((get2) => Function2.pipe(get2(node), Option3.flatMap(match3), Option3.map((matched) => runEffectSyncWithFallback(actions(matched, get2), context2, id, []).map((action) => ({
1247
1510
  ...action,
@@ -1251,7 +1514,7 @@ var createExtension = (options) => Effect.map(Effect.context(), (context2) => {
1251
1514
  const resolverExtension = resolver ? (nodeId) => Atom3.make((get2) => runEffectSyncWithFallback(resolver(nodeId, get2), context2, id, null) ?? null) : void 0;
1252
1515
  return createExtensionRaw({
1253
1516
  id,
1254
- relation,
1517
+ relation: relation2,
1255
1518
  position,
1256
1519
  connector: connectorExtension,
1257
1520
  actions: actionsExtension,
@@ -1265,16 +1528,35 @@ var createConnectorWithRuntime = (extensionId, matcher, factory, context2) => {
1265
1528
  return (node) => Atom3.make((get2) => Function2.pipe(get2(node), Option3.flatMap(matcher), Option3.map((data) => runEffectSyncWithFallback(factory(data, get2), context2, extensionId, [])), Option3.getOrElse(() => [])));
1266
1529
  };
1267
1530
  var createTypeExtension = (options) => {
1268
- const { id, type, actions, connector, relation, position } = options;
1531
+ const { id, type, actions, connector, relation: relation2, position } = options;
1269
1532
  return createExtension({
1270
1533
  id,
1271
1534
  match: whenEchoType(type),
1272
1535
  actions,
1273
1536
  connector,
1274
- relation,
1537
+ relation: relation2,
1275
1538
  position
1276
1539
  });
1277
1540
  };
1541
+ var qualifyNodeArgs = (parentId) => (nodes) => nodes.map((node) => {
1542
+ validateSegmentId(node.id);
1543
+ const qualified = qualifyId(parentId, node.id);
1544
+ return {
1545
+ ...node,
1546
+ id: qualified,
1547
+ nodes: node.nodes ? qualifyNodeArgs(qualified)(node.nodes) : void 0
1548
+ };
1549
+ });
1550
+ var connectorKey = (id, relation2) => `${id}${Separators.primary}${relationKey(relation2)}`;
1551
+ var relationFromConnectorKey = (key) => {
1552
+ const separatorIndex = key.indexOf(Separators.primary);
1553
+ const id = key.slice(0, separatorIndex);
1554
+ return {
1555
+ id,
1556
+ relation: relationFromKey(key.slice(separatorIndex + 1))
1557
+ };
1558
+ };
1559
+ var subscriptionKey = (id, kind, detail) => detail != null ? `${id}${Separators.primary}${kind}${Separators.primary}${detail}` : `${id}${Separators.primary}${kind}`;
1278
1560
  var flattenExtensions = (extension, acc = []) => {
1279
1561
  if (Array2.isArray(extension)) {
1280
1562
  return [