@dxos/app-graph 0.6.3-main.9e4e207 → 0.6.3-next.2f65b78
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.
- package/dist/lib/browser/index.mjs +191 -582
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +185 -587
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/graph-builder.d.ts +7 -99
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +47 -96
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/helpers.d.ts +12 -0
- package/dist/types/src/helpers.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +40 -99
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/package.json +12 -14
- package/src/graph-builder.ts +19 -332
- package/src/graph.test.ts +179 -431
- package/src/graph.ts +149 -336
- package/src/helpers.ts +27 -0
- package/src/index.ts +1 -0
- package/src/node.ts +42 -15
- package/src/stories/EchoGraph.stories.tsx +102 -84
- package/dist/types/src/graph-builder.test.d.ts +0 -2
- package/dist/types/src/graph-builder.test.d.ts.map +0 -1
- package/src/graph-builder.test.ts +0 -310
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// packages/sdk/app-graph/src/graph.ts
|
|
2
|
-
import {
|
|
3
|
-
import { Trigger } from "@dxos/async";
|
|
2
|
+
import { untracked } from "@preact/signals-core";
|
|
4
3
|
import { create } from "@dxos/echo-schema";
|
|
5
4
|
import { invariant } from "@dxos/invariant";
|
|
6
5
|
import { nonNullable } from "@dxos/util";
|
|
@@ -14,56 +13,51 @@ var isActionLike = (data) => isAction(data) || isActionGroup(data);
|
|
|
14
13
|
|
|
15
14
|
// packages/sdk/app-graph/src/graph.ts
|
|
16
15
|
var __dxlog_file = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph.ts";
|
|
17
|
-
var graphSymbol = Symbol("graph");
|
|
18
|
-
var getGraph = (node) => {
|
|
19
|
-
const graph = node[graphSymbol];
|
|
20
|
-
invariant(graph, "Node is not associated with a graph.", {
|
|
21
|
-
F: __dxlog_file,
|
|
22
|
-
L: 20,
|
|
23
|
-
S: void 0,
|
|
24
|
-
A: [
|
|
25
|
-
"graph",
|
|
26
|
-
"'Node is not associated with a graph.'"
|
|
27
|
-
]
|
|
28
|
-
});
|
|
29
|
-
return graph;
|
|
30
|
-
};
|
|
31
16
|
var ROOT_ID = "root";
|
|
32
|
-
var ROOT_TYPE = "dxos.org/type/GraphRoot";
|
|
33
|
-
var ACTION_TYPE = "dxos.org/type/GraphAction";
|
|
34
|
-
var ACTION_GROUP_TYPE = "dxos.org/type/GraphActionGroup";
|
|
35
|
-
var NODE_TIMEOUT = 5e3;
|
|
36
17
|
var Graph = class {
|
|
37
|
-
constructor(
|
|
38
|
-
this._waitingForNodes = {};
|
|
39
|
-
this._initialized = {};
|
|
18
|
+
constructor() {
|
|
40
19
|
/**
|
|
41
20
|
* @internal
|
|
42
21
|
*/
|
|
43
|
-
this._nodes = {
|
|
22
|
+
this._nodes = create({
|
|
23
|
+
[ROOT_ID]: {
|
|
24
|
+
id: ROOT_ID,
|
|
25
|
+
properties: {},
|
|
26
|
+
data: null
|
|
27
|
+
}
|
|
28
|
+
});
|
|
44
29
|
/**
|
|
45
30
|
* @internal
|
|
46
31
|
*/
|
|
47
|
-
|
|
48
|
-
this
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
32
|
+
// Key is the `${node.id}-${direction}` and value is an ordered list of node ids.
|
|
33
|
+
// Explicit type required because TS says this is not portable.
|
|
34
|
+
this._edges = create({});
|
|
35
|
+
this._constructNode = (nodeBase) => {
|
|
36
|
+
const node = {
|
|
37
|
+
...nodeBase,
|
|
38
|
+
edges: ({ direction = "outbound" } = {}) => {
|
|
39
|
+
return this._edges[this.getEdgeKey(node.id, direction)];
|
|
40
|
+
},
|
|
41
|
+
nodes: ({ direction, filter } = {}) => {
|
|
42
|
+
const nodes = this._getNodes({
|
|
43
|
+
id: node.id,
|
|
44
|
+
direction
|
|
45
|
+
}).filter((n) => !isActionLike(n));
|
|
46
|
+
return filter ? nodes.filter((n) => filter(n, node)) : nodes;
|
|
47
|
+
},
|
|
48
|
+
node: (id) => {
|
|
49
|
+
return this._getNodes({
|
|
50
|
+
id
|
|
51
|
+
}).find((node2) => node2.id === id);
|
|
52
|
+
},
|
|
53
|
+
actions: () => {
|
|
54
|
+
return this._getNodes({
|
|
55
|
+
id: node.id
|
|
56
|
+
}).filter(isActionLike);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
return node;
|
|
53
60
|
};
|
|
54
|
-
this._onInitialNode = onInitialNode;
|
|
55
|
-
this._onInitialNodes = onInitialNodes;
|
|
56
|
-
this._onRemoveNode = onRemoveNode;
|
|
57
|
-
this._nodes[ROOT_ID] = this._constructNode({
|
|
58
|
-
id: ROOT_ID,
|
|
59
|
-
type: ROOT_TYPE,
|
|
60
|
-
properties: {},
|
|
61
|
-
data: null
|
|
62
|
-
});
|
|
63
|
-
this._edges[ROOT_ID] = create({
|
|
64
|
-
inbound: [],
|
|
65
|
-
outbound: []
|
|
66
|
-
});
|
|
67
61
|
}
|
|
68
62
|
/**
|
|
69
63
|
* Alias for `findNode('root')`.
|
|
@@ -74,14 +68,11 @@ var Graph = class {
|
|
|
74
68
|
/**
|
|
75
69
|
* Convert the graph to a JSON object.
|
|
76
70
|
*/
|
|
77
|
-
toJSON({ id = ROOT_ID, maxLength = 32
|
|
71
|
+
toJSON({ id = ROOT_ID, maxLength = 32 } = {}) {
|
|
78
72
|
const toJSON = (node, seen = []) => {
|
|
79
|
-
const nodes =
|
|
80
|
-
onlyLoaded
|
|
81
|
-
});
|
|
73
|
+
const nodes = node.nodes();
|
|
82
74
|
const obj = {
|
|
83
|
-
id: node.id.length > maxLength ? `${node.id.slice(0, maxLength - 3)}...` : node.id
|
|
84
|
-
type: node.type
|
|
75
|
+
id: node.id.length > maxLength ? `${node.id.slice(0, maxLength - 3)}...` : node.id
|
|
85
76
|
};
|
|
86
77
|
if (node.properties.label) {
|
|
87
78
|
obj.label = node.properties.label;
|
|
@@ -100,7 +91,7 @@ var Graph = class {
|
|
|
100
91
|
const root = this.findNode(id);
|
|
101
92
|
invariant(root, `Node not found: ${id}`, {
|
|
102
93
|
F: __dxlog_file,
|
|
103
|
-
L:
|
|
94
|
+
L: 91,
|
|
104
95
|
S: this,
|
|
105
96
|
A: [
|
|
106
97
|
"root",
|
|
@@ -111,639 +102,257 @@ var Graph = class {
|
|
|
111
102
|
}
|
|
112
103
|
/**
|
|
113
104
|
* Find the node with the given id in the graph.
|
|
114
|
-
*
|
|
115
|
-
* If a node is not found within the graph and an `onInitialNode` callback is provided,
|
|
116
|
-
* it is called with the id and type of the node, potentially initializing the node.
|
|
117
|
-
*/
|
|
118
|
-
findNode(id, type) {
|
|
119
|
-
const existingNode = this._nodes[id];
|
|
120
|
-
const nodeArg = !existingNode && this._onInitialNode?.(id, type);
|
|
121
|
-
return existingNode ?? (nodeArg ? this._addNode(nodeArg) : void 0);
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Wait for a node to be added to the graph.
|
|
125
|
-
*
|
|
126
|
-
* If the node is already present in the graph, the promise resolves immediately.
|
|
127
|
-
*
|
|
128
|
-
* @param id The id of the node to wait for.
|
|
129
|
-
* @param timeout The time in milliseconds to wait for the node to be added.
|
|
130
105
|
*/
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
106
|
+
findNode(id) {
|
|
107
|
+
const nodeBase = this._nodes[id];
|
|
108
|
+
if (!nodeBase) {
|
|
109
|
+
return void 0;
|
|
134
110
|
}
|
|
135
|
-
|
|
136
|
-
return trigger.wait({
|
|
137
|
-
timeout
|
|
138
|
-
});
|
|
111
|
+
return this._constructNode(nodeBase);
|
|
139
112
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const { onlyLoaded, relation, filter, type } = options;
|
|
145
|
-
const nodes = this._getNodes({
|
|
146
|
-
node,
|
|
147
|
-
relation,
|
|
148
|
-
type,
|
|
149
|
-
onlyLoaded
|
|
150
|
-
});
|
|
151
|
-
return nodes.filter((n) => untracked(() => !isActionLike(n))).filter((n) => filter?.(n, node) ?? true);
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Edges that this node is connected to in default order.
|
|
155
|
-
*/
|
|
156
|
-
edges(node, { relation = "outbound" } = {}) {
|
|
157
|
-
return this._edges[node.id]?.[relation] ?? [];
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Actions or action groups that this node is connected to in default order.
|
|
161
|
-
*/
|
|
162
|
-
actions(node, { onlyLoaded } = {}) {
|
|
163
|
-
return [
|
|
164
|
-
...this._getNodes({
|
|
165
|
-
node,
|
|
166
|
-
type: ACTION_GROUP_TYPE,
|
|
167
|
-
onlyLoaded
|
|
168
|
-
}),
|
|
169
|
-
...this._getNodes({
|
|
170
|
-
node,
|
|
171
|
-
type: ACTION_TYPE,
|
|
172
|
-
onlyLoaded
|
|
173
|
-
})
|
|
174
|
-
];
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Recursive depth-first traversal of the graph.
|
|
178
|
-
*
|
|
179
|
-
* @param options.node The node to start traversing from.
|
|
180
|
-
* @param options.relation The relation to traverse graph edges.
|
|
181
|
-
* @param options.visitor A callback which is called for each node visited during traversal.
|
|
182
|
-
*/
|
|
183
|
-
traverse({ visitor, node = this.root, relation = "outbound", onlyLoaded }, path = []) {
|
|
184
|
-
if (path.includes(node.id)) {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const shouldContinue = visitor(node, [
|
|
188
|
-
...path,
|
|
189
|
-
node.id
|
|
190
|
-
]);
|
|
191
|
-
if (shouldContinue === false) {
|
|
192
|
-
return;
|
|
113
|
+
_getNodes({ id, direction = "outbound" }) {
|
|
114
|
+
const edges = this._edges[this.getEdgeKey(id, direction)];
|
|
115
|
+
if (!edges) {
|
|
116
|
+
return [];
|
|
193
117
|
}
|
|
194
|
-
|
|
195
|
-
node,
|
|
196
|
-
relation,
|
|
197
|
-
onlyLoaded
|
|
198
|
-
})).forEach((child) => this.traverse({
|
|
199
|
-
node: child,
|
|
200
|
-
relation,
|
|
201
|
-
visitor,
|
|
202
|
-
onlyLoaded
|
|
203
|
-
}, [
|
|
204
|
-
...path,
|
|
205
|
-
node.id
|
|
206
|
-
]));
|
|
118
|
+
return edges.map((id2) => this.findNode(id2)).filter(nonNullable);
|
|
207
119
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
*
|
|
211
|
-
* @param options.node The node to start traversing from.
|
|
212
|
-
* @param options.relation The relation to traverse graph edges.
|
|
213
|
-
* @param options.visitor A callback which is called for each node visited during traversal.
|
|
214
|
-
*/
|
|
215
|
-
subscribeTraverse({ visitor, node = this.root, relation = "outbound", onlyLoaded }, currentPath = []) {
|
|
216
|
-
return effect(() => {
|
|
217
|
-
const path = [
|
|
218
|
-
...currentPath,
|
|
219
|
-
node.id
|
|
220
|
-
];
|
|
221
|
-
const result = visitor(node, path);
|
|
222
|
-
if (result === false) {
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
const nodes = this._getNodes({
|
|
226
|
-
node,
|
|
227
|
-
relation,
|
|
228
|
-
onlyLoaded
|
|
229
|
-
});
|
|
230
|
-
const nodeSubscriptions = nodes.map((n) => this.subscribeTraverse({
|
|
231
|
-
node: n,
|
|
232
|
-
visitor,
|
|
233
|
-
onlyLoaded
|
|
234
|
-
}, path));
|
|
235
|
-
return () => {
|
|
236
|
-
nodeSubscriptions.forEach((unsubscribe) => unsubscribe());
|
|
237
|
-
};
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Get the path between two nodes in the graph.
|
|
242
|
-
*/
|
|
243
|
-
getPath({ source = "root", target }) {
|
|
244
|
-
const start = this.findNode(source);
|
|
245
|
-
if (!start) {
|
|
246
|
-
return void 0;
|
|
247
|
-
}
|
|
248
|
-
let found;
|
|
249
|
-
this.traverse({
|
|
250
|
-
onlyLoaded: true,
|
|
251
|
-
node: start,
|
|
252
|
-
visitor: (node, path) => {
|
|
253
|
-
if (found) {
|
|
254
|
-
return false;
|
|
255
|
-
}
|
|
256
|
-
if (node.id === target) {
|
|
257
|
-
found = path;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
return found;
|
|
120
|
+
getEdgeKey(id, direction) {
|
|
121
|
+
return `${id}-${direction}`;
|
|
262
122
|
}
|
|
263
123
|
/**
|
|
264
124
|
* Add nodes to the graph.
|
|
265
|
-
*
|
|
266
|
-
* @internal
|
|
267
125
|
*/
|
|
268
|
-
|
|
269
|
-
return
|
|
126
|
+
addNodes(...nodes) {
|
|
127
|
+
return nodes.map((node) => this._addNode(node));
|
|
270
128
|
}
|
|
271
129
|
_addNode({ nodes, edges, ..._node }) {
|
|
272
130
|
return untracked(() => {
|
|
273
|
-
const
|
|
274
|
-
const node = existingNode ?? this._constructNode({
|
|
131
|
+
const node = {
|
|
275
132
|
data: null,
|
|
276
133
|
properties: {},
|
|
277
134
|
..._node
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const { data, properties, type } = _node;
|
|
281
|
-
if (data && data !== node.data) {
|
|
282
|
-
node.data = data;
|
|
283
|
-
}
|
|
284
|
-
if (type !== node.type) {
|
|
285
|
-
node.type = type;
|
|
286
|
-
}
|
|
287
|
-
for (const key in properties) {
|
|
288
|
-
if (properties[key] !== node.properties[key]) {
|
|
289
|
-
node.properties[key] = properties[key];
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
} else {
|
|
293
|
-
this._nodes[node.id] = node;
|
|
294
|
-
this._edges[node.id] = create({
|
|
295
|
-
inbound: [],
|
|
296
|
-
outbound: []
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
const trigger = this._waitingForNodes[node.id];
|
|
300
|
-
if (trigger) {
|
|
301
|
-
trigger.wake(node);
|
|
302
|
-
delete this._waitingForNodes[node.id];
|
|
303
|
-
}
|
|
135
|
+
};
|
|
136
|
+
this._nodes[node.id] = node;
|
|
304
137
|
if (nodes) {
|
|
305
138
|
nodes.forEach((subNode) => {
|
|
306
139
|
this._addNode(subNode);
|
|
307
|
-
this.
|
|
140
|
+
this.addEdge({
|
|
308
141
|
source: node.id,
|
|
309
142
|
target: subNode.id
|
|
310
143
|
});
|
|
311
144
|
});
|
|
312
145
|
}
|
|
313
146
|
if (edges) {
|
|
314
|
-
edges.forEach(([id,
|
|
147
|
+
edges.forEach(([id, direction]) => direction === "outbound" ? this.addEdge({
|
|
315
148
|
source: node.id,
|
|
316
149
|
target: id
|
|
317
|
-
}) : this.
|
|
150
|
+
}) : this.addEdge({
|
|
318
151
|
source: id,
|
|
319
152
|
target: node.id
|
|
320
153
|
}));
|
|
321
154
|
}
|
|
322
|
-
return node;
|
|
155
|
+
return this._constructNode(node);
|
|
323
156
|
});
|
|
324
157
|
}
|
|
325
158
|
/**
|
|
326
159
|
* Remove nodes from the graph.
|
|
327
160
|
*
|
|
328
|
-
* @param
|
|
161
|
+
* @param id The id of the node to remove.
|
|
329
162
|
* @param edges Whether to remove edges connected to the node from the graph as well.
|
|
330
|
-
* @internal
|
|
331
163
|
*/
|
|
332
|
-
|
|
333
|
-
batch(() => ids.forEach((id) => this._removeNode(id, edges)));
|
|
334
|
-
}
|
|
335
|
-
_removeNode(id, edges = false) {
|
|
164
|
+
removeNode(id, edges = false) {
|
|
336
165
|
untracked(() => {
|
|
337
166
|
const node = this.findNode(id);
|
|
338
167
|
if (!node) {
|
|
339
168
|
return;
|
|
340
169
|
}
|
|
341
170
|
if (edges) {
|
|
171
|
+
delete this._edges[this.getEdgeKey(id, "outbound")];
|
|
172
|
+
delete this._edges[this.getEdgeKey(id, "inbound")];
|
|
342
173
|
this._getNodes({
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
target: node2.id
|
|
349
|
-
});
|
|
350
|
-
});
|
|
174
|
+
id
|
|
175
|
+
}).forEach((node2) => this.removeEdge({
|
|
176
|
+
source: id,
|
|
177
|
+
target: node2.id
|
|
178
|
+
}));
|
|
351
179
|
this._getNodes({
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
target: id
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
delete this._edges[id];
|
|
180
|
+
id,
|
|
181
|
+
direction: "inbound"
|
|
182
|
+
}).forEach((node2) => this.removeEdge({
|
|
183
|
+
source: node2.id,
|
|
184
|
+
target: id
|
|
185
|
+
}));
|
|
362
186
|
}
|
|
363
187
|
delete this._nodes[id];
|
|
364
|
-
this._onRemoveNode?.(id);
|
|
365
188
|
});
|
|
366
189
|
}
|
|
367
190
|
/**
|
|
368
|
-
* Add
|
|
369
|
-
*
|
|
370
|
-
* @internal
|
|
191
|
+
* Add an edge to the graph.
|
|
371
192
|
*/
|
|
372
|
-
|
|
373
|
-
batch(() => edges.forEach((edge) => this._addEdge(edge)));
|
|
374
|
-
}
|
|
375
|
-
_addEdge({ source, target }) {
|
|
193
|
+
addEdge({ source, target }) {
|
|
376
194
|
untracked(() => {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
this._edges[target] = create({
|
|
385
|
-
inbound: [],
|
|
386
|
-
outbound: []
|
|
387
|
-
});
|
|
195
|
+
const outbound = this._edges[this.getEdgeKey(source, "outbound")];
|
|
196
|
+
if (!outbound) {
|
|
197
|
+
this._edges[this.getEdgeKey(source, "outbound")] = [
|
|
198
|
+
target
|
|
199
|
+
];
|
|
200
|
+
} else if (!outbound.includes(target)) {
|
|
201
|
+
outbound.push(target);
|
|
388
202
|
}
|
|
389
|
-
const
|
|
390
|
-
if (!
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (!
|
|
395
|
-
|
|
203
|
+
const inbound = this._edges[this.getEdgeKey(target, "inbound")];
|
|
204
|
+
if (!inbound) {
|
|
205
|
+
this._edges[this.getEdgeKey(target, "inbound")] = [
|
|
206
|
+
source
|
|
207
|
+
];
|
|
208
|
+
} else if (!inbound.includes(source)) {
|
|
209
|
+
inbound.push(source);
|
|
396
210
|
}
|
|
397
211
|
});
|
|
398
212
|
}
|
|
399
213
|
/**
|
|
400
|
-
* Remove edges from the graph.
|
|
401
|
-
* @internal
|
|
402
|
-
*/
|
|
403
|
-
_removeEdges(edges) {
|
|
404
|
-
batch(() => edges.forEach((edge) => this._removeEdge(edge)));
|
|
405
|
-
}
|
|
406
|
-
_removeEdge({ source, target }) {
|
|
407
|
-
untracked(() => {
|
|
408
|
-
batch(() => {
|
|
409
|
-
const outboundIndex = this._edges[source]?.outbound.findIndex((id) => id === target);
|
|
410
|
-
if (outboundIndex !== void 0 && outboundIndex !== -1) {
|
|
411
|
-
this._edges[source].outbound.splice(outboundIndex, 1);
|
|
412
|
-
}
|
|
413
|
-
const inboundIndex = this._edges[target]?.inbound.findIndex((id) => id === source);
|
|
414
|
-
if (inboundIndex !== void 0 && inboundIndex !== -1) {
|
|
415
|
-
this._edges[target].inbound.splice(inboundIndex, 1);
|
|
416
|
-
}
|
|
417
|
-
});
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
214
|
* Sort edges for a node.
|
|
422
215
|
*
|
|
423
216
|
* Edges not included in the sorted list are appended to the end of the list.
|
|
424
217
|
*
|
|
425
218
|
* @param nodeId The id of the node to sort edges for.
|
|
426
|
-
* @param
|
|
219
|
+
* @param direction The direction of the edges from the node to sort.
|
|
427
220
|
* @param edges The ordered list of edges.
|
|
428
|
-
* @ignore
|
|
429
221
|
*/
|
|
430
|
-
|
|
222
|
+
sortEdges(nodeId, direction, edges) {
|
|
431
223
|
untracked(() => {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
});
|
|
224
|
+
const current = this._edges[this.getEdgeKey(nodeId, direction)];
|
|
225
|
+
if (current) {
|
|
226
|
+
const unsorted = current.filter((id) => !edges.includes(id)) ?? [];
|
|
227
|
+
const sorted = edges.filter((id) => current.includes(id)) ?? [];
|
|
228
|
+
current.splice(0, current.length, ...[
|
|
229
|
+
...sorted,
|
|
230
|
+
...unsorted
|
|
231
|
+
]);
|
|
232
|
+
}
|
|
443
233
|
});
|
|
444
234
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
this.
|
|
451
|
-
if (
|
|
452
|
-
|
|
453
|
-
this._addEdges(nodes.map(({ id }) => relation === "outbound" ? {
|
|
454
|
-
source: node.id,
|
|
455
|
-
target: id
|
|
456
|
-
} : {
|
|
457
|
-
source: id,
|
|
458
|
-
target: node.id
|
|
459
|
-
}));
|
|
235
|
+
/**
|
|
236
|
+
* Remove an edge from the graph.
|
|
237
|
+
*/
|
|
238
|
+
removeEdge({ source, target }) {
|
|
239
|
+
untracked(() => {
|
|
240
|
+
const outboundIndex = this._edges[this.getEdgeKey(source, "outbound")]?.findIndex((id) => id === target);
|
|
241
|
+
if (outboundIndex !== -1) {
|
|
242
|
+
this._edges[this.getEdgeKey(source, "outbound")].splice(outboundIndex, 1);
|
|
460
243
|
}
|
|
244
|
+
const inboundIndex = this._edges[this.getEdgeKey(target, "inbound")]?.findIndex((id) => id === source);
|
|
245
|
+
if (inboundIndex !== -1) {
|
|
246
|
+
this._edges[this.getEdgeKey(target, "inbound")].splice(inboundIndex, 1);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Recursive depth-first traversal.
|
|
252
|
+
*
|
|
253
|
+
* @param options.node The node to start traversing from.
|
|
254
|
+
* @param options.direction The direction to traverse graph edges.
|
|
255
|
+
* @param options.filter A predicate to filter nodes which are passed to the `visitor` callback.
|
|
256
|
+
* @param options.visitor A callback which is called for each node visited during traversal.
|
|
257
|
+
*/
|
|
258
|
+
traverse({ node = this.root, direction = "outbound", filter, visitor }, path = []) {
|
|
259
|
+
if (path.includes(node.id)) {
|
|
260
|
+
return;
|
|
461
261
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
262
|
+
if (!filter || filter(node)) {
|
|
263
|
+
visitor?.(node, [
|
|
264
|
+
...path,
|
|
265
|
+
node.id
|
|
266
|
+
]);
|
|
467
267
|
}
|
|
268
|
+
Object.values(this._getNodes({
|
|
269
|
+
id: node.id,
|
|
270
|
+
direction
|
|
271
|
+
})).forEach((child) => this.traverse({
|
|
272
|
+
node: child,
|
|
273
|
+
direction,
|
|
274
|
+
filter,
|
|
275
|
+
visitor
|
|
276
|
+
}, [
|
|
277
|
+
...path,
|
|
278
|
+
node.id
|
|
279
|
+
]));
|
|
468
280
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
var __dxlog_file2 = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph-builder.ts";
|
|
477
|
-
var createExtension = (extension) => {
|
|
478
|
-
const { id, resolver, connector, actions, actionGroups, ...rest } = extension;
|
|
479
|
-
const getId = (key) => `${id}/${key}`;
|
|
480
|
-
return [
|
|
481
|
-
resolver ? {
|
|
482
|
-
id: getId("resolver"),
|
|
483
|
-
resolver
|
|
484
|
-
} : void 0,
|
|
485
|
-
connector ? {
|
|
486
|
-
...rest,
|
|
487
|
-
id: getId("connector"),
|
|
488
|
-
connector
|
|
489
|
-
} : void 0,
|
|
490
|
-
actionGroups ? {
|
|
491
|
-
...rest,
|
|
492
|
-
id: getId("actionGroups"),
|
|
493
|
-
type: ACTION_GROUP_TYPE,
|
|
494
|
-
relation: "outbound",
|
|
495
|
-
connector: ({ node }) => actionGroups({
|
|
496
|
-
node
|
|
497
|
-
})?.map((arg) => ({
|
|
498
|
-
...arg,
|
|
499
|
-
data: actionGroupSymbol,
|
|
500
|
-
type: ACTION_GROUP_TYPE
|
|
501
|
-
}))
|
|
502
|
-
} : void 0,
|
|
503
|
-
actions ? {
|
|
504
|
-
...rest,
|
|
505
|
-
id: getId("actions"),
|
|
506
|
-
type: ACTION_TYPE,
|
|
507
|
-
relation: "outbound",
|
|
508
|
-
connector: ({ node }) => actions({
|
|
509
|
-
node
|
|
510
|
-
})?.map((arg) => ({
|
|
511
|
-
...arg,
|
|
512
|
-
type: ACTION_TYPE
|
|
513
|
-
}))
|
|
514
|
-
} : void 0
|
|
515
|
-
].filter(nonNullable2);
|
|
516
|
-
};
|
|
517
|
-
var Dispatcher = class {
|
|
518
|
-
constructor() {
|
|
519
|
-
this.stateIndex = 0;
|
|
520
|
-
this.state = {};
|
|
521
|
-
this.cleanup = [];
|
|
522
|
-
}
|
|
523
|
-
};
|
|
524
|
-
var BuilderInternal = class {
|
|
525
|
-
};
|
|
526
|
-
var memoize = (fn, key = "result") => {
|
|
527
|
-
const dispatcher = BuilderInternal.currentDispatcher;
|
|
528
|
-
invariant2(dispatcher?.currentExtension, "memoize must be called within an extension", {
|
|
529
|
-
F: __dxlog_file2,
|
|
530
|
-
L: 129,
|
|
531
|
-
S: void 0,
|
|
532
|
-
A: [
|
|
533
|
-
"dispatcher?.currentExtension",
|
|
534
|
-
"'memoize must be called within an extension'"
|
|
535
|
-
]
|
|
536
|
-
});
|
|
537
|
-
const all = dispatcher.state[dispatcher.currentExtension][dispatcher.stateIndex] ?? {};
|
|
538
|
-
const current = all[key];
|
|
539
|
-
const result = current ? current.result : fn();
|
|
540
|
-
dispatcher.state[dispatcher.currentExtension][dispatcher.stateIndex] = {
|
|
541
|
-
...all,
|
|
542
|
-
[key]: {
|
|
543
|
-
result
|
|
281
|
+
/**
|
|
282
|
+
* Get the path between two nodes in the graph.
|
|
283
|
+
*/
|
|
284
|
+
getPath({ source = "root", target }) {
|
|
285
|
+
const start = this.findNode(source);
|
|
286
|
+
if (!start) {
|
|
287
|
+
return void 0;
|
|
544
288
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
L: 144,
|
|
555
|
-
S: void 0,
|
|
556
|
-
A: [
|
|
557
|
-
"dispatcher",
|
|
558
|
-
"'cleanup must be called within an extension'"
|
|
559
|
-
]
|
|
289
|
+
let found;
|
|
290
|
+
this.traverse({
|
|
291
|
+
node: start,
|
|
292
|
+
filter: () => !found,
|
|
293
|
+
visitor: (node, path) => {
|
|
294
|
+
if (node.id === target) {
|
|
295
|
+
found = path;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
560
298
|
});
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
};
|
|
564
|
-
var toSignal = (subscribe, get, key) => {
|
|
565
|
-
const thisSignal = memoize(() => {
|
|
566
|
-
return signal(get());
|
|
567
|
-
}, key);
|
|
568
|
-
const unsubscribe = memoize(() => {
|
|
569
|
-
return subscribe(() => thisSignal.value = get());
|
|
570
|
-
}, key);
|
|
571
|
-
cleanup(() => {
|
|
572
|
-
unsubscribe();
|
|
573
|
-
});
|
|
574
|
-
return thisSignal.value;
|
|
299
|
+
return found;
|
|
300
|
+
}
|
|
575
301
|
};
|
|
302
|
+
|
|
303
|
+
// packages/sdk/app-graph/src/graph-builder.ts
|
|
304
|
+
import { EventSubscriptions } from "@dxos/async";
|
|
576
305
|
var GraphBuilder = class {
|
|
577
306
|
constructor() {
|
|
578
|
-
this.
|
|
579
|
-
this.
|
|
580
|
-
this._resolverSubscriptions = /* @__PURE__ */ new Map();
|
|
581
|
-
this._connectorSubscriptions = /* @__PURE__ */ new Map();
|
|
582
|
-
this._nodeChanged = {};
|
|
583
|
-
this._graph = new Graph({
|
|
584
|
-
onInitialNode: (id, type) => this._onInitialNode(id, type),
|
|
585
|
-
onInitialNodes: (node, relation, type) => this._onInitialNodes(node, relation, type),
|
|
586
|
-
onRemoveNode: (id) => this._onRemoveNode(id)
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
get graph() {
|
|
590
|
-
return this._graph;
|
|
307
|
+
this._extensions = /* @__PURE__ */ new Map();
|
|
308
|
+
this._unsubscribe = new EventSubscriptions();
|
|
591
309
|
}
|
|
592
310
|
/**
|
|
593
311
|
* Register a node builder which will be called in order to construct the graph.
|
|
594
312
|
*/
|
|
595
|
-
addExtension(extension) {
|
|
596
|
-
|
|
597
|
-
extension.forEach((ext) => this.addExtension(ext));
|
|
598
|
-
return this;
|
|
599
|
-
}
|
|
600
|
-
this._dispatcher.state[extension.id] = [];
|
|
601
|
-
this._extensions[extension.id] = extension;
|
|
313
|
+
addExtension(id, extension) {
|
|
314
|
+
this._extensions.set(id, extension);
|
|
602
315
|
return this;
|
|
603
316
|
}
|
|
604
317
|
/**
|
|
605
318
|
* Remove a node builder from the graph builder.
|
|
606
319
|
*/
|
|
607
320
|
removeExtension(id) {
|
|
608
|
-
|
|
321
|
+
this._extensions.delete(id);
|
|
609
322
|
return this;
|
|
610
323
|
}
|
|
611
|
-
destroy() {
|
|
612
|
-
this._dispatcher.cleanup.forEach((fn) => fn());
|
|
613
|
-
this._resolverSubscriptions.forEach((unsubscribe) => unsubscribe());
|
|
614
|
-
this._connectorSubscriptions.forEach((unsubscribe) => unsubscribe());
|
|
615
|
-
this._resolverSubscriptions.clear();
|
|
616
|
-
this._connectorSubscriptions.clear();
|
|
617
|
-
}
|
|
618
324
|
/**
|
|
619
|
-
*
|
|
325
|
+
* Construct the graph, starting by calling all registered extensions.
|
|
326
|
+
* @param previousGraph If provided, the graph will be updated in place.
|
|
620
327
|
*/
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
]);
|
|
630
|
-
const nodes = Object.values(this._extensions).filter((extension) => relation === (extension.relation ?? "outbound")).flatMap((extension) => extension.connector?.({
|
|
631
|
-
node
|
|
632
|
-
}) ?? []).map((arg) => ({
|
|
633
|
-
id: arg.id,
|
|
634
|
-
type: arg.type,
|
|
635
|
-
data: arg.data ?? null,
|
|
636
|
-
properties: arg.properties ?? {}
|
|
637
|
-
}));
|
|
638
|
-
await Promise.all(nodes.map((n) => this.traverse({
|
|
639
|
-
node: n,
|
|
640
|
-
relation,
|
|
641
|
-
visitor
|
|
642
|
-
}, [
|
|
643
|
-
...path,
|
|
644
|
-
node.id
|
|
645
|
-
])));
|
|
646
|
-
}
|
|
647
|
-
_onInitialNode(nodeId, nodeType) {
|
|
648
|
-
this._nodeChanged[nodeId] = this._nodeChanged[nodeId] ?? signal({});
|
|
649
|
-
let initialized;
|
|
650
|
-
for (const { id, type, resolver } of Object.values(this._extensions)) {
|
|
651
|
-
if (!resolver || nodeType && type !== nodeType) {
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
|
-
const unsubscribe = effect2(() => {
|
|
655
|
-
this._dispatcher.currentExtension = id;
|
|
656
|
-
this._dispatcher.stateIndex = 0;
|
|
657
|
-
BuilderInternal.currentDispatcher = this._dispatcher;
|
|
658
|
-
const node = resolver({
|
|
659
|
-
id: nodeId
|
|
660
|
-
});
|
|
661
|
-
BuilderInternal.currentDispatcher = void 0;
|
|
662
|
-
if (node && initialized) {
|
|
663
|
-
this.graph._addNodes([
|
|
664
|
-
node
|
|
665
|
-
]);
|
|
666
|
-
if (this._nodeChanged[initialized.id]) {
|
|
667
|
-
this._nodeChanged[initialized.id].value = {};
|
|
668
|
-
}
|
|
669
|
-
} else if (node) {
|
|
670
|
-
initialized = node;
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
if (initialized) {
|
|
674
|
-
this._resolverSubscriptions.set(nodeId, unsubscribe);
|
|
675
|
-
break;
|
|
676
|
-
} else {
|
|
677
|
-
unsubscribe();
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
return initialized;
|
|
681
|
-
}
|
|
682
|
-
_onInitialNodes(node, nodesRelation, nodesType) {
|
|
683
|
-
this._nodeChanged[node.id] = this._nodeChanged[node.id] ?? signal({});
|
|
684
|
-
let initialized;
|
|
685
|
-
let previous = [];
|
|
686
|
-
this._connectorSubscriptions.set(node.id, effect2(() => {
|
|
687
|
-
Object.keys(this._extensions);
|
|
688
|
-
this._nodeChanged[node.id].value;
|
|
689
|
-
const nodes = [];
|
|
690
|
-
for (const { id, connector, filter, type, relation = "outbound" } of Object.values(this._extensions)) {
|
|
691
|
-
if (!connector || relation !== nodesRelation || nodesType && type !== nodesType || filter && !filter(node)) {
|
|
692
|
-
continue;
|
|
693
|
-
}
|
|
694
|
-
this._dispatcher.currentExtension = id;
|
|
695
|
-
this._dispatcher.stateIndex = 0;
|
|
696
|
-
BuilderInternal.currentDispatcher = this._dispatcher;
|
|
697
|
-
nodes.push(...connector({
|
|
698
|
-
node
|
|
699
|
-
}) ?? []);
|
|
700
|
-
BuilderInternal.currentDispatcher = void 0;
|
|
701
|
-
}
|
|
702
|
-
const ids = nodes.map((n) => n.id);
|
|
703
|
-
const removed = previous.filter((id) => !ids.includes(id));
|
|
704
|
-
previous = ids;
|
|
705
|
-
if (initialized) {
|
|
706
|
-
this.graph._removeNodes(removed, true);
|
|
707
|
-
this.graph._addNodes(nodes);
|
|
708
|
-
this.graph._addEdges(nodes.map(({ id }) => ({
|
|
709
|
-
source: node.id,
|
|
710
|
-
target: id
|
|
711
|
-
})));
|
|
712
|
-
this.graph._sortEdges(node.id, "outbound", nodes.map(({ id }) => id));
|
|
713
|
-
nodes.forEach((n) => {
|
|
714
|
-
if (this._nodeChanged[n.id]) {
|
|
715
|
-
this._nodeChanged[n.id].value = {};
|
|
716
|
-
}
|
|
717
|
-
});
|
|
718
|
-
} else {
|
|
719
|
-
initialized = nodes;
|
|
720
|
-
}
|
|
721
|
-
}));
|
|
722
|
-
return initialized;
|
|
328
|
+
build(previousGraph) {
|
|
329
|
+
this._unsubscribe.clear();
|
|
330
|
+
const graph = previousGraph ?? new Graph();
|
|
331
|
+
Array.from(this._extensions.values()).forEach((builder) => {
|
|
332
|
+
const unsubscribe = builder(graph);
|
|
333
|
+
unsubscribe && this._unsubscribe.add(unsubscribe);
|
|
334
|
+
});
|
|
335
|
+
return graph;
|
|
723
336
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// packages/sdk/app-graph/src/helpers.ts
|
|
340
|
+
var manageNodes = ({ graph, condition, nodes, removeEdges }) => {
|
|
341
|
+
if (condition) {
|
|
342
|
+
return graph.addNodes(...nodes);
|
|
343
|
+
} else {
|
|
344
|
+
nodes.forEach(({ id }) => graph.removeNode(id, removeEdges));
|
|
729
345
|
}
|
|
730
346
|
};
|
|
731
347
|
export {
|
|
732
|
-
ACTION_GROUP_TYPE,
|
|
733
|
-
ACTION_TYPE,
|
|
734
348
|
Graph,
|
|
735
349
|
GraphBuilder,
|
|
736
350
|
ROOT_ID,
|
|
737
|
-
ROOT_TYPE,
|
|
738
351
|
actionGroupSymbol,
|
|
739
|
-
cleanup,
|
|
740
|
-
createExtension,
|
|
741
|
-
getGraph,
|
|
742
352
|
isAction,
|
|
743
353
|
isActionGroup,
|
|
744
354
|
isActionLike,
|
|
745
355
|
isGraphNode,
|
|
746
|
-
|
|
747
|
-
toSignal
|
|
356
|
+
manageNodes
|
|
748
357
|
};
|
|
749
358
|
//# sourceMappingURL=index.mjs.map
|