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