@dxos/app-graph 0.8.2-staging.7ac8446 → 0.8.3-main.672df60

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 (33) hide show
  1. package/dist/lib/browser/index.mjs +593 -794
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +585 -785
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +593 -794
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/experimental/graph-projections.test.d.ts +25 -0
  11. package/dist/types/src/experimental/graph-projections.test.d.ts.map +1 -0
  12. package/dist/types/src/graph-builder.d.ts +48 -91
  13. package/dist/types/src/graph-builder.d.ts.map +1 -1
  14. package/dist/types/src/graph.d.ts +191 -98
  15. package/dist/types/src/graph.d.ts.map +1 -1
  16. package/dist/types/src/node.d.ts +3 -3
  17. package/dist/types/src/node.d.ts.map +1 -1
  18. package/dist/types/src/signals-integration.test.d.ts +2 -0
  19. package/dist/types/src/signals-integration.test.d.ts.map +1 -0
  20. package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
  21. package/dist/types/src/testing.d.ts +5 -0
  22. package/dist/types/src/testing.d.ts.map +1 -0
  23. package/dist/types/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +24 -17
  25. package/src/experimental/graph-projections.test.ts +56 -0
  26. package/src/graph-builder.test.ts +293 -310
  27. package/src/graph-builder.ts +235 -318
  28. package/src/graph.test.ts +314 -463
  29. package/src/graph.ts +452 -455
  30. package/src/node.ts +4 -4
  31. package/src/signals-integration.test.ts +218 -0
  32. package/src/stories/EchoGraph.stories.tsx +67 -76
  33. package/src/testing.ts +20 -0
@@ -1,26 +1,18 @@
1
1
  // packages/sdk/app-graph/src/graph.ts
2
- import { batch, effect, untracked } from "@preact/signals-core";
3
- import { asyncTimeout, Trigger } from "@dxos/async";
2
+ import { Registry, Rx } from "@effect-rx/rx-react";
3
+ import { Option, pipe, Record } from "effect";
4
+ import { Event, Trigger } from "@dxos/async";
5
+ import { todo } from "@dxos/debug";
4
6
  import { invariant } from "@dxos/invariant";
5
- import { create } from "@dxos/live-object";
6
7
  import { log } from "@dxos/log";
7
- import { isNonNullable, pick } from "@dxos/util";
8
-
9
- // packages/sdk/app-graph/src/node.ts
10
- var isGraphNode = (data) => data && typeof data === "object" && "id" in data && "properties" in data && data.properties ? typeof data.properties === "object" && "data" in data : false;
11
- var isAction = (data) => isGraphNode(data) ? typeof data.data === "function" : false;
12
- var actionGroupSymbol = Symbol("ActionGroup");
13
- var isActionGroup = (data) => isGraphNode(data) ? data.data === actionGroupSymbol : false;
14
- var isActionLike = (data) => isAction(data) || isActionGroup(data);
15
-
16
- // packages/sdk/app-graph/src/graph.ts
8
+ import { isNonNullable } from "@dxos/util";
17
9
  var __dxlog_file = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph.ts";
18
10
  var graphSymbol = Symbol("graph");
