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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/lib/browser/index.mjs +593 -794
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +585 -785
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +593 -794
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/experimental/graph-projections.test.d.ts +25 -0
  11. package/dist/types/src/experimental/graph-projections.test.d.ts.map +1 -0
  12. package/dist/types/src/graph-builder.d.ts +48 -91
  13. package/dist/types/src/graph-builder.d.ts.map +1 -1
  14. package/dist/types/src/graph.d.ts +191 -98
  15. package/dist/types/src/graph.d.ts.map +1 -1
  16. package/dist/types/src/node.d.ts +3 -3
  17. package/dist/types/src/node.d.ts.map +1 -1
  18. package/dist/types/src/signals-integration.test.d.ts +2 -0
  19. package/dist/types/src/signals-integration.test.d.ts.map +1 -0
  20. package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
  21. package/dist/types/src/testing.d.ts +5 -0
  22. package/dist/types/src/testing.d.ts.map +1 -0
  23. package/dist/types/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +24 -17
  25. package/src/experimental/graph-projections.test.ts +56 -0
  26. package/src/graph-builder.test.ts +293 -310
  27. package/src/graph-builder.ts +235 -318
  28. package/src/graph.test.ts +314 -463
  29. package/src/graph.ts +452 -455
  30. package/src/node.ts +4 -4
  31. package/src/signals-integration.test.ts +218 -0
  32. package/src/stories/EchoGraph.stories.tsx +67 -76
  33. package/src/testing.ts +20 -0
@@ -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 { create } 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,543 @@ 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 create({
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] = create({
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
+ id,
209
+ typeChanged,
210
+ dataChanged,
211
+ propertiesChanged
212
+ }, {
213
+ F: __dxlog_file,
214
+ L: 417,
215
+ S: this,
216
+ C: (f, a) => f(...a)
217
+ });
218
+ if (typeChanged || dataChanged || propertiesChanged) {
219
+ log("updating node", {
220
+ id,
221
+ type,
222
+ data,
223
+ properties
224
+ }, {
225
+ F: __dxlog_file,
226
+ L: 419,
227
+ S: this,
228
+ C: (f, a) => f(...a)
229
+ });
230
+ const newNode = Option.some({
231
+ ...node2,
232
+ type,
233
+ data,
234
+ properties: {
235
+ ...node2.properties,
236
+ ...properties
237
+ }
238
+ });
239
+ this._registry.set(nodeRx, newNode);
240
+ this.onNodeChanged.emit({
241
+ id,
242
+ node: newNode
243
+ });
244
+ }
245
+ },
246
+ onNone: () => {
247
+ log("new node", {
248
+ id,
249
+ type,
250
+ data,
251
+ properties
252
+ }, {
253
+ F: __dxlog_file,
254
+ L: 426,
255
+ S: this,
256
+ C: (f, a) => f(...a)
257
+ });
258
+ const newNode = this._constructNode({
259
+ id,
260
+ type,
261
+ data,
262
+ properties
263
+ });
264
+ this._registry.set(nodeRx, newNode);
265
+ this.onNodeChanged.emit({
266
+ id,
267
+ node: newNode
268
+ });
269
+ }
177
270
  });
