@dxos/app-graph 0.8.2-main.fbd8ed0 → 0.8.2-staging.7ac8446
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 +789 -541
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +780 -533
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +789 -541
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/graph-builder.d.ts +91 -48
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +98 -191
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +3 -3
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -23
- package/src/graph-builder.test.ts +310 -293
- package/src/graph-builder.ts +317 -209
- package/src/graph.test.ts +463 -314
- package/src/graph.ts +455 -452
- package/src/node.ts +4 -4
- package/src/stories/EchoGraph.stories.tsx +78 -57
- package/dist/types/src/experimental/graph-projections.test.d.ts +0 -25
- package/dist/types/src/experimental/graph-projections.test.d.ts.map +0 -1
- package/dist/types/src/signals-integration.test.d.ts +0 -2
- package/dist/types/src/signals-integration.test.d.ts.map +0 -1
- package/dist/types/src/testing.d.ts +0 -5
- package/dist/types/src/testing.d.ts.map +0 -1
- package/src/experimental/graph-projections.test.ts +0 -56
- package/src/signals-integration.test.ts +0 -218
- package/src/testing.ts +0 -20
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
// packages/sdk/app-graph/src/graph.ts
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { Event, Trigger } from "@dxos/async";
|
|
5
|
-
import { todo } from "@dxos/debug";
|
|
2
|
+
import { batch, effect, untracked } from "@preact/signals-core";
|
|
3
|
+
import { asyncTimeout, Trigger } from "@dxos/async";
|
|
6
4
|
import { invariant } from "@dxos/invariant";
|
|
5
|
+
import { create } from "@dxos/live-object";
|
|
7
6
|
import { log } from "@dxos/log";
|
|
8
|
-
import { isNonNullable } from "@dxos/util";
|
|
7
|
+
import { isNonNullable, pick } from "@dxos/util";
|
|
8
|
+
|
|
9
|
+
// packages/sdk/app-graph/src/node.ts
|
|
10
|
+
var isGraphNode = (data) => data && typeof data === "object" && "id" in data && "properties" in data && data.properties ? typeof data.properties === "object" && "data" in data : false;
|
|
11
|
+
var isAction = (data) => isGraphNode(data) ? typeof data.data === "function" : false;
|
|
12
|
+
var actionGroupSymbol = Symbol("ActionGroup");
|
|
13
|
+
var isActionGroup = (data) => isGraphNode(data) ? data.data === actionGroupSymbol : false;
|
|
14
|
+
var isActionLike = (data) => isAction(data) || isActionGroup(data);
|
|
15
|
+
|
|
16
|
+
// packages/sdk/app-graph/src/graph.ts
|
|
9
17
|
var __dxlog_file = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph.ts";
|
|
10
18
|
var graphSymbol = Symbol("graph");
|
|
11
19
|
var getGraph = (node) => {
|
|
12
20
|
const graph = node[graphSymbol];
|
|
13
21
|
invariant(graph, "Node is not associated with a graph.", {
|
|
14
22
|
F: __dxlog_file,
|
|
15
|
-
L:
|
|
23
|
+
L: 21,
|
|
16
24
|
S: void 0,
|
|
17
25
|
A: [
|
|
18
26
|
"graph",
|
|
@@ -25,496 +33,649 @@ var ROOT_ID = "root";
|
|
|
25
33
|
var ROOT_TYPE = "dxos.org/type/GraphRoot";
|
|
26
34
|
var ACTION_TYPE = "dxos.org/type/GraphAction";
|
|
27
35
|
var ACTION_GROUP_TYPE = "dxos.org/type/GraphActionGroup";
|
|
28
|
-
var
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
this.
|
|
32
|
-
this._initialized =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
/** @internal */
|
|
46
|
-
this._node = Rx.family((id) => {
|
|
47
|
-
const initial = Option.flatten(Record.get(this._initialNodes, id));
|
|
48
|
-
return Rx.make(initial).pipe(Rx.keepAlive, Rx.withLabel(`graph:node:${id}`));
|
|
49
|
-
});
|
|
50
|
-
this._nodeOrThrow = Rx.family((id) => {
|
|
51
|
-
return Rx.make((get) => {
|
|
52
|
-
const node = get(this._node(id));
|
|
53
|
-
invariant(Option.isSome(node), `Node not available: ${id}`, {
|
|
54
|
-
F: __dxlog_file,
|
|
55
|
-
L: 253,
|
|
56
|
-
S: this,
|
|
57
|
-
A: [
|
|
58
|
-
"Option.isSome(node)",
|
|
59
|
-
"`Node not available: ${id}`"
|
|
60
|
-
]
|
|
61
|
-
});
|
|
62
|
-
return node.value;
|
|
36
|
+
var DEFAULT_FILTER = (node) => untracked(() => !isActionLike(node));
|
|
37
|
+
var Graph = class _Graph {
|
|
38
|
+
constructor({ nodes, edges, onInitialNode, onInitialNodes, onRemoveNode } = {}) {
|
|
39
|
+
this._waitingForNodes = {};
|
|
40
|
+
this._initialized = {};
|
|
41
|
+
/**
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
this._nodes = {};
|
|
45
|
+
/**
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
this._edges = {};
|
|
49
|
+
this._constructNode = (node) => {
|
|
50
|
+
return create({
|
|
51
|
+
...node,
|
|
52
|
+
[graphSymbol]: this
|
|
63
53
|
});
|
|
64
|
-
}
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
inbound: [],
|
|
68
|
-
outbound: []
|
|
69
|
-
})));
|
|
70
|
-
return Rx.make(initial).pipe(Rx.keepAlive, Rx.withLabel(`graph:edges:${id}`));
|
|
71
|
-
});
|
|
72
|
-
// NOTE: Currently the argument to the family needs to be referentially stable for the rx to be referentially stable.
|
|
73
|
-
// TODO(wittjosiah): Rx feature request, support for something akin to `ComplexMap` to allow for complex arguments.
|
|
74
|
-
this._connections = Rx.family((key) => {
|
|
75
|
-
return Rx.make((get) => {
|
|
76
|
-
const [id, relation] = key.split("$");
|
|
77
|
-
const edges = get(this._edges(id));
|
|
78
|
-
return edges[relation].map((id2) => get(this._node(id2))).filter(Option.isSome).map((o) => o.value);
|
|
79
|
-
}).pipe(Rx.withLabel(`graph:connections:${key}`));
|
|
80
|
-
});
|
|
81
|
-
this._actions = Rx.family((id) => {
|
|
82
|
-
return Rx.make((get) => {
|
|
83
|
-
return get(this._connections(`${id}$outbound`)).filter((node) => node.type === ACTION_TYPE || node.type === ACTION_GROUP_TYPE);
|
|
84
|
-
}).pipe(Rx.withLabel(`graph:actions:${id}`));
|
|
85
|
-
});
|
|
86
|
-
this._json = Rx.family((id) => {
|
|
87
|
-
return Rx.make((get) => {
|
|
88
|
-
const toJSON = (node, seen = []) => {
|
|
89
|
-
const nodes = get(this.connections(node.id));
|
|
90
|
-
const obj = {
|
|
91
|
-
id: node.id.length > 32 ? `${node.id.slice(0, 32)}...` : node.id,
|
|
92
|
-
type: node.type
|
|
93
|
-
};
|
|
94
|
-
if (node.properties.label) {
|
|
95
|
-
obj.label = node.properties.label;
|
|
96
|
-
}
|
|
97
|
-
if (nodes.length) {
|
|
98
|
-
obj.nodes = nodes.map((n) => {
|
|
99
|
-
const nextSeen = [
|
|
100
|
-
...seen,
|
|
101
|
-
node.id
|
|
102
|
-
];
|
|
103
|
-
return nextSeen.includes(n.id) ? void 0 : toJSON(n, nextSeen);
|
|
104
|
-
}).filter(isNonNullable);
|
|
105
|
-
}
|
|
106
|
-
return obj;
|
|
107
|
-
};
|
|
108
|
-
const root = get(this.nodeOrThrow(id));
|
|
109
|
-
return toJSON(root);
|
|
110
|
-
}).pipe(Rx.withLabel(`graph:json:${id}`));
|
|
111
|
-
});
|
|
112
|
-
this._registry = registry ?? Registry.make();
|
|
113
|
-
this._onExpand = onExpand;
|
|
54
|
+
};
|
|
55
|
+
this._onInitialNode = onInitialNode;
|
|
56
|
+
this._onInitialNodes = onInitialNodes;
|
|
114
57
|
this._onRemoveNode = onRemoveNode;
|
|
58
|
+
this._nodes[ROOT_ID] = this._constructNode({
|
|
59
|
+
id: ROOT_ID,
|
|
60
|
+
type: ROOT_TYPE,
|
|
61
|
+
cacheable: [],
|
|
62
|
+
properties: {},
|
|
63
|
+
data: null
|
|
64
|
+
});
|
|
115
65
|
if (nodes) {
|
|
116
66
|
nodes.forEach((node) => {
|
|
117
|
-
|
|
67
|
+
const cacheable = Object.keys(node.properties ?? {});
|
|
68
|
+
if (node.type === ACTION_TYPE) {
|
|
69
|
+
this._addNode({
|
|
70
|
+
cacheable,
|
|
71
|
+
data: () => log.warn("Pickled action invocation", void 0, {
|
|
72
|
+
F: __dxlog_file,
|
|
73
|
+
L: 113,
|
|
74
|
+
S: this,
|
|
75
|
+
C: (f, a) => f(...a)
|
|
76
|
+
}),
|
|
77
|
+
...node
|
|
78
|
+
});
|
|
79
|
+
} else if (node.type === ACTION_GROUP_TYPE) {
|
|
80
|
+
this._addNode({
|
|
81
|
+
cacheable,
|
|
82
|
+
data: actionGroupSymbol,
|
|
83
|
+
...node
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
this._addNode({
|
|
87
|
+
cacheable,
|
|
88
|
+
...node
|
|
89
|
+
});
|
|
90
|
+
}
|
|
118
91
|
});
|
|
119
92
|
}
|
|
93
|
+
this._edges[ROOT_ID] = create({
|
|
94
|
+
inbound: [],
|
|
95
|
+
outbound: []
|
|
96
|
+
});
|
|
120
97
|
if (edges) {
|
|
121
98
|
Object.entries(edges).forEach(([source, edges2]) => {
|
|
122
|
-
|
|
99
|
+
edges2.forEach((target) => {
|
|
100
|
+
this._addEdge({
|
|
101
|
+
source,
|
|
102
|
+
target
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
this._sortEdges(source, "outbound", edges2);
|
|
123
106
|
});
|
|
124
107
|
}
|
|
125
108
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return this._node(id);
|
|
134
|
-
}
|
|
135
|
-
nodeOrThrow(id) {
|
|
136
|
-
return this._nodeOrThrow(id);
|
|
137
|
-
}
|
|
138
|
-
connections(id, relation = "outbound") {
|
|
139
|
-
return this._connections(`${id}$${relation}`);
|
|
140
|
-
}
|
|
141
|
-
actions(id) {
|
|
142
|
-
return this._actions(id);
|
|
143
|
-
}
|
|
144
|
-
edges(id) {
|
|
145
|
-
return this._edges(id);
|
|
109
|
+
static from(pickle, options = {}) {
|
|
110
|
+
const { nodes, edges } = JSON.parse(pickle);
|
|
111
|
+
return new _Graph({
|
|
112
|
+
nodes,
|
|
113
|
+
edges,
|
|
114
|
+
...options
|
|
115
|
+
});
|
|
146
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Alias for `findNode('root')`.
|
|
119
|
+
*/
|
|
147
120
|
get root() {
|
|
148
|
-
return this.
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const expanded = Record.get(this._expanded, key).pipe(Option.getOrElse(() => false));
|
|
177
|
-
log("expand", {
|
|
178
|
-
key,
|
|
179
|
-
expanded
|
|
180
|
-
}, {
|
|
121
|
+
return this.findNode(ROOT_ID);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Convert the graph to a JSON object.
|
|
125
|
+
*/
|
|
126
|
+
toJSON({ id = ROOT_ID, maxLength = 32 } = {}) {
|
|
127
|
+
const toJSON = (node, seen = []) => {
|
|
128
|
+
const nodes = this.nodes(node);
|
|
129
|
+
const obj = {
|
|
130
|
+
id: node.id.length > maxLength ? `${node.id.slice(0, maxLength - 3)}...` : node.id,
|
|
131
|
+
type: node.type
|
|
132
|
+
};
|
|
133
|
+
if (node.properties.label) {
|
|
134
|
+
obj.label = node.properties.label;
|
|
135
|
+
}
|
|
136
|
+
if (nodes.length) {
|
|
137
|
+
obj.nodes = nodes.map((n) => {
|
|
138
|
+
const nextSeen = [
|
|
139
|
+
...seen,
|
|
140
|
+
node.id
|
|
141
|
+
];
|
|
142
|
+
return nextSeen.includes(n.id) ? void 0 : toJSON(n, nextSeen);
|
|
143
|
+
}).filter(isNonNullable);
|
|
144
|
+
}
|
|
145
|
+
return obj;
|
|
146
|
+
};
|
|
147
|
+
const root = this.findNode(id);
|
|
148
|
+
invariant(root, `Node not found: ${id}`, {
|
|
181
149
|
F: __dxlog_file,
|
|
182
|
-
L:
|
|
150
|
+
L: 171,
|
|
183
151
|
S: this,
|
|
184
|
-
|
|
152
|
+
A: [
|
|
153
|
+
"root",
|
|
154
|
+
"`Node not found: ${id}`"
|
|
155
|
+
]
|
|
185
156
|
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
157
|
+
return toJSON(root);
|
|
158
|
+
}
|
|
159
|
+
pickle() {
|
|
160
|
+
const nodes = Object.values(this._nodes).filter((node) => !!node.cacheable).map((node) => {
|
|
161
|
+
return {
|
|
162
|
+
id: node.id,
|
|
163
|
+
type: node.type,
|
|
164
|
+
properties: pick(node.properties, node.cacheable)
|
|
165
|
+
};
|
|
194
166
|
});
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const { id, type, data = null, properties = {} } = nodeArg;
|
|
198
|
-
const nodeRx = this._node(id);
|
|
199
|
-
const node = this._registry.get(nodeRx);
|
|
200
|
-
Option.match(node, {
|
|
201
|
-
onSome: (node2) => {
|
|
202
|
-
const typeChanged = node2.type !== type;
|
|
203
|
-
const dataChanged = node2.data !== data;
|
|
204
|
-
const propertiesChanged = Object.keys(properties).some((key) => node2.properties[key] !== properties[key]);
|
|
205
|
-
log("existing node", {
|
|
206
|
-
typeChanged,
|
|
207
|
-
dataChanged,
|
|
208
|
-
propertiesChanged
|
|
209
|
-
}, {
|
|
210
|
-
F: __dxlog_file,
|
|
211
|
-
L: 417,
|
|
212
|
-
S: this,
|
|
213
|
-
C: (f, a) => f(...a)
|
|
214
|
-
});
|
|
215
|
-
if (typeChanged || dataChanged || propertiesChanged) {
|
|
216
|
-
log("updating node", {
|
|
217
|
-
id,
|
|
218
|
-
type,
|
|
219
|
-
data,
|
|
220
|
-
properties
|
|
221
|
-
}, {
|
|
222
|
-
F: __dxlog_file,
|
|
223
|
-
L: 419,
|
|
224
|
-
S: this,
|
|
225
|
-
C: (f, a) => f(...a)
|
|
226
|
-
});
|
|
227
|
-
const newNode = Option.some({
|
|
228
|
-
...node2,
|
|
229
|
-
type,
|
|
230
|
-
data,
|
|
231
|
-
properties: {
|
|
232
|
-
...node2.properties,
|
|
233
|
-
...properties
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
this._registry.set(nodeRx, newNode);
|
|
237
|
-
this.onNodeChanged.emit({
|
|
238
|
-
id,
|
|
239
|
-
node: newNode
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
onNone: () => {
|
|
244
|
-
log("new node", {
|
|
245
|
-
id,
|
|
246
|
-
type,
|
|
247
|
-
data,
|
|
248
|
-
properties
|
|
249
|
-
}, {
|
|
250
|
-
F: __dxlog_file,
|
|
251
|
-
L: 426,
|
|
252
|
-
S: this,
|
|
253
|
-
C: (f, a) => f(...a)
|
|
254
|
-
});
|
|
255
|
-
const newNode = this._constructNode({
|
|
256
|
-
id,
|
|
257
|
-
type,
|
|
258
|
-
data,
|
|
259
|
-
properties
|
|
260
|
-
});
|
|
261
|
-
this._registry.set(nodeRx, newNode);
|
|
262
|
-
this.onNodeChanged.emit({
|
|
263
|
-
id,
|
|
264
|
-
node: newNode
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
if (nodes) {
|
|
269
|
-
this.addNodes(nodes);
|
|
270
|
-
const _edges = nodes.map((node2) => ({
|
|
271
|
-
source: id,
|
|
272
|
-
target: node2.id
|
|
273
|
-
}));
|
|
274
|
-
this.addEdges(_edges);
|
|
275
|
-
}
|
|
276
|
-
if (edges) {
|
|
277
|
-
todo();
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
removeNodes(ids, edges = false) {
|
|
281
|
-
Rx.batch(() => {
|
|
282
|
-
ids.map((id) => this.removeNode(id, edges));
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
removeNode(id, edges = false) {
|
|
286
|
-
const nodeRx = this._node(id);
|
|
287
|
-
this._registry.set(nodeRx, Option.none());
|
|
288
|
-
this.onNodeChanged.emit({
|
|
167
|
+
const cacheable = new Set(nodes.map((node) => node.id));
|
|
168
|
+
const edges = Object.fromEntries(Object.entries(this._edges).filter(([id]) => cacheable.has(id)).map(([id, { outbound }]) => [
|
|
289
169
|
id,
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
...inbound.map((source) => ({
|
|
296
|
-
source,
|
|
297
|
-
target: id
|
|
298
|
-
})),
|
|
299
|
-
...outbound.map((target) => ({
|
|
300
|
-
source: id,
|
|
301
|
-
target
|
|
302
|
-
}))
|
|
303
|
-
];
|
|
304
|
-
this.removeEdges(edges2);
|
|
305
|
-
}
|
|
306
|
-
this._onRemoveNode?.(id);
|
|
307
|
-
}
|
|
308
|
-
addEdges(edges) {
|
|
309
|
-
Rx.batch(() => {
|
|
310
|
-
edges.map((edge) => this.addEdge(edge));
|
|
170
|
+
outbound.filter((nodeId) => cacheable.has(nodeId))
|
|
171
|
+
]).toSorted(([a], [b]) => a.localeCompare(b)));
|
|
172
|
+
return JSON.stringify({
|
|
173
|
+
nodes,
|
|
174
|
+
edges
|
|
311
175
|
});
|
|
312
176
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
S: this,
|
|
324
|
-
C: (f, a) => f(...a)
|
|
325
|
-
});
|
|
326
|
-
this._registry.set(sourceRx, {
|
|
327
|
-
inbound: source.inbound,
|
|
328
|
-
outbound: [
|
|
329
|
-
...source.outbound,
|
|
330
|
-
edgeArg.target
|
|
331
|
-
]
|
|
332
|
-
});
|
|
177
|
+
/**
|
|
178
|
+
* Find the node with the given id in the graph.
|
|
179
|
+
*
|
|
180
|
+
* If a node is not found within the graph and an `onInitialNode` callback is provided,
|
|
181
|
+
* it is called with the id and type of the node, potentially initializing the node.
|
|
182
|
+
*/
|
|
183
|
+
findNode(id, expansion = true) {
|
|
184
|
+
const existingNode = this._nodes[id];
|
|
185
|
+
if (!existingNode && expansion) {
|
|
186
|
+
void this._onInitialNode?.(id);
|
|
333
187
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
188
|
+
return existingNode;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Wait for a node to be added to the graph.
|
|
192
|
+
*
|
|
193
|
+
* If the node is already present in the graph, the promise resolves immediately.
|
|
194
|
+
*
|
|
195
|
+
* @param id The id of the node to wait for.
|
|
196
|
+
* @param timeout The time in milliseconds to wait for the node to be added.
|
|
197
|
+
*/
|
|
198
|
+
async waitForNode(id, timeout) {
|
|
199
|
+
const trigger = this._waitingForNodes[id] ?? (this._waitingForNodes[id] = new Trigger());
|
|
200
|
+
const node = this.findNode(id);
|
|
201
|
+
if (node) {
|
|
202
|
+
delete this._waitingForNodes[id];
|
|
203
|
+
return node;
|
|
204
|
+
}
|
|
205
|
+
if (timeout === void 0) {
|
|
206
|
+
return trigger.wait();
|
|
207
|
+
} else {
|
|
208
|
+
return asyncTimeout(trigger.wait(), timeout, `Node not found: ${id}`);
|
|
353
209
|
}
|
|
354
210
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Nodes that this node is connected to in default order.
|
|
213
|
+
*/
|
|
214
|
+
nodes(node, options = {}) {
|
|
215
|
+
const { relation, expansion, filter = DEFAULT_FILTER, type } = options;
|
|
216
|
+
const nodes = this._getNodes({
|
|
217
|
+
node,
|
|
218
|
+
relation,
|
|
219
|
+
expansion,
|
|
220
|
+
type
|
|
358
221
|
});
|
|
222
|
+
return nodes.filter((n) => filter(n, node));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Edges that this node is connected to in default order.
|
|
226
|
+
*/
|
|
227
|
+
edges(node, { relation = "outbound" } = {}) {
|
|
228
|
+
return this._edges[node.id]?.[relation] ?? [];
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Actions or action groups that this node is connected to in default order.
|
|
232
|
+
*/
|
|
233
|
+
actions(node, { expansion } = {}) {
|
|
234
|
+
return [
|
|
235
|
+
...this._getNodes({
|
|
236
|
+
node,
|
|
237
|
+
expansion,
|
|
238
|
+
type: ACTION_GROUP_TYPE
|
|
239
|
+
}),
|
|
240
|
+
...this._getNodes({
|
|
241
|
+
node,
|
|
242
|
+
expansion,
|
|
243
|
+
type: ACTION_TYPE
|
|
244
|
+
})
|
|
245
|
+
];
|
|
359
246
|
}
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
if (
|
|
364
|
-
this.
|
|
365
|
-
|
|
366
|
-
outbound: source.outbound.filter((id) => id !== edgeArg.target)
|
|
367
|
-
});
|
|
247
|
+
async expand(node, relation = "outbound", type) {
|
|
248
|
+
const key = this._key(node, relation, type);
|
|
249
|
+
const initialized = this._initialized[key];
|
|
250
|
+
if (!initialized && this._onInitialNodes) {
|
|
251
|
+
await this._onInitialNodes(node, relation, type);
|
|
252
|
+
this._initialized[key] = true;
|
|
368
253
|
}
|
|
369
|
-
const targetRx = this._edges(edgeArg.target);
|
|
370
|
-
const target = this._registry.get(targetRx);
|
|
371
|
-
if (target.inbound.includes(edgeArg.source)) {
|
|
372
|
-
this._registry.set(targetRx, {
|
|
373
|
-
inbound: target.inbound.filter((id) => id !== edgeArg.source),
|
|
374
|
-
outbound: target.outbound
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
if (removeOrphans) {
|
|
378
|
-
const source2 = this._registry.get(sourceRx);
|
|
379
|
-
const target2 = this._registry.get(targetRx);
|
|
380
|
-
if (source2.outbound.length === 0 && source2.inbound.length === 0 && edgeArg.source !== ROOT_ID) {
|
|
381
|
-
this.removeNodes([
|
|
382
|
-
edgeArg.source
|
|
383
|
-
]);
|
|
384
|
-
}
|
|
385
|
-
if (target2.outbound.length === 0 && target2.inbound.length === 0 && edgeArg.target !== ROOT_ID) {
|
|
386
|
-
this.removeNodes([
|
|
387
|
-
edgeArg.target
|
|
388
|
-
]);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
sortEdges(id, relation, order) {
|
|
393
|
-
const edgesRx = this._edges(id);
|
|
394
|
-
const edges = this._registry.get(edgesRx);
|
|
395
|
-
const unsorted = edges[relation].filter((id2) => !order.includes(id2)) ?? [];
|
|
396
|
-
const sorted = order.filter((id2) => edges[relation].includes(id2)) ?? [];
|
|
397
|
-
edges[relation].splice(0, edges[relation].length, ...[
|
|
398
|
-
...sorted,
|
|
399
|
-
...unsorted
|
|
400
|
-
]);
|
|
401
|
-
this._registry.set(edgesRx, edges);
|
|
402
254
|
}
|
|
403
|
-
|
|
404
|
-
|
|
255
|
+
_key(node, relation, type) {
|
|
256
|
+
return `${node.id}-${relation}-${type}`;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Recursive depth-first traversal of the graph.
|
|
260
|
+
*
|
|
261
|
+
* @param options.node The node to start traversing from.
|
|
262
|
+
* @param options.relation The relation to traverse graph edges.
|
|
263
|
+
* @param options.visitor A callback which is called for each node visited during traversal.
|
|
264
|
+
*/
|
|
265
|
+
traverse({ visitor, node = this.root, relation = "outbound", expansion }, path = []) {
|
|
266
|
+
if (path.includes(node.id)) {
|
|
405
267
|
return;
|
|
406
268
|
}
|
|
407
|
-
const node = this.getNodeOrThrow(source);
|
|
408
269
|
const shouldContinue = visitor(node, [
|
|
409
270
|
...path,
|
|
410
|
-
|
|
271
|
+
node.id
|
|
411
272
|
]);
|
|
412
273
|
if (shouldContinue === false) {
|
|
413
274
|
return;
|
|
414
275
|
}
|
|
415
|
-
Object.values(this.
|
|
416
|
-
|
|
276
|
+
Object.values(this._getNodes({
|
|
277
|
+
node,
|
|
417
278
|
relation,
|
|
418
|
-
|
|
279
|
+
expansion
|
|
280
|
+
})).forEach((child) => this.traverse({
|
|
281
|
+
node: child,
|
|
282
|
+
relation,
|
|
283
|
+
visitor,
|
|
284
|
+
expansion
|
|
419
285
|
}, [
|
|
420
286
|
...path,
|
|
421
|
-
|
|
287
|
+
node.id
|
|
422
288
|
]));
|
|
423
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Recursive depth-first traversal of the graph wrapping each visitor call in an effect.
|
|
292
|
+
*
|
|
293
|
+
* @param options.node The node to start traversing from.
|
|
294
|
+
* @param options.relation The relation to traverse graph edges.
|
|
295
|
+
* @param options.visitor A callback which is called for each node visited during traversal.
|
|
296
|
+
*/
|
|
297
|
+
subscribeTraverse({ visitor, node = this.root, relation = "outbound", expansion }, currentPath = []) {
|
|
298
|
+
return effect(() => {
|
|
299
|
+
const path = [
|
|
300
|
+
...currentPath,
|
|
301
|
+
node.id
|
|
302
|
+
];
|
|
303
|
+
const result = visitor(node, path);
|
|
304
|
+
if (result === false) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const nodes = this._getNodes({
|
|
308
|
+
node,
|
|
309
|
+
relation,
|
|
310
|
+
expansion
|
|
311
|
+
});
|
|
312
|
+
const nodeSubscriptions = nodes.map((n) => this.subscribeTraverse({
|
|
313
|
+
node: n,
|
|
314
|
+
visitor,
|
|
315
|
+
expansion
|
|
316
|
+
}, path));
|
|
317
|
+
return () => {
|
|
318
|
+
nodeSubscriptions.forEach((unsubscribe) => unsubscribe());
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Get the path between two nodes in the graph.
|
|
324
|
+
*/
|
|
424
325
|
getPath({ source = "root", target }) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
326
|
+
const start = this.findNode(source);
|
|
327
|
+
if (!start) {
|
|
328
|
+
return void 0;
|
|
329
|
+
}
|
|
330
|
+
let found;
|
|
331
|
+
this.traverse({
|
|
332
|
+
node: start,
|
|
333
|
+
visitor: (node, path) => {
|
|
334
|
+
if (found) {
|
|
335
|
+
return false;
|
|
436
336
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
337
|
+
if (node.id === target) {
|
|
338
|
+
found = path;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
return found;
|
|
440
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* Wait for the path between two nodes in the graph to be established.
|
|
346
|
+
*/
|
|
441
347
|
async waitForPath(params, { timeout = 5e3, interval = 500 } = {}) {
|
|
442
348
|
const path = this.getPath(params);
|
|
443
|
-
if (
|
|
444
|
-
return path
|
|
349
|
+
if (path) {
|
|
350
|
+
return path;
|
|
445
351
|
}
|
|
446
352
|
const trigger = new Trigger();
|
|
447
353
|
const i = setInterval(() => {
|
|
448
354
|
const path2 = this.getPath(params);
|
|
449
|
-
if (
|
|
450
|
-
trigger.wake(path2
|
|
355
|
+
if (path2) {
|
|
356
|
+
trigger.wake(path2);
|
|
451
357
|
}
|
|
452
358
|
}, interval);
|
|
453
359
|
return trigger.wait({
|
|
454
360
|
timeout
|
|
455
361
|
}).finally(() => clearInterval(i));
|
|
456
362
|
}
|
|
457
|
-
/**
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
363
|
+
/**
|
|
364
|
+
* Add nodes to the graph.
|
|
365
|
+
*
|
|
366
|
+
* @internal
|
|
367
|
+
*/
|
|
368
|
+
_addNodes(nodes) {
|
|
369
|
+
return batch(() => nodes.map((node) => this._addNode(node)));
|
|
370
|
+
}
|
|
371
|
+
_addNode({ nodes, edges, ..._node }) {
|
|
372
|
+
return untracked(() => {
|
|
373
|
+
const existingNode = this._nodes[_node.id];
|
|
374
|
+
const node = existingNode ?? this._constructNode({
|
|
375
|
+
data: null,
|
|
376
|
+
properties: {},
|
|
377
|
+
..._node
|
|
378
|
+
});
|
|
379
|
+
if (existingNode) {
|
|
380
|
+
const { data = null, properties, type } = _node;
|
|
381
|
+
if (data !== node.data) {
|
|
382
|
+
node.data = data;
|
|
383
|
+
}
|
|
384
|
+
if (type !== node.type) {
|
|
385
|
+
node.type = type;
|
|
386
|
+
}
|
|
387
|
+
for (const key in properties) {
|
|
388
|
+
if (properties[key] !== node.properties[key]) {
|
|
389
|
+
node.properties[key] = properties[key];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
this._nodes[node.id] = node;
|
|
394
|
+
this._edges[node.id] = create({
|
|
395
|
+
inbound: [],
|
|
396
|
+
outbound: []
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const trigger = this._waitingForNodes[node.id];
|
|
400
|
+
if (trigger) {
|
|
401
|
+
trigger.wake(node);
|
|
402
|
+
delete this._waitingForNodes[node.id];
|
|
403
|
+
}
|
|
404
|
+
if (nodes) {
|
|
405
|
+
nodes.forEach((subNode) => {
|
|
406
|
+
this._addNode(subNode);
|
|
407
|
+
this._addEdge({
|
|
408
|
+
source: node.id,
|
|
409
|
+
target: subNode.id
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (edges) {
|
|
414
|
+
edges.forEach(([id, relation]) => relation === "outbound" ? this._addEdge({
|
|
415
|
+
source: node.id,
|
|
416
|
+
target: id
|
|
417
|
+
}) : this._addEdge({
|
|
418
|
+
source: id,
|
|
419
|
+
target: node.id
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
return node;
|
|
464
423
|
});
|
|
465
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Remove nodes from the graph.
|
|
427
|
+
*
|
|
428
|
+
* @param ids The id of the node to remove.
|
|
429
|
+
* @param edges Whether to remove edges connected to the node from the graph as well.
|
|
430
|
+
* @internal
|
|
431
|
+
*/
|
|
432
|
+
_removeNodes(ids, edges = false) {
|
|
433
|
+
batch(() => ids.forEach((id) => this._removeNode(id, edges)));
|
|
434
|
+
}
|
|
435
|
+
_removeNode(id, edges = false) {
|
|
436
|
+
untracked(() => {
|
|
437
|
+
const node = this.findNode(id, false);
|
|
438
|
+
if (!node) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (edges) {
|
|
442
|
+
this._getNodes({
|
|
443
|
+
node
|
|
444
|
+
}).forEach((node2) => {
|
|
445
|
+
this._removeEdge({
|
|
446
|
+
source: id,
|
|
447
|
+
target: node2.id
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
this._getNodes({
|
|
451
|
+
node,
|
|
452
|
+
relation: "inbound"
|
|
453
|
+
}).forEach((node2) => {
|
|
454
|
+
this._removeEdge({
|
|
455
|
+
source: node2.id,
|
|
456
|
+
target: id
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
delete this._edges[id];
|
|
460
|
+
}
|
|
461
|
+
delete this._nodes[id];
|
|
462
|
+
Object.keys(this._initialized).filter((key) => key.startsWith(id)).forEach((key) => {
|
|
463
|
+
delete this._initialized[key];
|
|
464
|
+
});
|
|
465
|
+
void this._onRemoveNode?.(id);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Add edges to the graph.
|
|
470
|
+
*
|
|
471
|
+
* @internal
|
|
472
|
+
*/
|
|
473
|
+
_addEdges(edges) {
|
|
474
|
+
batch(() => edges.forEach((edge) => this._addEdge(edge)));
|
|
475
|
+
}
|
|
476
|
+
_addEdge({ source, target }) {
|
|
477
|
+
untracked(() => {
|
|
478
|
+
if (!this._edges[source]) {
|
|
479
|
+
this._edges[source] = create({
|
|
480
|
+
inbound: [],
|
|
481
|
+
outbound: []
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
if (!this._edges[target]) {
|
|
485
|
+
this._edges[target] = create({
|
|
486
|
+
inbound: [],
|
|
487
|
+
outbound: []
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
const sourceEdges = this._edges[source];
|
|
491
|
+
if (!sourceEdges.outbound.includes(target)) {
|
|
492
|
+
sourceEdges.outbound.push(target);
|
|
493
|
+
}
|
|
494
|
+
const targetEdges = this._edges[target];
|
|
495
|
+
if (!targetEdges.inbound.includes(source)) {
|
|
496
|
+
targetEdges.inbound.push(source);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Remove edges from the graph.
|
|
502
|
+
* @internal
|
|
503
|
+
*/
|
|
504
|
+
_removeEdges(edges, removeOrphans = false) {
|
|
505
|
+
batch(() => edges.forEach((edge) => this._removeEdge(edge, removeOrphans)));
|
|
506
|
+
}
|
|
507
|
+
_removeEdge({ source, target }, removeOrphans = false) {
|
|
508
|
+
untracked(() => {
|
|
509
|
+
batch(() => {
|
|
510
|
+
const outboundIndex = this._edges[source]?.outbound.findIndex((id) => id === target);
|
|
511
|
+
if (outboundIndex !== void 0 && outboundIndex !== -1) {
|
|
512
|
+
this._edges[source].outbound.splice(outboundIndex, 1);
|
|
513
|
+
}
|
|
514
|
+
const inboundIndex = this._edges[target]?.inbound.findIndex((id) => id === source);
|
|
515
|
+
if (inboundIndex !== void 0 && inboundIndex !== -1) {
|
|
516
|
+
this._edges[target].inbound.splice(inboundIndex, 1);
|
|
517
|
+
}
|
|
518
|
+
if (removeOrphans) {
|
|
519
|
+
if (this._edges[source]?.outbound.length === 0 && this._edges[source]?.inbound.length === 0 && source !== ROOT_ID) {
|
|
520
|
+
this._removeNode(source, true);
|
|
521
|
+
}
|
|
522
|
+
if (this._edges[target]?.outbound.length === 0 && this._edges[target]?.inbound.length === 0 && target !== ROOT_ID) {
|
|
523
|
+
this._removeNode(target, true);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Sort edges for a node.
|
|
531
|
+
*
|
|
532
|
+
* Edges not included in the sorted list are appended to the end of the list.
|
|
533
|
+
*
|
|
534
|
+
* @param nodeId The id of the node to sort edges for.
|
|
535
|
+
* @param relation The relation of the edges from the node to sort.
|
|
536
|
+
* @param edges The ordered list of edges.
|
|
537
|
+
* @ignore
|
|
538
|
+
*/
|
|
539
|
+
_sortEdges(nodeId, relation, edges) {
|
|
540
|
+
untracked(() => {
|
|
541
|
+
batch(() => {
|
|
542
|
+
const current = this._edges[nodeId];
|
|
543
|
+
if (current) {
|
|
544
|
+
const unsorted = current[relation].filter((id) => !edges.includes(id)) ?? [];
|
|
545
|
+
const sorted = edges.filter((id) => current[relation].includes(id)) ?? [];
|
|
546
|
+
current[relation].splice(0, current[relation].length, ...[
|
|
547
|
+
...sorted,
|
|
548
|
+
...unsorted
|
|
549
|
+
]);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
_getNodes({ node, relation = "outbound", type, expansion }) {
|
|
555
|
+
if (expansion) {
|
|
556
|
+
void this.expand(node, relation, type);
|
|
557
|
+
}
|
|
558
|
+
const edges = this._edges[node.id];
|
|
559
|
+
if (!edges) {
|
|
560
|
+
return [];
|
|
561
|
+
} else {
|
|
562
|
+
return edges[relation].map((id) => this._nodes[id]).filter(isNonNullable).filter((n) => !type || n.type === type);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
466
565
|
};
|
|
467
566
|
|
|
468
567
|
// packages/sdk/app-graph/src/graph-builder.ts
|
|
469
|
-
import {
|
|
470
|
-
import {
|
|
471
|
-
import {
|
|
568
|
+
import { effect as effect2, signal, untracked as untracked2 } from "@preact/signals-core";
|
|
569
|
+
import { Trigger as Trigger2 } from "@dxos/async";
|
|
570
|
+
import { invariant as invariant2 } from "@dxos/invariant";
|
|
571
|
+
import { create as create2 } from "@dxos/live-object";
|
|
472
572
|
import { log as log2 } from "@dxos/log";
|
|
473
|
-
import { byPosition,
|
|
474
|
-
|
|
475
|
-
// packages/sdk/app-graph/src/node.ts
|
|
476
|
-
var isGraphNode = (data) => data && typeof data === "object" && "id" in data && "properties" in data && data.properties ? typeof data.properties === "object" && "data" in data : false;
|
|
477
|
-
var isAction = (data) => isGraphNode(data) ? typeof data.data === "function" : false;
|
|
478
|
-
var actionGroupSymbol = Symbol("ActionGroup");
|
|
479
|
-
var isActionGroup = (data) => isGraphNode(data) ? data.data === actionGroupSymbol : false;
|
|
480
|
-
var isActionLike = (data) => isAction(data) || isActionGroup(data);
|
|
481
|
-
|
|
482
|
-
// packages/sdk/app-graph/src/graph-builder.ts
|
|
573
|
+
import { byPosition, isNode, isNonNullable as isNonNullable2 } from "@dxos/util";
|
|
483
574
|
var __dxlog_file2 = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph-builder.ts";
|
|
575
|
+
var NODE_RESOLVER_TIMEOUT = 1e3;
|
|
484
576
|
var createExtension = (extension) => {
|
|
485
|
-
const { id, position = "static",
|
|
577
|
+
const { id, position = "static", resolver, connector, actions, actionGroups, ...rest } = extension;
|
|
486
578
|
const getId = (key) => `${id}/${key}`;
|
|
487
|
-
const actionGroups = _actionGroups && Rx2.family((node) => _actionGroups(node).pipe(Rx2.withLabel(`graph-builder:actionGroups:${id}`)));
|
|
488
|
-
const actions = _actions && Rx2.family((node) => _actions(node).pipe(Rx2.withLabel(`graph-builder:actions:${id}`)));
|
|
489
579
|
return [
|
|
490
|
-
|
|
580
|
+
resolver ? {
|
|
581
|
+
id: getId("resolver"),
|
|
582
|
+
position,
|
|
583
|
+
resolver
|
|
584
|
+
} : void 0,
|
|
491
585
|
connector ? {
|
|
586
|
+
...rest,
|
|
492
587
|
id: getId("connector"),
|
|
493
588
|
position,
|
|
494
|
-
|
|
495
|
-
connector: Rx2.family((key) => connector(key).pipe(Rx2.withLabel(`graph-builder:connector:${id}`)))
|
|
589
|
+
connector
|
|
496
590
|
} : void 0,
|
|
497
591
|
actionGroups ? {
|
|
592
|
+
...rest,
|
|
498
593
|
id: getId("actionGroups"),
|
|
499
594
|
position,
|
|
595
|
+
type: ACTION_GROUP_TYPE,
|
|
500
596
|
relation: "outbound",
|
|
501
|
-
connector:
|
|
597
|
+
connector: ({ node }) => actionGroups({
|
|
598
|
+
node
|
|
599
|
+
})?.map((arg) => ({
|
|
502
600
|
...arg,
|
|
503
601
|
data: actionGroupSymbol,
|
|
504
602
|
type: ACTION_GROUP_TYPE
|
|
505
|
-
}))
|
|
603
|
+
}))
|
|
506
604
|
} : void 0,
|
|
507
605
|
actions ? {
|
|
606
|
+
...rest,
|
|
508
607
|
id: getId("actions"),
|
|
509
608
|
position,
|
|
609
|
+
type: ACTION_TYPE,
|
|
510
610
|
relation: "outbound",
|
|
511
|
-
connector:
|
|
611
|
+
connector: ({ node }) => actions({
|
|
612
|
+
node
|
|
613
|
+
})?.map((arg) => ({
|
|
512
614
|
...arg,
|
|
513
615
|
type: ACTION_TYPE
|
|
514
|
-
}))
|
|
616
|
+
}))
|
|
515
617
|
} : void 0
|
|
516
618
|
].filter(isNonNullable2);
|
|
517
619
|
};
|
|
620
|
+
var Dispatcher = class {
|
|
621
|
+
constructor() {
|
|
622
|
+
this.stateIndex = 0;
|
|
623
|
+
this.state = {};
|
|
624
|
+
this.cleanup = [];
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
var BuilderInternal = class {
|
|
628
|
+
};
|
|
629
|
+
var memoize = (fn, key = "result") => {
|
|
630
|
+
const dispatcher = BuilderInternal.currentDispatcher;
|
|
631
|
+
invariant2(dispatcher?.currentExtension, "memoize must be called within an extension", {
|
|
632
|
+
F: __dxlog_file2,
|
|
633
|
+
L: 135,
|
|
634
|
+
S: void 0,
|
|
635
|
+
A: [
|
|
636
|
+
"dispatcher?.currentExtension",
|
|
637
|
+
"'memoize must be called within an extension'"
|
|
638
|
+
]
|
|
639
|
+
});
|
|
640
|
+
const all = dispatcher.state[dispatcher.currentExtension][dispatcher.stateIndex] ?? {};
|
|
641
|
+
const current = all[key];
|
|
642
|
+
const result = current ? current.result : fn();
|
|
643
|
+
dispatcher.state[dispatcher.currentExtension][dispatcher.stateIndex] = {
|
|
644
|
+
...all,
|
|
645
|
+
[key]: {
|
|
646
|
+
result
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
dispatcher.stateIndex++;
|
|
650
|
+
return result;
|
|
651
|
+
};
|
|
652
|
+
var cleanup = (fn) => {
|
|
653
|
+
memoize(() => {
|
|
654
|
+
const dispatcher = BuilderInternal.currentDispatcher;
|
|
655
|
+
invariant2(dispatcher, "cleanup must be called within an extension", {
|
|
656
|
+
F: __dxlog_file2,
|
|
657
|
+
L: 150,
|
|
658
|
+
S: void 0,
|
|
659
|
+
A: [
|
|
660
|
+
"dispatcher",
|
|
661
|
+
"'cleanup must be called within an extension'"
|
|
662
|
+
]
|
|
663
|
+
});
|
|
664
|
+
dispatcher.cleanup.push(fn);
|
|
665
|
+
});
|
|
666
|
+
};
|
|
667
|
+
var toSignal = (subscribe, get, key) => {
|
|
668
|
+
const thisSignal = memoize(() => {
|
|
669
|
+
return signal(get());
|
|
670
|
+
}, key);
|
|
671
|
+
const unsubscribe = memoize(() => {
|
|
672
|
+
return subscribe(() => thisSignal.value = get());
|
|
673
|
+
}, key);
|
|
674
|
+
cleanup(() => {
|
|
675
|
+
unsubscribe();
|
|
676
|
+
});
|
|
677
|
+
return thisSignal.value;
|
|
678
|
+
};
|
|
518
679
|
var flattenExtensions = (extension, acc = []) => {
|
|
519
680
|
if (Array.isArray(extension)) {
|
|
520
681
|
return [
|
|
@@ -529,75 +690,107 @@ var flattenExtensions = (extension, acc = []) => {
|
|
|
529
690
|
}
|
|
530
691
|
};
|
|
531
692
|
var GraphBuilder = class _GraphBuilder {
|
|
532
|
-
constructor(
|
|
533
|
-
|
|
693
|
+
constructor(params = {}) {
|
|
694
|
+
this._dispatcher = new Dispatcher();
|
|
695
|
+
this._extensions = create2({});
|
|
696
|
+
this._resolverSubscriptions = /* @__PURE__ */ new Map();
|
|
534
697
|
this._connectorSubscriptions = /* @__PURE__ */ new Map();
|
|
535
|
-
this.
|
|
536
|
-
this.
|
|
537
|
-
return Rx2.make((get) => {
|
|
538
|
-
const [id, relation] = key.split("+");
|
|
539
|
-
const node = this._graph.node(id);
|
|
540
|
-
return pipe2(
|
|
541
|
-
get(this._extensions),
|
|
542
|
-
Record2.values,
|
|
543
|
-
// TODO(wittjosiah): Sort on write rather than read.
|
|
544
|
-
Array.sortBy(byPosition),
|
|
545
|
-
Array.filter(({ relation: _relation = "outbound" }) => _relation === relation),
|
|
546
|
-
Array.map(({ connector }) => connector?.(node)),
|
|
547
|
-
Array.filter(isNonNullable2),
|
|
548
|
-
Array.flatMap((result) => get(result))
|
|
549
|
-
);
|
|
550
|
-
}).pipe(Rx2.withLabel(`graph-builder:connectors:${key}`));
|
|
551
|
-
});
|
|
552
|
-
this._registry = registry ?? Registry2.make();
|
|
698
|
+
this._nodeChanged = {};
|
|
699
|
+
this._initialized = {};
|
|
553
700
|
this._graph = new Graph({
|
|
554
701
|
...params,
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
// onInitialize: (id) => this._onInitialize(id),
|
|
702
|
+
onInitialNode: async (id) => this._onInitialNode(id),
|
|
703
|
+
onInitialNodes: async (node, relation, type) => this._onInitialNodes(node, relation, type),
|
|
558
704
|
onRemoveNode: (id) => this._onRemoveNode(id)
|
|
559
705
|
});
|
|
560
706
|
}
|
|
561
|
-
static from(pickle
|
|
707
|
+
static from(pickle) {
|
|
562
708
|
if (!pickle) {
|
|
563
|
-
return new _GraphBuilder(
|
|
564
|
-
registry
|
|
565
|
-
});
|
|
709
|
+
return new _GraphBuilder();
|
|
566
710
|
}
|
|
567
711
|
const { nodes, edges } = JSON.parse(pickle);
|
|
568
712
|
return new _GraphBuilder({
|
|
569
713
|
nodes,
|
|
570
|
-
edges
|
|
571
|
-
registry
|
|
714
|
+
edges
|
|
572
715
|
});
|
|
573
716
|
}
|
|
717
|
+
/**
|
|
718
|
+
* If graph is being restored from a pickle, the data will be null.
|
|
719
|
+
* Initialize the data of each node by calling resolvers.
|
|
720
|
+
* Wait until all of the initial nodes have resolved.
|
|
721
|
+
*/
|
|
722
|
+
async initialize() {
|
|
723
|
+
Object.keys(this._graph._nodes).filter((id) => id !== ROOT_ID).forEach((id) => this._initialized[id] = new Trigger2());
|
|
724
|
+
Object.keys(this._graph._nodes).forEach((id) => this._onInitialNode(id));
|
|
725
|
+
await Promise.all(Object.entries(this._initialized).map(async ([id, trigger]) => {
|
|
726
|
+
try {
|
|
727
|
+
await trigger.wait({
|
|
728
|
+
timeout: NODE_RESOLVER_TIMEOUT
|
|
729
|
+
});
|
|
730
|
+
} catch {
|
|
731
|
+
log2.error("node resolver timeout", {
|
|
732
|
+
id
|
|
733
|
+
}, {
|
|
734
|
+
F: __dxlog_file2,
|
|
735
|
+
L: 244,
|
|
736
|
+
S: this,
|
|
737
|
+
C: (f, a) => f(...a)
|
|
738
|
+
});
|
|
739
|
+
this.graph._removeNodes([
|
|
740
|
+
id
|
|
741
|
+
]);
|
|
742
|
+
}
|
|
743
|
+
}));
|
|
744
|
+
}
|
|
574
745
|
get graph() {
|
|
575
746
|
return this._graph;
|
|
576
747
|
}
|
|
748
|
+
/**
|
|
749
|
+
* @reactive
|
|
750
|
+
*/
|
|
577
751
|
get extensions() {
|
|
578
|
-
return this._extensions;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
752
|
+
return Object.values(this._extensions);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Register a node builder which will be called in order to construct the graph.
|
|
756
|
+
*/
|
|
757
|
+
addExtension(extension) {
|
|
758
|
+
const extensions = flattenExtensions(extension);
|
|
759
|
+
untracked2(() => {
|
|
760
|
+
extensions.forEach((extension2) => {
|
|
761
|
+
this._dispatcher.state[extension2.id] = [];
|
|
762
|
+
this._extensions[extension2.id] = extension2;
|
|
763
|
+
});
|
|
584
764
|
});
|
|
585
765
|
return this;
|
|
586
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Remove a node builder from the graph builder.
|
|
769
|
+
*/
|
|
587
770
|
removeExtension(id) {
|
|
588
|
-
|
|
589
|
-
|
|
771
|
+
untracked2(() => {
|
|
772
|
+
delete this._extensions[id];
|
|
773
|
+
});
|
|
590
774
|
return this;
|
|
591
775
|
}
|
|
592
|
-
|
|
593
|
-
|
|
776
|
+
destroy() {
|
|
777
|
+
this._dispatcher.cleanup.forEach((fn) => fn());
|
|
778
|
+
this._resolverSubscriptions.forEach((unsubscribe) => unsubscribe());
|
|
779
|
+
this._connectorSubscriptions.forEach((unsubscribe) => unsubscribe());
|
|
780
|
+
this._resolverSubscriptions.clear();
|
|
781
|
+
this._connectorSubscriptions.clear();
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* A graph traversal using just the connector extensions, without subscribing to any signals or persisting any nodes.
|
|
785
|
+
*/
|
|
786
|
+
async explore({ node = this._graph.root, relation = "outbound", visitor }, path = []) {
|
|
787
|
+
if (path.includes(node.id)) {
|
|
594
788
|
return;
|
|
595
789
|
}
|
|
596
790
|
if (!isNode()) {
|
|
597
791
|
const { yieldOrContinue } = await import("main-thread-scheduling");
|
|
598
792
|
await yieldOrContinue("idle");
|
|
599
793
|
}
|
|
600
|
-
const node = registry.get(this._graph.nodeOrThrow(source));
|
|
601
794
|
const shouldContinue = await visitor(node, [
|
|
602
795
|
...path,
|
|
603
796
|
node.id
|
|
@@ -605,104 +798,158 @@ var GraphBuilder = class _GraphBuilder {
|
|
|
605
798
|
if (shouldContinue === false) {
|
|
606
799
|
return;
|
|
607
800
|
}
|
|
608
|
-
const nodes = Object.values(this.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
801
|
+
const nodes = Object.values(this._extensions).filter((extension) => relation === (extension.relation ?? "outbound")).filter((extension) => !extension.filter || extension.filter(node)).flatMap((extension) => {
|
|
802
|
+
this._dispatcher.currentExtension = extension.id;
|
|
803
|
+
this._dispatcher.stateIndex = 0;
|
|
804
|
+
BuilderInternal.currentDispatcher = this._dispatcher;
|
|
805
|
+
const result = extension.connector?.({
|
|
806
|
+
node
|
|
807
|
+
}) ?? [];
|
|
808
|
+
BuilderInternal.currentDispatcher = void 0;
|
|
809
|
+
return result;
|
|
810
|
+
}).map((arg) => ({
|
|
811
|
+
id: arg.id,
|
|
812
|
+
type: arg.type,
|
|
813
|
+
cacheable: arg.cacheable,
|
|
814
|
+
data: arg.data ?? null,
|
|
815
|
+
properties: arg.properties ?? {}
|
|
620
816
|
}));
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
registry.dispose();
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
destroy() {
|
|
627
|
-
this._connectorSubscriptions.forEach((unsubscribe) => unsubscribe());
|
|
628
|
-
this._connectorSubscriptions.clear();
|
|
629
|
-
}
|
|
630
|
-
_onExpand(id, relation) {
|
|
631
|
-
log2("onExpand", {
|
|
632
|
-
id,
|
|
817
|
+
await Promise.all(nodes.map((n) => this.explore({
|
|
818
|
+
node: n,
|
|
633
819
|
relation,
|
|
634
|
-
|
|
635
|
-
},
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
820
|
+
visitor
|
|
821
|
+
}, [
|
|
822
|
+
...path,
|
|
823
|
+
node.id
|
|
824
|
+
])));
|
|
825
|
+
}
|
|
826
|
+
_onInitialNode(nodeId) {
|
|
827
|
+
this._nodeChanged[nodeId] = this._nodeChanged[nodeId] ?? signal({});
|
|
828
|
+
this._resolverSubscriptions.set(nodeId, effect2(() => {
|
|
829
|
+
const extensions = Object.values(this._extensions).toSorted(byPosition);
|
|
830
|
+
for (const { id, resolver } of extensions) {
|
|
831
|
+
if (!resolver) {
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
this._dispatcher.currentExtension = id;
|
|
835
|
+
this._dispatcher.stateIndex = 0;
|
|
836
|
+
BuilderInternal.currentDispatcher = this._dispatcher;
|
|
837
|
+
let node;
|
|
838
|
+
try {
|
|
839
|
+
node = resolver({
|
|
840
|
+
id: nodeId
|
|
841
|
+
});
|
|
842
|
+
} catch (err) {
|
|
843
|
+
log2.catch(err, {
|
|
844
|
+
extension: id
|
|
845
|
+
}, {
|
|
846
|
+
F: __dxlog_file2,
|
|
847
|
+
L: 359,
|
|
848
|
+
S: this,
|
|
849
|
+
C: (f, a) => f(...a)
|
|
850
|
+
});
|
|
851
|
+
log2.error(`Previous error occurred in extension: ${id}`, void 0, {
|
|
852
|
+
F: __dxlog_file2,
|
|
853
|
+
L: 360,
|
|
854
|
+
S: this,
|
|
855
|
+
C: (f, a) => f(...a)
|
|
856
|
+
});
|
|
857
|
+
} finally {
|
|
858
|
+
BuilderInternal.currentDispatcher = void 0;
|
|
859
|
+
}
|
|
860
|
+
const trigger = this._initialized[nodeId];
|
|
861
|
+
if (node) {
|
|
862
|
+
this.graph._addNodes([
|
|
863
|
+
node
|
|
864
|
+
]);
|
|
865
|
+
trigger?.wake();
|
|
866
|
+
if (this._nodeChanged[node.id]) {
|
|
867
|
+
this._nodeChanged[node.id].value = {};
|
|
868
|
+
}
|
|
869
|
+
break;
|
|
870
|
+
} else if (node === false) {
|
|
871
|
+
this.graph._removeNodes([
|
|
872
|
+
nodeId
|
|
873
|
+
]);
|
|
874
|
+
trigger?.wake();
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}));
|
|
879
|
+
}
|
|
880
|
+
_onInitialNodes(node, nodesRelation, nodesType) {
|
|
881
|
+
this._nodeChanged[node.id] = this._nodeChanged[node.id] ?? signal({});
|
|
882
|
+
let first = true;
|
|
642
883
|
let previous = [];
|
|
643
|
-
|
|
884
|
+
this._connectorSubscriptions.set(node.id, effect2(() => {
|
|
885
|
+
if (!first && !this._connectorSubscriptions.has(node.id)) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
first = false;
|
|
889
|
+
Object.keys(this._extensions);
|
|
890
|
+
this._nodeChanged[node.id].value;
|
|
891
|
+
const nodes = [];
|
|
892
|
+
const extensions = Object.values(this._extensions).toSorted(byPosition);
|
|
893
|
+
for (const { id, connector, filter, type, relation = "outbound" } of extensions) {
|
|
894
|
+
if (!connector || relation !== nodesRelation || nodesType && type !== nodesType || filter && !filter(node)) {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
this._dispatcher.currentExtension = id;
|
|
898
|
+
this._dispatcher.stateIndex = 0;
|
|
899
|
+
BuilderInternal.currentDispatcher = this._dispatcher;
|
|
900
|
+
try {
|
|
901
|
+
nodes.push(...connector({
|
|
902
|
+
node
|
|
903
|
+
}) ?? []);
|
|
904
|
+
} catch (err) {
|
|
905
|
+
log2.catch(err, {
|
|
906
|
+
extension: id
|
|
907
|
+
}, {
|
|
908
|
+
F: __dxlog_file2,
|
|
909
|
+
L: 421,
|
|
910
|
+
S: this,
|
|
911
|
+
C: (f, a) => f(...a)
|
|
912
|
+
});
|
|
913
|
+
log2.error(`Previous error occurred in extension: ${id}`, void 0, {
|
|
914
|
+
F: __dxlog_file2,
|
|
915
|
+
L: 422,
|
|
916
|
+
S: this,
|
|
917
|
+
C: (f, a) => f(...a)
|
|
918
|
+
});
|
|
919
|
+
} finally {
|
|
920
|
+
BuilderInternal.currentDispatcher = void 0;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
644
923
|
const ids = nodes.map((n) => n.id);
|
|
645
|
-
const removed = previous.filter((
|
|
924
|
+
const removed = previous.filter((id) => !ids.includes(id));
|
|
646
925
|
previous = ids;
|
|
647
|
-
|
|
648
|
-
id,
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
this._graph.addEdges(nodes.map((node) => relation === "outbound" ? {
|
|
665
|
-
source: id,
|
|
666
|
-
target: node.id
|
|
667
|
-
} : {
|
|
668
|
-
source: node.id,
|
|
669
|
-
target: id
|
|
670
|
-
}));
|
|
671
|
-
this._graph.sortEdges(id, relation, nodes.map(({ id: id2 }) => id2));
|
|
926
|
+
this.graph._removeEdges(removed.map((target) => ({
|
|
927
|
+
source: node.id,
|
|
928
|
+
target
|
|
929
|
+
})), true);
|
|
930
|
+
this.graph._addNodes(nodes);
|
|
931
|
+
this.graph._addEdges(nodes.map(({ id }) => nodesRelation === "outbound" ? {
|
|
932
|
+
source: node.id,
|
|
933
|
+
target: id
|
|
934
|
+
} : {
|
|
935
|
+
source: id,
|
|
936
|
+
target: node.id
|
|
937
|
+
}));
|
|
938
|
+
this.graph._sortEdges(node.id, nodesRelation, nodes.map(({ id }) => id));
|
|
939
|
+
nodes.forEach((n) => {
|
|
940
|
+
if (this._nodeChanged[n.id]) {
|
|
941
|
+
this._nodeChanged[n.id].value = {};
|
|
942
|
+
}
|
|
672
943
|
});
|
|
673
|
-
}
|
|
674
|
-
immediate: true
|
|
675
|
-
});
|
|
676
|
-
this._connectorSubscriptions.set(id, cancel);
|
|
944
|
+
}));
|
|
677
945
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
this._connectorSubscriptions.get(id)?.();
|
|
684
|
-
this._connectorSubscriptions.delete(id);
|
|
946
|
+
async _onRemoveNode(nodeId) {
|
|
947
|
+
this._resolverSubscriptions.get(nodeId)?.();
|
|
948
|
+
this._connectorSubscriptions.get(nodeId)?.();
|
|
949
|
+
this._resolverSubscriptions.delete(nodeId);
|
|
950
|
+
this._connectorSubscriptions.delete(nodeId);
|
|
685
951
|
}
|
|
686
952
|
};
|
|
687
|
-
var rxFromSignal = (cb) => {
|
|
688
|
-
return Rx2.make((get) => {
|
|
689
|
-
const dispose = effect(() => {
|
|
690
|
-
get.setSelf(cb());
|
|
691
|
-
});
|
|
692
|
-
get.addFinalizer(() => dispose());
|
|
693
|
-
return cb();
|
|
694
|
-
});
|
|
695
|
-
};
|
|
696
|
-
var observableFamily = Rx2.family((observable) => {
|
|
697
|
-
return Rx2.make((get) => {
|
|
698
|
-
const subscription = observable.subscribe((value) => get.setSelf(value));
|
|
699
|
-
get.addFinalizer(() => subscription.unsubscribe());
|
|
700
|
-
return observable.get();
|
|
701
|
-
});
|
|
702
|
-
});
|
|
703
|
-
var rxFromObservable = (observable) => {
|
|
704
|
-
return observableFamily(observable);
|
|
705
|
-
};
|
|
706
953
|
export {
|
|
707
954
|
ACTION_GROUP_TYPE,
|
|
708
955
|
ACTION_TYPE,
|
|
@@ -711,6 +958,7 @@ export {
|
|
|
711
958
|
ROOT_ID,
|
|
712
959
|
ROOT_TYPE,
|
|
713
960
|
actionGroupSymbol,
|
|
961
|
+
cleanup,
|
|
714
962
|
createExtension,
|
|
715
963
|
flattenExtensions,
|
|
716
964
|
getGraph,
|
|
@@ -718,7 +966,7 @@ export {
|
|
|
718
966
|
isActionGroup,
|
|
719
967
|
isActionLike,
|
|
720
968
|
isGraphNode,
|
|
721
|
-
|
|
722
|
-
|
|
969
|
+
memoize,
|
|
970
|
+
toSignal
|
|
723
971
|
};
|
|
724
972
|
//# sourceMappingURL=index.mjs.map
|