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