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

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 +789 -541
  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 +780 -533
  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 +789 -541
  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/graph-builder.d.ts +91 -48
  11. package/dist/types/src/graph-builder.d.ts.map +1 -1
  12. package/dist/types/src/graph.d.ts +98 -191
  13. package/dist/types/src/graph.d.ts.map +1 -1
  14. package/dist/types/src/node.d.ts +3 -3
  15. package/dist/types/src/node.d.ts.map +1 -1
  16. package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
  17. package/dist/types/tsconfig.tsbuildinfo +1 -1
  18. package/package.json +16 -23
  19. package/src/graph-builder.test.ts +310 -293
  20. package/src/graph-builder.ts +317 -209
  21. package/src/graph.test.ts +463 -314
  22. package/src/graph.ts +455 -452
  23. package/src/node.ts +4 -4
  24. package/src/stories/EchoGraph.stories.tsx +78 -57
  25. package/dist/types/src/experimental/graph-projections.test.d.ts +0 -25
  26. package/dist/types/src/experimental/graph-projections.test.d.ts.map +0 -1
  27. package/dist/types/src/signals-integration.test.d.ts +0 -2
  28. package/dist/types/src/signals-integration.test.d.ts.map +0 -1
  29. package/dist/types/src/testing.d.ts +0 -5
  30. package/dist/types/src/testing.d.ts.map +0 -1
  31. package/src/experimental/graph-projections.test.ts +0 -56
  32. package/src/signals-integration.test.ts +0 -218
  33. package/src/testing.ts +0 -20
@@ -35,6 +35,7 @@ __export(node_exports, {
35
35
  ROOT_ID: () => ROOT_ID,
36
36
  ROOT_TYPE: () => ROOT_TYPE,
37
37
  actionGroupSymbol: () => actionGroupSymbol,
38
+ cleanup: () => cleanup,
38
39
  createExtension: () => createExtension,
39
40
  flattenExtensions: () => flattenExtensions,
40
41
  getGraph: () => getGraph,
@@ -42,29 +43,34 @@ __export(node_exports, {
42
43
  isActionGroup: () => isActionGroup,
43
44
  isActionLike: () => isActionLike,
44
45
  isGraphNode: () => isGraphNode,
45
- rxFromObservable: () => rxFromObservable,
46
- rxFromSignal: () => rxFromSignal
46
+ memoize: () => memoize,
47
+ toSignal: () => toSignal
47
48
  });
48
49
  module.exports = __toCommonJS(node_exports);
49
- var import_rx_react = require("@effect-rx/rx-react");
50
- var import_effect = require("effect");
50
+ var import_signals_core = require("@preact/signals-core");
51
51
  var import_async = require("@dxos/async");
52
- var import_debug = require("@dxos/debug");
53
52
  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_rx_react2 = require("@effect-rx/rx-react");
57
- var import_signals_core = require("@preact/signals-core");
58
- var import_effect2 = require("effect");
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");
59
60
  var import_log2 = require("@dxos/log");
60
61
  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);
61
67
  var __dxlog_file = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph.ts";
62
68
  var graphSymbol = Symbol("graph");
63
69
  var getGraph = (node) => {
64
70
  const graph = node[graphSymbol];
65
71
  (0, import_invariant.invariant)(graph, "Node is not associated with a graph.", {
66
72
  F: __dxlog_file,
67
- L: 25,
73
+ L: 21,
68
74
  S: void 0,
69
75
  A: [
70
76
  "graph",
@@ -77,484 +83,637 @@ var ROOT_ID = "root";
77
83
  var ROOT_TYPE = "dxos.org/type/GraphRoot";
78
84
  var ACTION_TYPE = "dxos.org/type/GraphAction";
79
85
  var ACTION_GROUP_TYPE = "dxos.org/type/GraphActionGroup";
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;
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
114
97
  });
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;
98
+ };
99
+ this._onInitialNode = onInitialNode;
100
+ this._onInitialNodes = onInitialNodes;
163
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
+ });
164
109
  if (nodes) {
165
110
  nodes.forEach((node) => {
166
- import_effect.Record.set(this._initialNodes, node.id, this._constructNode(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
+ }
167
135
  });
168
136
  }
137
+ this._edges[ROOT_ID] = (0, import_live_object.create)({
138
+ inbound: [],
139
+ outbound: []
140
+ });
169
141
  if (edges) {
170
142
  Object.entries(edges).forEach(([source, edges2]) => {
171
- import_effect.Record.set(this._initialEdges, source, edges2);
143
+ edges2.forEach((target) => {
144
+ this._addEdge({
145
+ source,
146
+ target
147
+ });
148
+ });
149
+ this._sortEdges(source, "outbound", edges2);
172
150
  });
173
151
  }
174
152
  }
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);
153
+ static from(pickle, options = {}) {
154
+ const { nodes, edges } = JSON.parse(pickle);
155
+ return new _Graph({
156
+ nodes,
157
+ edges,
158
+ ...options
159
+ });
195
160
  }
