@dxos/app-graph 0.8.4-main.ef1bc66f44 → 0.8.4-main.f466a3d56e

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.
Files changed (56) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/{browser/index.mjs → neutral/chunk-32XXJE6M.mjs} +564 -384
  4. package/dist/lib/neutral/chunk-32XXJE6M.mjs.map +7 -0
  5. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  6. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  7. package/dist/lib/neutral/index.mjs +40 -0
  8. package/dist/lib/neutral/index.mjs.map +7 -0
  9. package/dist/lib/neutral/meta.json +1 -0
  10. package/dist/lib/neutral/scheduler.mjs +15 -0
  11. package/dist/lib/neutral/scheduler.mjs.map +7 -0
  12. package/dist/lib/neutral/testing/index.mjs +40 -0
  13. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  14. package/dist/types/src/atoms.d.ts.map +1 -1
  15. package/dist/types/src/graph-builder.d.ts +13 -9
  16. package/dist/types/src/graph-builder.d.ts.map +1 -1
  17. package/dist/types/src/graph.d.ts +13 -17
  18. package/dist/types/src/graph.d.ts.map +1 -1
  19. package/dist/types/src/index.d.ts +1 -0
  20. package/dist/types/src/index.d.ts.map +1 -1
  21. package/dist/types/src/node-matcher.d.ts +46 -20
  22. package/dist/types/src/node-matcher.d.ts.map +1 -1
  23. package/dist/types/src/node.d.ts +21 -5
  24. package/dist/types/src/node.d.ts.map +1 -1
  25. package/dist/types/src/scheduler.browser.d.ts +2 -0
  26. package/dist/types/src/scheduler.browser.d.ts.map +1 -0
  27. package/dist/types/src/scheduler.d.ts +8 -0
  28. package/dist/types/src/scheduler.d.ts.map +1 -0
  29. package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
  30. package/dist/types/src/testing/index.d.ts +2 -0
  31. package/dist/types/src/testing/index.d.ts.map +1 -0
  32. package/dist/types/src/testing/setup-graph-builder.d.ts +31 -0
  33. package/dist/types/src/testing/setup-graph-builder.d.ts.map +1 -0
  34. package/dist/types/src/util.d.ts +40 -0
  35. package/dist/types/src/util.d.ts.map +1 -0
  36. package/dist/types/tsconfig.tsbuildinfo +1 -1
  37. package/package.json +38 -27
  38. package/src/graph-builder.test.ts +695 -102
  39. package/src/graph-builder.ts +223 -76
  40. package/src/graph.test.ts +187 -52
  41. package/src/graph.ts +177 -98
  42. package/src/index.ts +1 -0
  43. package/src/node-matcher.ts +61 -31
  44. package/src/node.ts +46 -5
  45. package/src/scheduler.browser.ts +5 -0
  46. package/src/scheduler.ts +17 -0
  47. package/src/stories/EchoGraph.stories.tsx +94 -66
  48. package/src/stories/Tree.tsx +1 -1
  49. package/src/testing/index.ts +5 -0
  50. package/src/testing/setup-graph-builder.ts +41 -0
  51. package/src/util.ts +101 -0
  52. package/dist/lib/browser/index.mjs.map +0 -7
  53. package/dist/lib/browser/meta.json +0 -1
  54. package/dist/lib/node-esm/index.mjs +0 -1298
  55. package/dist/lib/node-esm/index.mjs.map +0 -7
  56. package/dist/lib/node-esm/meta.json +0 -1
@@ -1,24 +1,99 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, { get: all[name], enumerable: true });
5
- };
1
+ import {
2
+ __export
3
+ } from "./chunk-J5LGTIGS.mjs";
6
4
 
