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