@dxos/app-graph 0.8.4-main.bc674ce → 0.8.4-main.c351d160a8

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