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