@dxos/app-graph 0.8.2-staging.7ac8446 → 0.8.2

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