@dxos/app-graph 0.6.3-main.40d1cec → 0.6.3-main.9e4e207
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 +582 -191
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +587 -185
- 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 +99 -7
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph-builder.test.d.ts +2 -0
- package/dist/types/src/graph-builder.test.d.ts.map +1 -0
- package/dist/types/src/graph.d.ts +96 -47
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +99 -40
- 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 +14 -12
- package/src/graph-builder.test.ts +310 -0
- package/src/graph-builder.ts +332 -19
- package/src/graph.test.ts +431 -179
- package/src/graph.ts +336 -149
- package/src/index.ts +0 -1
- package/src/node.ts +15 -42
- package/src/stories/EchoGraph.stories.tsx +84 -102
- package/dist/types/src/helpers.d.ts +0 -12
- package/dist/types/src/helpers.d.ts.map +0 -1
- package/src/helpers.ts +0 -27
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// packages/sdk/app-graph/src/graph.ts
|
|
2
|
-
import { untracked } from "@preact/signals-core";
|
|
2
|
+
import { batch, effect, untracked } from "@preact/signals-core";
|
|
3
|
+
import { Trigger } from "@dxos/async";
|
|
3
4
|
import { create } from "@dxos/echo-schema";
|
|
4
5
|
import { invariant } from "@dxos/invariant";
|
|
5
6
|
import { nonNullable } from "@dxos/util";
|
|
@@ -13,51 +14,56 @@ var isActionLike = (data) => isAction(data) || isActionGroup(data);
|
|
|
13
14
|
|
|
14
15
|
// packages/sdk/app-graph/src/graph.ts
|
|
15
16
|
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
|
+
};
|
|
16
31
|
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;
|
|
17
36
|
var Graph = class {
|
|
18
|
-
constructor() {
|
|
37
|
+
constructor({ onInitialNode, onInitialNodes, onRemoveNode } = {}) {
|
|
38
|
+
this._waitingForNodes = {};
|
|
39
|
+
this._initialized = {};
|
|
19
40
|
/**
|
|
20
41
|
* @internal
|
|
21
42
|
*/
|
|
22
|
-
this._nodes =
|
|
23
|
-
[ROOT_ID]: {
|
|
24
|
-
id: ROOT_ID,
|
|
25
|
-
properties: {},
|
|
26
|
-
data: null
|
|
27
|
-
}
|
|
28
|
-
});
|
|
43
|
+
this._nodes = {};
|
|
29
44
|
/**
|
|
30
45
|
* @internal
|
|
31
46
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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;
|
|
47
|
+
this._edges = {};
|
|
48
|
+
this._constructNode = (node) => {
|
|
49
|
+
return create({
|
|
50
|
+
...node,
|
|
51
|
+
[graphSymbol]: this
|
|
52
|
+
});
|
|
60
53
|
};
|
|
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
|
+
});
|
|
61
67
|
}
|
|
62
68
|
/**
|
|
63
69
|
* Alias for `findNode('root')`.
|
|
@@ -68,11 +74,14 @@ var Graph = class {
|
|
|
68
74
|
/**
|
|
69
75
|
* Convert the graph to a JSON object.
|
|
70
76
|
*/
|
|
71
|
-
toJSON({ id = ROOT_ID, maxLength = 32 } = {}) {
|
|
77
|
+
toJSON({ id = ROOT_ID, maxLength = 32, onlyLoaded = true } = {}) {
|
|
72
78
|
const toJSON = (node, seen = []) => {
|
|
73
|
-
const nodes =
|
|
79
|
+
const nodes = this.nodes(node, {
|
|
80
|
+
onlyLoaded
|
|
81
|
+
});
|
|
74
82
|
const obj = {
|
|
75
|
-
id: node.id.length > maxLength ? `${node.id.slice(0, maxLength - 3)}...` : node.id
|
|
83
|
+
id: node.id.length > maxLength ? `${node.id.slice(0, maxLength - 3)}...` : node.id,
|
|
84
|
+
type: node.type
|
|
76
85
|
};
|
|
77
86
|
if (node.properties.label) {
|
|
78
87
|
obj.label = node.properties.label;
|
|
@@ -91,7 +100,7 @@ var Graph = class {
|
|
|
91
100
|
const root = this.findNode(id);
|
|
92
101
|
invariant(root, `Node not found: ${id}`, {
|
|
93
102
|
F: __dxlog_file,
|
|
94
|
-
L:
|
|
103
|
+
L: 140,
|
|
95
104
|
S: this,
|
|
96
105
|
A: [
|
|
97
106
|
"root",
|
|
@@ -102,257 +111,639 @@ var Graph = class {
|
|
|
102
111
|
}
|
|
103
112
|
/**
|
|
104
113
|
* 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.
|
|
105
117
|
*/
|
|
106
|
-
findNode(id) {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
*/
|
|
131
|
+
waitForNode(id, timeout = NODE_TIMEOUT) {
|
|
132
|
+
if (this._nodes[id]) {
|
|
133
|
+
return Promise.resolve(this._nodes[id]);
|
|
110
134
|
}
|
|
111
|
-
|
|
135
|
+
const trigger = this._waitingForNodes[id] ?? (this._waitingForNodes[id] = new Trigger());
|
|
136
|
+
return trigger.wait({
|
|
137
|
+
timeout
|
|
138
|
+
});
|
|
112
139
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Nodes that this node is connected to in default order.
|
|
142
|
+
*/
|
|
143
|
+
nodes(node, options = {}) {
|
|
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;
|
|
117
186
|
}
|
|
118
|
-
|
|
187
|
+
const shouldContinue = visitor(node, [
|
|
188
|
+
...path,
|
|
189
|
+
node.id
|
|
190
|
+
]);
|
|
191
|
+
if (shouldContinue === false) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
Object.values(this._getNodes({
|
|
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
|
+
]));
|
|
119
207
|
}
|
|
120
|
-
|
|
121
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Recursive depth-first traversal of the graph wrapping each visitor call in an effect.
|
|
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;
|
|
122
262
|
}
|
|
123
263
|
/**
|
|
124
264
|
* Add nodes to the graph.
|
|
265
|
+
*
|
|
266
|
+
* @internal
|
|
125
267
|
*/
|
|
126
|
-
|
|
127
|
-
return nodes.map((node) => this._addNode(node));
|
|
268
|
+
_addNodes(nodes) {
|
|
269
|
+
return batch(() => nodes.map((node) => this._addNode(node)));
|
|
128
270
|
}
|
|
129
271
|
_addNode({ nodes, edges, ..._node }) {
|
|
130
272
|
return untracked(() => {
|
|
131
|
-
const
|
|
273
|
+
const existingNode = this._nodes[_node.id];
|
|
274
|
+
const node = existingNode ?? this._constructNode({
|
|
132
275
|
data: null,
|
|
133
276
|
properties: {},
|
|
134
277
|
..._node
|
|
135
|
-
};
|
|
136
|
-
|
|
278
|
+
});
|
|
279
|
+
if (existingNode) {
|
|
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
|
+
}
|
|
137
304
|
if (nodes) {
|
|
138
305
|
nodes.forEach((subNode) => {
|
|
139
306
|
this._addNode(subNode);
|
|
140
|
-
this.
|
|
307
|
+
this._addEdge({
|
|
141
308
|
source: node.id,
|
|
142
309
|
target: subNode.id
|
|
143
310
|
});
|
|
144
311
|
});
|
|
145
312
|
}
|
|
146
313
|
if (edges) {
|
|
147
|
-
edges.forEach(([id,
|
|
314
|
+
edges.forEach(([id, relation]) => relation === "outbound" ? this._addEdge({
|
|
148
315
|
source: node.id,
|
|
149
316
|
target: id
|
|
150
|
-
}) : this.
|
|
317
|
+
}) : this._addEdge({
|
|
151
318
|
source: id,
|
|
152
319
|
target: node.id
|
|
153
320
|
}));
|
|
154
321
|
}
|
|
155
|
-
return
|
|
322
|
+
return node;
|
|
156
323
|
});
|
|
157
324
|
}
|
|
158
325
|
/**
|
|
159
326
|
* Remove nodes from the graph.
|
|
160
327
|
*
|
|
161
|
-
* @param
|
|
328
|
+
* @param ids The id of the node to remove.
|
|
162
329
|
* @param edges Whether to remove edges connected to the node from the graph as well.
|
|
330
|
+
* @internal
|
|
163
331
|
*/
|
|
164
|
-
|
|
332
|
+
_removeNodes(ids, edges = false) {
|
|
333
|
+
batch(() => ids.forEach((id) => this._removeNode(id, edges)));
|
|
334
|
+
}
|
|
335
|
+
_removeNode(id, edges = false) {
|
|
165
336
|
untracked(() => {
|
|
166
337
|
const node = this.findNode(id);
|
|
167
338
|
if (!node) {
|
|
168
339
|
return;
|
|
169
340
|
}
|
|
170
341
|
if (edges) {
|
|
171
|
-
delete this._edges[this.getEdgeKey(id, "outbound")];
|
|
172
|
-
delete this._edges[this.getEdgeKey(id, "inbound")];
|
|
173
342
|
this._getNodes({
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
343
|
+
node,
|
|
344
|
+
onlyLoaded: true
|
|
345
|
+
}).forEach((node2) => {
|
|
346
|
+
this._removeEdge({
|
|
347
|
+
source: id,
|
|
348
|
+
target: node2.id
|
|
349
|
+
});
|
|
350
|
+
});
|
|
179
351
|
this._getNodes({
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
352
|
+
node,
|
|
353
|
+
relation: "inbound",
|
|
354
|
+
onlyLoaded: true
|
|
355
|
+
}).forEach((node2) => {
|
|
356
|
+
this._removeEdge({
|
|
357
|
+
source: node2.id,
|
|
358
|
+
target: id
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
delete this._edges[id];
|
|
186
362
|
}
|
|
187
363
|
delete this._nodes[id];
|
|
364
|
+
this._onRemoveNode?.(id);
|
|
188
365
|
});
|
|
189
366
|
}
|
|
190
367
|
/**
|
|
191
|
-
* Add
|
|
368
|
+
* Add edges to the graph.
|
|
369
|
+
*
|
|
370
|
+
* @internal
|
|
192
371
|
*/
|
|
193
|
-
|
|
372
|
+
_addEdges(edges) {
|
|
373
|
+
batch(() => edges.forEach((edge) => this._addEdge(edge)));
|
|
374
|
+
}
|
|
375
|
+
_addEdge({ source, target }) {
|
|
194
376
|
untracked(() => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
377
|
+
if (!this._edges[source]) {
|
|
378
|
+
this._edges[source] = create({
|
|
379
|
+
inbound: [],
|
|
380
|
+
outbound: []
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
if (!this._edges[target]) {
|
|
384
|
+
this._edges[target] = create({
|
|
385
|
+
inbound: [],
|
|
386
|
+
outbound: []
|
|
387
|
+
});
|
|
202
388
|
}
|
|
203
|
-
const
|
|
204
|
-
if (!
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
inbound.push(source);
|
|
389
|
+
const sourceEdges = this._edges[source];
|
|
390
|
+
if (!sourceEdges.outbound.includes(target)) {
|
|
391
|
+
sourceEdges.outbound.push(target);
|
|
392
|
+
}
|
|
393
|
+
const targetEdges = this._edges[target];
|
|
394
|
+
if (!targetEdges.inbound.includes(source)) {
|
|
395
|
+
targetEdges.inbound.push(source);
|
|
210
396
|
}
|
|
211
397
|
});
|
|
212
398
|
}
|
|
213
399
|
/**
|
|
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
|
+
/**
|
|
214
421
|
* Sort edges for a node.
|
|
215
422
|
*
|
|
216
423
|
* Edges not included in the sorted list are appended to the end of the list.
|
|
217
424
|
*
|
|
218
425
|
* @param nodeId The id of the node to sort edges for.
|
|
219
|
-
* @param
|
|
426
|
+
* @param relation The relation of the edges from the node to sort.
|
|
220
427
|
* @param edges The ordered list of edges.
|
|
428
|
+
* @ignore
|
|
221
429
|
*/
|
|
222
|
-
|
|
430
|
+
_sortEdges(nodeId, relation, edges) {
|
|
223
431
|
untracked(() => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
...
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
432
|
+
batch(() => {
|
|
433
|
+
const current = this._edges[nodeId];
|
|
434
|
+
if (current) {
|
|
435
|
+
const unsorted = current[relation].filter((id) => !edges.includes(id)) ?? [];
|
|
436
|
+
const sorted = edges.filter((id) => current[relation].includes(id)) ?? [];
|
|
437
|
+
current[relation].splice(0, current[relation].length, ...[
|
|
438
|
+
...sorted,
|
|
439
|
+
...unsorted
|
|
440
|
+
]);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
233
443
|
});
|
|
234
444
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (
|
|
242
|
-
this.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
445
|
+
_getNodes({ node, relation = "outbound", type, onlyLoaded }) {
|
|
446
|
+
const key = `${node.id}-${relation}-${type}`;
|
|
447
|
+
const initialized = this._initialized[key];
|
|
448
|
+
if (!initialized && !onlyLoaded && this._onInitialNodes) {
|
|
449
|
+
const args = this._onInitialNodes(node, relation, type)?.filter((n) => !type || n.type === type);
|
|
450
|
+
this._initialized[key] = true;
|
|
451
|
+
if (args && args.length > 0) {
|
|
452
|
+
const nodes = this._addNodes(args);
|
|
453
|
+
this._addEdges(nodes.map(({ id }) => relation === "outbound" ? {
|
|
454
|
+
source: node.id,
|
|
455
|
+
target: id
|
|
456
|
+
} : {
|
|
457
|
+
source: id,
|
|
458
|
+
target: node.id
|
|
459
|
+
}));
|
|
247
460
|
}
|
|
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;
|
|
261
461
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
]);
|
|
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
|
-
]));
|
|
280
|
-
}
|
|
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;
|
|
462
|
+
const edges = this._edges[node.id];
|
|
463
|
+
if (!edges) {
|
|
464
|
+
return [];
|
|
465
|
+
} else {
|
|
466
|
+
return edges[relation].map((id) => this._nodes[id]).filter(nonNullable).filter((n) => !type || n.type === type);
|
|
288
467
|
}
|
|
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
|
-
}
|
|
298
|
-
});
|
|
299
|
-
return found;
|
|
300
468
|
}
|
|
301
469
|
};
|
|
302
470
|
|
|
303
471
|
// packages/sdk/app-graph/src/graph-builder.ts
|
|
304
|
-
import {
|
|
472
|
+
import { effect as effect2, signal } from "@preact/signals-core";
|
|
473
|
+
import { create as create2 } from "@dxos/echo-schema";
|
|
474
|
+
import { invariant as invariant2 } from "@dxos/invariant";
|
|
475
|
+
import { nonNullable as nonNullable2 } from "@dxos/util";
|
|
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
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
dispatcher.stateIndex++;
|
|
547
|
+
return result;
|
|
548
|
+
};
|
|
549
|
+
var cleanup = (fn) => {
|
|
550
|
+
memoize(() => {
|
|
551
|
+
const dispatcher = BuilderInternal.currentDispatcher;
|
|
552
|
+
invariant2(dispatcher, "cleanup must be called within an extension", {
|
|
553
|
+
F: __dxlog_file2,
|
|
554
|
+
L: 144,
|
|
555
|
+
S: void 0,
|
|
556
|
+
A: [
|
|
557
|
+
"dispatcher",
|
|
558
|
+
"'cleanup must be called within an extension'"
|
|
559
|
+
]
|
|
560
|
+
});
|
|
561
|
+
dispatcher.cleanup.push(fn);
|
|
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;
|
|
575
|
+
};
|
|
305
576
|
var GraphBuilder = class {
|
|
306
577
|
constructor() {
|
|
307
|
-
this.
|
|
308
|
-
this.
|
|
578
|
+
this._dispatcher = new Dispatcher();
|
|
579
|
+
this._extensions = create2({});
|
|
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;
|
|
309
591
|
}
|
|
310
592
|
/**
|
|
311
593
|
* Register a node builder which will be called in order to construct the graph.
|
|
312
594
|
*/
|
|
313
|
-
addExtension(
|
|
314
|
-
|
|
595
|
+
addExtension(extension) {
|
|
596
|
+
if (Array.isArray(extension)) {
|
|
597
|
+
extension.forEach((ext) => this.addExtension(ext));
|
|
598
|
+
return this;
|
|
599
|
+
}
|
|
600
|
+
this._dispatcher.state[extension.id] = [];
|
|
601
|
+
this._extensions[extension.id] = extension;
|
|
315
602
|
return this;
|
|
316
603
|
}
|
|
317
604
|
/**
|
|
318
605
|
* Remove a node builder from the graph builder.
|
|
319
606
|
*/
|
|
320
607
|
removeExtension(id) {
|
|
321
|
-
this._extensions
|
|
608
|
+
delete this._extensions[id];
|
|
322
609
|
return this;
|
|
323
610
|
}
|
|
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
|
+
}
|
|
324
618
|
/**
|
|
325
|
-
*
|
|
326
|
-
* @param previousGraph If provided, the graph will be updated in place.
|
|
619
|
+
* Traverse a graph using just the connector extensions, without subscribing to any signals or persisting any nodes.
|
|
327
620
|
*/
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
621
|
+
// TODO(wittjosiah): Rename? This is not traversing the graph proper.
|
|
622
|
+
async traverse({ node, relation = "outbound", visitor }, path = []) {
|
|
623
|
+
if (path.includes(node.id)) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
visitor(node, [
|
|
627
|
+
...path,
|
|
628
|
+
node.id
|
|
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
|
+
])));
|
|
336
646
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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;
|
|
723
|
+
}
|
|
724
|
+
_onRemoveNode(nodeId) {
|
|
725
|
+
this._resolverSubscriptions.get(nodeId)?.();
|
|
726
|
+
this._connectorSubscriptions.get(nodeId)?.();
|
|
727
|
+
this._resolverSubscriptions.delete(nodeId);
|
|
728
|
+
this._connectorSubscriptions.delete(nodeId);
|
|
345
729
|
}
|
|
346
730
|
};
|
|
347
731
|
export {
|
|
732
|
+
ACTION_GROUP_TYPE,
|
|
733
|
+
ACTION_TYPE,
|
|
348
734
|
Graph,
|
|
349
735
|
GraphBuilder,
|
|
350
736
|
ROOT_ID,
|
|
737
|
+
ROOT_TYPE,
|
|
351
738
|
actionGroupSymbol,
|
|
739
|
+
cleanup,
|
|
740
|
+
createExtension,
|
|
741
|
+
getGraph,
|
|
352
742
|
isAction,
|
|
353
743
|
isActionGroup,
|
|
354
744
|
isActionLike,
|
|
355
745
|
isGraphNode,
|
|
356
|
-
|
|
746
|
+
memoize,
|
|
747
|
+
toSignal
|
|
357
748
|
};
|
|
358
749
|
//# sourceMappingURL=index.mjs.map
|