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