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