271
+ if (nodes) {
272
+ this.addNodes(nodes);
273
+ const _edges = nodes.map((node2) => ({
274
+ source: id,
275
+ target: node2.id
276
+ }));
277
+ this.addEdges(_edges);
278
+ }
279
+ if (edges) {
280
+ todo();
281
+ }
178
282
  }
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);
283
+ removeNodes(ids, edges = false) {
284
+ Rx.batch(() => {
285
+ ids.map((id) => this.removeNode(id, edges));
286
+ });
287
+ }
288
+ removeNode(id, edges = false) {
289
+ const nodeRx = this._node(id);
290
+ this._registry.set(nodeRx, Option.none());
291
+ this.onNodeChanged.emit({
292
+ id,
293
+ node: Option.none()
294
+ });
295
+ if (edges) {
296
+ const { inbound, outbound } = this._registry.get(this._edges(id));
297
+ const edges2 = [
298
+ ...inbound.map((source) => ({
299
+ source,
300
+ target: id
301
+ })),
302
+ ...outbound.map((target) => ({
303
+ source: id,
304
+ target
305
+ }))
306
+ ];
307
+ this.removeEdges(edges2);
189
308
  }
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;
309
+ this._onRemoveNode?.(id);
310
+ }
311
+ addEdges(edges) {
312
+ Rx.batch(() => {
313
+ edges.map((edge) => this.addEdge(edge));
314
+ });
315
+ }
316
+ addEdge(edgeArg) {
317
+ const sourceRx = this._edges(edgeArg.source);
318
+ const source = this._registry.get(sourceRx);
319
+ if (!source.outbound.includes(edgeArg.target)) {
320
+ log("add outbound edge", {
321
+ source: edgeArg.source,
322
+ target: edgeArg.target
323
+ }, {
324
+ F: __dxlog_file,
325
+ L: 481,
326
+ S: this,
327
+ C: (f, a) => f(...a)
328
+ });
329
+ this._registry.set(sourceRx, {
330
+ inbound: source.inbound,
331
+ outbound: [
332
+ ...source.outbound,
333
+ edgeArg.target
334
+ ]
335
+ });
206
336
  }
207
- if (timeout === void 0) {
208
- return trigger.wait();
209
- } else {
210
- return asyncTimeout(trigger.wait(), timeout, `Node not found: ${id}`);
337
+ const targetRx = this._edges(edgeArg.target);
338
+ const target = this._registry.get(targetRx);
339
+ if (!target.inbound.includes(edgeArg.source)) {
340
+ log("add inbound edge", {
341
+ source: edgeArg.source,
342
+ target: edgeArg.target
343
+ }, {
344
+ F: __dxlog_file,
345
+ L: 488,
346
+ S: this,
347
+ C: (f, a) => f(...a)
348
+ });
349
+ this._registry.set(targetRx, {
350
+ inbound: [
351
+ ...target.inbound,
352
+ edgeArg.source
353
+ ],
354
+ outbound: target.outbound
355
+ });
211
356
  }
212
357
  }
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
358
+ removeEdges(edges, removeOrphans = false) {
359
+ Rx.batch(() => {
360
+ edges.map((edge) => this.removeEdge(edge, removeOrphans));
223
361
  });
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
362
  }
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;
363
+ removeEdge(edgeArg, removeOrphans = false) {
364
+ const sourceRx = this._edges(edgeArg.source);
365
+ const source = this._registry.get(sourceRx);
366
+ if (source.outbound.includes(edgeArg.target)) {
367
+ this._registry.set(sourceRx, {
368
+ inbound: source.inbound,
369
+ outbound: source.outbound.filter((id) => id !== edgeArg.target)
370
+ });
371
+ }
372
+ const targetRx = this._edges(edgeArg.target);
373
+ const target = this._registry.get(targetRx);
374
+ if (target.inbound.includes(edgeArg.source)) {
375
+ this._registry.set(targetRx, {
376
+ inbound: target.inbound.filter((id) => id !== edgeArg.source),
377
+ outbound: target.outbound
378
+ });
255
379
  }
380
+ if (removeOrphans) {
381
+ const source2 = this._registry.get(sourceRx);
382
+ const target2 = this._registry.get(targetRx);
383
+ if (source2.outbound.length === 0 && source2.inbound.length === 0 && edgeArg.source !== ROOT_ID) {
384
+ this.removeNodes([
385
+ edgeArg.source
386
+ ]);
387
+ }
388
+ if (target2.outbound.length === 0 && target2.inbound.length === 0 && edgeArg.target !== ROOT_ID) {
389
+ this.removeNodes([
390
+ edgeArg.target
391
+ ]);
392
+ }
393
+ }
394
+ }
395
+ sortEdges(id, relation, order) {
396
+ const edgesRx = this._edges(id);
397
+ const edges = this._registry.get(edgesRx);
398
+ const unsorted = edges[relation].filter((id2) => !order.includes(id2)) ?? [];
399
+ const sorted = order.filter((id2) => edges[relation].includes(id2)) ?? [];
400
+ edges[relation].splice(0, edges[relation].length, ...[
401
+ ...sorted,
402
+ ...unsorted
403
+ ]);
404
+ this._registry.set(edgesRx, edges);
256
405
  }
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)) {
406
+ traverse({ visitor, source = ROOT_ID, relation = "outbound" }, path = []) {
407
+ if (path.includes(source)) {
269
408
  return;
270
409
  }
410
+ const node = this.getNodeOrThrow(source);
271
411
  const shouldContinue = visitor(node, [
272
412
  ...path,
273
- node.id
413
+ source
274
414
  ]);
275
415
  if (shouldContinue === false) {
276
416
  return;
277
417
  }
278
- Object.values(this._getNodes({
279
- node,
280
- relation,
281
- expansion
282
- })).forEach((child) => this.traverse({
283
- node: child,
418
+ Object.values(this.getConnections(source, relation)).forEach((child) => this.traverse({
419
+ source: child.id,
284
420
  relation,
285
- visitor,
286
- expansion
421
+ visitor
287
422
  }, [
288
423
  ...path,
289
- node.id
424
+ source
290
425
  ]));
291
426
  }
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
427
  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;
428
+ return pipe(this.getNode(source), Option.flatMap((node) => {
429
+ let found = Option.none();
430
+ this.traverse({
431
+ source: node.id,
432
+ visitor: (node2, path) => {
433
+ if (Option.isSome(found)) {
434
+ return false;
435
+ }
436
+ if (node2.id === target) {
437
+ found = Option.some(path);
438
+ }
341
439
  }
342
- }
343
- });
344
- return found;
440
+ });
441
+ return found;
442
+ }));
345
443
  }