161
+ /**
162
+ * Alias for `findNode('root')`.
163
+ */
196
164
  get root() {
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
- }, {
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}`, {
230
193
  F: __dxlog_file,
231
- L: 395,
194
+ L: 171,
232
195
  S: this,
233
- C: (f, a) => f(...a)
234
- });
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));
243
- });
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
- }
196
+ A: [
197
+ "root",
198
+ "`Node not found: ${id}`"
199
+ ]
316
200
  });
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));
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
+ };
332
210
  });
333
- }
334
- removeNode(id, edges = false) {
335
- const nodeRx = this._node(id);
336
- this._registry.set(nodeRx, import_effect.Option.none());
337
- this.onNodeChanged.emit({
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 }]) => [
338
213
  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);
354
- }
355
- this._onRemoveNode?.(id);
356
- }
357
- addEdges(edges) {
358
- import_rx_react.Rx.batch(() => {
359
- edges.map((edge) => this.addEdge(edge));
214
+ outbound.filter((nodeId) => cacheable.has(nodeId))
215
+ ]).toSorted(([a], [b]) => a.localeCompare(b)));
216
+ return JSON.stringify({
217
+ nodes,
218
+ edges
360
219
  });
361
220
  }
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
- });
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);
382
231
  }
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
- });
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;
248
+ }
249
+ if (timeout === void 0) {
250
+ return trigger.wait();
251
+ } else {
252
+ return (0, import_async.asyncTimeout)(trigger.wait(), timeout, `Node not found: ${id}`);
402
253
  }
403
254
  }
404
- removeEdges(edges, removeOrphans = false) {
405
- import_rx_react.Rx.batch(() => {
406
- edges.map((edge) => this.removeEdge(edge, removeOrphans));
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
407
265
  });
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
+ ];
408
290
  }
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
- });
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;
417
297
  }
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);
451
298
  }
452
- traverse({ visitor, source = ROOT_ID, relation = "outbound" }, path = []) {
453
- if (path.includes(source)) {
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)) {
454
311
  return;
455
312
  }
456
- const node = this.getNodeOrThrow(source);
457
313
  const shouldContinue = visitor(node, [
458
314
  ...path,
459
- source
315
+ node.id
460
316
  ]);
461
317
  if (shouldContinue === false) {
462
318
  return;
463
319
  }
464
- Object.values(this.getConnections(source, relation)).forEach((child) => this.traverse({
465
- source: child.id,
320
+ Object.values(this._getNodes({
321
+ node,
466
322
  relation,
467
- visitor
323
+ expansion
324
+ })).forEach((child) => this.traverse({
325
+ node: child,
326
+ relation,
327
+ visitor,
328
+ expansion
468
329
  }, [
469
330
  ...path,
470
- source
331
+ node.id
471
332
  ]));
472
333
  }
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
+ */
473
369
  getPath({ source = "root", target }) {
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
- }
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;
485
380
  }
486
- });
487
- return found;
488
- }));
381
+ if (node.id === target) {
382
+ found = path;
383
+ }
384
+ }
385
+ });
386
+ return found;
489
387
  }
388
+ /**
389
+ * Wait for the path between two nodes in the graph to be established.
390
+ */
490
391
  async waitForPath(params, { timeout = 5e3, interval = 500 } = {}) {
491
392
  const path = this.getPath(params);
492
- if (import_effect.Option.isSome(path)) {
493
- return path.value;
393
+ if (path) {
394
+ return path;
494
395
  }
495
396
  const trigger = new import_async.Trigger();
496
397
  const i = setInterval(() => {
497
398
  const path2 = this.getPath(params);
498
- if (import_effect.Option.isSome(path2)) {
499
- trigger.wake(path2.value);
399
+ if (path2) {
400
+ trigger.wake(path2);
500
401
  }
501
402
  }, interval);
502
403
  return trigger.wait({
503
404
  timeout
504
405
  }).finally(() => clearInterval(i));
505
406
  }
506
- /** @internal */
507
- _constructNode(node) {
508
- return import_effect.Option.some({
509
- [graphSymbol]: this,
510
- data: null,
511
- properties: {},
512
- ...node
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;
513
467
  });
514
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
+ });
596
+ });
597
+ }
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
+ }
515
609
  };
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);
521
610
  var __dxlog_file2 = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph-builder.ts";
611
+ var NODE_RESOLVER_TIMEOUT = 1e3;
522
612
  var createExtension = (extension) => {
523
- const { id, position = "static", relation = "outbound", connector, actions: _actions, actionGroups: _actionGroups } = extension;
613
+ const { id, position = "static", resolver, connector, actions, actionGroups, ...rest } = extension;
524
614
  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}`)));
