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