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