@dxos/app-graph 0.8.2-main.f081794 → 0.8.2-main.fbd8ed0

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 +541 -789
  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 +533 -780
  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 +541 -789
  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 +23 -16
  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 +209 -317
  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 +56 -77
  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,484 @@ 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.live)({
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.live)({
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
+ typeChanged,
256
+ dataChanged,
257
+ propertiesChanged
258
+ }, {
259
+ F: __dxlog_file,
260
+ L: 417,
261
+ S: this,
262
+ C: (f, a) => f(...a)
263
+ });
264
+ if (typeChanged || dataChanged || propertiesChanged) {
265
+ (0, import_log.log)("updating node", {
266
+ id,
267
+ type,
268
+ data,
269
+ properties
270
+ }, {
271
+ F: __dxlog_file,
272
+ L: 419,
273
+ S: this,
274
+ C: (f, a) => f(...a)
275
+ });
276
+ const newNode = import_effect.Option.some({
277
+ ...node2,
278
+ type,
279
+ data,
280
+ properties: {
281
+ ...node2.properties,
282
+ ...properties
283
+ }
284
+ });
285
+ this._registry.set(nodeRx, newNode);
286
+ this.onNodeChanged.emit({
287
+ id,
288
+ node: newNode
289
+ });
290
+ }
291
+ },
292
+ onNone: () => {
293
+ (0, import_log.log)("new node", {
294
+ id,
295
+ type,
296
+ data,
297
+ properties
298
+ }, {
299
+ F: __dxlog_file,
300
+ L: 426,
301
+ S: this,
302
+ C: (f, a) => f(...a)
303
+ });
304
+ const newNode = this._constructNode({
305
+ id,
306
+ type,
307
+ data,
308
+ properties
309
+ });
310
+ this._registry.set(nodeRx, newNode);
311
+ this.onNodeChanged.emit({
312
+ id,
313
+ node: newNode
314
+ });
315
+ }
316
+ });
317
+ if (nodes) {
318
+ this.addNodes(nodes);
319
+ const _edges = nodes.map((node2) => ({
320
+ source: id,
321
+ target: node2.id
322
+ }));
323
+ this.addEdges(_edges);
324
+ }
325
+ if (edges) {
326
+ (0, import_debug.todo)();
327
+ }
328
+ }
329
+ removeNodes(ids, edges = false) {
330
+ import_rx_react.Rx.batch(() => {
331
+ ids.map((id) => this.removeNode(id, edges));
219
332
  });
220
333
  }
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);
334
+ removeNode(id, edges = false) {
335
+ const nodeRx = this._node(id);
336
+ this._registry.set(nodeRx, import_effect.Option.none());
337
+ this.onNodeChanged.emit({
338
+ id,
339
+ node: import_effect.Option.none()
340
+ });
341
+ if (edges) {
342
+ const { inbound, outbound } = this._registry.get(this._edges(id));
343
+ const edges2 = [
344
+ ...inbound.map((source) => ({
345
+ source,
346
+ target: id
347
+ })),
348
+ ...outbound.map((target) => ({
349
+ source: id,
350
+ target
351
+ }))
352
+ ];
353
+ this.removeEdges(edges2);
231
354
  }
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;
355
+ this._onRemoveNode?.(id);
356
+ }
357
+ addEdges(edges) {
358
+ import_rx_react.Rx.batch(() => {
359
+ edges.map((edge) => this.addEdge(edge));
360
+ });
361
+ }
362
+ addEdge(edgeArg) {
363
+ const sourceRx = this._edges(edgeArg.source);
364
+ const source = this._registry.get(sourceRx);
365
+ if (!source.outbound.includes(edgeArg.target)) {
366
+ (0, import_log.log)("add outbound edge", {
367
+ source: edgeArg.source,
368
+ target: edgeArg.target
369
+ }, {
370
+ F: __dxlog_file,
371
+ L: 481,
372
+ S: this,
373
+ C: (f, a) => f(...a)
374
+ });
375
+ this._registry.set(sourceRx, {
376
+ inbound: source.inbound,
377
+ outbound: [
378
+ ...source.outbound,
379
+ edgeArg.target
380
+ ]
381
+ });
248
382
  }
249
- if (timeout === void 0) {
250
- return trigger.wait();
251
- } else {
252
- return (0, import_async.asyncTimeout)(trigger.wait(), timeout, `Node not found: ${id}`);
383
+ const targetRx = this._edges(edgeArg.target);
384
+ const target = this._registry.get(targetRx);
385
+ if (!target.inbound.includes(edgeArg.source)) {
386
+ (0, import_log.log)("add inbound edge", {
387
+ source: edgeArg.source,
388
+ target: edgeArg.target
389
+ }, {
390
+ F: __dxlog_file,
391
+ L: 488,
392
+ S: this,
393
+ C: (f, a) => f(...a)
394
+ });
395
+ this._registry.set(targetRx, {
396
+ inbound: [
397
+ ...target.inbound,
398
+ edgeArg.source
399
+ ],
400
+ outbound: target.outbound
401
+ });
253
402
  }
254
403
  }
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
404
+ removeEdges(edges, removeOrphans = false) {
405
+ import_rx_react.Rx.batch(() => {
406
+ edges.map((edge) => this.removeEdge(edge, removeOrphans));
265
407
  });
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
408
  }
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;
409
+ removeEdge(edgeArg, removeOrphans = false) {
410
+ const sourceRx = this._edges(edgeArg.source);
411
+ const source = this._registry.get(sourceRx);
412
+ if (source.outbound.includes(edgeArg.target)) {
413
+ this._registry.set(sourceRx, {
414
+ inbound: source.inbound,
415
+ outbound: source.outbound.filter((id) => id !== edgeArg.target)
416
+ });
297
417
  }
418
+ const targetRx = this._edges(edgeArg.target);
419
+ const target = this._registry.get(targetRx);
420
+ if (target.inbound.includes(edgeArg.source)) {
421
+ this._registry.set(targetRx, {
422
+ inbound: target.inbound.filter((id) => id !== edgeArg.source),
423
+ outbound: target.outbound
424
+ });
425
+ }
426
+ if (removeOrphans) {
427
+ const source2 = this._registry.get(sourceRx);
428
+ const target2 = this._registry.get(targetRx);
429
+ if (source2.outbound.length === 0 && source2.inbound.length === 0 && edgeArg.source !== ROOT_ID) {
430
+ this.removeNodes([
431
+ edgeArg.source
432
+ ]);
433
+ }
434
+ if (target2.outbound.length === 0 && target2.inbound.length === 0 && edgeArg.target !== ROOT_ID) {
435
+ this.removeNodes([
436
+ edgeArg.target
437
+ ]);
438
+ }
439
+ }
440
+ }
441
+ sortEdges(id, relation, order) {
442
+ const edgesRx = this._edges(id);
443
+ const edges = this._registry.get(edgesRx);
444
+ const unsorted = edges[relation].filter((id2) => !order.includes(id2)) ?? [];
445
+ const sorted = order.filter((id2) => edges[relation].includes(id2)) ?? [];
446
+ edges[relation].splice(0, edges[relation].length, ...[
447
+ ...sorted,
448
+ ...unsorted
449
+ ]);
450
+ this._registry.set(edgesRx, edges);
298
451
  }
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)) {
452
+ traverse({ visitor, source = ROOT_ID, relation = "outbound" }, path = []) {
453
+ if (path.includes(source)) {
311
454
  return;
312
455
  }
456
+ const node = this.getNodeOrThrow(source);
313
457
  const shouldContinue = visitor(node, [
314
458
  ...path,
315
- node.id
459
+ source
316
460
  ]);
317
461
  if (shouldContinue === false) {
318
462
  return;
319
463
  }
320
- Object.values(this._getNodes({
321
- node,
322
- relation,
323
- expansion
324
- })).forEach((child) => this.traverse({
325
- node: child,
464
+ Object.values(this.getConnections(source, relation)).forEach((child) => this.traverse({
465
+ source: child.id,
326
466
  relation,
327
- visitor,
328
- expansion
467
+ visitor
329
468
  }, [
330
469
  ...path,
331
- node.id
470
+ source
332
471
  ]));
333
472
  }
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
473
  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;
474
+ return (0, import_effect.pipe)(this.getNode(source), import_effect.Option.flatMap((node) => {
475
+ let found = import_effect.Option.none();
476
+ this.traverse({
477
+ source: node.id,
478
+ visitor: (node2, path) => {
479
+ if (import_effect.Option.isSome(found)) {
480
+ return false;
481
+ }
482
+ if (node2.id === target) {
483
+ found = import_effect.Option.some(path);
484
+ }
383
485
  }
384
- }
385
- });
386
- return found;
486
+ });
487
+ return found;
488
+ }));
387
489
  }