346
- /**
347
- * Wait for the path between two nodes in the graph to be established.
348
- */
349
444
  async waitForPath(params, { timeout = 5e3, interval = 500 } = {}) {
350
445
  const path = this.getPath(params);
351
- if (path) {
352
- return path;
446
+ if (Option.isSome(path)) {
447
+ return path.value;
353
448
  }
354
449
  const trigger = new Trigger();
355
450
  const i = setInterval(() => {
356
451
  const path2 = this.getPath(params);
357
- if (path2) {
358
- trigger.wake(path2);
452
+ if (Option.isSome(path2)) {
453
+ trigger.wake(path2.value);
359
454
  }
360
455
  }, interval);
361
456
  return trigger.wait({
362
457
  timeout
363
458
  }).finally(() => clearInterval(i));
364
459
  }
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] = create({
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] = create({
482
- inbound: [],
483
- outbound: []
484
- });
485
- }
486
- if (!this._edges[target]) {
487
- this._edges[target] = create({
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
- });
460
+ /** @internal */
461
+ _constructNode(node) {
462
+ return Option.some({
463
+ [graphSymbol]: this,
464
+ data: null,
465
+ properties: {},
466
+ ...node
554
467
  });
555
468
  }
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
469
  };
568
470
 
569
471
  // 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 { create as create2 } from "@dxos/live-object";
472
+ import { Registry as Registry2, Rx as Rx2 } from "@effect-rx/rx-react";
473
+ import { effect } from "@preact/signals-core";
474
+ import { Array, pipe as pipe2, Record as Record2 } from "effect";
574
475
  import { log as log2 } from "@dxos/log";
575
- import { byPosition, isNode, isNonNullable as isNonNullable2 } from "@dxos/util";
476
+ import { byPosition, getDebugName, isNode, isNonNullable as isNonNullable2 } from "@dxos/util";
477
+
478
+ // packages/sdk/app-graph/src/node.ts
479
+ var isGraphNode = (data) => data && typeof data === "object" && "id" in data && "properties" in data && data.properties ? typeof data.properties === "object" && "data" in data : false;
480
+ var isAction = (data) => isGraphNode(data) ? typeof data.data === "function" : false;
481
+ var actionGroupSymbol = Symbol("ActionGroup");
482
+ var isActionGroup = (data) => isGraphNode(data) ? data.data === actionGroupSymbol : false;
483
+ var isActionLike = (data) => isAction(data) || isActionGroup(data);
484
+
485
+ // packages/sdk/app-graph/src/graph-builder.ts
576
486
  var __dxlog_file2 = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph-builder.ts";
577
- var NODE_RESOLVER_TIMEOUT = 1e3;
578
487
  var createExtension = (extension) => {
579
- const { id, position = "static", resolver, connector, actions, actionGroups, ...rest } = extension;
488
+ const { id, position = "static", relation = "outbound", connector: _connector, actions: _actions, actionGroups: _actionGroups } = extension;
580
489
  const getId = (key) => `${id}/${key}`;
490
+ const connector = _connector && Rx2.family((node) => _connector(node).pipe(Rx2.withLabel(`graph-builder:_connector:${id}`)));
491
+ const actionGroups = _actionGroups && Rx2.family((node) => _actionGroups(node).pipe(Rx2.withLabel(`graph-builder:_actionGroups:${id}`)));
492
+ const actions = _actions && Rx2.family((node) => _actions(node).pipe(Rx2.withLabel(`graph-builder:_actions:${id}`)));
581
493
  return [
582
- resolver ? {
583
- id: getId("resolver"),
584
- position,
585
- resolver
586
- } : void 0,
494
+ // resolver ? { id: getId('resolver'), position, resolver } : undefined,
587
495
  connector ? {
588
- ...rest,
589
496
  id: getId("connector"),
590
497
  position,
591
- connector
498
+ relation,
499
+ connector: Rx2.family((node) => Rx2.make((get) => {
500
+ try {
501
+ return get(connector(node));
502
+ } catch {
503
+ log2.warn("Error in connector", {
504
+ id: getId("connector"),
505
+ node
506
+ }, {
507
+ F: __dxlog_file2,
508
+ L: 101,
509
+ S: void 0,
510
+ C: (f, a) => f(...a)
511
+ });
512
+ return [];
513
+ }
514
+ }).pipe(Rx2.withLabel(`graph-builder:connector:${id}`)))
592
515
  } : void 0,
593
516
  actionGroups ? {
594
- ...rest,
595
517
  id: getId("actionGroups"),
596
518
  position,
597
- type: ACTION_GROUP_TYPE,
598
519
  relation: "outbound",
599
- connector: ({ node }) => actionGroups({
600
- node
601
- })?.map((arg) => ({
602
- ...arg,
603
- data: actionGroupSymbol,
604
- type: ACTION_GROUP_TYPE
605
- }))
520
+ connector: Rx2.family((node) => Rx2.make((get) => {
521
+ try {
522
+ return get(actionGroups(node)).map((arg) => ({
523
+ ...arg,
524
+ data: actionGroupSymbol,
525
+ type: ACTION_GROUP_TYPE
526
+ }));
527
+ } catch {
528
+ log2.warn("Error in actionGroups", {
529
+ id: getId("actionGroups"),
530
+ node
531
+ }, {
532
+ F: __dxlog_file2,
533
+ L: 122,
534
+ S: void 0,
535
+ C: (f, a) => f(...a)
536
+ });
537
+ return [];
538
+ }
539
+ }).pipe(Rx2.withLabel(`graph-builder:connector:actionGroups:${id}`)))
606
540
  } : void 0,
607
541
  actions ? {
608
- ...rest,
609
542
  id: getId("actions"),
610
543
  position,
611
- type: ACTION_TYPE,
612
544
  relation: "outbound",
613
- connector: ({ node }) => actions({
614
- node
615
- })?.map((arg) => ({
616
- ...arg,
617
- type: ACTION_TYPE
618
- }))
545
+ connector: Rx2.family((node) => Rx2.make((get) => {
546
+ try {
547
+ return get(actions(node)).map((arg) => ({
548
+ ...arg,
549
+ type: ACTION_TYPE
550
+ }));
551
+ } catch {
552
+ log2.warn("Error in actions", {
553
+ id: getId("actions"),
554
+ node
555
+ }, {
556
+ F: __dxlog_file2,
557
+ L: 139,
558
+ S: void 0,
559
+ C: (f, a) => f(...a)
560
+ });
561
+ return [];
562
+ }
563
+ }).pipe(Rx2.withLabel(`graph-builder:connector:actions:${id}`)))
619
564
  } : void 0
620
565
  ].filter(isNonNullable2);
621
566
  };
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
567
  var flattenExtensions = (extension, acc = []) => {
682
568
  if (Array.isArray(extension)) {
683
569
  return [
@@ -692,107 +578,75 @@ var flattenExtensions = (extension, acc = []) => {
692
578
  }
693
579
  };
694
580
  var GraphBuilder = class _GraphBuilder {
695
- constructor(params = {}) {
696
- this._dispatcher = new Dispatcher();
697
- this._extensions = create2({});
698
- this._resolverSubscriptions = /* @__PURE__ */ new Map();
581
+ constructor({ registry, ...params } = {}) {
582
+ // TODO(wittjosiah): Use Context.
699
583
  this._connectorSubscriptions = /* @__PURE__ */ new Map();
700
- this._nodeChanged = {};
701
- this._initialized = {};
584
+ this._extensions = Rx2.make(Record2.empty()).pipe(Rx2.keepAlive, Rx2.withLabel("graph-builder:extensions"));
585
+ this._connectors = Rx2.family((key) => {
586
+ return Rx2.make((get) => {
587
+ const [id, relation] = key.split("+");
588
+ const node = this._graph.node(id);
589
+ return pipe2(
590
+ get(this._extensions),
591
+ Record2.values,
592
+ // TODO(wittjosiah): Sort on write rather than read.
593
+ Array.sortBy(byPosition),
594
+ Array.filter(({ relation: _relation = "outbound" }) => _relation === relation),
595
+ Array.map(({ connector }) => connector?.(node)),
596
+ Array.filter(isNonNullable2),
597
+ Array.flatMap((result) => get(result))
598
+ );
599
+ }).pipe(Rx2.withLabel(`graph-builder:connectors:${key}`));
600
+ });
601
+ this._registry = registry ?? Registry2.make();
702
602
  this._graph = new Graph({
703
603
  ...params,
704
- onInitialNode: async (id) => this._onInitialNode(id),
705
- onInitialNodes: async (node, relation, type) => this._onInitialNodes(node, relation, type),
604
+ registry: this._registry,
605
+ onExpand: (id, relation) => this._onExpand(id, relation),
606
+ // onInitialize: (id) => this._onInitialize(id),
706
607
  onRemoveNode: (id) => this._onRemoveNode(id)
707
608
  });
708
609
  }
709
- static from(pickle) {
610
+ static from(pickle, registry) {
710
611
  if (!pickle) {
711
- return new _GraphBuilder();
612
+ return new _GraphBuilder({
613
+ registry
614
+ });
712
615
  }
713
616
  const { nodes, edges } = JSON.parse(pickle);
714
617
  return new _GraphBuilder({
715
618
  nodes,
716
- edges
619
+ edges,
620
+ registry
717
621
  });
718
622
  }
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
623
  get graph() {
748
624
  return this._graph;
749
625
  }
750
- /**
751
- * @reactive
752
- */
753
626
  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
- });
627
+ return this._extensions;
628
+ }
629
+ addExtension(extensions) {
630
+ flattenExtensions(extensions).forEach((extension) => {
631
+ const extensions2 = this._registry.get(this._extensions);
632
+ this._registry.set(this._extensions, Record2.set(extensions2, extension.id, extension));
766
633
  });
767
634
  return this;
768
635
  }
769
- /**
770
- * Remove a node builder from the graph builder.
771
- */
772
636
  removeExtension(id) {
773
- untracked2(() => {
774
- delete this._extensions[id];
775
- });
637
+ const extensions = this._registry.get(this._extensions);
638
+ this._registry.set(this._extensions, Record2.remove(extensions, id));
776
639
  return this;
777
640
  }
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)) {
641
+ async explore({ registry = Registry2.make(), source = ROOT_ID, relation = "outbound", visitor }, path = []) {
642
+ if (path.includes(source)) {
790
643
  return;
791
644
  }
792
645
  if (!isNode()) {
793
646
  const { yieldOrContinue } = await import("main-thread-scheduling");
794
647
  await yieldOrContinue("idle");
795
648
  }
649
+ const node = registry.get(this._graph.nodeOrThrow(source));
796
650
  const shouldContinue = await visitor(node, [
797
651
  ...path,
798
652
  node.id
@@ -800,158 +654,104 @@ var GraphBuilder = class _GraphBuilder {
800
654
  if (shouldContinue === false) {
801
655
  return;
802
656
  }
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
- }
657
+ 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))));
658
+ await Promise.all(nodes.map((nodeArg) => {
659
+ registry.set(this._graph._node(nodeArg.id), this._graph._constructNode(nodeArg));
660
+ return this.explore({
661
+ registry,
662
+ source: nodeArg.id,
663
+ relation,
664
+ visitor
665
+ }, [
666
+ ...path,
667
+ node.id
668
+ ]);
880
669
  }));
670
+ if (registry !== this._registry) {
671
+ registry.reset();
672
+ registry.dispose();
673
+ }
881
674
  }