527
615
  return [
528
- // resolver ? { id: getId('resolver'), position, resolver } : undefined,
616
+ resolver ? {
617
+ id: getId("resolver"),
618
+ position,
619
+ resolver
620
+ } : void 0,
529
621
  connector ? {
622
+ ...rest,
530
623
  id: getId("connector"),
531
624
  position,
532
- relation,
533
- connector: import_rx_react2.Rx.family((key) => connector(key).pipe(import_rx_react2.Rx.withLabel(`graph-builder:connector:${id}`)))
625
+ connector
534
626
  } : void 0,
535
627
  actionGroups ? {
628
+ ...rest,
536
629
  id: getId("actionGroups"),
537
630
  position,
631
+ type: ACTION_GROUP_TYPE,
538
632
  relation: "outbound",
539
- connector: import_rx_react2.Rx.family((node) => import_rx_react2.Rx.make((get) => get(actionGroups(node)).map((arg) => ({
633
+ connector: ({ node }) => actionGroups({
634
+ node
635
+ })?.map((arg) => ({
540
636
  ...arg,
541
637
  data: actionGroupSymbol,
542
638
  type: ACTION_GROUP_TYPE
543
- }))).pipe(import_rx_react2.Rx.withLabel(`graph-builder:connector:actionGroups:${id}`)))
639
+ }))
544
640
  } : void 0,
545
641
  actions ? {
642
+ ...rest,
546
643
  id: getId("actions"),
547
644
  position,
645
+ type: ACTION_TYPE,
548
646
  relation: "outbound",
549
- connector: import_rx_react2.Rx.family((node) => import_rx_react2.Rx.make((get) => get(actions(node)).map((arg) => ({
647
+ connector: ({ node }) => actions({
648
+ node
649
+ })?.map((arg) => ({
550
650
  ...arg,
551
651
  type: ACTION_TYPE
552
- }))).pipe(import_rx_react2.Rx.withLabel(`graph-builder:connector:actions:${id}`)))
652
+ }))
553
653
  } : void 0
554
654
  ].filter(import_util2.isNonNullable);
555
655
  };
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
+ };
556
715
  var flattenExtensions = (extension, acc = []) => {
557
- if (import_effect2.Array.isArray(extension)) {
716
+ if (Array.isArray(extension)) {
558
717
  return [
559
718
  ...acc,
560
719
  ...extension.flatMap((ext) => flattenExtensions(ext, acc))
@@ -567,74 +726,107 @@ var flattenExtensions = (extension, acc = []) => {
567
726
  }
568
727
  };
569
728
  var GraphBuilder = class _GraphBuilder {
570
- constructor({ registry, ...params } = {}) {
729
+ constructor(params = {}) {
730
+ this._dispatcher = new Dispatcher();
731
+ this._extensions = (0, import_live_object2.create)({});
732
+ this._resolverSubscriptions = /* @__PURE__ */ new Map();
571
733
  this._connectorSubscriptions = /* @__PURE__ */ new Map();
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();
734
+ this._nodeChanged = {};
735
+ this._initialized = {};
590
736
  this._graph = new Graph({
591
737
  ...params,
592
- registry: this._registry,
593
- onExpand: (id, relation) => this._onExpand(id, relation),
594
- // onInitialize: (id) => this._onInitialize(id),
738
+ onInitialNode: async (id) => this._onInitialNode(id),
739
+ onInitialNodes: async (node, relation, type) => this._onInitialNodes(node, relation, type),
595
740
  onRemoveNode: (id) => this._onRemoveNode(id)
596
741
  });
597
742
  }
598
- static from(pickle, registry) {
743
+ static from(pickle) {
599
744
  if (!pickle) {
600
- return new _GraphBuilder({
601
- registry
602
- });
745
+ return new _GraphBuilder();
603
746
  }
604
747
  const { nodes, edges } = JSON.parse(pickle);
605
748
  return new _GraphBuilder({
606
749
  nodes,
607
- edges,
608
- registry
750
+ edges
609
751
  });
610
752
  }
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
+ }
611
781
  get graph() {
612
782
  return this._graph;
613
783
  }
784
+ /**
785
+ * @reactive
786
+ */
614
787
  get extensions() {
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));
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
+ });
621
800
  });