7
- // src/atoms.ts
8
- var atoms_exports = {};
9
- __export(atoms_exports, {
10
- fromObservable: () => fromObservable
5
+ // src/node.ts
6
+ var node_exports = {};
7
+ __export(node_exports, {
8
+ ActionGroupType: () => ActionGroupType,
9
+ ActionType: () => ActionType,
10
+ RootId: () => RootId,
11
+ RootType: () => RootType,
12
+ actionGroupSymbol: () => actionGroupSymbol,
13
+ actionRelation: () => actionRelation,
14
+ childRelation: () => childRelation,
15
+ isAction: () => isAction,
16
+ isActionGroup: () => isActionGroup,
17
+ isActionLike: () => isActionLike,
18
+ isGraphNode: () => isGraphNode,
19
+ make: () => make,
20
+ makeAction: () => makeAction,
21
+ makeActionGroup: () => makeActionGroup,
22
+ relation: () => relation
11
23
  });
12
- import { Atom } from "@effect-atom/atom-react";
13
- var observableFamily = Atom.family((observable) => {
14
- return Atom.make((get2) => {
15
- const subscription = observable.subscribe((value) => get2.setSelf(value));
16
- get2.addFinalizer(() => subscription.unsubscribe());
17
- return observable.get();
18
- });
24
+ var RootId = "root";
25
+ var RootType = "org.dxos.type.graphRoot";
26
+ var ActionType = "org.dxos.type.graphAction";
27
+ var ActionGroupType = "org.dxos.type.graphActionGroup";
28
+ var relation = (kind, direction = "outbound") => ({
29
+ kind,
30
+ direction
31
+ });
32
+ var childRelation = (direction = "outbound") => relation("child", direction);
33
+ var actionRelation = (direction = "outbound") => relation("action", direction);
34
+ var isGraphNode = (data) => data && typeof data === "object" && "id" in data && "properties" in data && data.properties ? typeof data.properties === "object" && "data" in data : false;
35
+ var isAction = (data) => isGraphNode(data) ? typeof data.data === "function" && data.type === ActionType : false;
36
+ var actionGroupSymbol = /* @__PURE__ */ Symbol("ActionGroup");
37
+ var isActionGroup = (data) => isGraphNode(data) ? data.data === actionGroupSymbol && data.type === ActionGroupType : false;
38
+ var isActionLike = (data) => isAction(data) || isActionGroup(data);
39
+ var make = (arg) => arg;
40
+ var makeAction = (arg) => ({
41
+ ...arg,
42
+ type: ActionType
43
+ });
44
+ var makeActionGroup = (arg) => ({
45
+ ...arg,
46
+ type: ActionGroupType,
47
+ data: actionGroupSymbol
19
48
  });
20
- var fromObservable = (observable) => {
21
- return observableFamily(observable);
49
+
50
+ // src/util.ts
51
+ import { invariant } from "@dxos/invariant";
52
+ var __dxlog_file = "/__w/dxos/dxos/packages/sdk/app-graph/src/util.ts";
53
+ var PRIMARY = "";
54
+ var SECONDARY = "";
55
+ var PATH = "/";
56
+ var primaryKey = (...parts) => parts.join(PRIMARY);
57
+ var primaryParts = (key) => key.split(PRIMARY);
58
+ var secondaryKey = (...parts) => parts.join(SECONDARY);
59
+ var secondaryParts = (key) => key.split(SECONDARY);
60
+ var normalizeRelation = (relation2) => relation2 == null ? childRelation() : typeof relation2 === "string" ? relation(relation2) : relation2;
61
+ var shallowEqual = (a, b) => {
62
+ if (a === b) {
63
+ return true;
64
+ }
65
+ if (a == null || b == null || typeof a !== "object" || typeof b !== "object") {
66
+ return false;
67
+ }
68
+ const keysA = Object.keys(a);
69
+ const keysB = Object.keys(b);
70
+ if (keysA.length !== keysB.length) {
71
+ return false;
72
+ }
73
+ return keysA.every((k) => a[k] === b[k]);
74
+ };
75
+ var nodeArgsUnchanged = (prev, next) => {
76
+ if (prev.length !== next.length) {
77
+ return false;
78
+ }
79
+ return prev.every((prevNode, idx) => {
80
+ const nextNode = next[idx];
81
+ return prevNode.id === nextNode.id && prevNode.type === nextNode.type && shallowEqual(prevNode.data, nextNode.data) && shallowEqual(prevNode.properties, nextNode.properties) && nodeArgsUnchanged(prevNode.nodes ?? [], nextNode.nodes ?? []);
82
+ });
83
+ };
84
+ var qualifyId = (parentId, ...segmentIds) => [
85
+ parentId,
86
+ ...segmentIds
87
+ ].join(PATH);
88
+ var validateSegmentId = (id) => {
89
+ invariant(!id.includes(PATH), `Node segment ID must not contain '${PATH}': ${id}`, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 56, S: void 0, A: ["!id.includes(PATH)", "`Node segment ID must not contain '${PATH}': ${id}`"] });
90
+ };
91
+ var getParentId = (qualifiedId) => {
92
+ const lastSlash = qualifiedId.lastIndexOf(PATH);
93
+ return lastSlash > 0 ? qualifiedId.slice(0, lastSlash) : void 0;
94
+ };
95
+ var getSegmentId = (qualifiedId) => {
96
+ return qualifiedId.split(PATH).pop() ?? qualifiedId;
22
97
  };
23
98
 
24
99
  // src/graph.ts
@@ -40,7 +115,9 @@ __export(graph_exports, {
40
115
  getPath: () => getPath,
41
116
  getRoot: () => getRoot,
42
117
  initialize: () => initialize,
43
- make: () => make,
118
+ make: () => make2,
119
+ relationFromKey: () => relationFromKey,
120
+ relationKey: () => relationKey,
44
121
  removeEdge: () => removeEdge,
45
122
  removeEdges: () => removeEdges,
46
123
  removeNode: () => removeNode,
@@ -50,58 +127,25 @@ __export(graph_exports, {
50
127
  traverse: () => traverse,
51
128
  waitForPath: () => waitForPath
52
129
  });
53
- import { Atom as Atom2, Registry } from "@effect-atom/atom-react";
130
+ import { Atom, Registry } from "@effect-atom/atom-react";
54
131
  import * as Function from "effect/Function";
55
132
  import * as Option from "effect/Option";
56
133
  import * as Pipeable from "effect/Pipeable";
57
134
  import * as Record from "effect/Record";
58
135
  import { Event, Trigger } from "@dxos/async";
59
136
  import { todo } from "@dxos/debug";
60
- import { invariant } from "@dxos/invariant";
137
+ import { invariant as invariant2 } from "@dxos/invariant";
61
138
  import { log } from "@dxos/log";
62
139
  import { isNonNullable } from "@dxos/util";
63
-
64
- // src/node.ts
65
- var node_exports = {};
66
- __export(node_exports, {
67
- ActionGroupType: () => ActionGroupType,
68
- ActionType: () => ActionType,
69
- RootId: () => RootId,
70
- RootType: () => RootType,
71
- actionGroupSymbol: () => actionGroupSymbol,
72
- isAction: () => isAction,
73
- isActionGroup: () => isActionGroup,
74
- isActionLike: () => isActionLike,
75
- isGraphNode: () => isGraphNode
76
- });
77
- var RootId = "root";
78
- var RootType = "dxos.org/type/GraphRoot";
79
- var ActionType = "dxos.org/type/GraphAction";
80
- var ActionGroupType = "dxos.org/type/GraphActionGroup";
81
- var isGraphNode = (data) => data && typeof data === "object" && "id" in data && "properties" in data && data.properties ? typeof data.properties === "object" && "data" in data : false;
82
- var isAction = (data) => isGraphNode(data) ? typeof data.data === "function" && data.type === ActionType : false;
83
- var actionGroupSymbol = Symbol("ActionGroup");
84
- var isActionGroup = (data) => isGraphNode(data) ? data.data === actionGroupSymbol && data.type === ActionGroupType : false;
85
- var isActionLike = (data) => isAction(data) || isActionGroup(data);
86
-
87
- // 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,
95
- S: void 0,
96
- A: [
97
- "graph",
98
- "'Node is not associated with a graph.'"
99
- ]
100
- });
144
+ invariant2(graph, "Node is not associated with a graph.", { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 21, S: void 0, A: ["graph", "'Node is not associated with a graph.'"] });
101
145
  return graph;
102
146
  };
103
- var GraphTypeId = Symbol.for("@dxos/app-graph/Graph");
104
- var GraphKind = Symbol.for("@dxos/app-graph/GraphKind");
147
+ var GraphTypeId = /* @__PURE__ */ Symbol.for("@dxos/app-graph/Graph");
148
+ var GraphKind = /* @__PURE__ */ Symbol.for("@dxos/app-graph/GraphKind");
105
149
  var GraphImpl = class {
106
150
  [GraphTypeId] = GraphTypeId;
107
151
  [GraphKind] = "writable";
@@ -114,6 +158,7 @@ var GraphImpl = class {
114
158
  _onRemoveNode;
115
159
  _registry;
116
160
  _expanded = Record.empty();
161
+ _pendingExpands = /* @__PURE__ */ new Set();
117
162
  _initialized = Record.empty();
118
163
  _initialEdges = Record.empty();
119
164
  _initialNodes = Record.fromEntries([
@@ -128,50 +173,46 @@ var GraphImpl = class {
128
173
  ]
129
174
  ]);
130
175
  /** @internal */
131
- _node = Atom2.family((id) => {
176
+ _node = Atom.family((id) => {
132
177
  const initial = Option.flatten(Record.get(this._initialNodes, id));
133
- return Atom2.make(initial).pipe(Atom2.keepAlive, Atom2.withLabel(`graph:node:${id}`));
178
+ return Atom.make(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:node:${id}`));
134
179
  });
135
- _nodeOrThrow = Atom2.family((id) => {
136
- return Atom2.make((get2) => {
180
+ _nodeOrThrow = Atom.family((id) => {
181
+ return Atom.make((get2) => {
137
182
  const node = get2(this._node(id));
138
- invariant(Option.isSome(node), `Node not available: ${id}`, {
139
- F: __dxlog_file,
140
- L: 174,
141
- S: this,
142
- A: [
143
- "Option.isSome(node)",
144
- "`Node not available: ${id}`"
145
- ]
146
- });
183
+ invariant2(Option.isSome(node), `Node not available: ${id}`, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 67, S: this, A: ["Option.isSome(node)", "`Node not available: ${id}`"] });
147
184
  return node.value;
148
185
  });
149
186
  });
150
- _edges = Atom2.family((id) => {
151
- const initial = Record.get(this._initialEdges, id).pipe(Option.getOrElse(() => ({
152
- inbound: [],
153
- outbound: []
154
- })));
155
- return Atom2.make(initial).pipe(Atom2.keepAlive, Atom2.withLabel(`graph:edges:${id}`));
187
+ _edges = Atom.family((id) => {
188
+ const initial = Record.get(this._initialEdges, id).pipe(Option.getOrElse(() => ({})));
189
+ return Atom.make(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:edges:${id}`));
156
190
  });
157
191
  // NOTE: Currently the argument to the family needs to be referentially stable for the atom to be referentially stable.
158
192
  // TODO(wittjosiah): Atom feature request, support for something akin to `ComplexMap` to allow for complex arguments.
159
- _connections = Atom2.family((key) => {
160
- return Atom2.make((get2) => {
161
- const [id, relation] = key.split("$");
193
+ _connections = Atom.family((key) => {
194
+ return Atom.make((get2) => {
195
+ const parts = key ? primaryParts(key) : [];
196
+ if (parts.length < 2 || !parts[0]) {
197
+ return [];
198
+ }
199
+ const { id, relation: relation2 } = relationFromConnectionKey(key);
162
200
  const edges = get2(this._edges(id));
163
- return edges[relation].map((id2) => get2(this._node(id2))).filter(Option.isSome).map((o) => o.value);
164
- }).pipe(Atom2.withLabel(`graph:connections:${key}`));
201
+ return (edges[relationKey(relation2)] ?? []).map((id2) => get2(this._node(id2))).filter(Option.isSome).map((o) => o.value);
202
+ }).pipe(Atom.withLabel(`graph:connections:${key}`));
165
203
  });
166
- _actions = Atom2.family((id) => {
167
- return Atom2.make((get2) => {
168
- return get2(this._connections(`${id}$outbound`)).filter((node) => node.type === ActionType || node.type === ActionGroupType);
169
- }).pipe(Atom2.withLabel(`graph:actions:${id}`));
204
+ _actions = Atom.family((id) => {
205
+ return Atom.make((get2) => {
206
+ if (!id) {
207
+ return [];
208
+ }
209
+ return get2(this._connections(connectionKey(id, actionRelation())));
210
+ }).pipe(Atom.withLabel(`graph:actions:${id}`));
170
211
  });
171
- _json = Atom2.family((id) => {
172
- return Atom2.make((get2) => {
212
+ _json = Atom.family((id) => {
213
+ return Atom.make((get2) => {
173
214
  const toJSON2 = (node, seen = []) => {
174
- const nodes = get2(this._connections(`${node.id}$outbound`));
215
+ const nodes = get2(this._connections(connectionKey(node.id, "child")));
175
216
  const obj = {
176
217
  id: node.id,
177
218
  type: node.type
@@ -192,7 +233,7 @@ var GraphImpl = class {
192
233
  };
193
234
  const root = get2(this._nodeOrThrow(id));
194
235
  return toJSON2(root);
195
- }).pipe(Atom2.withLabel(`graph:json:${id}`));
236
+ }).pipe(Atom.withLabel(`graph:json:${id}`));
196
237
  });
197
238
  constructor({ registry, nodes, edges, onInitialize, onExpand, onRemoveNode } = {}) {
198
239
  this._registry = registry ?? Registry.make();
@@ -219,8 +260,8 @@ var GraphImpl = class {
219
260
  nodeOrThrow(id) {
220
261
  return nodeOrThrowImpl(this, id);
221
262
  }
222
- connections(id, relation = "outbound") {
223
- return connectionsImpl(this, id, relation);
263
+ connections(id, relation2) {
264
+ return connectionsImpl(this, id, relation2);
224
265
  }
225
266
  actions(id) {
226
267
  return actionsImpl(this, id);
@@ -257,9 +298,9 @@ var nodeOrThrowImpl = (graph, id) => {
257
298
  const internal = getInternal(graph);
258
299
  return internal._nodeOrThrow(id);
259
300
  };
260
- var connectionsImpl = (graph, id, relation = "outbound") => {
301
+ var connectionsImpl = (graph, id, relation2) => {
261
302
  const internal = getInternal(graph);
262
- return internal._connections(`${id}$${relation}`);
303
+ return internal._connections(connectionKey(id, relation2));
263
304
  };
264
305
  var actionsImpl = (graph, id) => {
265
306
  const internal = getInternal(graph);
@@ -298,19 +339,20 @@ function getNodeOrThrow(graphOrId, id) {
298
339
  function getRoot(graph) {
299
340
  return getNodeOrThrowImpl(graph, RootId);
300
341
  }
301
- var getConnectionsImpl = (graph, id, relation = "outbound") => {
342
+ var getConnectionsImpl = (graph, id, relation2) => {
302
343
  const internal = getInternal(graph);
303
- return internal._registry.get(connectionsImpl(graph, id, relation));
344
+ return internal._registry.get(connectionsImpl(graph, id, relation2));
304
345
  };
305
- function getConnections(graphOrId, idOrRelation, relation) {
346
+ function getConnections(graphOrId, idOrRelation, relation2) {
306
347
  if (typeof graphOrId === "string") {
307
348
  const id = graphOrId;
308
- const rel = (typeof idOrRelation === "string" ? "outbound" : idOrRelation) ?? "outbound";
349
+ const rel = idOrRelation;
309
350
  return (graph) => getConnectionsImpl(graph, id, rel);
310
351
  } else {
311
352
  const graph = graphOrId;
312
353
  const id = idOrRelation;
313
- const rel = relation ?? "outbound";
354
+ invariant2(relation2 !== void 0, "Relation is required.", { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 272, S: this, A: ["relation !== undefined", "'Relation is required.'"] });
355
+ const rel = relation2;
314
356
  return getConnectionsImpl(graph, id, rel);
315
357
  }
316
358
  }
@@ -341,7 +383,7 @@ function getEdges(graphOrId, id) {
341
383
  }
342
384
  }
343
385
  var traverseImpl = (graph, options, path = []) => {
344
- const { visitor, source = RootId, relation = "outbound" } = options;
386
+ const { visitor, source = RootId, relation: relation2 } = options;
345
387
  if (path.includes(source)) {
346
388
  return;
347
389
  }
@@ -353,14 +395,25 @@ var traverseImpl = (graph, options, path = []) => {
353
395
  if (shouldContinue === false) {
354
396
  return;
355
397
  }
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
- ]));
398
+ const relations = Array.isArray(relation2) ? relation2 : [
399
+ relation2
400
+ ];
401
+ const seen = /* @__PURE__ */ new Set();
402
+ for (const rel of relations) {
403
+ for (const connected of getConnections(graph, source, rel)) {
404
+ if (!seen.has(connected.id)) {
405
+ seen.add(connected.id);
406
+ traverseImpl(graph, {
407
+ source: connected.id,
408
+ relation: relation2,
409
+ visitor
410
+ }, [
411
+ ...path,
412
+ source
413
+ ]);
414
+ }
415
+ }
416
+ }
364
417
  };
365
418
  function traverse(graphOrOptions, optionsOrPath, path) {
366
419
  if (typeof graphOrOptions === "object" && "visitor" in graphOrOptions) {
@@ -379,6 +432,7 @@ var getPathImpl = (graph, params) => {
379
432
  let found = Option.none();
380
433
  traverseImpl(graph, {
381
434
  source: node.id,
435
+ relation: "child",
382
436
  visitor: (node2, path) => {
383
437
  if (Option.isSome(found)) {
384
438
  return false;
@@ -434,15 +488,10 @@ var initializeImpl = async (graph, id) => {
434
488
  log("initialize", {
435
489
  id,
436
490
  initialized
437
- }, {
438
- F: __dxlog_file,
439
- L: 661,
440
- S: void 0,
441
- C: (f, a) => f(...a)
442
- });
491
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 431, S: void 0 });
443
492
  if (!initialized) {
444
- await internal._onInitialize?.(id);
445
493
  Record.set(internal._initialized, id, true);
494
+ await internal._onInitialize?.(id);
446
495
  }
447
496
  return graph;
448
497
  };
@@ -455,65 +504,79 @@ function initialize(graphOrId, id) {
455
504
  return initializeImpl(graph, id);
456
505
  }
457
506
  }
458
- var expandImpl = (graph, id, relation = "outbound") => {
507
+ var expandImpl = (graph, id, relation2) => {
459
508
  const internal = getInternal(graph);
460
- const key = `${id}$${relation}`;
509
+ const normalizedRelation = normalizeRelation(relation2);
510
+ const key = primaryKey(id, relationKey(normalizedRelation));
511
+ const nodeOpt = internal._registry.get(internal._node(id));
512
+ if (Option.isNone(nodeOpt)) {
513
+ internal._pendingExpands.add(key);
514
+ log("expand", {
515
+ key,
516
+ deferred: true
517
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 463, S: void 0 });
518
+ return graph;
519
+ }
461
520
  const expanded = Record.get(internal._expanded, key).pipe(Option.getOrElse(() => false));
462
521
  log("expand", {
463
522
  key,
464
523
  expanded
465
- }, {
466
- F: __dxlog_file,
467
- L: 702,
468
- S: void 0,
469
- C: (f, a) => f(...a)
470
- });
524
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 470, S: void 0 });
471
525
  if (!expanded) {
472
- internal._onExpand?.(id, relation);
473
526
  Record.set(internal._expanded, key, true);
527
+ internal._onExpand?.(id, normalizedRelation);
474
528
  }
475
529
  return graph;
476
530
  };
477
- function expand(graphOrId, idOrRelation, relation) {
531
+ function expand(graphOrId, idOrRelation, relation2) {
478
532
  if (typeof graphOrId === "string") {
479
533
  const id = graphOrId;
480
- const rel = (typeof idOrRelation === "string" ? "outbound" : idOrRelation) ?? "outbound";
534
+ const rel = idOrRelation;
481
535
  return (graph) => expandImpl(graph, id, rel);
482
536
  } else {
483
537
  const graph = graphOrId;
484
538
  const id = idOrRelation;
485
- const rel = relation ?? "outbound";
539
+ invariant2(relation2 !== void 0, "Relation is required.", { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 490, S: this, A: ["relation !== undefined", "'Relation is required.'"] });
540
+ const rel = relation2;
486
541
  return expandImpl(graph, id, rel);
487
542
  }
488
543
  }
489
- var sortEdgesImpl = (graph, id, relation, order) => {
544
+ var sortEdgesImpl = (graph, id, relation2, order) => {
490
545
  const internal = getInternal(graph);
491
546
  const edgesAtom = internal._edges(id);
492
547
  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, ...[
548
+ const relationId = relationKey(relation2);
549
+ const current = edges[relationId] ?? [];
550
+ const unsorted = current.filter((id2) => !order.includes(id2));
551
+ const sorted = order.filter((id2) => current.includes(id2));
552
+ const newOrder = [
496
553
  ...sorted,
497
554
  ...unsorted
498
- ]);
499
- internal._registry.set(edgesAtom, edges);
555
+ ];
556
+ if (newOrder.length === current.length && newOrder.every((id2, i) => id2 === current[i])) {
557
+ return graph;
558
+ }
559
+ internal._registry.set(edgesAtom, {
560
+ ...edges,
561
+ [relationId]: newOrder
562
+ });
500
563
  return graph;
501
564
  };
502
565
  function sortEdges(graphOrId, idOrRelation, relationOrOrder, order) {
503
566
  if (typeof graphOrId === "string") {
504
567
  const id = graphOrId;
505
- const relation = idOrRelation;
568
+ const relation2 = idOrRelation;
506
569
  const order2 = relationOrOrder;
507
- return (graph) => sortEdgesImpl(graph, id, relation, order2);
570
+ return (graph) => sortEdgesImpl(graph, id, relation2, order2);
508
571
  } else {
509
572
  const graph = graphOrId;
510
573
  const id = idOrRelation;
511
- const relation = relationOrOrder;
512
- return sortEdgesImpl(graph, id, relation, order);
574
+ const relation2 = relationOrOrder;
575
+ return sortEdgesImpl(graph, id, relation2, order);
513
576
  }
514
577
  }
515
578
  var addNodesImpl = (graph, nodes) => {
516
- Atom2.batch(() => {
579
+ Atom.batch(() => {
517
580
  nodes.map((node) => addNodeImpl(graph, node));
518
581
  });
519
582
  return graph;
@@ -535,31 +598,21 @@ var addNodeImpl = (graph, nodeArg) => {
535
598
  Option.match(existingNode, {
536
599
  onSome: (existing) => {
537
600
  const typeChanged = existing.type !== type;
538
- const dataChanged = existing.data !== data;
601
+ const dataChanged = !shallowEqual(existing.data, data);
539
602
  const propertiesChanged = Object.keys(properties).some((key) => existing.properties[key] !== properties[key]);
540
603
  log("existing node", {
541
604
  id,
542
605
  typeChanged,
543
606
  dataChanged,
544
607
  propertiesChanged
545
- }, {
546
- F: __dxlog_file,
547
- L: 847,
548
- S: void 0,
549
- C: (f, a) => f(...a)
550
- });
608
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 565, S: void 0 });
551
609
  if (typeChanged || dataChanged || propertiesChanged) {
552
610
  log("updating node", {
553
611
  id,
554
612
  type,
555
613
  data,
556
614
  properties
557
- }, {
558
- F: __dxlog_file,
559
- L: 854,
560
- S: void 0,
561
- C: (f, a) => f(...a)
562
- });
615
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 572, S: void 0 });
563
616
  const newNode = Option.some({
564
617
  ...existing,
565
618
  ...rest,
@@ -583,12 +636,7 @@ var addNodeImpl = (graph, nodeArg) => {
583
636
  type,
584
637
  data,
585
638
  properties
586
- }, {
587
- F: __dxlog_file,
588
- L: 867,
589
- S: void 0,
590
- C: (f, a) => f(...a)
591
- });
639
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 596, S: void 0 });
592
640
  const newNode = internal._constructNode({
593
641
  id,
594
642
  type,
@@ -601,13 +649,23 @@ var addNodeImpl = (graph, nodeArg) => {
601
649
  id,
602
650
  node: newNode
603
651
  });
652
+ const toApply = [
653
+ ...internal._pendingExpands
654
+ ].filter((k) => primaryParts(k)[0] === id);
655
+ for (const pendingKey of toApply) {
656
+ internal._pendingExpands.delete(pendingKey);
657
+ const relation2 = relationFromKey(primaryParts(pendingKey)[1]);
658
+ Record.set(internal._expanded, pendingKey, true);
659
+ internal._onExpand?.(id, relation2);
660
+ }
604
661
  }
605
662
  });
606
663
  if (nodes) {
607
664
  addNodesImpl(graph, nodes);
608
665
  const _edges = nodes.map((node) => ({
609
666
  source: id,
610
- target: node.id
667
+ target: node.id,
668
+ relation: "child"
611
669
  }));
612
670
  addEdgesImpl(graph, _edges);
613
671
  }
@@ -626,7 +684,7 @@ function addNode(graphOrNodeArg, nodeArg) {
626
684
  }
627
685
  }
628
686
  var removeNodesImpl = (graph, ids, edges = false) => {
629
- Atom2.batch(() => {
687
+ Atom.batch(() => {
630
688
  ids.map((id) => removeNodeImpl(graph, id, edges));
631
689
  });
632
690
  return graph;
@@ -652,17 +710,27 @@ var removeNodeImpl = (graph, id, edges = false) => {
652
710
  node: Option.none()
653
711
  });
654
712
  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
- ];
713
+ const nodeEdges = internal._registry.get(internal._edges(id));
714
+ const edgesToRemove = [];
715
+ for (const [relationKeyValue, relatedIds] of Object.entries(nodeEdges)) {
716
+ const relation2 = relationFromKey(relationKeyValue);
717
+ const isInboundRelation = relation2.direction === "inbound";
718
+ for (const relatedId of relatedIds) {
719
+ if (isInboundRelation) {
720
+ edgesToRemove.push({
721
+ source: relatedId,
722
+ target: id,
723
+ relation: inverseRelation(relation2)
724
+ });
725
+ } else {
726
+ edgesToRemove.push({
727
+ source: id,
728
+ target: relatedId,
729
+ relation: relation2
730
+ });
731
+ }
732
+ }
733
+ }
666
734
  removeEdgesImpl(graph, edgesToRemove);
667
735
  }
668
736
  internal._onRemoveNode?.(id);
@@ -681,7 +749,7 @@ function removeNode(graphOrId, idOrEdges, edges) {
681
749
  }
682
750
  }
683
751
  var addEdgesImpl = (graph, edges) => {
684
- Atom2.batch(() => {
752
+ Atom.batch(() => {
685
753
  edges.map((edge) => addEdgeImpl(graph, edge));
686
754
  });
687
755
  return graph;
@@ -696,45 +764,43 @@ function addEdges(graphOrEdges, edges) {
696
764
  }
697
765
  }
698
766
  var addEdgeImpl = (graph, edgeArg) => {
767
+ const relation2 = normalizeRelation(edgeArg.relation);
768
+ const relationId = relationKey(relation2);
769
+ const inverse = inverseRelation(relation2);
770
+ const inverseId = relationKey(inverse);
699
771
  const internal = getInternal(graph);
700
772
  const sourceAtom = internal._edges(edgeArg.source);
701
773
  const source = internal._registry.get(sourceAtom);
702
- if (!source.outbound.includes(edgeArg.target)) {
703
- log("add outbound edge", {
774
+ const sourceList = source[relationId] ?? [];
775
+ if (!sourceList.includes(edgeArg.target)) {
776
+ log("add edge", {
704
777
  source: edgeArg.source,
705
- target: edgeArg.target
706
- }, {
707
- F: __dxlog_file,
708
- L: 1026,
709
- S: void 0,
710
- C: (f, a) => f(...a)
711
- });
778
+ target: edgeArg.target,
779
+ relation: relationId
780
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 758, S: void 0 });
712
781
  internal._registry.set(sourceAtom, {
713
- inbound: source.inbound,
714
- outbound: [
715
- ...source.outbound,
782
+ ...source,
783
+ [relationId]: [
784
+ ...sourceList,
716
785
  edgeArg.target
717
786
  ]
718
787
  });
719
788
  }
720
789
  const targetAtom = internal._edges(edgeArg.target);
721
790
  const target = internal._registry.get(targetAtom);
722
- if (!target.inbound.includes(edgeArg.source)) {
723
- log("add inbound edge", {
791
+ const targetList = target[inverseId] ?? [];
792
+ if (!targetList.includes(edgeArg.source)) {
793
+ log("add inverse edge", {
724
794
  source: edgeArg.source,
725
- target: edgeArg.target
726
- }, {
727
- F: __dxlog_file,
728
- L: 1039,
729
- S: void 0,
730
- C: (f, a) => f(...a)
731
- });
795
+ target: edgeArg.target,
796
+ relation: inverseId
797
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 775, S: void 0 });
732
798
  internal._registry.set(targetAtom, {
733
- inbound: [
734
- ...target.inbound,
799
+ ...target,
800
+ [inverseId]: [
801
+ ...targetList,
735
802
  edgeArg.source
736
- ],
737
- outbound: target.outbound
803
+ ]
738
804
  });
739
805
  }
740
806
  return graph;
@@ -749,7 +815,7 @@ function addEdge(graphOrEdgeArg, edgeArg) {
749
815
  }
750
816
  }
751
817
  var removeEdgesImpl = (graph, edges, removeOrphans = false) => {
752
- Atom2.batch(() => {
818
+ Atom.batch(() => {
753
819
  edges.map((edge) => removeEdgeImpl(graph, edge, removeOrphans));
754
820
  });
755
821
  return graph;
@@ -767,32 +833,39 @@ function removeEdges(graphOrEdges, edgesOrRemoveOrphans, removeOrphans) {
767
833
  }
768
834
  }
769
835
  var removeEdgeImpl = (graph, edgeArg, removeOrphans = false) => {
836
+ const relation2 = normalizeRelation(edgeArg.relation);
837
+ const relationId = relationKey(relation2);
838
+ const inverse = inverseRelation(relation2);
839
+ const inverseId = relationKey(inverse);
770
840
  const internal = getInternal(graph);
771
841
  const sourceAtom = internal._edges(edgeArg.source);
772
842
  const source = internal._registry.get(sourceAtom);
773
- if (source.outbound.includes(edgeArg.target)) {
843
+ const sourceList = source[relationId] ?? [];
844
+ if (sourceList.includes(edgeArg.target)) {
774
845
  internal._registry.set(sourceAtom, {
775
- inbound: source.inbound,
776
- outbound: source.outbound.filter((id) => id !== edgeArg.target)
846
+ ...source,
847
+ [relationId]: sourceList.filter((id) => id !== edgeArg.target)
777
848
  });
778
849
  }
779
850
  const targetAtom = internal._edges(edgeArg.target);
780
851
  const target = internal._registry.get(targetAtom);
781
- if (target.inbound.includes(edgeArg.source)) {
852
+ const targetList = target[inverseId] ?? [];
853
+ if (targetList.includes(edgeArg.source)) {
782
854
  internal._registry.set(targetAtom, {
783
- inbound: target.inbound.filter((id) => id !== edgeArg.source),
784
- outbound: target.outbound
855
+ ...target,
856
+ [inverseId]: targetList.filter((id) => id !== edgeArg.source)
785
857
  });
786
858
  }
787
859
  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) {
860
+ const sourceAfter = internal._registry.get(sourceAtom);
861
+ const targetAfter = internal._registry.get(targetAtom);
862
+ const isEmpty = (edges) => Object.values(edges).every((ids) => ids.length === 0);
863
+ if (isEmpty(sourceAfter) && edgeArg.source !== RootId) {
791
864
  removeNodesImpl(graph, [
792
865
  edgeArg.source
793
866
  ]);
794
867
  }
795
- if (target2.outbound.length === 0 && target2.inbound.length === 0 && edgeArg.target !== RootId) {
868
+ if (isEmpty(targetAfter) && edgeArg.target !== RootId) {
796
869
  removeNodesImpl(graph, [
797
870
  edgeArg.target
798
871
  ]);
@@ -812,35 +885,33 @@ function removeEdge(graphOrEdgeArg, edgeArgOrRemoveOrphans, removeOrphans) {
812
885
  return removeEdgeImpl(graph, edgeArg, removeOrphansArg);
813
886
  }
814
887
  }
815
- var make = (params) => {
888
+ var make2 = (params) => {
816
889
  return new GraphImpl(params);
817
890
  };
818
-
819
- // src/graph-builder.ts
820
- var graph_builder_exports = {};
821
- __export(graph_builder_exports, {
822
- GraphBuilderTypeId: () => GraphBuilderTypeId,
823
- addExtension: () => addExtension,
824
- createConnector: () => createConnector,
825
- createExtension: () => createExtension,
826
- createExtensionRaw: () => createExtensionRaw,
827
- createTypeExtension: () => createTypeExtension,
828
- destroy: () => destroy,
829
- explore: () => explore,
830
- flattenExtensions: () => flattenExtensions,
831
- from: () => from,
832
- make: () => make2,
833
- removeExtension: () => removeExtension
834
- });
835
- import { Atom as Atom3, Registry as Registry2 } from "@effect-atom/atom-react";
836
- import * as Array2 from "effect/Array";
837
- import * as Effect from "effect/Effect";
838
- import * as Function2 from "effect/Function";
839
- import * as Option3 from "effect/Option";
840
- import * as Pipeable2 from "effect/Pipeable";
841
- import * as Record2 from "effect/Record";
842
- import { log as log2 } from "@dxos/log";
843
- import { byPosition, getDebugName, isNode, isNonNullable as isNonNullable2 } from "@dxos/util";
891
+ var relationKey = (relation2) => {
892
+ const normalized = normalizeRelation(relation2);
893
+ return secondaryKey(normalized.kind, normalized.direction);
894
+ };
895
+ var relationFromKey = (encoded) => {
896
+ const parts = secondaryParts(encoded);
897
+ invariant2(parts.length === 2 && parts[0].length > 0 && parts[1].length > 0, `Invalid relation key: ${encoded}`, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 894, S: void 0, A: ["parts.length === 2 && parts[0].length > 0 && parts[1].length > 0", "`Invalid relation key: ${encoded}`"] });
898
+ const [kind, directionRaw] = parts;
899
+ invariant2(directionRaw === "outbound" || directionRaw === "inbound", `Invalid relation direction: ${directionRaw}`, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 896, S: void 0, A: ["directionRaw === 'outbound' || directionRaw === 'inbound'", "`Invalid relation direction: ${directionRaw}`"] });
900
+ return relation(kind, directionRaw);
901
+ };
902
+ var connectionKey = (id, relation2) => primaryKey(id, relationKey(relation2));
903
+ var relationFromConnectionKey = (key) => {
904
+ const [id, encodedRelation] = primaryParts(key);
905
+ invariant2(id && encodedRelation, `Invalid connection key: ${key}`, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 902, S: void 0, A: ["id && encodedRelation", "`Invalid connection key: ${key}`"] });
906
+ return {
907
+ id,
908
+ relation: relationFromKey(encodedRelation)
909
+ };
910
+ };
911
+ var inverseRelation = (relation2) => {
912
+ const normalized = normalizeRelation(relation2);
913
+ return relation(normalized.kind, normalized.direction === "outbound" ? "inbound" : "outbound");
914
+ };
844
915
 
845
916
  // src/node-matcher.ts
846
917
  var node_matcher_exports = {};
@@ -864,19 +935,23 @@ var whenNodeType = (type) => (node) => node.type === type ? Option2.some(node) :
864
935
  var whenEchoType = (type) => (node) => Obj.instanceOf(type, node.data) ? Option2.some(node.data) : Option2.none();
865
936
  var whenEchoObject = (node) => Obj.isObject(node.data) ? Option2.some(node.data) : Option2.none();
866
937
  var whenAll = (...matchers) => (node) => {
867
- for (const matcher of matchers) {
868
- const result = matcher(node);
938
+ let first = Option2.none();
939
+ for (const candidate of matchers) {
940
+ const result = candidate(node);
869
941
  if (Option2.isNone(result)) {
870
942
  return Option2.none();
871
943
  }
944
+ if (Option2.isNone(first)) {
945
+ first = result;
946
+ }
872
947
  }
873
- return Option2.some(node);
948
+ return first;
874
949
  };
875
950
  var whenAny = (...matchers) => (node) => {
876
- for (const matcher of matchers) {
877
- const result = matcher(node);
951
+ for (const candidate of matchers) {
952
+ const result = candidate(node);
878
953
  if (Option2.isSome(result)) {
879
- return Option2.some(node);
954
+ return result;
880
955
  }
881
956
  }
882
957
  return Option2.none();
@@ -886,25 +961,68 @@ var whenEchoObjectMatches = (node) => Obj.isObject(node.data) ? Option2.some(nod
886
961
  var whenNot = (matcher) => (node) => Option2.isNone(matcher(node)) ? Option2.some(node) : Option2.none();
887
962
 
888
963
  // 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");
964
+ var graph_builder_exports = {};
965
+ __export(graph_builder_exports, {
966
+ GraphBuilderTypeId: () => GraphBuilderTypeId,
967
+ addExtension: () => addExtension,
968
+ createConnector: () => createConnector,
969
+ createExtension: () => createExtension,
970
+ createExtensionRaw: () => createExtensionRaw,
971
+ createTypeExtension: () => createTypeExtension,
972
+ destroy: () => destroy,
973
+ explore: () => explore,
974
+ flattenExtensions: () => flattenExtensions,
975
+ flush: () => flush,
976
+ from: () => from,
977
+ make: () => make3,
978
+ removeExtension: () => removeExtension
979
+ });
980
+ import { Atom as Atom2, Registry as Registry2 } from "@effect-atom/atom-react";
981
+ import * as Array2 from "effect/Array";
982
+ import * as Effect from "effect/Effect";
983
+ import * as Function2 from "effect/Function";
984
+ import * as Option3 from "effect/Option";
985
+ import * as Pipeable2 from "effect/Pipeable";
986
+ import * as Record2 from "effect/Record";
987
+ import { log as log2 } from "@dxos/log";
988
+ import { byPosition, getDebugName, isNonNullable as isNonNullable2 } from "@dxos/util";
989
+ import { scheduleTask, yieldOrContinue } from "#scheduler";
990
+ var __dxlog_file3 = "/__w/dxos/dxos/packages/sdk/app-graph/src/graph-builder.ts";
991
+ var GraphBuilderTypeId = /* @__PURE__ */ Symbol.for("@dxos/app-graph/GraphBuilder");
891
992
  var GraphBuilderImpl = class {
892
993
  [GraphBuilderTypeId] = GraphBuilderTypeId;
893
994
  pipe() {
894
995
  return Pipeable2.pipeArguments(this, arguments);
895
996
  }
896
997
  // TODO(wittjosiah): Use Context.
998
+ /** Active subscriptions keyed by composite ID, cleaned up on node removal. */
897
999
  _subscriptions = /* @__PURE__ */ new Map();
898
- _extensions = Atom3.make(Record2.empty()).pipe(Atom3.keepAlive, Atom3.withLabel("graph-builder:extensions"));
1000
+ /** Connector updates pending flush, keyed by connector key. */
1001
+ _dirtyConnectors = /* @__PURE__ */ new Map();
1002
+ /** Last-flushed node IDs per connector key, used for edge removal on update. */
1003
+ _connectorPrevious = /* @__PURE__ */ new Map();
1004
+ /** All inline-descendant IDs per connector key, used to remove stale inline nodes on update. */
1005
+ _connectorPreviousInlineIds = /* @__PURE__ */ new Map();
1006
+ /** Last-flushed node args per connector key, used for change detection. */
1007
+ _connectorPreviousArgs = /* @__PURE__ */ new Map();
1008
+ /** Whether a dirty-flush task is already scheduled. */
1009
+ _flushScheduled = false;
1010
+ /** Resolves when the current flush completes. */
1011
+ _flushPromise = Promise.resolve();
1012
+ /** Registered builder extensions keyed by extension ID. */
1013
+ _extensions = Atom2.make(Record2.empty()).pipe(Atom2.keepAlive, Atom2.withLabel("graph-builder:extensions"));
1014
+ /** Triggers signalling that a node's resolver has fired at least once. */
899
1015
  _initialized = {};
1016
+ /** Shared atom registry for reactive subscriptions. */
900
1017
  _registry;
1018
+ /** Backing graph with internal accessors for node atoms and construction. */
901
1019
  _graph;
902
1020
  constructor({ registry, ...params } = {}) {
903
1021
  this._registry = registry ?? Registry2.make();
904
- const graph = make({
1022
+ const graph = make2({
905
1023
  ...params,
906
1024
  registry: this._registry,
907
- onExpand: (id, relation) => this._onExpand(id, relation),
1025
+ onExpand: (id, relation2) => this._onExpand(id, relation2),
908
1026
  onInitialize: (id) => this._onInitialize(id),
909
1027
  onRemoveNode: (id) => this._onRemoveNode(id)
910
1028
  });
@@ -916,130 +1034,181 @@ var GraphBuilderImpl = class {
916
1034
  get extensions() {
917
1035
  return this._extensions;
918
1036
  }
919
- _resolvers = Atom3.family((id) => {
920
- return Atom3.make((get2) => {
1037
+ /** Apply a set of node changes for a single connector key. */
1038
+ _applyConnectorUpdate(key, nodes, previous) {
1039
+ const { id, relation: relation2 } = relationFromConnectorKey(key);
1040
+ const ids = nodes.map((node) => node.id);
1041
+ const removed = previous.filter((pid) => !ids.includes(pid));
1042
+ this._connectorPrevious.set(key, ids);
1043
+ this._connectorPreviousArgs.set(key, nodes);
1044
+ const currentInlineIds = collectAllInlineIds(nodes);
1045
+ const previousInlineIds = this._connectorPreviousInlineIds.get(key) ?? [];
1046
+ const staleInlineIds = previousInlineIds.filter((pid) => !currentInlineIds.includes(pid));
1047
+ this._connectorPreviousInlineIds.set(key, currentInlineIds);
1048
+ removeNodes(this._graph, staleInlineIds, true);
1049
+ removeEdges(this._graph, removed.map((target) => ({
1050
+ source: id,
1051
+ target,
1052
+ relation: relation2
1053
+ })), true);
1054
+ addNodes(this._graph, nodes);
1055
+ addEdges(this._graph, nodes.map((node) => ({
1056
+ source: id,
1057
+ target: node.id,
1058
+ relation: relation2
1059
+ })));
1060
+ if (ids.length > 0) {
1061
+ const sortedIds = [
1062
+ ...nodes
1063
+ ].sort((a, b) => byPosition(a.properties ?? {}, b.properties ?? {})).map((n) => n.id);
1064
+ sortEdges(this._graph, id, relation2, sortedIds);
1065
+ }
1066
+ }
1067
+ _scheduleDirtyFlush() {
1068
+ if (!this._flushScheduled) {
1069
+ this._flushScheduled = true;
1070
+ this._flushPromise = scheduleTask(() => {
1071
+ this._flushScheduled = false;
1072
+ while (this._dirtyConnectors.size > 0) {
1073
+ const entries = [
1074
+ ...this._dirtyConnectors.entries()
1075
+ ];
1076
+ this._dirtyConnectors.clear();
1077
+ Atom2.batch(() => {
1078
+ for (const [key, { nodes, previous }] of entries) {
1079
+ this._applyConnectorUpdate(key, nodes, previous);
1080
+ }
1081
+ });
1082
+ }
1083
+ }, {
1084
+ strategy: "smooth"
1085
+ });
1086
+ }
1087
+ }
1088
+ _resolvers = Atom2.family((id) => {
1089
+ return Atom2.make((get2) => {
921
1090
  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);
922
1091
  });
923
1092
  });
924
- _connectors = Atom3.family((key) => {
925
- return Atom3.make((get2) => {
926
- const [id, relation] = key.split("+");
1093
+ _connectors = Atom2.family((key) => {
1094
+ return Atom2.make((get2) => {
1095
+ const { id, relation: relation2 } = relationFromConnectorKey(key);
927
1096
  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
- );
938
- }).pipe(Atom3.withLabel(`graph-builder:connectors:${key}`));
1097
+ const sourceNode = Option3.getOrElse(get2(node), () => void 0);
1098
+ if (!sourceNode) {
1099
+ return [];
1100
+ }
1101
+ const extensions = Function2.pipe(get2(this._extensions), Record2.values, Array2.sortBy(byPosition), Array2.filter((ext) => relationKey(ext.relation ?? "child") === relationKey(relation2) && ext.connector != null));
1102
+ const nodes = [];
1103
+ for (const ext of extensions) {
1104
+ const result = get2(ext.connector(node));
1105
+ nodes.push(...result);
1106
+ }
1107
+ return nodes;
1108
+ }).pipe(Atom2.withLabel(`graph-builder:connectors:${key}`));
939
1109
  });
940
- _onExpand(id, relation) {
1110
+ _onExpand(id, relation2) {
941
1111
  log2("onExpand", {
942
1112
  id,
943
- relation,
1113
+ relation: relation2,
944
1114
  registry: getDebugName(this._registry)
945
- }, {
946
- F: __dxlog_file2,
947
- L: 176,
948
- S: this,
949
- C: (f, a) => f(...a)
950
- });
951
- const connectors = this._connectors(`${id}+${relation}`);
952
- let previous = [];
953
- const cancel = this._registry.subscribe(connectors, (nodes) => {
1115
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file3, L: 136, S: this });
1116
+ this._expandRelation(id, relation2);
1117
+ if (relation2.kind === "child" && relation2.direction === "outbound") {
1118
+ expand(this._graph, id, "action");
1119
+ }
1120
+ }
1121
+ _expandRelation(id, relation2) {
1122
+ const key = connectorKey(id, relation2);
1123
+ const connectors = this._connectors(key);
1124
+ const cancel = this._registry.subscribe(connectors, (rawNodes) => {
1125
+ const nodes = qualifyNodeArgs(id)(rawNodes);
1126
+ const previous = this._connectorPrevious.get(key) ?? [];
954
1127
  const ids = nodes.map((n) => n.id);
955
- const removed = previous.filter((id2) => !ids.includes(id2));
956
- previous = ids;
1128
+ if (ids.length === previous.length && ids.every((nodeId, idx) => nodeId === previous[idx])) {
1129
+ const prevArgs = this._connectorPreviousArgs.get(key);
1130
+ if (prevArgs && nodeArgsUnchanged(prevArgs, nodes)) {
1131
+ return;
1132
+ }
1133
+ }
957
1134
  log2("update", {
958
1135
  id,
959
- relation,
960
- ids,
961
- removed
962
- }, {
963
- F: __dxlog_file2,
964
- L: 187,
965
- S: this,
966
- C: (f, a) => f(...a)
1136
+ relation: relation2,
1137
+ ids
1138
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file3, L: 160, S: this });
1139
+ this._dirtyConnectors.set(key, {
1140
+ nodes,
1141
+ previous
967
1142
  });
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
- }
1143
+ this._scheduleDirtyFlush();
990
1144
  }, {
991
1145
  immediate: true
992
1146
  });
993
- this._subscriptions.set(id, cancel);
1147
+ this._subscriptions.set(subscriptionKey(id, "expand", key), cancel);
994
1148
  }
995
- // TODO(wittjosiah): If the same node is added by a connector, the resolver should probably cancel itself?
996
1149
  async _onInitialize(id) {
997
1150
  log2("onInitialize", {
998
1151
  id
999
- }, {
1000
- F: __dxlog_file2,
1001
- L: 227,
1002
- S: this,
1003
- C: (f, a) => f(...a)
1004
- });
1152
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file3, L: 176, S: this });
1005
1153
  const resolver = this._resolvers(id);
1006
1154
  const cancel = this._registry.subscribe(resolver, (node) => {
1007
1155
  const trigger = this._initialized[id];
1156
+ const connectorOwned = [
1157
+ ...this._connectorPrevious.values()
1158
+ ].some((ids) => ids.includes(id));
1008
1159
  Option3.match(node, {
1009
1160
  onSome: (node2) => {
1010
- addNodes(this._graph, [
1011
- node2
1012
- ]);
1161
+ if (!connectorOwned) {
1162
+ addNodes(this._graph, [
1163
+ node2
1164
+ ]);
1165
+ const parentId = getParentId(id);
1166
+ if (parentId) {
1167
+ addEdges(this._graph, [
1168
+ {
1169
+ source: parentId,
1170
+ target: id,
1171
+ relation: "child"
1172
+ }
1173
+ ]);
1174
+ }
1175
+ }
1013
1176
  trigger?.wake();
1014
1177
  },
1015
1178
  onNone: () => {
1016
1179
  trigger?.wake();
1017
- removeNodes(this._graph, [
1018
- id
1019
- ]);
1180
+ if (!connectorOwned) {
1181
+ removeNodes(this._graph, [
1182
+ id
1183
+ ]);
1184
+ }
1020
1185
  }
1021
1186
  });
1022
1187
  }, {
1023
1188
  immediate: true
1024
1189
  });
1025
- this._subscriptions.set(id, cancel);
1190
+ this._subscriptions.set(subscriptionKey(id, "init"), cancel);
1026
1191
  }
1027
1192
  _onRemoveNode(id) {
1028
- this._subscriptions.get(id)?.();
1029
- this._subscriptions.delete(id);
1193
+ for (const [key, cleanup] of this._subscriptions) {
1194
+ if (primaryParts(key)[0] === id) {
1195
+ cleanup();
1196
+ this._subscriptions.delete(key);
1197
+ }
1198
+ }
1030
1199
  }
1031
1200
  };
1032
- var make2 = (params) => {
1201
+ var make3 = (params) => {
1033
1202
  return new GraphBuilderImpl(params);
1034
1203
  };
1035
1204
  var from = (pickle, registry) => {
1036
1205
  if (!pickle) {
1037
- return make2({
1206
+ return make3({
1038
1207
  registry
1039
1208
  });
1040
1209
  }
1041
1210
  const { nodes, edges } = JSON.parse(pickle);
1042
- return make2({
1211
+ return make3({
1043
1212
  nodes,
1044
1213
  edges,
1045
1214
  registry
@@ -1079,14 +1248,11 @@ function removeExtension(builderOrId, id) {
1079
1248
  }
1080
1249
  var exploreImpl = async (builder, options, path = []) => {
1081
1250
  const internal = builder;
1082
- const { registry = Registry2.make(), source = RootId, relation = "outbound", visitor } = options;
1251
+ const { registry = Registry2.make(), source = RootId, relation: relation2, visitor } = options;
1083
1252
  if (path.includes(source)) {
1084
1253
  return;
1085
1254
  }
1086
- if (!isNode()) {
1087
- const { yieldOrContinue } = await import("main-thread-scheduling");
1088
- await yieldOrContinue("idle");
1089
- }
1255
+ await yieldOrContinue("idle");
1090
1256
  const node = registry.get(internal._graph.nodeOrThrow(source));
1091
1257
  const shouldContinue = await visitor(node, [
1092
1258
  ...path,
@@ -1095,13 +1261,13 @@ var exploreImpl = async (builder, options, path = []) => {
1095
1261
  if (shouldContinue === false) {
1096
1262
  return;
1097
1263
  }
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))));
1264
+ 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
1265
  await Promise.all(nodes.map((nodeArg) => {
1100
1266
  registry.set(internal._graph._node(nodeArg.id), internal._graph._constructNode(nodeArg));
1101
1267
  return exploreImpl(builder, {
1102
1268
  registry,
1103
1269
  source: nodeArg.id,
1104
- relation,
1270
+ relation: relation2,
1105
1271
  visitor
1106
1272
  }, [
1107
1273
  ...path,
@@ -1137,13 +1303,17 @@ function destroy(builder) {
1137
1303
  return destroyImpl(builder);
1138
1304
  }
1139
1305
  }
1306
+ var flush = (builder) => {
1307
+ return builder._flushPromise;
1308
+ };
1140
1309
  var createExtensionRaw = (extension) => {
1141
- const { id, position = "static", relation = "outbound", resolver: _resolver, connector: _connector, actions: _actions, actionGroups: _actionGroups } = extension;
1310
+ const { id, position, relation: relation2 = "child", resolver: _resolver, connector: _connector, actions: _actions, actionGroups: _actionGroups } = extension;
1311
+ const normalizedRelation = normalizeRelation(relation2);
1142
1312
  const getId = (key) => `${id}/${key}`;
1143
- const resolver = _resolver && Atom3.family((id2) => _resolver(id2).pipe(Atom3.withLabel(`graph-builder:_resolver:${id2}`)));
1144
- const connector = _connector && Atom3.family((node) => _connector(node).pipe(Atom3.withLabel(`graph-builder:_connector:${id}`)));
1145
- const actionGroups = _actionGroups && Atom3.family((node) => _actionGroups(node).pipe(Atom3.withLabel(`graph-builder:_actionGroups:${id}`)));
1146
- const actions = _actions && Atom3.family((node) => _actions(node).pipe(Atom3.withLabel(`graph-builder:_actions:${id}`)));
1313
+ const resolver = _resolver && Atom2.family((id2) => _resolver(id2).pipe(Atom2.withLabel(`graph-builder:_resolver:${id2}`)));
1314
+ const connector = _connector && Atom2.family((node) => _connector(node).pipe(Atom2.withLabel(`graph-builder:_connector:${id}`)));
1315
+ const actionGroups = _actionGroups && Atom2.family((node) => _actionGroups(node).pipe(Atom2.withLabel(`graph-builder:_actionGroups:${id}`)));
1316
+ const actions = _actions && Atom2.family((node) => _actions(node).pipe(Atom2.withLabel(`graph-builder:_actions:${id}`)));
1147
1317
  return [
1148
1318
  resolver ? {
1149
1319
  id: getId("resolver"),
@@ -1153,8 +1323,8 @@ var createExtensionRaw = (extension) => {
1153
1323
  connector ? {
1154
1324
  id: getId("connector"),
1155
1325
  position,
1156
- relation,
1157
- connector: Atom3.family((node) => Atom3.make((get2) => {
1326
+ relation: normalizedRelation,
1327
+ connector: Atom2.family((node) => Atom2.make((get2) => {
1158
1328
  try {
1159
1329
  return get2(connector(node));
1160
1330
  } catch (error) {
@@ -1162,21 +1332,16 @@ var createExtensionRaw = (extension) => {
1162
1332
  id: getId("connector"),
1163
1333
  node,
1164
1334
  error
1165
- }, {
1166
- F: __dxlog_file2,
1167
- L: 509,
1168
- S: void 0,
1169
- C: (f, a) => f(...a)
1170
- });
1335
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file3, L: 383, S: void 0 });
1171
1336
  return [];
1172
1337
  }
1173
- }).pipe(Atom3.withLabel(`graph-builder:connector:${id}`)))
1338
+ }).pipe(Atom2.withLabel(`graph-builder:connector:${id}`)))
1174
1339
  } : void 0,
1175
1340
  actionGroups ? {
1176
1341
  id: getId("actionGroups"),
1177
1342
  position,
1178
- relation: "outbound",
1179
- connector: Atom3.family((node) => Atom3.make((get2) => {
1343
+ relation: actionRelation(),
1344
+ connector: Atom2.family((node) => Atom2.make((get2) => {
1180
1345
  try {
1181
1346
  return get2(actionGroups(node)).map((arg) => ({
1182
1347
  ...arg,
@@ -1188,21 +1353,16 @@ var createExtensionRaw = (extension) => {
1188
1353
  id: getId("actionGroups"),
1189
1354
  node,
1190
1355
  error
1191
- }, {
1192
- F: __dxlog_file2,
1193
- L: 530,
1194
- S: void 0,
1195
- C: (f, a) => f(...a)
1196
- });
1356
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file3, L: 404, S: void 0 });
1197
1357
  return [];
1198
1358
  }
1199
- }).pipe(Atom3.withLabel(`graph-builder:connector:actionGroups:${id}`)))
1359
+ }).pipe(Atom2.withLabel(`graph-builder:connector:actionGroups:${id}`)))
1200
1360
  } : void 0,
1201
1361
  actions ? {
1202
1362
  id: getId("actions"),
1203
1363
  position,
1204
- relation: "outbound",
1205
- connector: Atom3.family((node) => Atom3.make((get2) => {
1364
+ relation: actionRelation(),
1365
+ connector: Atom2.family((node) => Atom2.make((get2) => {
1206
1366
  try {
1207
1367
  return get2(actions(node)).map((arg) => ({
1208
1368
  ...arg,
@@ -1213,15 +1373,10 @@ var createExtensionRaw = (extension) => {
1213
1373
  id: getId("actions"),
1214
1374
  node,
1215
1375
  error
1216
- }, {
1217
- F: __dxlog_file2,
1218
- L: 547,
1219
- S: void 0,
1220
- C: (f, a) => f(...a)
1221
- });
1376
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file3, L: 424, S: void 0 });
1222
1377
  return [];
1223
1378
  }
1224
- }).pipe(Atom3.withLabel(`graph-builder:connector:actions:${id}`)))
1379
+ }).pipe(Atom2.withLabel(`graph-builder:connector:actions:${id}`)))
1225
1380
  } : void 0
1226
1381
  ].filter(isNonNullable2);
1227
1382
  };
@@ -1230,27 +1385,22 @@ var runEffectSyncWithFallback = (effect, context2, extensionId, fallback) => {
1230
1385
  log2.warn("Extension failed", {
1231
1386
  extension: extensionId,
1232
1387
  error
1233
- }, {
1234
- F: __dxlog_file2,
1235
- L: 590,
1236
- S: void 0,
1237
- C: (f, a) => f(...a)
1238
- });
1388
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file3, L: 441, S: void 0 });
1239
1389
  return Effect.succeed(fallback);
1240
1390
  })));
1241
1391
  };
1242
1392
  var createExtension = (options) => Effect.map(Effect.context(), (context2) => {
1243
- const { id, match: match3, actions, connector, resolver, relation, position } = options;
1393
+ const { id, match: match3, actions, connector, resolver, relation: relation2, position } = options;
1244
1394
  const connectorExtension = connector ? createConnectorWithRuntime(id, match3, connector, context2) : void 0;
1245
- 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) => ({
1395
+ const actionsExtension = actions ? (node) => Atom2.make((get2) => Function2.pipe(get2(node), Option3.flatMap(match3), Option3.map((matched) => runEffectSyncWithFallback(actions(matched, get2), context2, id, []).map((action) => ({
1246
1396
  ...action,
1247
1397
  // Attach captured context for action execution.
1248
1398
  _actionContext: context2
1249
1399
  }))), Option3.getOrElse(() => []))) : void 0;
1250
- const resolverExtension = resolver ? (nodeId) => Atom3.make((get2) => runEffectSyncWithFallback(resolver(nodeId, get2), context2, id, null) ?? null) : void 0;
1400
+ const resolverExtension = resolver ? (nodeId) => Atom2.make((get2) => runEffectSyncWithFallback(resolver(nodeId, get2), context2, id, null) ?? null) : void 0;
1251
1401
  return createExtensionRaw({
1252
1402
  id,
1253
- relation,
1403
+ relation: relation2,
1254
1404
  position,
1255
1405
  connector: connectorExtension,
1256
1406
  actions: actionsExtension,
@@ -1258,22 +1408,44 @@ var createExtension = (options) => Effect.map(Effect.context(), (context2) => {
1258
1408
  });
1259
1409
  });
1260
1410
  var createConnector = (matcher, factory) => {
1261
- return (node) => Atom3.make((get2) => Function2.pipe(get2(node), Option3.flatMap(matcher), Option3.map((data) => factory(data, get2)), Option3.getOrElse(() => [])));
1411
+ return (node) => Atom2.make((get2) => Function2.pipe(get2(node), Option3.flatMap(matcher), Option3.map((data) => factory(data, get2)), Option3.getOrElse(() => [])));
1262
1412
  };
1263
1413
  var createConnectorWithRuntime = (extensionId, matcher, factory, context2) => {
1264
- return (node) => Atom3.make((get2) => Function2.pipe(get2(node), Option3.flatMap(matcher), Option3.map((data) => runEffectSyncWithFallback(factory(data, get2), context2, extensionId, [])), Option3.getOrElse(() => [])));
1414
+ return (node) => Atom2.make((get2) => Function2.pipe(get2(node), Option3.flatMap(matcher), Option3.map((data) => runEffectSyncWithFallback(factory(data, get2), context2, extensionId, [])), Option3.getOrElse(() => [])));
1265
1415
  };
1266
1416
  var createTypeExtension = (options) => {
1267
- const { id, type, actions, connector, relation, position } = options;
1417
+ const { id, type, actions, connector, relation: relation2, position } = options;
1268
1418
  return createExtension({
1269
1419
  id,
1270
1420
  match: whenEchoType(type),
1271
1421
  actions,
1272
1422
  connector,
1273
- relation,
1423
+ relation: relation2,
1274
1424
  position
1275
1425
  });
1276
1426
  };
1427
+ var qualifyNodeArgs = (parentId) => (nodes) => nodes.map((node) => {
1428
+ validateSegmentId(node.id);
1429
+ const qualified = qualifyId(parentId, node.id);
1430
+ return {
1431
+ ...node,
1432
+ id: qualified,
1433
+ nodes: node.nodes ? qualifyNodeArgs(qualified)(node.nodes) : void 0
1434
+ };
1435
+ });
1436
+ var collectAllInlineIds = (nodes) => nodes.flatMap((node) => node.nodes ? [
1437
+ ...node.nodes.map((child) => child.id),
1438
+ ...collectAllInlineIds(node.nodes)
1439
+ ] : []);
1440
+ var connectorKey = (id, relation2) => primaryKey(id, relationKey(relation2));
1441
+ var relationFromConnectorKey = (key) => {
1442
+ const [id, encodedRelation] = primaryParts(key);
1443
+ return {
1444
+ id,
1445
+ relation: relationFromKey(encodedRelation)
1446
+ };
1447
+ };
1448
+ var subscriptionKey = (id, kind, detail) => detail != null ? primaryKey(id, kind, detail) : primaryKey(id, kind);
1277
1449
  var flattenExtensions = (extension, acc = []) => {
1278
1450
  if (Array2.isArray(extension)) {
1279
1451
  return [
@@ -1287,11 +1459,19 @@ var flattenExtensions = (extension, acc = []) => {
1287
1459
  ];
1288
1460
  }
1289
1461
  };
1462
+
1290
1463
  export {
1291
- atoms_exports as CreateAtom,
1292
- graph_exports as Graph,
1293
- graph_builder_exports as GraphBuilder,
1294
- node_exports as Node,
1295
- node_matcher_exports as NodeMatcher
1464
+ node_exports,
1465
+ qualifyId,
1466
+ getParentId,
1467
+ getSegmentId,
1468
+ getNode,
1469
+ expand,
1470
+ graph_exports,
1471
+ node_matcher_exports,
1472
+ make3 as make,
1473
+ addExtension,
1474
+ flush,
1475
+ graph_builder_exports
1296
1476
  };
1297
- //# sourceMappingURL=index.mjs.map
1477
+ //# sourceMappingURL=chunk-32XXJE6M.mjs.map