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