388
- /**
389
- * Wait for the path between two nodes in the graph to be established.
390
- */
391
490
  async waitForPath(params, { timeout = 5e3, interval = 500 } = {}) {
392
491
  const path = this.getPath(params);
393
- if (path) {
394
- return path;
492
+ if (import_effect.Option.isSome(path)) {
493
+ return path.value;
395
494
  }
396
495
  const trigger = new import_async.Trigger();
397
496
  const i = setInterval(() => {
398
497
  const path2 = this.getPath(params);
399
- if (path2) {
400
- trigger.wake(path2);
498
+ if (import_effect.Option.isSome(path2)) {
499
+ trigger.wake(path2.value);
401
500
  }
402
501
  }, interval);
403
502
  return trigger.wait({
404
503
  timeout
405
504
  }).finally(() => clearInterval(i));
406
505
  }
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.live)({
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.live)({
524
- inbound: [],
525
- outbound: []
526
- });
527
- }
528
- if (!this._edges[target]) {
529
- this._edges[target] = (0, import_live_object.live)({
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
- });
506
+ /** @internal */
507
+ _constructNode(node) {
508
+ return import_effect.Option.some({
509
+ [graphSymbol]: this,
510
+ data: null,
511
+ properties: {},
512
+ ...node
596
513
  });
597
514
  }
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
515
  };
516
+ var isGraphNode = (data) => data && typeof data === "object" && "id" in data && "properties" in data && data.properties ? typeof data.properties === "object" && "data" in data : false;
517
+ var isAction = (data) => isGraphNode(data) ? typeof data.data === "function" : false;
518
+ var actionGroupSymbol = Symbol("ActionGroup");
519
+ var isActionGroup = (data) => isGraphNode(data) ? data.data === actionGroupSymbol : false;
520
+ var isActionLike = (data) => isAction(data) || isActionGroup(data);
610
521
  var __dxlog_file2 = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph-builder.ts";
611
- var NODE_RESOLVER_TIMEOUT = 1e3;
612
522
  var createExtension = (extension) => {
613
- const { id, position = "static", resolver, connector, actions, actionGroups, ...rest } = extension;
523
+ const { id, position = "static", relation = "outbound", connector, actions: _actions, actionGroups: _actionGroups } = extension;
614
524
  const getId = (key) => `${id}/${key}`;
525
+ const actionGroups = _actionGroups && import_rx_react2.Rx.family((node) => _actionGroups(node).pipe(import_rx_react2.Rx.withLabel(`graph-builder:actionGroups:${id}`)));
526
+ const actions = _actions && import_rx_react2.Rx.family((node) => _actions(node).pipe(import_rx_react2.Rx.withLabel(`graph-builder:actions:${id}`)));
615
527
  return [
616
- resolver ? {
617
- id: getId("resolver"),
618
- position,
619
- resolver
620
- } : void 0,
528
+ // resolver ? { id: getId('resolver'), position, resolver } : undefined,
621
529
  connector ? {
622
- ...rest,
623
530
  id: getId("connector"),
624
531
  position,
625
- connector
532
+ relation,
533
+ connector: import_rx_react2.Rx.family((key) => connector(key).pipe(import_rx_react2.Rx.withLabel(`graph-builder:connector:${id}`)))
626
534
  } : void 0,
627
535
  actionGroups ? {
628
- ...rest,
629
536
  id: getId("actionGroups"),
630
537
  position,
631
- type: ACTION_GROUP_TYPE,
632
538
  relation: "outbound",
633
- connector: ({ node }) => actionGroups({
634
- node
635
- })?.map((arg) => ({
539
+ connector: import_rx_react2.Rx.family((node) => import_rx_react2.Rx.make((get) => get(actionGroups(node)).map((arg) => ({
636
540
  ...arg,
637
541
  data: actionGroupSymbol,
638
542
  type: ACTION_GROUP_TYPE
639
- }))
543
+ }))).pipe(import_rx_react2.Rx.withLabel(`graph-builder:connector:actionGroups:${id}`)))
640
544
  } : void 0,
641
545
  actions ? {
642
- ...rest,
643
546
  id: getId("actions"),
644
547
  position,
645
- type: ACTION_TYPE,
646
548
  relation: "outbound",
647
- connector: ({ node }) => actions({
648
- node
649
- })?.map((arg) => ({
549
+ connector: import_rx_react2.Rx.family((node) => import_rx_react2.Rx.make((get) => get(actions(node)).map((arg) => ({
650
550
  ...arg,
651
551
  type: ACTION_TYPE
652
- }))
552
+ }))).pipe(import_rx_react2.Rx.withLabel(`graph-builder:connector:actions:${id}`)))
653
553
  } : void 0
654
554
  ].filter(import_util2.isNonNullable);
655
555
  };
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
556
  var flattenExtensions = (extension, acc = []) => {
716
- if (Array.isArray(extension)) {
557
+ if (import_effect2.Array.isArray(extension)) {
717
558
  return [
718
559
  ...acc,
719
560
  ...extension.flatMap((ext) => flattenExtensions(ext, acc))
@@ -726,107 +567,74 @@ var flattenExtensions = (extension, acc = []) => {
726
567
  }
727
568
  };
728
569
  var GraphBuilder = class _GraphBuilder {
729
- constructor(params = {}) {
730
- this._dispatcher = new Dispatcher();
731
- this._extensions = (0, import_live_object2.live)({});
732
- this._resolverSubscriptions = /* @__PURE__ */ new Map();
570
+ constructor({ registry, ...params } = {}) {
733
571
  this._connectorSubscriptions = /* @__PURE__ */ new Map();
734
- this._nodeChanged = {};
735
- this._initialized = {};
572
+ 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"));
573
+ this._connectors = import_rx_react2.Rx.family((key) => {
574
+ return import_rx_react2.Rx.make((get) => {
575
+ const [id, relation] = key.split("+");
576
+ const node = this._graph.node(id);
577
+ return (0, import_effect2.pipe)(
578
+ get(this._extensions),
579
+ import_effect2.Record.values,
580
+ // TODO(wittjosiah): Sort on write rather than read.
581
+ import_effect2.Array.sortBy(import_util2.byPosition),
582
+ import_effect2.Array.filter(({ relation: _relation = "outbound" }) => _relation === relation),
583
+ import_effect2.Array.map(({ connector }) => connector?.(node)),
584
+ import_effect2.Array.filter(import_util2.isNonNullable),
585
+ import_effect2.Array.flatMap((result) => get(result))
586
+ );
587
+ }).pipe(import_rx_react2.Rx.withLabel(`graph-builder:connectors:${key}`));
588
+ });
589
+ this._registry = registry ?? import_rx_react2.Registry.make();
736
590
  this._graph = new Graph({
737
591
  ...params,
738
- onInitialNode: async (id) => this._onInitialNode(id),
739
- onInitialNodes: async (node, relation, type) => this._onInitialNodes(node, relation, type),
592
+ registry: this._registry,
593
+ onExpand: (id, relation) => this._onExpand(id, relation),
594
+ // onInitialize: (id) => this._onInitialize(id),
740
595
  onRemoveNode: (id) => this._onRemoveNode(id)
741
596
  });
742
597
  }
743
- static from(pickle) {
598
+ static from(pickle, registry) {
744
599
  if (!pickle) {
745
- return new _GraphBuilder();
600
+ return new _GraphBuilder({
601
+ registry
602
+ });
746
603
  }
747
604
  const { nodes, edges } = JSON.parse(pickle);
748
605
  return new _GraphBuilder({
749
606
  nodes,
750
- edges
607
+ edges,
608
+ registry
751
609
  });
752
610
  }
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
611
  get graph() {
782
612
  return this._graph;
783
613
  }
784
- /**
785
- * @reactive
786
- */
787
614
  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
- });
615
+ return this._extensions;
616
+ }
617
+ addExtension(extensions) {
618
+ flattenExtensions(extensions).forEach((extension) => {
619
+ const extensions2 = this._registry.get(this._extensions);
620
+ this._registry.set(this._extensions, import_effect2.Record.set(extensions2, extension.id, extension));
800
621
  });
801
622
  return this;
802
623
  }
803
- /**
804
- * Remove a node builder from the graph builder.
805
- */
806
624
  removeExtension(id) {
807
- (0, import_signals_core2.untracked)(() => {
808
- delete this._extensions[id];
809
- });
625
+ const extensions = this._registry.get(this._extensions);
626
+ this._registry.set(this._extensions, import_effect2.Record.remove(extensions, id));
810
627
  return this;
811
628
  }
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)) {
629
+ async explore({ registry = import_rx_react2.Registry.make(), source = ROOT_ID, relation = "outbound", visitor }, path = []) {
630
+ if (path.includes(source)) {
824
631
  return;
825
632
  }
826
633
  if (!(0, import_util2.isNode)()) {
827
634
  const { yieldOrContinue } = await import("main-thread-scheduling");
828
635
  await yieldOrContinue("idle");
829
636
  }
637
+ const node = registry.get(this._graph.nodeOrThrow(source));
830
638
  const shouldContinue = await visitor(node, [
831
639
  ...path,
832
640
  node.id
@@ -834,158 +642,104 @@ var GraphBuilder = class _GraphBuilder {
834
642
  if (shouldContinue === false) {
835
643
  return;
836
644
  }
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
- }
645
+ 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))));
646
+ await Promise.all(nodes.map((nodeArg) => {
647
+ registry.set(this._graph._node(nodeArg.id), this._graph._constructNode(nodeArg));
648
+ return this.explore({
649
+ registry,
650
+ source: nodeArg.id,
651
+ relation,
652
+ visitor
653
+ }, [
654
+ ...path,
655
+ node.id
656
+ ]);
914
657
  }));
658
+ if (registry !== this._registry) {
659
+ registry.reset();
660
+ registry.dispose();
661
+ }
662
+ }
663
+ destroy() {
664
+ this._connectorSubscriptions.forEach((unsubscribe) => unsubscribe());
665
+ this._connectorSubscriptions.clear();
915
666
  }
916
- _onInitialNodes(node, nodesRelation, nodesType) {
917
- this._nodeChanged[node.id] = this._nodeChanged[node.id] ?? (0, import_signals_core2.signal)({});
918
- let first = true;
667
+ _onExpand(id, relation) {
668
+ (0, import_log2.log)("onExpand", {
669
+ id,
670
+ relation,
671
+ registry: (0, import_util2.getDebugName)(this._registry)
672
+ }, {
673
+ F: __dxlog_file2,
674
+ L: 276,
675
+ S: this,
676
+ C: (f, a) => f(...a)
677
+ });
678
+ const connectors = this._connectors(`${id}+${relation}`);
919
679
  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
- }
680
+ const cancel = this._registry.subscribe(connectors, (nodes) => {
959
681
  const ids = nodes.map((n) => n.id);
960
- const removed = previous.filter((id) => !ids.includes(id));
682
+ const removed = previous.filter((id2) => !ids.includes(id2));
961
683
  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
- }
684
+ (0, import_log2.log)("update", {
685
+ id,
686
+ relation,
687
+ ids,
688
+ removed
689
+ }, {
690
+ F: __dxlog_file2,
691
+ L: 287,
692
+ S: this,
693
+ C: (f, a) => f(...a)
979
694
  });
980
- }));
695
+ import_rx_react2.Rx.batch(() => {
696
+ this._graph.removeEdges(removed.map((target) => ({
697
+ source: id,
698
+ target
699
+ })), true);
700
+ this._graph.addNodes(nodes);
701
+ this._graph.addEdges(nodes.map((node) => relation === "outbound" ? {
702
+ source: id,
703
+ target: node.id
704
+ } : {
705
+ source: node.id,
706
+ target: id
707
+ }));
708
+ this._graph.sortEdges(id, relation, nodes.map(({ id: id2 }) => id2));
709
+ });
710
+ }, {
711
+ immediate: true
712
+ });
713
+ this._connectorSubscriptions.set(id, cancel);
981
714
  }