882
- _onInitialNodes(node, nodesRelation, nodesType) {
883
- this._nodeChanged[node.id] = this._nodeChanged[node.id] ?? signal({});
884
- let first = true;
675
+ destroy() {
676
+ this._connectorSubscriptions.forEach((unsubscribe) => unsubscribe());
677
+ this._connectorSubscriptions.clear();
678
+ }
679
+ _onExpand(id, relation) {
680
+ log2("onExpand", {
681
+ id,
682
+ relation,
683
+ registry: getDebugName(this._registry)
684
+ }, {
685
+ F: __dxlog_file2,
686
+ L: 301,
687
+ S: this,
688
+ C: (f, a) => f(...a)
689
+ });
690
+ const connectors = this._connectors(`${id}+${relation}`);
885
691
  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
- }
692
+ const cancel = this._registry.subscribe(connectors, (nodes) => {
925
693
  const ids = nodes.map((n) => n.id);
926
- const removed = previous.filter((id) => !ids.includes(id));
694
+ const removed = previous.filter((id2) => !ids.includes(id2));
927
695
  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
- }
696
+ log2("update", {
697
+ id,
698
+ relation,
699
+ ids,
700
+ removed
701
+ }, {
702
+ F: __dxlog_file2,
703
+ L: 312,
704
+ S: this,
705
+ C: (f, a) => f(...a)
945
706
  });
946
- }));
707
+ Rx2.batch(() => {
708
+ this._graph.removeEdges(removed.map((target) => ({
709
+ source: id,
710
+ target
711
+ })), true);
712
+ this._graph.addNodes(nodes);
713
+ this._graph.addEdges(nodes.map((node) => relation === "outbound" ? {
714
+ source: id,
715
+ target: node.id
716
+ } : {
717
+ source: node.id,
718
+ target: id
719
+ }));
720
+ this._graph.sortEdges(id, relation, nodes.map(({ id: id2 }) => id2));
721
+ });
722
+ }, {
723
+ immediate: true
724
+ });
725
+ this._connectorSubscriptions.set(id, cancel);
947
726
  }
