@dxos/app-graph 0.8.2-main.f081794 → 0.8.2-main.fbd8ed0
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 +541 -789
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +533 -780
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +541 -789
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/experimental/graph-projections.test.d.ts +25 -0
- package/dist/types/src/experimental/graph-projections.test.d.ts.map +1 -0
- package/dist/types/src/graph-builder.d.ts +48 -91
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +191 -98
- 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/signals-integration.test.d.ts +2 -0
- package/dist/types/src/signals-integration.test.d.ts.map +1 -0
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/src/testing.d.ts +5 -0
- package/dist/types/src/testing.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +23 -16
- package/src/experimental/graph-projections.test.ts +56 -0
- package/src/graph-builder.test.ts +293 -310
- package/src/graph-builder.ts +209 -317
- package/src/graph.test.ts +314 -463
- package/src/graph.ts +452 -455
- package/src/node.ts +4 -4
- package/src/signals-integration.test.ts +218 -0
- package/src/stories/EchoGraph.stories.tsx +56 -77
- package/src/testing.ts +20 -0
|
@@ -1,26 +1,18 @@
|
|
|
1
1
|
// packages/sdk/app-graph/src/graph.ts
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Registry, Rx } from "@effect-rx/rx-react";
|
|
3
|
+
import { Option, pipe, Record } from "effect";
|
|
4
|
+
import { Event, Trigger } from "@dxos/async";
|
|
5
|
+
import { todo } from "@dxos/debug";
|
|
4
6
|
import { invariant } from "@dxos/invariant";
|
|
5
|
-
import { live } from "@dxos/live-object";
|
|
6
7
|
import { log } from "@dxos/log";
|
|
7
|
-
import { isNonNullable
|
|
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
|
|
8
|
+
import { isNonNullable } from "@dxos/util";
|
|
17
9
|
var __dxlog_file = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph.ts";
|
|
18
10
|
var graphSymbol = Symbol("graph");
|
|
19
11
|
var getGraph = (node) => {
|
|
20
12
|
const graph = node[graphSymbol];
|
|
21
13
|
invariant(graph, "Node is not associated with a graph.", {
|
|
22
14
|
F: __dxlog_file,
|
|
23
|
-
L:
|
|
15
|
+
L: 25,
|
|
24
16
|
S: void 0,
|
|
25
17
|
A: [
|
|
26
18
|
"graph",
|
|
@@ -33,649 +25,496 @@ var ROOT_ID = "root";
|
|
|
33
25
|
var ROOT_TYPE = "dxos.org/type/GraphRoot";
|
|
34
26
|
var ACTION_TYPE = "dxos.org/type/GraphAction";
|
|
35
27
|
var ACTION_GROUP_TYPE = "dxos.org/type/GraphActionGroup";
|
|
36
|
-
var
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.
|
|
40
|
-
this._initialized =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
28
|
+
var Graph = class {
|
|
29
|
+
constructor({ registry, nodes, edges, onExpand, onRemoveNode } = {}) {
|
|
30
|
+
this.onNodeChanged = new Event();
|
|
31
|
+
this._expanded = Record.empty();
|
|
32
|
+
this._initialized = Record.empty();
|
|
33
|
+
this._initialEdges = Record.empty();
|
|
34
|
+
this._initialNodes = Record.fromEntries([
|
|
35
|
+
[
|
|
36
|
+
ROOT_ID,
|
|
37
|
+
this._constructNode({
|
|
38
|
+
id: ROOT_ID,
|
|
39
|
+
type: ROOT_TYPE,
|
|
40
|
+
data: null,
|
|
41
|
+
properties: {}
|
|
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;
|
|
53
63
|
});
|
|
54
|
-
};
|
|
55
|
-
this._onInitialNode = onInitialNode;
|
|
56
|
-
this._onInitialNodes = onInitialNodes;
|
|
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
64
|
});
|
|
65
|
+
this._edges = Rx.family((id) => {
|
|
66
|
+
const initial = Record.get(this._initialEdges, id).pipe(Option.getOrElse(() => ({
|
|
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;
|
|
114
|
+
this._onRemoveNode = onRemoveNode;
|
|
65
115
|
if (nodes) {
|
|
66
116
|
nodes.forEach((node) => {
|
|
67
|
-
|
|
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
|
-
}
|
|
117
|
+
Record.set(this._initialNodes, node.id, this._constructNode(node));
|
|
91
118
|
});
|
|
92
119
|
}
|
|
93
|
-
this._edges[ROOT_ID] = live({
|
|
94
|
-
inbound: [],
|
|
95
|
-
outbound: []
|
|
96
|
-
});
|
|
97
120
|
if (edges) {
|
|
98
121
|
Object.entries(edges).forEach(([source, edges2]) => {
|
|
99
|
-
|
|
100
|
-
this._addEdge({
|
|
101
|
-
source,
|
|
102
|
-
target
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
this._sortEdges(source, "outbound", edges2);
|
|
122
|
+
Record.set(this._initialEdges, source, edges2);
|
|
106
123
|
});
|
|
107
124
|
}
|
|
108
125
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
toJSON(id = ROOT_ID) {
|
|
127
|
+
return this._registry.get(this._json(id));
|
|
128
|
+
}
|
|
129
|
+
json(id = ROOT_ID) {
|
|
130
|
+
return this._json(id);
|
|
131
|
+
}
|
|
132
|
+
node(id) {
|
|
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);
|
|
116
146
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Alias for `findNode('root')`.
|
|
119
|
-
*/
|
|
120
147
|
get root() {
|
|
121
|
-
return this.
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
return this.getNodeOrThrow(ROOT_ID);
|
|
149
|
+
}
|
|
150
|
+
getNode(id) {
|
|
151
|
+
return this._registry.get(this.node(id));
|
|
152
|
+
}
|
|
153
|
+
getNodeOrThrow(id) {
|
|
154
|
+
return this._registry.get(this.nodeOrThrow(id));
|
|
155
|
+
}
|
|
156
|
+
getConnections(id, relation = "outbound") {
|
|
157
|
+
return this._registry.get(this.connections(id, relation));
|
|
158
|
+
}
|
|
159
|
+
getActions(id) {
|
|
160
|
+
return this._registry.get(this.actions(id));
|
|
161
|
+
}
|
|
162
|
+
getEdges(id) {
|
|
163
|
+
return this._registry.get(this.edges(id));
|
|
164
|
+
}
|
|
165
|
+
// TODO(wittjosiah): On initialize to restore state from cache.
|
|
166
|
+
// async initialize(id: string) {
|
|
167
|
+
// const initialized = Record.get(this._initialized, id).pipe(Option.getOrElse(() => false));
|
|
168
|
+
// log('initialize', { id, initialized });
|
|
169
|
+
// if (!initialized) {
|
|
170
|
+
// await this._onInitialize?.(id);
|
|
171
|
+
// Record.set(this._initialized, id, true);
|
|
172
|
+
// }
|
|
173
|
+
// }
|
|
174
|
+
expand(id, relation = "outbound") {
|
|
175
|
+
const key = `${id}$${relation}`;
|
|
176
|
+
const expanded = Record.get(this._expanded, key).pipe(Option.getOrElse(() => false));
|
|
177
|
+
log("expand", {
|
|
178
|
+
key,
|
|
179
|
+
expanded
|
|
180
|
+
}, {
|
|
149
181
|
F: __dxlog_file,
|
|
150
|
-
L:
|
|
182
|
+
L: 395,
|
|
151
183
|
S: this,
|
|
152
|
-
|
|
153
|
-
"root",
|
|
154
|
-
"`Node not found: ${id}`"
|
|
155
|
-
]
|
|
184
|
+
C: (f, a) => f(...a)
|
|
156
185
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
};
|
|
186
|
+
if (!expanded) {
|
|
187
|
+
this._onExpand?.(id, relation);
|
|
188
|
+
Record.set(this._expanded, key, true);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
addNodes(nodes) {
|
|
192
|
+
Rx.batch(() => {
|
|
193
|
+
nodes.map((node) => this.addNode(node));
|
|
166
194
|
});
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
195
|
+
}
|
|
196
|
+
addNode({ nodes, edges, ...nodeArg }) {
|
|
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));
|
|
175
283
|
});
|
|
176
284
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
285
|
+
removeNode(id, edges = false) {
|
|
286
|
+
const nodeRx = this._node(id);
|
|
287
|
+
this._registry.set(nodeRx, Option.none());
|
|
288
|
+
this.onNodeChanged.emit({
|
|
289
|
+
id,
|
|
290
|
+
node: Option.none()
|
|
291
|
+
});
|
|
292
|
+
if (edges) {
|
|
293
|
+
const { inbound, outbound } = this._registry.get(this._edges(id));
|
|
294
|
+
const edges2 = [
|
|
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);
|
|
187
305
|
}
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
306
|
+
this._onRemoveNode?.(id);
|
|
307
|
+
}
|
|
308
|
+
addEdges(edges) {
|
|
309
|
+
Rx.batch(() => {
|
|
310
|
+
edges.map((edge) => this.addEdge(edge));
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
addEdge(edgeArg) {
|
|
314
|
+
const sourceRx = this._edges(edgeArg.source);
|
|
315
|
+
const source = this._registry.get(sourceRx);
|
|
316
|
+
if (!source.outbound.includes(edgeArg.target)) {
|
|
317
|
+
log("add outbound edge", {
|
|
318
|
+
source: edgeArg.source,
|
|
319
|
+
target: edgeArg.target
|
|
320
|
+
}, {
|
|
321
|
+
F: __dxlog_file,
|
|
322
|
+
L: 481,
|
|
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
|
+
});
|
|
204
333
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
334
|
+
const targetRx = this._edges(edgeArg.target);
|
|
335
|
+
const target = this._registry.get(targetRx);
|
|
336
|
+
if (!target.inbound.includes(edgeArg.source)) {
|
|
337
|
+
log("add inbound edge", {
|
|
338
|
+
source: edgeArg.source,
|
|
339
|
+
target: edgeArg.target
|
|
340
|
+
}, {
|
|
341
|
+
F: __dxlog_file,
|
|
342
|
+
L: 488,
|
|
343
|
+
S: this,
|
|
344
|
+
C: (f, a) => f(...a)
|
|
345
|
+
});
|
|
346
|
+
this._registry.set(targetRx, {
|
|
347
|
+
inbound: [
|
|
348
|
+
...target.inbound,
|
|
349
|
+
edgeArg.source
|
|
350
|
+
],
|
|
351
|
+
outbound: target.outbound
|
|
352
|
+
});
|
|
209
353
|
}
|
|
210
354
|
}
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
355
|
+
removeEdges(edges, removeOrphans = false) {
|
|
356
|
+
Rx.batch(() => {
|
|
357
|
+
edges.map((edge) => this.removeEdge(edge, removeOrphans));
|
|
221
358
|
});
|
|
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
|
-
];
|
|
246
359
|
}
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
360
|
+
removeEdge(edgeArg, removeOrphans = false) {
|
|
361
|
+
const sourceRx = this._edges(edgeArg.source);
|
|
362
|
+
const source = this._registry.get(sourceRx);
|
|
363
|
+
if (source.outbound.includes(edgeArg.target)) {
|
|
364
|
+
this._registry.set(sourceRx, {
|
|
365
|
+
inbound: source.inbound,
|
|
366
|
+
outbound: source.outbound.filter((id) => id !== edgeArg.target)
|
|
367
|
+
});
|
|
253
368
|
}
|
|
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);
|
|
254
402
|
}
|
|
255
|
-
|
|
256
|
-
|
|
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)) {
|
|
403
|
+
traverse({ visitor, source = ROOT_ID, relation = "outbound" }, path = []) {
|
|
404
|
+
if (path.includes(source)) {
|
|
267
405
|
return;
|
|
268
406
|
}
|
|
407
|
+
const node = this.getNodeOrThrow(source);
|
|
269
408
|
const shouldContinue = visitor(node, [
|
|
270
409
|
...path,
|
|
271
|
-
|
|
410
|
+
source
|
|
272
411
|
]);
|
|
273
412
|
if (shouldContinue === false) {
|
|
274
413
|
return;
|
|
275
414
|
}
|
|
276
|
-
Object.values(this.
|
|
277
|
-
|
|
278
|
-
relation,
|
|
279
|
-
expansion
|
|
280
|
-
})).forEach((child) => this.traverse({
|
|
281
|
-
node: child,
|
|
415
|
+
Object.values(this.getConnections(source, relation)).forEach((child) => this.traverse({
|
|
416
|
+
source: child.id,
|
|
282
417
|
relation,
|
|
283
|
-
visitor
|
|
284
|
-
expansion
|
|
418
|
+
visitor
|
|
285
419
|
}, [
|
|
286
420
|
...path,
|
|
287
|
-
|
|
421
|
+
source
|
|
288
422
|
]));
|
|
289
423
|
}
|
|
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
|
-
*/
|
|
325
424
|
getPath({ source = "root", target }) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (node.id === target) {
|
|
338
|
-
found = path;
|
|
425
|
+
return pipe(this.getNode(source), Option.flatMap((node) => {
|
|
426
|
+
let found = Option.none();
|
|
427
|
+
this.traverse({
|
|
428
|
+
source: node.id,
|
|
429
|
+
visitor: (node2, path) => {
|
|
430
|
+
if (Option.isSome(found)) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
if (node2.id === target) {
|
|
434
|
+
found = Option.some(path);
|
|
435
|
+
}
|
|
339
436
|
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
437
|
+
});
|
|
438
|
+
return found;
|
|
439
|
+
}));
|
|
343
440
|
}
|
|
344
|
-
/**
|
|
345
|
-
* Wait for the path between two nodes in the graph to be established.
|
|
346
|
-
*/
|
|
347
441
|
async waitForPath(params, { timeout = 5e3, interval = 500 } = {}) {
|
|
348
442
|
const path = this.getPath(params);
|
|
349
|
-
if (path) {
|
|
350
|
-
return path;
|
|
443
|
+
if (Option.isSome(path)) {
|
|
444
|
+
return path.value;
|
|
351
445
|
}
|
|
352
446
|
const trigger = new Trigger();
|
|
353
447
|
const i = setInterval(() => {
|
|
354
448
|
const path2 = this.getPath(params);
|
|
355
|
-
if (path2) {
|
|
356
|
-
trigger.wake(path2);
|
|
449
|
+
if (Option.isSome(path2)) {
|
|
450
|
+
trigger.wake(path2.value);
|
|
357
451
|
}
|
|
358
452
|
}, interval);
|
|
359
453
|
return trigger.wait({
|
|
360
454
|
timeout
|
|
361
455
|
}).finally(() => clearInterval(i));
|
|
362
456
|
}
|
|
363
|
-
/**
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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] = live({
|
|
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;
|
|
423
|
-
});
|
|
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] = live({
|
|
480
|
-
inbound: [],
|
|
481
|
-
outbound: []
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
if (!this._edges[target]) {
|
|
485
|
-
this._edges[target] = live({
|
|
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
|
-
});
|
|
457
|
+
/** @internal */
|
|
458
|
+
_constructNode(node) {
|
|
459
|
+
return Option.some({
|
|
460
|
+
[graphSymbol]: this,
|
|
461
|
+
data: null,
|
|
462
|
+
properties: {},
|
|
463
|
+
...node
|
|
552
464
|
});
|
|
553
465
|
}
|
|
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
|
-
}
|
|
565
466
|
};
|
|
566
467
|
|
|
567
468
|
// packages/sdk/app-graph/src/graph-builder.ts
|
|
568
|
-
import {
|
|
569
|
-
import {
|
|
570
|
-
import {
|
|
571
|
-
import { live as live2 } from "@dxos/live-object";
|
|
469
|
+
import { Registry as Registry2, Rx as Rx2 } from "@effect-rx/rx-react";
|
|
470
|
+
import { effect } from "@preact/signals-core";
|
|
471
|
+
import { Array, pipe as pipe2, Record as Record2 } from "effect";
|
|
572
472
|
import { log as log2 } from "@dxos/log";
|
|
573
|
-
import { byPosition, isNode, isNonNullable as isNonNullable2 } from "@dxos/util";
|
|
473
|
+
import { byPosition, getDebugName, isNode, isNonNullable as isNonNullable2 } from "@dxos/util";
|
|
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
|
|
574
483
|
var __dxlog_file2 = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph-builder.ts";
|
|
575
|
-
var NODE_RESOLVER_TIMEOUT = 1e3;
|
|
576
484
|
var createExtension = (extension) => {
|
|
577
|
-
const { id, position = "static",
|
|
485
|
+
const { id, position = "static", relation = "outbound", connector, actions: _actions, actionGroups: _actionGroups } = extension;
|
|
578
486
|
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}`)));
|
|
579
489
|
return [
|
|
580
|
-
resolver ? {
|
|
581
|
-
id: getId("resolver"),
|
|
582
|
-
position,
|
|
583
|
-
resolver
|
|
584
|
-
} : void 0,
|
|
490
|
+
// resolver ? { id: getId('resolver'), position, resolver } : undefined,
|
|
585
491
|
connector ? {
|
|
586
|
-
...rest,
|
|
587
492
|
id: getId("connector"),
|
|
588
493
|
position,
|
|
589
|
-
|
|
494
|
+
relation,
|
|
495
|
+
connector: Rx2.family((key) => connector(key).pipe(Rx2.withLabel(`graph-builder:connector:${id}`)))
|
|
590
496
|
} : void 0,
|
|
591
497
|
actionGroups ? {
|
|
592
|
-
...rest,
|
|
593
498
|
id: getId("actionGroups"),
|
|
594
499
|
position,
|
|
595
|
-
type: ACTION_GROUP_TYPE,
|
|
596
500
|
relation: "outbound",
|
|
597
|
-
connector: (
|
|
598
|
-
node
|
|
599
|
-
})?.map((arg) => ({
|
|
501
|
+
connector: Rx2.family((node) => Rx2.make((get) => get(actionGroups(node)).map((arg) => ({
|
|
600
502
|
...arg,
|
|
601
503
|
data: actionGroupSymbol,
|
|
602
504
|
type: ACTION_GROUP_TYPE
|
|
603
|
-
}))
|
|
505
|
+
}))).pipe(Rx2.withLabel(`graph-builder:connector:actionGroups:${id}`)))
|
|
604
506
|
} : void 0,
|
|
605
507
|
actions ? {
|
|
606
|
-
...rest,
|
|
607
508
|
id: getId("actions"),
|
|
608
509
|
position,
|
|
609
|
-
type: ACTION_TYPE,
|
|
610
510
|
relation: "outbound",
|
|
611
|
-
connector: (
|
|
612
|
-
node
|
|
613
|
-
})?.map((arg) => ({
|
|
511
|
+
connector: Rx2.family((node) => Rx2.make((get) => get(actions(node)).map((arg) => ({
|
|
614
512
|
...arg,
|
|
615
513
|
type: ACTION_TYPE
|
|
616
|
-
}))
|
|
514
|
+
}))).pipe(Rx2.withLabel(`graph-builder:connector:actions:${id}`)))
|
|
617
515
|
} : void 0
|
|
618
516
|
].filter(isNonNullable2);
|
|
619
517
|
};
|
|
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
|
-
};
|
|
679
518
|
var flattenExtensions = (extension, acc = []) => {
|
|
680
519
|
if (Array.isArray(extension)) {
|
|
681
520
|
return [
|
|
@@ -690,107 +529,75 @@ var flattenExtensions = (extension, acc = []) => {
|
|
|
690
529
|
}
|
|
691
530
|
};
|
|
692
531
|
var GraphBuilder = class _GraphBuilder {
|
|
693
|
-
constructor(params = {}) {
|
|
694
|
-
|
|
695
|
-
this._extensions = live2({});
|
|
696
|
-
this._resolverSubscriptions = /* @__PURE__ */ new Map();
|
|
532
|
+
constructor({ registry, ...params } = {}) {
|
|
533
|
+
// TODO(wittjosiah): Use Context.
|
|
697
534
|
this._connectorSubscriptions = /* @__PURE__ */ new Map();
|
|
698
|
-
this.
|
|
699
|
-
this.
|
|
535
|
+
this._extensions = Rx2.make(Record2.empty()).pipe(Rx2.keepAlive, Rx2.withLabel("graph-builder:extensions"));
|
|
536
|
+
this._connectors = Rx2.family((key) => {
|
|
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();
|
|
700
553
|
this._graph = new Graph({
|
|
701
554
|
...params,
|
|
702
|
-
|
|
703
|
-
|
|
555
|
+
registry: this._registry,
|
|
556
|
+
onExpand: (id, relation) => this._onExpand(id, relation),
|
|
557
|
+
// onInitialize: (id) => this._onInitialize(id),
|
|
704
558
|
onRemoveNode: (id) => this._onRemoveNode(id)
|
|
705
559
|
});
|
|
706
560
|
}
|
|
707
|
-
static from(pickle) {
|
|
561
|
+
static from(pickle, registry) {
|
|
708
562
|
if (!pickle) {
|
|
709
|
-
return new _GraphBuilder(
|
|
563
|
+
return new _GraphBuilder({
|
|
564
|
+
registry
|
|
565
|
+
});
|
|
710
566
|
}
|
|
711
567
|
const { nodes, edges } = JSON.parse(pickle);
|
|
712
568
|
return new _GraphBuilder({
|
|
713
569
|
nodes,
|
|
714
|
-
edges
|
|
570
|
+
edges,
|
|
571
|
+
registry
|
|
715
572
|
});
|
|
716
573
|
}
|
|
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
|
-
}
|
|
745
574
|
get graph() {
|
|
746
575
|
return this._graph;
|
|
747
576
|
}
|
|
748
|
-
/**
|
|
749
|
-
* @reactive
|
|
750
|
-
*/
|
|
751
577
|
get extensions() {
|
|
752
|
-
return
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const extensions = flattenExtensions(extension);
|
|
759
|
-
untracked2(() => {
|
|
760
|
-
extensions.forEach((extension2) => {
|
|
761
|
-
this._dispatcher.state[extension2.id] = [];
|
|
762
|
-
this._extensions[extension2.id] = extension2;
|
|
763
|
-
});
|
|
578
|
+
return this._extensions;
|
|
579
|
+
}
|
|
580
|
+
addExtension(extensions) {
|
|
581
|
+
flattenExtensions(extensions).forEach((extension) => {
|
|
582
|
+
const extensions2 = this._registry.get(this._extensions);
|
|
583
|
+
this._registry.set(this._extensions, Record2.set(extensions2, extension.id, extension));
|
|
764
584
|
});
|
|
765
585
|
return this;
|
|
766
586
|
}
|
|
767
|
-
/**
|
|
768
|
-
* Remove a node builder from the graph builder.
|
|
769
|
-
*/
|
|
770
587
|
removeExtension(id) {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
});
|
|
588
|
+
const extensions = this._registry.get(this._extensions);
|
|
589
|
+
this._registry.set(this._extensions, Record2.remove(extensions, id));
|
|
774
590
|
return this;
|
|
775
591
|
}
|
|
776
|
-
|
|
777
|
-
|
|
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)) {
|
|
592
|
+
async explore({ registry = Registry2.make(), source = ROOT_ID, relation = "outbound", visitor }, path = []) {
|
|
593
|
+
if (path.includes(source)) {
|
|
788
594
|
return;
|
|
789
595
|
}
|
|
790
596
|
if (!isNode()) {
|
|
791
597
|
const { yieldOrContinue } = await import("main-thread-scheduling");
|
|
792
598
|
await yieldOrContinue("idle");
|
|
793
599
|
}
|
|
600
|
+
const node = registry.get(this._graph.nodeOrThrow(source));
|
|
794
601
|
const shouldContinue = await visitor(node, [
|
|
795
602
|
...path,
|
|
796
603
|
node.id
|
|
@@ -798,158 +605,104 @@ var GraphBuilder = class _GraphBuilder {
|
|
|
798
605
|
if (shouldContinue === false) {
|
|
799
606
|
return;
|
|
800
607
|
}
|
|
801
|
-
const nodes = Object.values(this._extensions).filter((extension) => relation === (extension.relation ?? "outbound")).
|
|
802
|
-
|
|
803
|
-
this.
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
cacheable: arg.cacheable,
|
|
814
|
-
data: arg.data ?? null,
|
|
815
|
-
properties: arg.properties ?? {}
|
|
816
|
-
}));
|
|
817
|
-
await Promise.all(nodes.map((n) => this.explore({
|
|
818
|
-
node: n,
|
|
819
|
-
relation,
|
|
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
|
-
}
|
|
608
|
+
const nodes = Object.values(this._registry.get(this._extensions)).filter((extension) => relation === (extension.relation ?? "outbound")).map((extension) => extension.connector).filter(isNonNullable2).flatMap((connector) => registry.get(connector(this._graph.node(source))));
|
|
609
|
+
await Promise.all(nodes.map((nodeArg) => {
|
|
610
|
+
registry.set(this._graph._node(nodeArg.id), this._graph._constructNode(nodeArg));
|
|
611
|
+
return this.explore({
|
|
612
|
+
registry,
|
|
613
|
+
source: nodeArg.id,
|
|
614
|
+
relation,
|
|
615
|
+
visitor
|
|
616
|
+
}, [
|
|
617
|
+
...path,
|
|
618
|
+
node.id
|
|
619
|
+
]);
|
|
878
620
|
}));
|
|
621
|
+
if (registry !== this._registry) {
|
|
622
|
+
registry.reset();
|
|
623
|
+
registry.dispose();
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
destroy() {
|
|
627
|
+
this._connectorSubscriptions.forEach((unsubscribe) => unsubscribe());
|
|
628
|
+
this._connectorSubscriptions.clear();
|
|
879
629
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
630
|
+
_onExpand(id, relation) {
|
|
631
|
+
log2("onExpand", {
|
|
632
|
+
id,
|
|
633
|
+
relation,
|
|
634
|
+
registry: getDebugName(this._registry)
|
|
635
|
+
}, {
|
|
636
|
+
F: __dxlog_file2,
|
|
637
|
+
L: 276,
|
|
638
|
+
S: this,
|
|
639
|
+
C: (f, a) => f(...a)
|
|
640
|
+
});
|
|
641
|
+
const connectors = this._connectors(`${id}+${relation}`);
|
|
883
642
|
let previous = [];
|
|
884
|
-
this.
|
|
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
|
-
}
|
|
643
|
+
const cancel = this._registry.subscribe(connectors, (nodes) => {
|
|
923
644
|
const ids = nodes.map((n) => n.id);
|
|
924
|
-
const removed = previous.filter((
|
|
645
|
+
const removed = previous.filter((id2) => !ids.includes(id2));
|
|
925
646
|
previous = ids;
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
}
|
|
647
|
+
log2("update", {
|
|
648
|
+
id,
|
|
649
|
+
relation,
|
|
650
|
+
ids,
|
|
651
|
+
removed
|
|
652
|
+
}, {
|
|
653
|
+
F: __dxlog_file2,
|
|
654
|
+
L: 287,
|
|
655
|
+
S: this,
|
|
656
|
+
C: (f, a) => f(...a)
|
|
943
657
|
});
|
|
944
|
-
|
|
658
|
+
Rx2.batch(() => {
|
|
659
|
+
this._graph.removeEdges(removed.map((target) => ({
|
|
660
|
+
source: id,
|
|
661
|
+
target
|
|
662
|
+
})), true);
|
|
663
|
+
this._graph.addNodes(nodes);
|
|
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));
|
|
672
|
+
});
|
|
673
|
+
}, {
|
|
674
|
+
immediate: true
|
|
675
|
+
});
|
|
676
|
+
this._connectorSubscriptions.set(id, cancel);
|
|
945
677
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
678
|
+
// TODO(wittjosiah): On initialize to restore state from cache.
|
|
679
|
+
// private async _onInitialize(id: string) {
|
|
680
|
+
// log('onInitialize', { id });
|
|
681
|
+
// }
|
|
682
|
+
_onRemoveNode(id) {
|
|
683
|
+
this._connectorSubscriptions.get(id)?.();
|
|
684
|
+
this._connectorSubscriptions.delete(id);
|
|
951
685
|
}
|
|
952
686
|
};
|
|
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
|
+
};
|
|
953
706
|
export {
|
|
954
707
|
ACTION_GROUP_TYPE,
|
|
955
708
|
ACTION_TYPE,
|
|
@@ -958,7 +711,6 @@ export {
|
|
|
958
711
|
ROOT_ID,
|
|
959
712
|
ROOT_TYPE,
|
|
960
713
|
actionGroupSymbol,
|
|
961
|
-
cleanup,
|
|
962
714
|
createExtension,
|
|
963
715
|
flattenExtensions,
|
|
964
716
|
getGraph,
|
|
@@ -966,7 +718,7 @@ export {
|
|
|
966
718
|
isActionGroup,
|
|
967
719
|
isActionLike,
|
|
968
720
|
isGraphNode,
|
|
969
|
-
|
|
970
|
-
|
|
721
|
+
rxFromObservable,
|
|
722
|
+
rxFromSignal
|
|
971
723
|
};
|
|
972
724
|
//# sourceMappingURL=index.mjs.map
|