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

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