982
- async _onRemoveNode(nodeId) {
983
- this._resolverSubscriptions.get(nodeId)?.();
984
- this._connectorSubscriptions.get(nodeId)?.();
985
- this._resolverSubscriptions.delete(nodeId);
986
- this._connectorSubscriptions.delete(nodeId);
715
+ // TODO(wittjosiah): On initialize to restore state from cache.
716
+ // private async _onInitialize(id: string) {
717
+ // log('onInitialize', { id });
718
+ // }
719
+ _onRemoveNode(id) {
720
+ this._connectorSubscriptions.get(id)?.();
721
+ this._connectorSubscriptions.delete(id);
987
722
  }
988
723
  };
724
+ var rxFromSignal = (cb) => {
725
+ return import_rx_react2.Rx.make((get) => {
726
+ const dispose = (0, import_signals_core.effect)(() => {
727
+ get.setSelf(cb());
728
+ });
729
+ get.addFinalizer(() => dispose());
730
+ return cb();
731
+ });
732
+ };
733
+ var observableFamily = import_rx_react2.Rx.family((observable) => {
734
+ return import_rx_react2.Rx.make((get) => {
735
+ const subscription = observable.subscribe((value) => get.setSelf(value));
736
+ get.addFinalizer(() => subscription.unsubscribe());
737
+ return observable.get();
738
+ });
739
+ });
740
+ var rxFromObservable = (observable) => {
741
+ return observableFamily(observable);
742
+ };
989
743
  // Annotate the CommonJS export names for ESM import in node:
990
744
  0 && (module.exports = {
991
745
  ACTION_GROUP_TYPE,
@@ -995,7 +749,6 @@ var GraphBuilder = class _GraphBuilder {
995
749
  ROOT_ID,
996
750
  ROOT_TYPE,
997
751
  actionGroupSymbol,
998
- cleanup,
999
752
  createExtension,
1000
753
  flattenExtensions,
1001
754
  getGraph,
@@ -1003,7 +756,7 @@ var GraphBuilder = class _GraphBuilder {
1003
756
  isActionGroup,
1004
757
  isActionLike,
1005
758
  isGraphNode,
1006
- memoize,
1007
- toSignal
759
+ rxFromObservable,
760
+ rxFromSignal
1008
761
  });
1009
762
  //# sourceMappingURL=index.cjs.map