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