622
801
  return this;
623
802
  }
803
+ /**
804
+ * Remove a node builder from the graph builder.
805
+ */
624
806
  removeExtension(id) {
625
- const extensions = this._registry.get(this._extensions);
626
- this._registry.set(this._extensions, import_effect2.Record.remove(extensions, id));
807
+ (0, import_signals_core2.untracked)(() => {
808
+ delete this._extensions[id];
809
+ });
627
810
  return this;
628
811
  }
629
- async explore({ registry = import_rx_react2.Registry.make(), source = ROOT_ID, relation = "outbound", visitor }, path = []) {
630
- if (path.includes(source)) {
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)) {
631
824
  return;
632
825
  }
633
826
  if (!(0, import_util2.isNode)()) {
634
827
  const { yieldOrContinue } = await import("main-thread-scheduling");
635
828
  await yieldOrContinue("idle");
636
829
  }
637
- const node = registry.get(this._graph.nodeOrThrow(source));
638
830
  const shouldContinue = await visitor(node, [
639
831
  ...path,
640
832
  node.id
@@ -642,104 +834,158 @@ var GraphBuilder = class _GraphBuilder {
642
834
  if (shouldContinue === false) {
643
835
  return;
644
836
  }
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
- ]);
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 ?? {}
657
852
  }));
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();
666
- }
667
- _onExpand(id, relation) {
668
- (0, import_log2.log)("onExpand", {
669
- id,
853
+ await Promise.all(nodes.map((n) => this.explore({
854
+ node: n,
670
855
  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}`);
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
+ }
914
+ }));
915
+ }
916
+ _onInitialNodes(node, nodesRelation, nodesType) {
917
+ this._nodeChanged[node.id] = this._nodeChanged[node.id] ?? (0, import_signals_core2.signal)({});
918
+ let first = true;
679
919
  let previous = [];
680
- const cancel = this._registry.subscribe(connectors, (nodes) => {
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
+ }
681
959
  const ids = nodes.map((n) => n.id);
682
- const removed = previous.filter((id2) => !ids.includes(id2));
960
+ const removed = previous.filter((id) => !ids.includes(id));
683
961
  previous = ids;
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)
694
- });
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));
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
+ }
709
979
  });
710
- }, {
711
- immediate: true
712
- });
713
- this._connectorSubscriptions.set(id, cancel);
980
+ }));
714
981
  }
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);
982
+ async _onRemoveNode(nodeId) {
983
+ this._resolverSubscriptions.get(nodeId)?.();
984
+ this._connectorSubscriptions.get(nodeId)?.();
985
+ this._resolverSubscriptions.delete(nodeId);
986
+ this._connectorSubscriptions.delete(nodeId);
722
987
  }
723
988
  };
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
- };
743
989
  // Annotate the CommonJS export names for ESM import in node:
744
990
  0 && (module.exports = {
745
991
  ACTION_GROUP_TYPE,
@@ -749,6 +995,7 @@ var rxFromObservable = (observable) => {
749
995
  ROOT_ID,
750
996
  ROOT_TYPE,
751
997
  actionGroupSymbol,
998
+ cleanup,
752
999
  createExtension,
753
1000
  flattenExtensions,
754
1001
  getGraph,
@@ -756,7 +1003,7 @@ var rxFromObservable = (observable) => {
756
1003
  isActionGroup,
757
1004
  isActionLike,
758
1005
  isGraphNode,
759
- rxFromObservable,
760
- rxFromSignal
1006
+ memoize,
1007
+ toSignal
761
1008
  });
762
1009
  //# sourceMappingURL=index.cjs.map