19
11
  var getGraph = (node) => {
20
12
  const graph = node[graphSymbol];
21
13
  invariant(graph, "Node is not associated with a graph.", {
22
14
  F: __dxlog_file,
23
- L: 21,
15
+ L: 25,
24
16
  S: void 0,
25
17
  A: [
26
18
  "graph",
@@ -33,649 +25,543 @@ var ROOT_ID = "root";
33
25
  var ROOT_TYPE = "dxos.org/type/GraphRoot";
34
26
  var ACTION_TYPE = "dxos.org/type/GraphAction";
35
27
  var ACTION_GROUP_TYPE = "dxos.org/type/GraphActionGroup";
36
- var DEFAULT_FILTER = (node) => untracked(() => !isActionLike(node));
37
- var Graph = class _Graph {
38
- constructor({ nodes, edges, onInitialNode, onInitialNodes, onRemoveNode } = {}) {
39
- this._waitingForNodes = {};
40
- this._initialized = {};
41
- /**
42
- * @internal
43
- */
44
- this._nodes = {};
45
- /**
46
- * @internal
47
- */
48
- this._edges = {};
49
- this._constructNode = (node) => {
50
- return create({
51
- ...node,
52
- [graphSymbol]: this
28
+ var Graph = class {
29
+ constructor({ registry, nodes, edges, onExpand, onRemoveNode } = {}) {
30
+ this.onNodeChanged = new Event();
31
+ this._expanded = Record.empty();
32
+ this._initialized = Record.empty();
33
+ this._initialEdges = Record.empty();
34
+ this._initialNodes = Record.fromEntries([
35
+ [
36
+ ROOT_ID,
37
+ this._constructNode({
38
+ id: ROOT_ID,
39
+ type: ROOT_TYPE,
40
+ data: null,
41
+ properties: {}
42
+ })
43
+ ]
44
+ ]);
45
+ /** @internal */
46
+ this._node = Rx.family((id) => {
47
+ const initial = Option.flatten(Record.get(this._initialNodes, id));
48
+ return Rx.make(initial).pipe(Rx.keepAlive, Rx.withLabel(`graph:node:${id}`));
49
+ });
50
+ this._nodeOrThrow = Rx.family((id) => {
51
+ return Rx.make((get) => {
52
+ const node = get(this._node(id));
53
+ invariant(Option.isSome(node), `Node not available: ${id}`, {
54
+ F: __dxlog_file,
55
+ L: 253,
56
+ S: this,
57
+ A: [
58
+ "Option.isSome(node)",
59
+ "`Node not available: ${id}`"
60
+ ]
61
+ });
62
+ return node.value;
53
63
  });
54
- };
55
- this._onInitialNode = onInitialNode;
56
- this._onInitialNodes = onInitialNodes;
57
- this._onRemoveNode = onRemoveNode;
58
- this._nodes[ROOT_ID] = this._constructNode({
59
- id: ROOT_ID,
60
- type: ROOT_TYPE,
61
- cacheable: [],
62
- properties: {},
63
- data: null
64
64
  });
65
+ this._edges = Rx.family((id) => {
66
+ const initial = Record.get(this._initialEdges, id).pipe(Option.getOrElse(() => ({
67
+ inbound: [],
68
+ outbound: []
69
+ })));
70
+ return Rx.make(initial).pipe(Rx.keepAlive, Rx.withLabel(`graph:edges:${id}`));
71
+ });
72
+ // NOTE: Currently the argument to the family needs to be referentially stable for the rx to be referentially stable.
73
+ // TODO(wittjosiah): Rx feature request, support for something akin to `ComplexMap` to allow for complex arguments.
74
+ this._connections = Rx.family((key) => {
75
+ return Rx.make((get) => {
76
+ const [id, relation] = key.split("$");
77
+ const edges = get(this._edges(id));
78
+ return edges[relation].map((id2) => get(this._node(id2))).filter(Option.isSome).map((o) => o.value);
79
+ }).pipe(Rx.withLabel(`graph:connections:${key}`));
80
+ });
81
+ this._actions = Rx.family((id) => {
82
+ return Rx.make((get) => {
83
+ return get(this._connections(`${id}$outbound`)).filter((node) => node.type === ACTION_TYPE || node.type === ACTION_GROUP_TYPE);
84
+ }).pipe(Rx.withLabel(`graph:actions:${id}`));
85
+ });
86
+ this._json = Rx.family((id) => {
87
+ return Rx.make((get) => {
88
+ const toJSON = (node, seen = []) => {
89
+ const nodes = get(this.connections(node.id));
90
+ const obj = {
91
+ id: node.id.length > 32 ? `${node.id.slice(0, 32)}...` : node.id,
92
+ type: node.type
93
+ };
94
+ if (node.properties.label) {
95
+ obj.label = node.properties.label;
96
+ }
97
+ if (nodes.length) {
98
+ obj.nodes = nodes.map((n) => {
99
+ const nextSeen = [
100
+ ...seen,
101
+ node.id
102
+ ];
103
+ return nextSeen.includes(n.id) ? void 0 : toJSON(n, nextSeen);
104
+ }).filter(isNonNullable);
105
+ }
106
+ return obj;
107
+ };
108
+ const root = get(this.nodeOrThrow(id));
109
+ return toJSON(root);
110
+ }).pipe(Rx.withLabel(`graph:json:${id}`));
111
+ });
112
+ this._registry = registry ?? Registry.make();
113
+ this._onExpand = onExpand;
114
+ this._onRemoveNode = onRemoveNode;
65
115
  if (nodes) {
66
116
  nodes.forEach((node) => {
67
- const cacheable = Object.keys(node.properties ?? {});
68
- if (node.type === ACTION_TYPE) {
69
- this._addNode({
70
- cacheable,
71
- data: () => log.warn("Pickled action invocation", void 0, {
72
- F: __dxlog_file,
73
- L: 113,
74
- S: this,
75
- C: (f, a) => f(...a)
76
- }),
77
- ...node
78
- });
79
- } else if (node.type === ACTION_GROUP_TYPE) {
80
- this._addNode({
81
- cacheable,
82
- data: actionGroupSymbol,
83
- ...node
84
- });
85
- } else {
86
- this._addNode({
87
- cacheable,
88
- ...node
89
- });
90
- }
117
+ Record.set(this._initialNodes, node.id, this._constructNode(node));
91
118
  });
92
119
  }
93
- this._edges[ROOT_ID] = create({
94
- inbound: [],
95
- outbound: []
96
- });
97
120
  if (edges) {
98
121
  Object.entries(edges).forEach(([source, edges2]) => {
99
- edges2.forEach((target) => {
100
- this._addEdge({
101
- source,
102
- target
103
- });
104
- });
105
- this._sortEdges(source, "outbound", edges2);
122
+ Record.set(this._initialEdges, source, edges2);
106
123
  });
107
124
  }
108
125
  }
109
- static from(pickle, options = {}) {
110
- const { nodes, edges } = JSON.parse(pickle);
111
- return new _Graph({
112
- nodes,
113
- edges,
114
- ...options
115
- });
126
+ toJSON(id = ROOT_ID) {
127
+ return this._registry.get(this._json(id));
128
+ }
129
+ json(id = ROOT_ID) {
130
+ return this._json(id);
131
+ }
132
+ node(id) {
133
+ return this._node(id);
134
+ }
135
+ nodeOrThrow(id) {
136
+ return this._nodeOrThrow(id);
137
+ }
138
+ connections(id, relation = "outbound") {
139
+ return this._connections(`${id}$${relation}`);
140
+ }
141
+ actions(id) {
142
+ return this._actions(id);
143
+ }
144
+ edges(id) {
145
+ return this._edges(id);
116
146
  }
117
- /**
118
- * Alias for `findNode('root')`.
119
- */
120
147
  get root() {
121
- return this.findNode(ROOT_ID);
122
- }
123
- /**
124
- * Convert the graph to a JSON object.
125
- */
126
- toJSON({ id = ROOT_ID, maxLength = 32 } = {}) {
127
- const toJSON = (node, seen = []) => {
128
- const nodes = this.nodes(node);
129
- const obj = {
130
- id: node.id.length > maxLength ? `${node.id.slice(0, maxLength - 3)}...` : node.id,
131
- type: node.type
132
- };
133
- if (node.properties.label) {
134
- obj.label = node.properties.label;
135
- }
136
- if (nodes.length) {
137
- obj.nodes = nodes.map((n) => {
138
- const nextSeen = [
139
- ...seen,
140
- node.id
141
- ];
142
- return nextSeen.includes(n.id) ? void 0 : toJSON(n, nextSeen);
143
- }).filter(isNonNullable);
144
- }
145
- return obj;
146
- };
147
- const root = this.findNode(id);
148
- invariant(root, `Node not found: ${id}`, {
148
+ return this.getNodeOrThrow(ROOT_ID);
149
+ }
150
+ getNode(id) {
151
+ return this._registry.get(this.node(id));
152
+ }
153
+ getNodeOrThrow(id) {
154
+ return this._registry.get(this.nodeOrThrow(id));
155
+ }
156
+ getConnections(id, relation = "outbound") {
157
+ return this._registry.get(this.connections(id, relation));
158
+ }
159
+ getActions(id) {
160
+ return this._registry.get(this.actions(id));
161
+ }
162
+ getEdges(id) {
163
+ return this._registry.get(this.edges(id));
164
+ }
165
+ // TODO(wittjosiah): On initialize to restore state from cache.
166
+ // async initialize(id: string) {
167
+ // const initialized = Record.get(this._initialized, id).pipe(Option.getOrElse(() => false));
168
+ // log('initialize', { id, initialized });
169
+ // if (!initialized) {
170
+ // await this._onInitialize?.(id);
171
+ // Record.set(this._initialized, id, true);
172
+ // }
173
+ // }
174
+ expand(id, relation = "outbound") {
175
+ const key = `${id}$${relation}`;
176
+ const expanded = Record.get(this._expanded, key).pipe(Option.getOrElse(() => false));
177
+ log("expand", {
178
+ key,
179
+ expanded
180
+ }, {
149
181
  F: __dxlog_file,
150
- L: 171,
182
+ L: 395,
151
183
  S: this,
152
- A: [
153
- "root",
154
- "`Node not found: ${id}`"
155
- ]
184
+ C: (f, a) => f(...a)
156
185
  });
157
- return toJSON(root);
158
- }
159
- pickle() {
160
- const nodes = Object.values(this._nodes).filter((node) => !!node.cacheable).map((node) => {
161
- return {
162
- id: node.id,
163
- type: node.type,
164
- properties: pick(node.properties, node.cacheable)
165
- };
186
+ if (!expanded) {
187
+ this._onExpand?.(id, relation);
188
+ Record.set(this._expanded, key, true);
189
+ }
190
+ }
191
+ addNodes(nodes) {
192
+ Rx.batch(() => {
193
+ nodes.map((node) => this.addNode(node));
166
194
  });
167
- const cacheable = new Set(nodes.map((node) => node.id));
168
- const edges = Object.fromEntries(Object.entries(this._edges).filter(([id]) => cacheable.has(id)).map(([id, { outbound }]) => [
169
- id,
170
- outbound.filter((nodeId) => cacheable.has(nodeId))
171
- ]).toSorted(([a], [b]) => a.localeCompare(b)));
172
- return JSON.stringify({
173
- nodes,
174
- edges
195
+ }
196
+ addNode({ nodes, edges, ...nodeArg }) {
197
+ const { id, type, data = null, properties = {} } = nodeArg;
198
+ const nodeRx = this._node(id);
199
+ const node = this._registry.get(nodeRx);
200
+ Option.match(node, {
201
+ onSome: (node2) => {
202
+ const typeChanged = node2.type !== type;
203
+ const dataChanged = node2.data !== data;
204
+ const propertiesChanged = Object.keys(properties).some((key) => node2.properties[key] !== properties[key]);
205
+ log("existing node", {
206
+ id,
207
+ typeChanged,
208
+ dataChanged,
209
+ propertiesChanged
210
+ }, {
211
+ F: __dxlog_file,
212
+ L: 417,
213
+ S: this,
214
+ C: (f, a) => f(...a)
215
+ });
216
+ if (typeChanged || dataChanged || propertiesChanged) {
217
+ log("updating node", {
218
+ id,
219
+ type,
220
+ data,
221
+ properties
222
+ }, {
223
+ F: __dxlog_file,
224
+ L: 419,
225
+ S: this,
226
+ C: (f, a) => f(...a)
227
+ });
228
+ const newNode = Option.some({
229
+ ...node2,
230
+ type,
231
+ data,
232
+ properties: {
233
+ ...node2.properties,
234
+ ...properties
235
+ }
236
+ });
237
+ this._registry.set(nodeRx, newNode);
238
+ this.onNodeChanged.emit({
239
+ id,
240
+ node: newNode
241
+ });
242
+ }
243
+ },
244
+ onNone: () => {
245
+ log("new node", {
246
+ id,
247
+ type,
248
+ data,
249
+ properties
250
+ }, {
251
+ F: __dxlog_file,
252
+ L: 426,
253
+ S: this,
254
+ C: (f, a) => f(...a)
255
+ });
256
+ const newNode = this._constructNode({
257
+ id,
258
+ type,
259
+ data,
260
+ properties
261
+ });
262
+ this._registry.set(nodeRx, newNode);
263
+ this.onNodeChanged.emit({
264
+ id,
265
+ node: newNode
266
+ });
267
+ }
175
268
  });
269
+ if (nodes) {
270
+ this.addNodes(nodes);
271
+ const _edges = nodes.map((node2) => ({
272
+ source: id,
273
+ target: node2.id
274
+ }));
275
+ this.addEdges(_edges);
276
+ }
277
+ if (edges) {
278
+ todo();
279
+ }
176
280
  }
177
- /**
178
- * Find the node with the given id in the graph.
179
- *
180
- * If a node is not found within the graph and an `onInitialNode` callback is provided,
181
- * it is called with the id and type of the node, potentially initializing the node.
182
- */
183
- findNode(id, expansion = true) {
184
- const existingNode = this._nodes[id];
185
- if (!existingNode && expansion) {
186
- void this._onInitialNode?.(id);
281
+ removeNodes(ids, edges = false) {
282
+ Rx.batch(() => {
283
+ ids.map((id) => this.removeNode(id, edges));
284
+ });
285
+ }
286
+ removeNode(id, edges = false) {
287
+ const nodeRx = this._node(id);
288
+ this._registry.set(nodeRx, Option.none());
289
+ this.onNodeChanged.emit({
290
+ id,
291
+ node: Option.none()
292
+ });
293
+ if (edges) {
294
+ const { inbound, outbound } = this._registry.get(this._edges(id));
295
+ const edges2 = [
296
+ ...inbound.map((source) => ({
297
+ source,
298
+ target: id
299
+ })),
300
+ ...outbound.map((target) => ({
301
+ source: id,
302
+ target
303
+ }))
304
+ ];
305
+ this.removeEdges(edges2);
187
306
  }
188
- return existingNode;
189
- }
190
- /**
191
- * Wait for a node to be added to the graph.
192
- *
193
- * If the node is already present in the graph, the promise resolves immediately.
194
- *
195
- * @param id The id of the node to wait for.
196
- * @param timeout The time in milliseconds to wait for the node to be added.
197
- */
198
- async waitForNode(id, timeout) {
199
- const trigger = this._waitingForNodes[id] ?? (this._waitingForNodes[id] = new Trigger());
200
- const node = this.findNode(id);
201
- if (node) {
202
- delete this._waitingForNodes[id];
203
- return node;
307
+ this._onRemoveNode?.(id);
308
+ }
309
+ addEdges(edges) {
310
+ Rx.batch(() => {
311
+ edges.map((edge) => this.addEdge(edge));
312
+ });
313
+ }
314
+ addEdge(edgeArg) {
315
+ const sourceRx = this._edges(edgeArg.source);
316
+ const source = this._registry.get(sourceRx);
317
+ if (!source.outbound.includes(edgeArg.target)) {
318
+ log("add outbound edge", {
319
+ source: edgeArg.source,
320
+ target: edgeArg.target
321
+ }, {
322
+ F: __dxlog_file,
323
+ L: 481,
324
+ S: this,
325
+ C: (f, a) => f(...a)
326
+ });
327
+ this._registry.set(sourceRx, {
328
+ inbound: source.inbound,
329
+ outbound: [
330
+ ...source.outbound,
331
+ edgeArg.target
332
+ ]
333
+ });
204
334
  }
205
- if (timeout === void 0) {
206
- return trigger.wait();
207
- } else {
208
- return asyncTimeout(trigger.wait(), timeout, `Node not found: ${id}`);
335
+ const targetRx = this._edges(edgeArg.target);
336
+ const target = this._registry.get(targetRx);
337
+ if (!target.inbound.includes(edgeArg.source)) {
338
+ log("add inbound edge", {
339
+ source: edgeArg.source,
340
+ target: edgeArg.target
341
+ }, {
342
+ F: __dxlog_file,
343
+ L: 488,
344
+ S: this,
345
+ C: (f, a) => f(...a)
346
+ });
347
+ this._registry.set(targetRx, {
348
+ inbound: [
349
+ ...target.inbound,
350
+ edgeArg.source
351
+ ],
352
+ outbound: target.outbound
353
+ });
209
354
  }
210
355
  }
211
- /**
212
- * Nodes that this node is connected to in default order.
213
- */
214
- nodes(node, options = {}) {
215
- const { relation, expansion, filter = DEFAULT_FILTER, type } = options;
216
- const nodes = this._getNodes({
217
- node,
218
- relation,
219
- expansion,
220
- type
356
+ removeEdges(edges, removeOrphans = false) {
357
+ Rx.batch(() => {
358
+ edges.map((edge) => this.removeEdge(edge, removeOrphans));
221
359
  });
222
- return nodes.filter((n) => filter(n, node));
223
- }
224
- /**
225
- * Edges that this node is connected to in default order.
226
- */
227
- edges(node, { relation = "outbound" } = {}) {
228
- return this._edges[node.id]?.[relation] ?? [];
229
- }
230
- /**
231
- * Actions or action groups that this node is connected to in default order.
232
- */
233
- actions(node, { expansion } = {}) {
234
- return [
235
- ...this._getNodes({
236
- node,
237
- expansion,
238
- type: ACTION_GROUP_TYPE
239
- }),
240
- ...this._getNodes({
241
- node,
242
- expansion,
243
- type: ACTION_TYPE
244
- })
245
- ];
246
360
  }
247
- async expand(node, relation = "outbound", type) {
248
- const key = this._key(node, relation, type);
249
- const initialized = this._initialized[key];
250
- if (!initialized && this._onInitialNodes) {
251
- await this._onInitialNodes(node, relation, type);
252
- this._initialized[key] = true;
361
+ removeEdge(edgeArg, removeOrphans = false) {
362
+ const sourceRx = this._edges(edgeArg.source);
363
+ const source = this._registry.get(sourceRx);
364
+ if (source.outbound.includes(edgeArg.target)) {
365
+ this._registry.set(sourceRx, {
366
+ inbound: source.inbound,
367
+ outbound: source.outbound.filter((id) => id !== edgeArg.target)
368
+ });
369
+ }
370
+ const targetRx = this._edges(edgeArg.target);
371
+ const target = this._registry.get(targetRx);
372
+ if (target.inbound.includes(edgeArg.source)) {
373
+ this._registry.set(targetRx, {
374
+ inbound: target.inbound.filter((id) => id !== edgeArg.source),
375
+ outbound: target.outbound
376
+ });
253
377
  }
378
+ if (removeOrphans) {
379
+ const source2 = this._registry.get(sourceRx);
380
+ const target2 = this._registry.get(targetRx);
381
+ if (source2.outbound.length === 0 && source2.inbound.length === 0 && edgeArg.source !== ROOT_ID) {
382
+ this.removeNodes([
383
+ edgeArg.source
384
+ ]);
385
+ }
386
+ if (target2.outbound.length === 0 && target2.inbound.length === 0 && edgeArg.target !== ROOT_ID) {
387
+ this.removeNodes([
388
+ edgeArg.target
389
+ ]);
390
+ }
391
+ }
392
+ }
393
+ sortEdges(id, relation, order) {
394
+ const edgesRx = this._edges(id);
395
+ const edges = this._registry.get(edgesRx);
396
+ const unsorted = edges[relation].filter((id2) => !order.includes(id2)) ?? [];
397
+ const sorted = order.filter((id2) => edges[relation].includes(id2)) ?? [];
398
+ edges[relation].splice(0, edges[relation].length, ...[
399
+ ...sorted,
400
+ ...unsorted
401
+ ]);
402
+ this._registry.set(edgesRx, edges);
254
403
  }
255
- _key(node, relation, type) {
256
- return `${node.id}-${relation}-${type}`;
257
- }
258
- /**
259
- * Recursive depth-first traversal of the graph.
260
- *
261
- * @param options.node The node to start traversing from.
262
- * @param options.relation The relation to traverse graph edges.
263
- * @param options.visitor A callback which is called for each node visited during traversal.
264
- */
265
- traverse({ visitor, node = this.root, relation = "outbound", expansion }, path = []) {
266
- if (path.includes(node.id)) {
404
+ traverse({ visitor, source = ROOT_ID, relation = "outbound" }, path = []) {
405
+ if (path.includes(source)) {
267
406
  return;
268
407
  }
408
+ const node = this.getNodeOrThrow(source);
269
409
  const shouldContinue = visitor(node, [
270
410
  ...path,
271
- node.id
411
+ source
272
412
  ]);
273
413
  if (shouldContinue === false) {
274
414
  return;
275
415
  }
276
- Object.values(this._getNodes({
277
- node,
278
- relation,
279
- expansion
280
- })).forEach((child) => this.traverse({
281
- node: child,
416
+ Object.values(this.getConnections(source, relation)).forEach((child) => this.traverse({
417
+ source: child.id,
282
418
  relation,
283
- visitor,
284
- expansion
419
+ visitor
285
420
  }, [
286
421
  ...path,
287
- node.id
422
+ source
288
423
  ]));
289
424
  }
290
- /**
291
- * Recursive depth-first traversal of the graph wrapping each visitor call in an effect.
292
- *
293
- * @param options.node The node to start traversing from.
294
- * @param options.relation The relation to traverse graph edges.
295
- * @param options.visitor A callback which is called for each node visited during traversal.
296
- */
297
- subscribeTraverse({ visitor, node = this.root, relation = "outbound", expansion }, currentPath = []) {
298
- return effect(() => {
299
- const path = [
300
- ...currentPath,
301
- node.id
302
- ];
303
- const result = visitor(node, path);
304
- if (result === false) {
305
- return;
306
- }
307
- const nodes = this._getNodes({
308
- node,
309
- relation,
310
- expansion
311
- });
312
- const nodeSubscriptions = nodes.map((n) => this.subscribeTraverse({
313
- node: n,
314
- visitor,
315
- expansion
316
- }, path));
317
- return () => {
318
- nodeSubscriptions.forEach((unsubscribe) => unsubscribe());
319
- };
320
- });
321
- }
322
- /**
323
- * Get the path between two nodes in the graph.
324
- */
325
425
  getPath({ source = "root", target }) {
326
- const start = this.findNode(source);
327
- if (!start) {
328
- return void 0;
329
- }
330
- let found;
331
- this.traverse({
332
- node: start,
333
- visitor: (node, path) => {
334
- if (found) {
335
- return false;
336
- }
337
- if (node.id === target) {
338
- found = path;
426
+ return pipe(this.getNode(source), Option.flatMap((node) => {
427
+ let found = Option.none();
428
+ this.traverse({
429
+ source: node.id,
430
+ visitor: (node2, path) => {
431
+ if (Option.isSome(found)) {
432
+ return false;
433
+ }
434
+ if (node2.id === target) {
435
+ found = Option.some(path);
436
+ }
339
437
  }
340
- }
341
- });
342
- return found;
438
+ });
439
+ return found;
440
+ }));
343
441
  }
344
- /**
345
- * Wait for the path between two nodes in the graph to be established.
346
- */
347
442
  async waitForPath(params, { timeout = 5e3, interval = 500 } = {}) {
348
443
  const path = this.getPath(params);
349
- if (path) {
350
- return path;
444
+ if (Option.isSome(path)) {
445
+ return path.value;
351
446
  }
352
447
  const trigger = new Trigger();
353
448
  const i = setInterval(() => {
354
449
  const path2 = this.getPath(params);
355
- if (path2) {
356
- trigger.wake(path2);
450
+ if (Option.isSome(path2)) {
451
+ trigger.wake(path2.value);
357
452
  }
358
453
  }, interval);
359
454
  return trigger.wait({
360
455
  timeout
361
456
  }).finally(() => clearInterval(i));
362
457
  }
363
- /**
364
- * Add nodes to the graph.
365
- *
366
- * @internal
367
- */
368
- _addNodes(nodes) {
369
- return batch(() => nodes.map((node) => this._addNode(node)));
370
- }
371
- _addNode({ nodes, edges, ..._node }) {
372
- return untracked(() => {
373
- const existingNode = this._nodes[_node.id];
374
- const node = existingNode ?? this._constructNode({
375
- data: null,
376
- properties: {},
377
- ..._node
378
- });
379
- if (existingNode) {
380
- const { data = null, properties, type } = _node;
381
- if (data !== node.data) {
382
- node.data = data;
383
- }
384
- if (type !== node.type) {
385
- node.type = type;
386
- }
387
- for (const key in properties) {
388
- if (properties[key] !== node.properties[key]) {
389
- node.properties[key] = properties[key];
390
- }
391
- }
392
- } else {
393
- this._nodes[node.id] = node;
394
- this._edges[node.id] = create({
395
- inbound: [],
396
- outbound: []
397
- });
398
- }
399
- const trigger = this._waitingForNodes[node.id];
400
- if (trigger) {
401
- trigger.wake(node);
402
- delete this._waitingForNodes[node.id];
403
- }
404
- if (nodes) {
405
- nodes.forEach((subNode) => {
406
- this._addNode(subNode);
407
- this._addEdge({
408
- source: node.id,
409
- target: subNode.id
410
- });
411
- });
412
- }
413
- if (edges) {
414
- edges.forEach(([id, relation]) => relation === "outbound" ? this._addEdge({
415
- source: node.id,
416
- target: id
417
- }) : this._addEdge({
418
- source: id,
419
- target: node.id
420
- }));
421
- }
422
- return node;
423
- });
424
- }
425
- /**
426
- * Remove nodes from the graph.
427
- *
428
- * @param ids The id of the node to remove.
429
- * @param edges Whether to remove edges connected to the node from the graph as well.
430
- * @internal
431
- */
432
- _removeNodes(ids, edges = false) {
433
- batch(() => ids.forEach((id) => this._removeNode(id, edges)));
434
- }
435
- _removeNode(id, edges = false) {
436
- untracked(() => {
437
- const node = this.findNode(id, false);
438
- if (!node) {
439
- return;
440
- }
441
- if (edges) {
442
- this._getNodes({
443
- node
444
- }).forEach((node2) => {
445
- this._removeEdge({
446
- source: id,
447
- target: node2.id
448
- });
449
- });
450
- this._getNodes({
451
- node,
452
- relation: "inbound"
453
- }).forEach((node2) => {
454
- this._removeEdge({
455
- source: node2.id,
456
- target: id
457
- });
458
- });
459
- delete this._edges[id];
460
- }
461
- delete this._nodes[id];
462
- Object.keys(this._initialized).filter((key) => key.startsWith(id)).forEach((key) => {
463
- delete this._initialized[key];
464
- });
465
- void this._onRemoveNode?.(id);
466
- });
467
- }
468
- /**
469
- * Add edges to the graph.
470
- *
471
- * @internal
472
- */
473
- _addEdges(edges) {
474
- batch(() => edges.forEach((edge) => this._addEdge(edge)));
475
- }
476
- _addEdge({ source, target }) {
477
- untracked(() => {
478
- if (!this._edges[source]) {
479
- this._edges[source] = create({
480
- inbound: [],
481
- outbound: []
482
- });
483
- }
484
- if (!this._edges[target]) {
485
- this._edges[target] = create({
486
- inbound: [],
487
- outbound: []
488
- });
489
- }
490
- const sourceEdges = this._edges[source];
491
- if (!sourceEdges.outbound.includes(target)) {
492
- sourceEdges.outbound.push(target);
493
- }
494
- const targetEdges = this._edges[target];
495
- if (!targetEdges.inbound.includes(source)) {
496
- targetEdges.inbound.push(source);
497
- }
498
- });
499
- }
500
- /**
501
- * Remove edges from the graph.
502
- * @internal
503
- */
504
- _removeEdges(edges, removeOrphans = false) {
505
- batch(() => edges.forEach((edge) => this._removeEdge(edge, removeOrphans)));
506
- }
507
- _removeEdge({ source, target }, removeOrphans = false) {
508
- untracked(() => {
509
- batch(() => {
510
- const outboundIndex = this._edges[source]?.outbound.findIndex((id) => id === target);
511
- if (outboundIndex !== void 0 && outboundIndex !== -1) {
512
- this._edges[source].outbound.splice(outboundIndex, 1);
513
- }
514
- const inboundIndex = this._edges[target]?.inbound.findIndex((id) => id === source);
515
- if (inboundIndex !== void 0 && inboundIndex !== -1) {
516
- this._edges[target].inbound.splice(inboundIndex, 1);
517
- }
518
- if (removeOrphans) {
519
- if (this._edges[source]?.outbound.length === 0 && this._edges[source]?.inbound.length === 0 && source !== ROOT_ID) {
520
- this._removeNode(source, true);
521
- }
522
- if (this._edges[target]?.outbound.length === 0 && this._edges[target]?.inbound.length === 0 && target !== ROOT_ID) {
523
- this._removeNode(target, true);
524
- }
525
- }
526
- });
527
- });
528
- }
529
- /**
530
- * Sort edges for a node.
531
- *
532
- * Edges not included in the sorted list are appended to the end of the list.
533
- *
534
- * @param nodeId The id of the node to sort edges for.
535
- * @param relation The relation of the edges from the node to sort.
536
- * @param edges The ordered list of edges.
537
- * @ignore
538
- */
539
- _sortEdges(nodeId, relation, edges) {
540
- untracked(() => {
541
- batch(() => {
542
- const current = this._edges[nodeId];
543
- if (current) {
544
- const unsorted = current[relation].filter((id) => !edges.includes(id)) ?? [];
545
- const sorted = edges.filter((id) => current[relation].includes(id)) ?? [];
546
- current[relation].splice(0, current[relation].length, ...[
547
- ...sorted,
548
- ...unsorted
549
- ]);
550
- }
551
- });
458
+ /** @internal */
459
+ _constructNode(node) {
460
+ return Option.some({
461
+ [graphSymbol]: this,
462
+ data: null,
463
+ properties: {},
464
+ ...node
552
465
  });
553
466
  }
554
- _getNodes({ node, relation = "outbound", type, expansion }) {
555
- if (expansion) {
556
- void this.expand(node, relation, type);
557
- }
558
- const edges = this._edges[node.id];
559
- if (!edges) {
560
- return [];
561
- } else {
562
- return edges[relation].map((id) => this._nodes[id]).filter(isNonNullable).filter((n) => !type || n.type === type);
563
- }
564
- }
565
467
  };
566
468
 
567
469
  // packages/sdk/app-graph/src/graph-builder.ts
568
- import { effect as effect2, signal, untracked as untracked2 } from "@preact/signals-core";
569
- import { Trigger as Trigger2 } from "@dxos/async";
570
- import { invariant as invariant2 } from "@dxos/invariant";
571
- import { create as create2 } from "@dxos/live-object";
470
+ import { Registry as Registry2, Rx as Rx2 } from "@effect-rx/rx-react";
471
+ import { effect } from "@preact/signals-core";
472
+ import { Array, pipe as pipe2, Record as Record2 } from "effect";
572
473
  import { log as log2 } from "@dxos/log";
573
- import { byPosition, isNode, isNonNullable as isNonNullable2 } from "@dxos/util";
474
+ import { byPosition, getDebugName, isNode, isNonNullable as isNonNullable2 } from "@dxos/util";
475
+
476
+ // packages/sdk/app-graph/src/node.ts
477
+ var isGraphNode = (data) => data && typeof data === "object" && "id" in data && "properties" in data && data.properties ? typeof data.properties === "object" && "data" in data : false;
478
+ var isAction = (data) => isGraphNode(data) ? typeof data.data === "function" : false;
479
+ var actionGroupSymbol = Symbol("ActionGroup");
480
+ var isActionGroup = (data) => isGraphNode(data) ? data.data === actionGroupSymbol : false;
481
+ var isActionLike = (data) => isAction(data) || isActionGroup(data);
482
+
483
+ // packages/sdk/app-graph/src/graph-builder.ts
574
484
  var __dxlog_file2 = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph-builder.ts";
575
- var NODE_RESOLVER_TIMEOUT = 1e3;
576
485
  var createExtension = (extension) => {
577
- const { id, position = "static", resolver, connector, actions, actionGroups, ...rest } = extension;
486
+ const { id, position = "static", relation = "outbound", connector: _connector, actions: _actions, actionGroups: _actionGroups } = extension;
578
487
  const getId = (key) => `${id}/${key}`;
488
+ const connector = _connector && Rx2.family((node) => _connector(node).pipe(Rx2.withLabel(`graph-builder:_connector:${id}`)));
489
+ const actionGroups = _actionGroups && Rx2.family((node) => _actionGroups(node).pipe(Rx2.withLabel(`graph-builder:_actionGroups:${id}`)));
490
+ const actions = _actions && Rx2.family((node) => _actions(node).pipe(Rx2.withLabel(`graph-builder:_actions:${id}`)));
579
491
  return [
580
- resolver ? {
581
- id: getId("resolver"),
582
- position,
583
- resolver
584
- } : void 0,
492
+ // resolver ? { id: getId('resolver'), position, resolver } : undefined,
585
493
  connector ? {
586
- ...rest,
587
494
  id: getId("connector"),
588
495
  position,
589
- connector
496
+ relation,
497
+ connector: Rx2.family((node) => Rx2.make((get) => {
498
+ try {
499
+ return get(connector(node));
500
+ } catch {
501
+ log2.warn("Error in connector", {
502
+ id: getId("connector"),
503
+ node
504
+ }, {
505
+ F: __dxlog_file2,
506
+ L: 101,
507
+ S: void 0,
508
+ C: (f, a) => f(...a)
509
+ });
510
+ return [];
511
+ }
512
+ }).pipe(Rx2.withLabel(`graph-builder:connector:${id}`)))
590
513
  } : void 0,
591
514
  actionGroups ? {
592
- ...rest,
593
515
  id: getId("actionGroups"),
594
516
  position,
595
- type: ACTION_GROUP_TYPE,
596
517
  relation: "outbound",
597
- connector: ({ node }) => actionGroups({
598
- node
599
- })?.map((arg) => ({
600
- ...arg,
601
- data: actionGroupSymbol,
602
- type: ACTION_GROUP_TYPE
603
- }))
518
+ connector: Rx2.family((node) => Rx2.make((get) => {
519
+ try {
520
+ return get(actionGroups(node)).map((arg) => ({
521
+ ...arg,
522
+ data: actionGroupSymbol,
523
+ type: ACTION_GROUP_TYPE
524
+ }));
525
+ } catch {
526
+ log2.warn("Error in actionGroups", {
527
+ id: getId("actionGroups"),
528
+ node
529
+ }, {
530
+ F: __dxlog_file2,
531
+ L: 122,
532
+ S: void 0,
533
+ C: (f, a) => f(...a)
534
+ });
535
+ return [];
536
+ }
537
+ }).pipe(Rx2.withLabel(`graph-builder:connector:actionGroups:${id}`)))
604
538
  } : void 0,
605
539
  actions ? {
606
- ...rest,
607
540
  id: getId("actions"),
608
541
  position,
609
- type: ACTION_TYPE,
610
542
  relation: "outbound",
611
- connector: ({ node }) => actions({
612
- node
613
- })?.map((arg) => ({
614
- ...arg,
615
- type: ACTION_TYPE
616
- }))
543
+ connector: Rx2.family((node) => Rx2.make((get) => {
544
+ try {
545
+ return get(actions(node)).map((arg) => ({
546
+ ...arg,
547
+ type: ACTION_TYPE
548
+ }));
549
+ } catch {
550
+ log2.warn("Error in actions", {
551
+ id: getId("actions"),
552
+ node
553
+ }, {
554
+ F: __dxlog_file2,
555
+ L: 139,
556
+ S: void 0,
557
+ C: (f, a) => f(...a)
558
+ });
559
+ return [];
560
+ }
561
+ }).pipe(Rx2.withLabel(`graph-builder:connector:actions:${id}`)))
617
562
  } : void 0
618
563
  ].filter(isNonNullable2);
619
564
  };
620
- var Dispatcher = class {
621
- constructor() {
622
- this.stateIndex = 0;
623
- this.state = {};
624
- this.cleanup = [];
625
- }
626
- };
627
- var BuilderInternal = class {
628
- };
629
- var memoize = (fn, key = "result") => {
630
- const dispatcher = BuilderInternal.currentDispatcher;
631
- invariant2(dispatcher?.currentExtension, "memoize must be called within an extension", {
632
- F: __dxlog_file2,
633
- L: 135,
634
- S: void 0,
635
- A: [
636
- "dispatcher?.currentExtension",
637
- "'memoize must be called within an extension'"
638
- ]
639
- });
640
- const all = dispatcher.state[dispatcher.currentExtension][dispatcher.stateIndex] ?? {};
641
- const current = all[key];
642
- const result = current ? current.result : fn();
643
- dispatcher.state[dispatcher.currentExtension][dispatcher.stateIndex] = {
644
- ...all,
645
- [key]: {
646
- result
647
- }
648
- };
649
- dispatcher.stateIndex++;
650
- return result;
651
- };
652
- var cleanup = (fn) => {
653
- memoize(() => {
654
- const dispatcher = BuilderInternal.currentDispatcher;
655
- invariant2(dispatcher, "cleanup must be called within an extension", {
656
- F: __dxlog_file2,
657
- L: 150,
658
- S: void 0,
659
- A: [
660
- "dispatcher",
661
- "'cleanup must be called within an extension'"
662
- ]
663
- });
664
- dispatcher.cleanup.push(fn);
665
- });
666
- };
667
- var toSignal = (subscribe, get, key) => {
668
- const thisSignal = memoize(() => {
669
- return signal(get());
670
- }, key);
671
- const unsubscribe = memoize(() => {
672
- return subscribe(() => thisSignal.value = get());
673
- }, key);
674
- cleanup(() => {
675
- unsubscribe();
676
- });
677
- return thisSignal.value;
678
- };
679
565
  var flattenExtensions = (extension, acc = []) => {
680
566
  if (Array.isArray(extension)) {
681
567
  return [
@@ -690,107 +576,75 @@ var flattenExtensions = (extension, acc = []) => {
690
576
  }
691
577
  };
692
578
  var GraphBuilder = class _GraphBuilder {
693
- constructor(params = {}) {
694
- this._dispatcher = new Dispatcher();
695
- this._extensions = create2({});
696
- this._resolverSubscriptions = /* @__PURE__ */ new Map();
579
+ constructor({ registry, ...params } = {}) {
580
+ // TODO(wittjosiah): Use Context.
697
581
  this._connectorSubscriptions = /* @__PURE__ */ new Map();
698
- this._nodeChanged = {};
699
- this._initialized = {};
582
+ this._extensions = Rx2.make(Record2.empty()).pipe(Rx2.keepAlive, Rx2.withLabel("graph-builder:extensions"));
583
+ this._connectors = Rx2.family((key) => {
584
+ return Rx2.make((get) => {
585
+ const [id, relation] = key.split("+");
586
+ const node = this._graph.node(id);
587
+ return pipe2(
588
+ get(this._extensions),
589
+ Record2.values,
590
+ // TODO(wittjosiah): Sort on write rather than read.
591
+ Array.sortBy(byPosition),
592
+ Array.filter(({ relation: _relation = "outbound" }) => _relation === relation),
593
+ Array.map(({ connector }) => connector?.(node)),
594
+ Array.filter(isNonNullable2),
595
+ Array.flatMap((result) => get(result))
596
+ );
597
+ }).pipe(Rx2.withLabel(`graph-builder:connectors:${key}`));
598
+ });
599
+ this._registry = registry ?? Registry2.make();
700
600
  this._graph = new Graph({
701
601
  ...params,
702
- onInitialNode: async (id) => this._onInitialNode(id),
703
- onInitialNodes: async (node, relation, type) => this._onInitialNodes(node, relation, type),
602
+ registry: this._registry,
603
+ onExpand: (id, relation) => this._onExpand(id, relation),
604
+ // onInitialize: (id) => this._onInitialize(id),
704
605
  onRemoveNode: (id) => this._onRemoveNode(id)
705
606
  });
706
607
  }
707
- static from(pickle) {
608
+ static from(pickle, registry) {
708
609
  if (!pickle) {
709
- return new _GraphBuilder();
610
+ return new _GraphBuilder({
611
+ registry
612
+ });
710
613
  }
711
614
  const { nodes, edges } = JSON.parse(pickle);
712
615
  return new _GraphBuilder({
713
616
  nodes,
714
- edges
617
+ edges,
618
+ registry
715
619
  });
716
620
  }
717
- /**
718
- * If graph is being restored from a pickle, the data will be null.
719
- * Initialize the data of each node by calling resolvers.
720
- * Wait until all of the initial nodes have resolved.
721
- */
722
- async initialize() {
723
- Object.keys(this._graph._nodes).filter((id) => id !== ROOT_ID).forEach((id) => this._initialized[id] = new Trigger2());
724
- Object.keys(this._graph._nodes).forEach((id) => this._onInitialNode(id));
725
- await Promise.all(Object.entries(this._initialized).map(async ([id, trigger]) => {
726
- try {
727
- await trigger.wait({
728
- timeout: NODE_RESOLVER_TIMEOUT
729
- });
730
- } catch {
731
- log2.error("node resolver timeout", {
732
- id
733
- }, {
734
- F: __dxlog_file2,
735
- L: 244,
736
- S: this,
737
- C: (f, a) => f(...a)
738
- });
739
- this.graph._removeNodes([
740
- id
741
- ]);
742
- }
743
- }));
744
- }
745
621
  get graph() {
746
622
  return this._graph;
747
623
  }
748
- /**
749
- * @reactive
750
- */
751
624
  get extensions() {
752
- return Object.values(this._extensions);
753
- }
754
- /**
755
- * Register a node builder which will be called in order to construct the graph.
756
- */
757
- addExtension(extension) {
758
- const extensions = flattenExtensions(extension);
759
- untracked2(() => {
760
- extensions.forEach((extension2) => {
761
- this._dispatcher.state[extension2.id] = [];
762
- this._extensions[extension2.id] = extension2;
763
- });
625
+ return this._extensions;
626
+ }
627
+ addExtension(extensions) {
628
+ flattenExtensions(extensions).forEach((extension) => {
629
+ const extensions2 = this._registry.get(this._extensions);
630
+ this._registry.set(this._extensions, Record2.set(extensions2, extension.id, extension));
764
631
  });
765
632
  return this;
766
633
  }
767
- /**
768
- * Remove a node builder from the graph builder.
769
- */
770
634
  removeExtension(id) {
771
- untracked2(() => {
772
- delete this._extensions[id];
773
- });
635
+ const extensions = this._registry.get(this._extensions);
636
+ this._registry.set(this._extensions, Record2.remove(extensions, id));
774
637
  return this;
775
638
  }
776
- destroy() {
777
- this._dispatcher.cleanup.forEach((fn) => fn());
778
- this._resolverSubscriptions.forEach((unsubscribe) => unsubscribe());
779
- this._connectorSubscriptions.forEach((unsubscribe) => unsubscribe());
780
- this._resolverSubscriptions.clear();
781
- this._connectorSubscriptions.clear();
782
- }
783
- /**
784
- * A graph traversal using just the connector extensions, without subscribing to any signals or persisting any nodes.
785
- */
786
- async explore({ node = this._graph.root, relation = "outbound", visitor }, path = []) {
787
- if (path.includes(node.id)) {
639
+ async explore({ registry = Registry2.make(), source = ROOT_ID, relation = "outbound", visitor }, path = []) {
640
+ if (path.includes(source)) {
788
641
  return;
789
642
  }
790
643
  if (!isNode()) {
791
644
  const { yieldOrContinue } = await import("main-thread-scheduling");
792
645
  await yieldOrContinue("idle");
793
646
  }
647
+ const node = registry.get(this._graph.nodeOrThrow(source));
794
648
  const shouldContinue = await visitor(node, [
795
649
  ...path,
796
650
  node.id
@@ -798,158 +652,104 @@ var GraphBuilder = class _GraphBuilder {
798
652
  if (shouldContinue === false) {
799
653
  return;
800
654
  }
801
- const nodes = Object.values(this._extensions).filter((extension) => relation === (extension.relation ?? "outbound")).filter((extension) => !extension.filter || extension.filter(node)).flatMap((extension) => {
802
- this._dispatcher.currentExtension = extension.id;
803
- this._dispatcher.stateIndex = 0;
804
- BuilderInternal.currentDispatcher = this._dispatcher;
805
- const result = extension.connector?.({
806
- node
807
- }) ?? [];
808
- BuilderInternal.currentDispatcher = void 0;
809
- return result;
810
- }).map((arg) => ({
811
- id: arg.id,
812
- type: arg.type,
813
- cacheable: arg.cacheable,
814
- data: arg.data ?? null,
815
- properties: arg.properties ?? {}
816
- }));
817
- await Promise.all(nodes.map((n) => this.explore({
818
- node: n,
819
- relation,
820
- visitor
821
- }, [
822
- ...path,
823
- node.id
824
- ])));
825
- }
826
- _onInitialNode(nodeId) {
827
- this._nodeChanged[nodeId] = this._nodeChanged[nodeId] ?? signal({});
828
- this._resolverSubscriptions.set(nodeId, effect2(() => {
829
- const extensions = Object.values(this._extensions).toSorted(byPosition);
830
- for (const { id, resolver } of extensions) {
831
- if (!resolver) {
832
- continue;
833
- }
834
- this._dispatcher.currentExtension = id;
835
- this._dispatcher.stateIndex = 0;
836
- BuilderInternal.currentDispatcher = this._dispatcher;
837
- let node;
838
- try {
839
- node = resolver({
840
- id: nodeId
841
- });
842
- } catch (err) {
843
- log2.catch(err, {
844
- extension: id
845
- }, {
846
- F: __dxlog_file2,
847
- L: 359,
848
- S: this,
849
- C: (f, a) => f(...a)
850
- });
851
- log2.error(`Previous error occurred in extension: ${id}`, void 0, {
852
- F: __dxlog_file2,
853
- L: 360,
854
- S: this,
855
- C: (f, a) => f(...a)
856
- });
857
- } finally {
858
- BuilderInternal.currentDispatcher = void 0;
859
- }
860
- const trigger = this._initialized[nodeId];
861
- if (node) {
862
- this.graph._addNodes([
863
- node
864
- ]);
865
- trigger?.wake();
866
- if (this._nodeChanged[node.id]) {
867
- this._nodeChanged[node.id].value = {};
868
- }
869
- break;
870
- } else if (node === false) {
871
- this.graph._removeNodes([
872
- nodeId
873
- ]);
874
- trigger?.wake();
875
- break;
876
- }
877
- }
655
+ const nodes = Object.values(this._registry.get(this._extensions)).filter((extension) => relation === (extension.relation ?? "outbound")).map((extension) => extension.connector).filter(isNonNullable2).flatMap((connector) => registry.get(connector(this._graph.node(source))));
656
+ await Promise.all(nodes.map((nodeArg) => {
657
+ registry.set(this._graph._node(nodeArg.id), this._graph._constructNode(nodeArg));
658
+ return this.explore({
659
+ registry,
660
+ source: nodeArg.id,
661
+ relation,
662
+ visitor
663
+ }, [
664
+ ...path,
665
+ node.id
666
+ ]);
878
667
  }));
668
+ if (registry !== this._registry) {
669
+ registry.reset();
670
+ registry.dispose();
671
+ }
879
672
  }
880
- _onInitialNodes(node, nodesRelation, nodesType) {
881
- this._nodeChanged[node.id] = this._nodeChanged[node.id] ?? signal({});
882
- let first = true;
673
+ destroy() {
674
+ this._connectorSubscriptions.forEach((unsubscribe) => unsubscribe());
675
+ this._connectorSubscriptions.clear();
676
+ }
677
+ _onExpand(id, relation) {
678
+ log2("onExpand", {
679
+ id,
680
+ relation,
681
+ registry: getDebugName(this._registry)
682
+ }, {
683
+ F: __dxlog_file2,
684
+ L: 301,
685
+ S: this,
686
+ C: (f, a) => f(...a)
687
+ });
688
+ const connectors = this._connectors(`${id}+${relation}`);
883
689
  let previous = [];
884
- this._connectorSubscriptions.set(node.id, effect2(() => {
885
- if (!first && !this._connectorSubscriptions.has(node.id)) {
886
- return;
887
- }
888
- first = false;
889
- Object.keys(this._extensions);
890
- this._nodeChanged[node.id].value;
891
- const nodes = [];
892
- const extensions = Object.values(this._extensions).toSorted(byPosition);
893
- for (const { id, connector, filter, type, relation = "outbound" } of extensions) {
894
- if (!connector || relation !== nodesRelation || nodesType && type !== nodesType || filter && !filter(node)) {
895
- continue;
896
- }
897
- this._dispatcher.currentExtension = id;
898
- this._dispatcher.stateIndex = 0;
899
- BuilderInternal.currentDispatcher = this._dispatcher;
900
- try {
901
- nodes.push(...connector({
902
- node
903
- }) ?? []);
904
- } catch (err) {
905
- log2.catch(err, {
906
- extension: id
907
- }, {
908
- F: __dxlog_file2,
909
- L: 421,
910
- S: this,
911
- C: (f, a) => f(...a)
912
- });
913
- log2.error(`Previous error occurred in extension: ${id}`, void 0, {
914
- F: __dxlog_file2,
915
- L: 422,
916
- S: this,
917
- C: (f, a) => f(...a)
918
- });
919
- } finally {
920
- BuilderInternal.currentDispatcher = void 0;
921
- }
922
- }
690
+ const cancel = this._registry.subscribe(connectors, (nodes) => {
923
691
  const ids = nodes.map((n) => n.id);
924
- const removed = previous.filter((id) => !ids.includes(id));
692
+ const removed = previous.filter((id2) => !ids.includes(id2));
925
693
  previous = ids;
926
- this.graph._removeEdges(removed.map((target) => ({
927
- source: node.id,
928
- target
929
- })), true);
930
- this.graph._addNodes(nodes);
931
- this.graph._addEdges(nodes.map(({ id }) => nodesRelation === "outbound" ? {
932
- source: node.id,
933
- target: id
934
- } : {
935
- source: id,
936
- target: node.id
937
- }));
938
- this.graph._sortEdges(node.id, nodesRelation, nodes.map(({ id }) => id));
939
- nodes.forEach((n) => {
940
- if (this._nodeChanged[n.id]) {
941
- this._nodeChanged[n.id].value = {};
942
- }
694
+ log2("update", {
695
+ id,
696
+ relation,
697
+ ids,
698
+ removed
699
+ }, {
700
+ F: __dxlog_file2,
701
+ L: 312,
702
+ S: this,
703
+ C: (f, a) => f(...a)
943
704
  });
944
- }));
705
+ Rx2.batch(() => {
706
+ this._graph.removeEdges(removed.map((target) => ({
707
+ source: id,
708
+ target
709
+ })), true);
710
+ this._graph.addNodes(nodes);
711
+ this._graph.addEdges(nodes.map((node) => relation === "outbound" ? {
712
+ source: id,
713
+ target: node.id
714
+ } : {
715
+ source: node.id,
716
+ target: id
717
+ }));
718
+ this._graph.sortEdges(id, relation, nodes.map(({ id: id2 }) => id2));
719
+ });
720
+ }, {
721
+ immediate: true
722
+ });
723
+ this._connectorSubscriptions.set(id, cancel);
945
724
  }
946
- async _onRemoveNode(nodeId) {
947
- this._resolverSubscriptions.get(nodeId)?.();
948
- this._connectorSubscriptions.get(nodeId)?.();
949
- this._resolverSubscriptions.delete(nodeId);
950
- this._connectorSubscriptions.delete(nodeId);
725
+ // TODO(wittjosiah): On initialize to restore state from cache.
726
+ // private async _onInitialize(id: string) {
727
+ // log('onInitialize', { id });
728
+ // }
729
+ _onRemoveNode(id) {
730
+ this._connectorSubscriptions.get(id)?.();
731
+ this._connectorSubscriptions.delete(id);
951
732
  }
952
733
  };
734
+ var rxFromSignal = (cb) => {
735
+ return Rx2.make((get) => {
736
+ const dispose = effect(() => {
737
+ get.setSelf(cb());
738
+ });
739
+ get.addFinalizer(() => dispose());
740
+ return cb();
741
+ });
742
+ };
743
+ var observableFamily = Rx2.family((observable) => {
744
+ return Rx2.make((get) => {
745
+ const subscription = observable.subscribe((value) => get.setSelf(value));
746
+ get.addFinalizer(() => subscription.unsubscribe());
747
+ return observable.get();
748
+ });
749
+ });
750
+ var rxFromObservable = (observable) => {
751
+ return observableFamily(observable);
752
+ };
953
753
  export {
954
754
  ACTION_GROUP_TYPE,
955
755
  ACTION_TYPE,
@@ -958,7 +758,6 @@ export {
958
758
  ROOT_ID,
959
759
  ROOT_TYPE,
960
760
  actionGroupSymbol,
961
- cleanup,
962
761
  createExtension,
963
762
  flattenExtensions,
964
763
  getGraph,
@@ -966,7 +765,7 @@ export {
966
765
  isActionGroup,
967
766
  isActionLike,
968
767
  isGraphNode,
969
- memoize,
970
- toSignal
768
+ rxFromObservable,
769
+ rxFromSignal
971
770
  };
972
771
  //# sourceMappingURL=index.mjs.map