948
- async _onRemoveNode(nodeId) {
949
- this._resolverSubscriptions.get(nodeId)?.();
950
- this._connectorSubscriptions.get(nodeId)?.();
951
- this._resolverSubscriptions.delete(nodeId);
952
- this._connectorSubscriptions.delete(nodeId);
727
+ // TODO(wittjosiah): On initialize to restore state from cache.
728
+ // private async _onInitialize(id: string) {
729
+ // log('onInitialize', { id });
730
+ // }
731
+ _onRemoveNode(id) {
732
+ this._connectorSubscriptions.get(id)?.();
733
+ this._connectorSubscriptions.delete(id);
953
734
  }
954
735
  };
736
+ var rxFromSignal = (cb) => {
737
+ return Rx2.make((get) => {
738
+ const dispose = effect(() => {
739
+ get.setSelf(cb());
740
+ });
741
+ get.addFinalizer(() => dispose());
742
+ return cb();
743
+ });
744
+ };
745
+ var observableFamily = Rx2.family((observable) => {
746
+ return Rx2.make((get) => {
747
+ const subscription = observable.subscribe((value) => get.setSelf(value));
748
+ get.addFinalizer(() => subscription.unsubscribe());
749
+ return observable.get();
750
+ });
751
+ });
752
+ var rxFromObservable = (observable) => {
753
+ return observableFamily(observable);
754
+ };
955
755
  export {
956
756
  ACTION_GROUP_TYPE,
957
757
  ACTION_TYPE,
@@ -960,7 +760,6 @@ export {
960
760
  ROOT_ID,
961
761
  ROOT_TYPE,
962
762
  actionGroupSymbol,
963
- cleanup,
964
763
  createExtension,
965
764
  flattenExtensions,
966
765
  getGraph,
@@ -968,7 +767,7 @@ export {
968
767
  isActionGroup,
969
768
  isActionLike,
970
769
  isGraphNode,
971
- memoize,
972
- toSignal
770
+ rxFromObservable,
771
+ rxFromSignal
973
772
  };
974
773
  //# sourceMappingURL=index.mjs.map