@dxos/app-graph 0.10.0 → 0.11.1
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/chunk-graph-builder.mjs +1901 -0
- package/dist/lib/chunk-graph-builder.mjs.map +1 -0
- package/dist/lib/index.mjs +281 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/scheduler.browser.mjs +2 -0
- package/dist/lib/scheduler.mjs +12 -0
- package/dist/lib/scheduler.mjs.map +1 -0
- package/dist/lib/testing.mjs +28 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/types/src/atoms.d.ts +1 -1
- package/dist/types/src/atoms.d.ts.map +1 -1
- package/dist/types/src/graph-builder.d.ts +128 -9
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +4 -1
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node-matcher.d.ts +1 -1
- package/dist/types/src/node-matcher.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +14 -2
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/path-resolution.d.ts +69 -0
- package/dist/types/src/path-resolution.d.ts.map +1 -0
- package/dist/types/src/path-resolution.test.d.ts +2 -0
- package/dist/types/src/path-resolution.test.d.ts.map +1 -0
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/src/testing/setup-graph-builder.d.ts +1 -1
- package/dist/types/src/testing/setup-graph-builder.d.ts.map +1 -1
- package/dist/types/src/util.test.d.ts +2 -0
- package/dist/types/src/util.test.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +22 -21
- package/src/atoms.ts +1 -1
- package/src/graph-builder.test.ts +21 -1
- package/src/graph-builder.ts +327 -58
- package/src/graph.test.ts +1 -1
- package/src/graph.ts +22 -16
- package/src/index.ts +1 -0
- package/src/node-matcher.test.ts +1 -1
- package/src/node-matcher.ts +1 -1
- package/src/node.ts +19 -2
- package/src/path-resolution.test.ts +520 -0
- package/src/path-resolution.ts +355 -0
- package/src/stories/EchoGraph.stories.tsx +97 -121
- package/src/testing/setup-graph-builder.ts +1 -1
- package/src/util.test.ts +85 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
- package/dist/lib/neutral/chunk-YMTZ7MPW.mjs +0 -1509
- package/dist/lib/neutral/chunk-YMTZ7MPW.mjs.map +0 -7
- package/dist/lib/neutral/index.mjs +0 -40
- package/dist/lib/neutral/index.mjs.map +0 -7
- package/dist/lib/neutral/meta.json +0 -1
- package/dist/lib/neutral/scheduler.mjs +0 -15
- package/dist/lib/neutral/scheduler.mjs.map +0 -7
- package/dist/lib/neutral/testing/index.mjs +0 -40
- package/dist/lib/neutral/testing/index.mjs.map +0 -7
|
@@ -0,0 +1,1901 @@
|
|
|
1
|
+
import { Atom, Registry } from "@effect-atom/atom";
|
|
2
|
+
import * as Function from "effect/Function";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import * as Pipeable from "effect/Pipeable";
|
|
5
|
+
import { Event, Trigger } from "@dxos/async";
|
|
6
|
+
import { todo } from "@dxos/debug";
|
|
7
|
+
import { invariant } from "@dxos/invariant";
|
|
8
|
+
import { log } from "@dxos/log";
|
|
9
|
+
import { Position, getDebugName, isNonNullable } from "@dxos/util";
|
|
10
|
+
import * as Array$1 from "effect/Array";
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import * as Record from "effect/Record";
|
|
13
|
+
import { scheduleTask, yieldOrContinue } from "#scheduler";
|
|
14
|
+
import { Entity, Obj } from "@dxos/echo";
|
|
15
|
+
//#region \0rolldown/runtime.js
|
|
16
|
+
var __defProp = Object.defineProperty;
|
|
17
|
+
var __exportAll = (all, no_symbols) => {
|
|
18
|
+
let target = {};
|
|
19
|
+
for (var name in all) __defProp(target, name, {
|
|
20
|
+
get: all[name],
|
|
21
|
+
enumerable: true
|
|
22
|
+
});
|
|
23
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
24
|
+
return target;
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/node.ts
|
|
28
|
+
var node_exports = /* @__PURE__ */ __exportAll({
|
|
29
|
+
ActionGroupType: () => ActionGroupType,
|
|
30
|
+
ActionType: () => ActionType,
|
|
31
|
+
RootId: () => RootId,
|
|
32
|
+
RootType: () => RootType,
|
|
33
|
+
actionGroupSymbol: () => actionGroupSymbol,
|
|
34
|
+
actionRelation: () => actionRelation,
|
|
35
|
+
childRelation: () => childRelation,
|
|
36
|
+
hasDisposition: () => hasDisposition,
|
|
37
|
+
isAction: () => isAction,
|
|
38
|
+
isActionGroup: () => isActionGroup,
|
|
39
|
+
isActionLike: () => isActionLike,
|
|
40
|
+
isGraphNode: () => isGraphNode,
|
|
41
|
+
make: () => make$2,
|
|
42
|
+
makeAction: () => makeAction,
|
|
43
|
+
makeActionGroup: () => makeActionGroup,
|
|
44
|
+
relation: () => relation
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* Root node ID.
|
|
48
|
+
*/
|
|
49
|
+
var RootId = "root";
|
|
50
|
+
/**
|
|
51
|
+
* Root node type.
|
|
52
|
+
*/
|
|
53
|
+
var RootType = "org.dxos.type.graphRoot";
|
|
54
|
+
/**
|
|
55
|
+
* Action node type.
|
|
56
|
+
*/
|
|
57
|
+
var ActionType = "org.dxos.type.graphAction";
|
|
58
|
+
/**
|
|
59
|
+
* Action group node type.
|
|
60
|
+
*/
|
|
61
|
+
var ActionGroupType = "org.dxos.type.graphActionGroup";
|
|
62
|
+
var relation = (kind, direction = "outbound") => ({
|
|
63
|
+
kind,
|
|
64
|
+
direction
|
|
65
|
+
});
|
|
66
|
+
var childRelation = (direction = "outbound") => relation("child", direction);
|
|
67
|
+
var actionRelation = (direction = "outbound") => relation("action", direction);
|
|
68
|
+
var isGraphNode = (data) => data && typeof data === "object" && "id" in data && "properties" in data && data.properties ? typeof data.properties === "object" && "data" in data : false;
|
|
69
|
+
var isAction = (data) => isGraphNode(data) ? typeof data.data === "function" && data.type === "org.dxos.type.graphAction" : false;
|
|
70
|
+
var actionGroupSymbol = Symbol("ActionGroup");
|
|
71
|
+
var isActionGroup = (data) => isGraphNode(data) ? data.data === actionGroupSymbol && data.type === "org.dxos.type.graphActionGroup" : false;
|
|
72
|
+
var isActionLike = (data) => isAction(data) || isActionGroup(data);
|
|
73
|
+
/**
|
|
74
|
+
* Tests whether a node's `disposition` property (a single string or an array, letting one node opt
|
|
75
|
+
* into multiple surfaces at once) includes any of `key`. Every surface that routes nodes/actions by
|
|
76
|
+
* disposition (toolbar, nav-tree list-item, sigil menu, …) should filter through this rather than
|
|
77
|
+
* comparing `properties.disposition` directly, so a node can multi-target surfaces.
|
|
78
|
+
*/
|
|
79
|
+
var hasDisposition = (node, key) => {
|
|
80
|
+
const disposition = node.properties.disposition;
|
|
81
|
+
const dispositions = Array.isArray(disposition) ? disposition : disposition !== void 0 ? [disposition] : [];
|
|
82
|
+
const keys = Array.isArray(key) ? key : [key];
|
|
83
|
+
return dispositions.some((candidate) => keys.includes(candidate));
|
|
84
|
+
};
|
|
85
|
+
/** Typed factory for constructing a NodeArg. Provides auto-complete and type validation. */
|
|
86
|
+
var make$2 = (arg) => arg;
|
|
87
|
+
/** Create an action node. Automatically sets `type: ActionType`. */
|
|
88
|
+
var makeAction = (arg) => ({
|
|
89
|
+
...arg,
|
|
90
|
+
type: ActionType
|
|
91
|
+
});
|
|
92
|
+
/** Create an action group node. Automatically sets `type` and `data`. */
|
|
93
|
+
var makeActionGroup = (arg) => ({
|
|
94
|
+
...arg,
|
|
95
|
+
type: ActionGroupType,
|
|
96
|
+
data: actionGroupSymbol
|
|
97
|
+
});
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/util.ts
|
|
100
|
+
var __dxlog_file$2 = "/__w/dxos/dxos/packages/sdk/app-graph/src/util.ts";
|
|
101
|
+
var PRIMARY = "";
|
|
102
|
+
var SECONDARY = "";
|
|
103
|
+
var PATH = "/";
|
|
104
|
+
/** Join parts with the primary separator. */
|
|
105
|
+
var primaryKey = (...parts) => parts.join(PRIMARY);
|
|
106
|
+
/** Split a key on the primary separator. */
|
|
107
|
+
var primaryParts = (key) => key.split(PRIMARY);
|
|
108
|
+
/** Join parts with the secondary separator. */
|
|
109
|
+
var secondaryKey = (...parts) => parts.join(SECONDARY);
|
|
110
|
+
/** Split a key on the secondary separator. */
|
|
111
|
+
var secondaryParts = (key) => key.split(SECONDARY);
|
|
112
|
+
/**
|
|
113
|
+
* Normalize a relation input to a full Relation object.
|
|
114
|
+
*/
|
|
115
|
+
var normalizeRelation = (relation$2) => relation$2 == null ? childRelation() : typeof relation$2 === "string" ? relation(relation$2) : relation$2;
|
|
116
|
+
/**
|
|
117
|
+
* Shallow-compare two values: same reference, or same own-keys with === values.
|
|
118
|
+
*/
|
|
119
|
+
var shallowEqual = (a, b) => {
|
|
120
|
+
if (a === b) return true;
|
|
121
|
+
if (a == null || b == null || typeof a !== "object" || typeof b !== "object") return false;
|
|
122
|
+
const keysA = Object.keys(a);
|
|
123
|
+
const keysB = Object.keys(b);
|
|
124
|
+
if (keysA.length !== keysB.length) return false;
|
|
125
|
+
return keysA.every((k) => a[k] === b[k]);
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Returns true if two NodeArg arrays are semantically identical (same id, type, data, properties per index).
|
|
129
|
+
* Inline child nodes (the `nodes` field) are compared recursively.
|
|
130
|
+
*/
|
|
131
|
+
var nodeArgsUnchanged = (prev, next) => {
|
|
132
|
+
if (prev.length !== next.length) return false;
|
|
133
|
+
return prev.every((prevNode, idx) => {
|
|
134
|
+
const nextNode = next[idx];
|
|
135
|
+
return prevNode.id === nextNode.id && prevNode.type === nextNode.type && shallowEqual(prevNode.data, nextNode.data) && shallowEqual(prevNode.properties, nextNode.properties) && nodeArgsUnchanged(prevNode.nodes ?? [], nextNode.nodes ?? []) && nodeArgsUnchanged(prevNode.actions ?? [], nextNode.actions ?? []);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Build a qualified node ID by joining path segments.
|
|
140
|
+
*/
|
|
141
|
+
var qualifyId = (parentId, ...segmentIds) => [parentId, ...segmentIds].join(PATH);
|
|
142
|
+
/**
|
|
143
|
+
* Validate that a segment ID does not contain the path separator.
|
|
144
|
+
*/
|
|
145
|
+
var validateSegmentId = (id) => {
|
|
146
|
+
invariant(!id.includes(PATH), `Node segment ID must not contain '${PATH}': ${id}`, {
|
|
147
|
+
"~LogMeta": "~LogMeta",
|
|
148
|
+
F: __dxlog_file$2,
|
|
149
|
+
L: 85,
|
|
150
|
+
S: void 0,
|
|
151
|
+
A: ["!id.includes(PATH)", "`Node segment ID must not contain '${PATH}': ${id}`"]
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Extract the parent qualified ID (everything before the last path separator).
|
|
156
|
+
* Returns undefined for IDs with no parent (single segment).
|
|
157
|
+
*/
|
|
158
|
+
var getParentId = (qualifiedId) => {
|
|
159
|
+
const lastSlash = qualifiedId.lastIndexOf(PATH);
|
|
160
|
+
return lastSlash > 0 ? qualifiedId.slice(0, lastSlash) : void 0;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Extract the last segment of a qualified ID.
|
|
164
|
+
*/
|
|
165
|
+
var getSegmentId = (qualifiedId) => {
|
|
166
|
+
return qualifiedId.split(PATH).pop() ?? qualifiedId;
|
|
167
|
+
};
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/graph.ts
|
|
170
|
+
var graph_exports = /* @__PURE__ */ __exportAll({
|
|
171
|
+
GraphKind: () => GraphKind,
|
|
172
|
+
GraphTypeId: () => GraphTypeId,
|
|
173
|
+
addEdge: () => addEdge,
|
|
174
|
+
addEdges: () => addEdges,
|
|
175
|
+
addNode: () => addNode,
|
|
176
|
+
addNodes: () => addNodes,
|
|
177
|
+
expand: () => expand,
|
|
178
|
+
getActions: () => getActions,
|
|
179
|
+
getConnections: () => getConnections,
|
|
180
|
+
getEdges: () => getEdges,
|
|
181
|
+
getGraph: () => getGraph,
|
|
182
|
+
getNode: () => getNode,
|
|
183
|
+
getNodeOrThrow: () => getNodeOrThrow,
|
|
184
|
+
getPath: () => getPath,
|
|
185
|
+
getRoot: () => getRoot,
|
|
186
|
+
initialize: () => initialize,
|
|
187
|
+
make: () => make$1,
|
|
188
|
+
relationFromKey: () => relationFromKey,
|
|
189
|
+
relationKey: () => relationKey,
|
|
190
|
+
removeEdge: () => removeEdge,
|
|
191
|
+
removeEdges: () => removeEdges,
|
|
192
|
+
removeNode: () => removeNode,
|
|
193
|
+
removeNodes: () => removeNodes,
|
|
194
|
+
sortEdges: () => sortEdges,
|
|
195
|
+
toJSON: () => toJSON,
|
|
196
|
+
traverse: () => traverse,
|
|
197
|
+
waitForPath: () => waitForPath
|
|
198
|
+
});
|
|
199
|
+
var __dxlog_file$1 = "/__w/dxos/dxos/packages/sdk/app-graph/src/graph.ts";
|
|
200
|
+
var graphSymbol = Symbol("graph");
|
|
201
|
+
/**
|
|
202
|
+
* Get the Graph a Node is currently associated with.
|
|
203
|
+
*/
|
|
204
|
+
var getGraph = (node) => {
|
|
205
|
+
const graph = node[graphSymbol];
|
|
206
|
+
invariant(graph, "Node is not associated with a graph.", {
|
|
207
|
+
"~LogMeta": "~LogMeta",
|
|
208
|
+
F: __dxlog_file$1,
|
|
209
|
+
L: 32,
|
|
210
|
+
S: void 0,
|
|
211
|
+
A: ["graph", "'Node is not associated with a graph.'"]
|
|
212
|
+
});
|
|
213
|
+
return graph;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Identifier denoting a Graph.
|
|
217
|
+
*/
|
|
218
|
+
var GraphTypeId = Symbol.for("@dxos/app-graph/Graph");
|
|
219
|
+
/**
|
|
220
|
+
* Identifier for the graph kind discriminator.
|
|
221
|
+
*/
|
|
222
|
+
var GraphKind = Symbol.for("@dxos/app-graph/GraphKind");
|
|
223
|
+
/**
|
|
224
|
+
* The Graph represents the user interface information architecture of the application constructed via plugins.
|
|
225
|
+
* @internal
|
|
226
|
+
*/
|
|
227
|
+
var GraphImpl = class {
|
|
228
|
+
[GraphTypeId] = GraphTypeId;
|
|
229
|
+
[GraphKind] = "writable";
|
|
230
|
+
pipe() {
|
|
231
|
+
return Pipeable.pipeArguments(this, arguments);
|
|
232
|
+
}
|
|
233
|
+
onNodeChanged = new Event();
|
|
234
|
+
_onExpand;
|
|
235
|
+
_onInitialize;
|
|
236
|
+
_onRemoveNode;
|
|
237
|
+
_registry;
|
|
238
|
+
_expanded = /* @__PURE__ */ new Set();
|
|
239
|
+
_pendingExpands = /* @__PURE__ */ new Set();
|
|
240
|
+
_initialized = /* @__PURE__ */ new Set();
|
|
241
|
+
_initialEdges = /* @__PURE__ */ new Map();
|
|
242
|
+
_initialNodes = /* @__PURE__ */ new Map([[RootId, this._constructNode({
|
|
243
|
+
id: RootId,
|
|
244
|
+
type: RootType,
|
|
245
|
+
data: null,
|
|
246
|
+
properties: {}
|
|
247
|
+
})]]);
|
|
248
|
+
/** @internal */
|
|
249
|
+
_node = Atom.family((id) => {
|
|
250
|
+
const initial = this._initialNodes.get(id) ?? Option.none();
|
|
251
|
+
return Atom.make(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:node:${id}`));
|
|
252
|
+
});
|
|
253
|
+
_nodeOrThrow = Atom.family((id) => {
|
|
254
|
+
return Atom.make((get) => {
|
|
255
|
+
const node = get(this._node(id));
|
|
256
|
+
invariant(Option.isSome(node), `Node not available: ${id}`, {
|
|
257
|
+
"~LogMeta": "~LogMeta",
|
|
258
|
+
F: __dxlog_file$1,
|
|
259
|
+
L: 171,
|
|
260
|
+
S: this,
|
|
261
|
+
A: ["Option.isSome(node)", "`Node not available: ${id}`"]
|
|
262
|
+
});
|
|
263
|
+
return node.value;
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
_edges = Atom.family((id) => {
|
|
267
|
+
const initial = this._initialEdges.get(id) ?? {};
|
|
268
|
+
return Atom.make(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:edges:${id}`));
|
|
269
|
+
});
|
|
270
|
+
_connections = Atom.family((key) => {
|
|
271
|
+
return Atom.make((get) => {
|
|
272
|
+
const parts = key ? primaryParts(key) : [];
|
|
273
|
+
if (parts.length < 2 || !parts[0]) return [];
|
|
274
|
+
const { id, relation } = relationFromConnectionKey(key);
|
|
275
|
+
return (get(this._edges(id))[relationKey(relation)] ?? []).map((id) => get(this._node(id))).filter(Option.isSome).map((o) => o.value);
|
|
276
|
+
}).pipe(Atom.withLabel(`graph:connections:${key}`));
|
|
277
|
+
});
|
|
278
|
+
_actions = Atom.family((id) => {
|
|
279
|
+
return Atom.make((get) => {
|
|
280
|
+
if (!id) return [];
|
|
281
|
+
return get(this._connections(connectionKey(id, actionRelation())));
|
|
282
|
+
}).pipe(Atom.withLabel(`graph:actions:${id}`));
|
|
283
|
+
});
|
|
284
|
+
_json = Atom.family((id) => {
|
|
285
|
+
return Atom.make((get) => {
|
|
286
|
+
const toJSON = (node, seen = []) => {
|
|
287
|
+
const nodes = get(this._connections(connectionKey(node.id, "child")));
|
|
288
|
+
const obj = {
|
|
289
|
+
id: node.id,
|
|
290
|
+
type: node.type
|
|
291
|
+
};
|
|
292
|
+
if (node.properties.label) obj.label = node.properties.label;
|
|
293
|
+
if (nodes.length) obj.nodes = nodes.map((n) => {
|
|
294
|
+
const nextSeen = [...seen, node.id];
|
|
295
|
+
return nextSeen.includes(n.id) ? void 0 : toJSON(n, nextSeen);
|
|
296
|
+
}).filter(isNonNullable);
|
|
297
|
+
return obj;
|
|
298
|
+
};
|
|
299
|
+
return toJSON(get(this._nodeOrThrow(id)));
|
|
300
|
+
}).pipe(Atom.withLabel(`graph:json:${id}`));
|
|
301
|
+
});
|
|
302
|
+
constructor({ registry, nodes, edges, onInitialize, onExpand, onRemoveNode } = {}) {
|
|
303
|
+
this._registry = registry ?? Registry.make();
|
|
304
|
+
this._onInitialize = onInitialize;
|
|
305
|
+
this._onExpand = onExpand;
|
|
306
|
+
this._onRemoveNode = onRemoveNode;
|
|
307
|
+
if (nodes) nodes.forEach((node) => {
|
|
308
|
+
this._initialNodes.set(node.id, this._constructNode(node));
|
|
309
|
+
});
|
|
310
|
+
if (edges) Object.entries(edges).forEach(([source, edges]) => {
|
|
311
|
+
this._initialEdges.set(source, edges);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
json(id = RootId) {
|
|
315
|
+
return jsonImpl(this, id);
|
|
316
|
+
}
|
|
317
|
+
node(id) {
|
|
318
|
+
return nodeImpl(this, id);
|
|
319
|
+
}
|
|
320
|
+
nodeOrThrow(id) {
|
|
321
|
+
return nodeOrThrowImpl(this, id);
|
|
322
|
+
}
|
|
323
|
+
connections(id, relation) {
|
|
324
|
+
return connectionsImpl(this, id, relation);
|
|
325
|
+
}
|
|
326
|
+
actions(id) {
|
|
327
|
+
return actionsImpl(this, id);
|
|
328
|
+
}
|
|
329
|
+
edges(id) {
|
|
330
|
+
return edgesImpl(this, id);
|
|
331
|
+
}
|
|
332
|
+
/** @internal */
|
|
333
|
+
_constructNode(node) {
|
|
334
|
+
return Option.some({
|
|
335
|
+
[graphSymbol]: this,
|
|
336
|
+
data: null,
|
|
337
|
+
properties: {},
|
|
338
|
+
...node
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* Internal helper to access GraphImpl internals.
|
|
344
|
+
* @internal
|
|
345
|
+
*/
|
|
346
|
+
var getInternal = (graph) => {
|
|
347
|
+
return graph;
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* Convert the graph to a JSON object.
|
|
351
|
+
*/
|
|
352
|
+
var toJSON = (graph, id = RootId) => {
|
|
353
|
+
const internal = getInternal(graph);
|
|
354
|
+
return internal._registry.get(internal._json(id));
|
|
355
|
+
};
|
|
356
|
+
/**
|
|
357
|
+
* Implementation helper for json.
|
|
358
|
+
*/
|
|
359
|
+
var jsonImpl = (graph, id = RootId) => {
|
|
360
|
+
return getInternal(graph)._json(id);
|
|
361
|
+
};
|
|
362
|
+
/**
|
|
363
|
+
* Implementation helper for node.
|
|
364
|
+
*/
|
|
365
|
+
var nodeImpl = (graph, id) => {
|
|
366
|
+
return getInternal(graph)._node(id);
|
|
367
|
+
};
|
|
368
|
+
/**
|
|
369
|
+
* Implementation helper for nodeOrThrow.
|
|
370
|
+
*/
|
|
371
|
+
var nodeOrThrowImpl = (graph, id) => {
|
|
372
|
+
return getInternal(graph)._nodeOrThrow(id);
|
|
373
|
+
};
|
|
374
|
+
/**
|
|
375
|
+
* Implementation helper for connections.
|
|
376
|
+
*/
|
|
377
|
+
var connectionsImpl = (graph, id, relation) => {
|
|
378
|
+
return getInternal(graph)._connections(connectionKey(id, relation));
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* Implementation helper for actions.
|
|
382
|
+
*/
|
|
383
|
+
var actionsImpl = (graph, id) => {
|
|
384
|
+
return getInternal(graph)._actions(id);
|
|
385
|
+
};
|
|
386
|
+
/**
|
|
387
|
+
* Implementation helper for edges.
|
|
388
|
+
*/
|
|
389
|
+
var edgesImpl = (graph, id) => {
|
|
390
|
+
return getInternal(graph)._edges(id);
|
|
391
|
+
};
|
|
392
|
+
/**
|
|
393
|
+
* Implementation helper for getNode.
|
|
394
|
+
*/
|
|
395
|
+
var getNodeImpl = (graph, id) => {
|
|
396
|
+
return getInternal(graph)._registry.get(nodeImpl(graph, id));
|
|
397
|
+
};
|
|
398
|
+
function getNode(graphOrId, id) {
|
|
399
|
+
if (typeof graphOrId === "string") {
|
|
400
|
+
const id = graphOrId;
|
|
401
|
+
return (graph) => getNodeImpl(graph, id);
|
|
402
|
+
} else return getNodeImpl(graphOrId, id);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Implementation helper for getNodeOrThrow.
|
|
406
|
+
*/
|
|
407
|
+
var getNodeOrThrowImpl = (graph, id) => {
|
|
408
|
+
return getInternal(graph)._registry.get(nodeOrThrowImpl(graph, id));
|
|
409
|
+
};
|
|
410
|
+
function getNodeOrThrow(graphOrId, id) {
|
|
411
|
+
if (typeof graphOrId === "string") {
|
|
412
|
+
const id = graphOrId;
|
|
413
|
+
return (graph) => getNodeOrThrowImpl(graph, id);
|
|
414
|
+
} else return getNodeOrThrowImpl(graphOrId, id);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Get the root node of the graph.
|
|
418
|
+
* This is an alias for `getNodeOrThrow(graph, ROOT_ID)`.
|
|
419
|
+
*/
|
|
420
|
+
function getRoot(graph) {
|
|
421
|
+
return getNodeOrThrowImpl(graph, RootId);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Implementation helper for getConnections.
|
|
425
|
+
*/
|
|
426
|
+
var getConnectionsImpl = (graph, id, relation) => {
|
|
427
|
+
return getInternal(graph)._registry.get(connectionsImpl(graph, id, relation));
|
|
428
|
+
};
|
|
429
|
+
function getConnections(graphOrId, idOrRelation, relation) {
|
|
430
|
+
if (typeof graphOrId === "string") {
|
|
431
|
+
const id = graphOrId;
|
|
432
|
+
const rel = idOrRelation;
|
|
433
|
+
return (graph) => getConnectionsImpl(graph, id, rel);
|
|
434
|
+
} else {
|
|
435
|
+
const graph = graphOrId;
|
|
436
|
+
const id = idOrRelation;
|
|
437
|
+
invariant(relation !== void 0, "Relation is required.", {
|
|
438
|
+
"~LogMeta": "~LogMeta",
|
|
439
|
+
F: __dxlog_file$1,
|
|
440
|
+
L: 448,
|
|
441
|
+
S: this,
|
|
442
|
+
A: ["relation !== undefined", "'Relation is required.'"]
|
|
443
|
+
});
|
|
444
|
+
return getConnectionsImpl(graph, id, relation);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Implementation helper for getActions.
|
|
449
|
+
*/
|
|
450
|
+
var getActionsImpl = (graph, id) => {
|
|
451
|
+
return getInternal(graph)._registry.get(actionsImpl(graph, id));
|
|
452
|
+
};
|
|
453
|
+
function getActions(graphOrId, id) {
|
|
454
|
+
if (typeof graphOrId === "string") {
|
|
455
|
+
const id = graphOrId;
|
|
456
|
+
return (graph) => getActionsImpl(graph, id);
|
|
457
|
+
} else return getActionsImpl(graphOrId, id);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Implementation helper for getEdges.
|
|
461
|
+
*/
|
|
462
|
+
var getEdgesImpl = (graph, id) => {
|
|
463
|
+
return getInternal(graph)._registry.get(edgesImpl(graph, id));
|
|
464
|
+
};
|
|
465
|
+
function getEdges(graphOrId, id) {
|
|
466
|
+
if (typeof graphOrId === "string") {
|
|
467
|
+
const id = graphOrId;
|
|
468
|
+
return (graph) => getEdgesImpl(graph, id);
|
|
469
|
+
} else return getEdgesImpl(graphOrId, id);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Recursive depth-first traversal of the graph.
|
|
473
|
+
*/
|
|
474
|
+
/**
|
|
475
|
+
* Implementation helper for traverse.
|
|
476
|
+
*/
|
|
477
|
+
var traverseImpl = (graph, options, path = []) => {
|
|
478
|
+
const { visitor, source = RootId, relation } = options;
|
|
479
|
+
if (path.includes(source)) return;
|
|
480
|
+
if (visitor(getNodeOrThrow(graph, source), [...path, source]) === false) return;
|
|
481
|
+
const relations = Array.isArray(relation) ? relation : [relation];
|
|
482
|
+
const seen = /* @__PURE__ */ new Set();
|
|
483
|
+
for (const rel of relations) for (const connected of getConnections(graph, source, rel)) if (!seen.has(connected.id)) {
|
|
484
|
+
seen.add(connected.id);
|
|
485
|
+
traverseImpl(graph, {
|
|
486
|
+
source: connected.id,
|
|
487
|
+
relation,
|
|
488
|
+
visitor
|
|
489
|
+
}, [...path, source]);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
function traverse(graphOrOptions, optionsOrPath, path) {
|
|
493
|
+
if (typeof graphOrOptions === "object" && "visitor" in graphOrOptions) {
|
|
494
|
+
const options = graphOrOptions;
|
|
495
|
+
const pathArg = Array.isArray(optionsOrPath) ? optionsOrPath : void 0;
|
|
496
|
+
return (graph) => traverseImpl(graph, options, pathArg);
|
|
497
|
+
} else return traverseImpl(graphOrOptions, optionsOrPath, path ?? (Array.isArray(optionsOrPath) ? optionsOrPath : void 0));
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Implementation helper for getPath.
|
|
501
|
+
*/
|
|
502
|
+
var getPathImpl = (graph, params) => {
|
|
503
|
+
return Function.pipe(getNode(graph, params.source ?? "root"), Option.flatMap((node) => {
|
|
504
|
+
let found = Option.none();
|
|
505
|
+
traverseImpl(graph, {
|
|
506
|
+
source: node.id,
|
|
507
|
+
relation: "child",
|
|
508
|
+
visitor: (node, path) => {
|
|
509
|
+
if (Option.isSome(found)) return false;
|
|
510
|
+
if (node.id === params.target) found = Option.some(path);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
return found;
|
|
514
|
+
}));
|
|
515
|
+
};
|
|
516
|
+
function getPath(graphOrParams, params) {
|
|
517
|
+
if (params === void 0 && typeof graphOrParams === "object" && "target" in graphOrParams) {
|
|
518
|
+
const params = graphOrParams;
|
|
519
|
+
return (graph) => getPathImpl(graph, params);
|
|
520
|
+
} else return getPathImpl(graphOrParams, params);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Implementation helper for waitForPath.
|
|
524
|
+
*/
|
|
525
|
+
var waitForPathImpl = (graph, params, options) => {
|
|
526
|
+
const { timeout = 5e3, interval = 500 } = options ?? {};
|
|
527
|
+
const path = getPathImpl(graph, params);
|
|
528
|
+
if (Option.isSome(path)) return Promise.resolve(path.value);
|
|
529
|
+
const trigger = new Trigger();
|
|
530
|
+
const i = setInterval(() => {
|
|
531
|
+
const path = getPathImpl(graph, params);
|
|
532
|
+
if (Option.isSome(path)) trigger.wake(path.value);
|
|
533
|
+
}, interval);
|
|
534
|
+
return trigger.wait({ timeout }).finally(() => clearInterval(i));
|
|
535
|
+
};
|
|
536
|
+
function waitForPath(graphOrParams, paramsOrOptions, options) {
|
|
537
|
+
if (typeof graphOrParams === "object" && "target" in graphOrParams) {
|
|
538
|
+
const params = graphOrParams;
|
|
539
|
+
const opts = typeof paramsOrOptions === "object" && !("target" in paramsOrOptions) ? paramsOrOptions : void 0;
|
|
540
|
+
return (graph) => waitForPathImpl(graph, params, opts);
|
|
541
|
+
} else return waitForPathImpl(graphOrParams, paramsOrOptions, options);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Implementation helper for initialize.
|
|
545
|
+
*/
|
|
546
|
+
var initializeImpl = async (graph, id) => {
|
|
547
|
+
const internal = getInternal(graph);
|
|
548
|
+
const initialized = internal._initialized.has(id);
|
|
549
|
+
log("initialize", {
|
|
550
|
+
id,
|
|
551
|
+
initialized
|
|
552
|
+
}, {
|
|
553
|
+
"~LogMeta": "~LogMeta",
|
|
554
|
+
F: __dxlog_file$1,
|
|
555
|
+
L: 670,
|
|
556
|
+
S: void 0
|
|
557
|
+
});
|
|
558
|
+
if (!initialized) {
|
|
559
|
+
internal._initialized.add(id);
|
|
560
|
+
await internal._onInitialize?.(id);
|
|
561
|
+
}
|
|
562
|
+
return graph;
|
|
563
|
+
};
|
|
564
|
+
function initialize(graphOrId, id) {
|
|
565
|
+
if (typeof graphOrId === "string") {
|
|
566
|
+
const id = graphOrId;
|
|
567
|
+
return (graph) => initializeImpl(graph, id);
|
|
568
|
+
} else return initializeImpl(graphOrId, id);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Implementation helper for expand.
|
|
572
|
+
* If the node does not exist yet, the expand is recorded as pending and applied when the node is added.
|
|
573
|
+
*/
|
|
574
|
+
var expandImpl = (graph, id, relation) => {
|
|
575
|
+
const internal = getInternal(graph);
|
|
576
|
+
const normalizedRelation = normalizeRelation(relation);
|
|
577
|
+
const key = primaryKey(id, relationKey(normalizedRelation));
|
|
578
|
+
const nodeOpt = internal._registry.get(internal._node(id));
|
|
579
|
+
if (Option.isNone(nodeOpt)) {
|
|
580
|
+
internal._pendingExpands.add(key);
|
|
581
|
+
log("expand", {
|
|
582
|
+
key,
|
|
583
|
+
deferred: true
|
|
584
|
+
}, {
|
|
585
|
+
"~LogMeta": "~LogMeta",
|
|
586
|
+
F: __dxlog_file$1,
|
|
587
|
+
L: 719,
|
|
588
|
+
S: void 0
|
|
589
|
+
});
|
|
590
|
+
return graph;
|
|
591
|
+
}
|
|
592
|
+
const expanded = internal._expanded.has(key);
|
|
593
|
+
log("expand", {
|
|
594
|
+
key,
|
|
595
|
+
expanded
|
|
596
|
+
}, {
|
|
597
|
+
"~LogMeta": "~LogMeta",
|
|
598
|
+
F: __dxlog_file$1,
|
|
599
|
+
L: 724,
|
|
600
|
+
S: void 0
|
|
601
|
+
});
|
|
602
|
+
if (!expanded) {
|
|
603
|
+
internal._expanded.add(key);
|
|
604
|
+
internal._onExpand?.(id, normalizedRelation);
|
|
605
|
+
}
|
|
606
|
+
return graph;
|
|
607
|
+
};
|
|
608
|
+
function expand(graphOrId, idOrRelation, relation) {
|
|
609
|
+
if (typeof graphOrId === "string") {
|
|
610
|
+
const id = graphOrId;
|
|
611
|
+
const rel = idOrRelation;
|
|
612
|
+
return (graph) => expandImpl(graph, id, rel);
|
|
613
|
+
} else {
|
|
614
|
+
const graph = graphOrId;
|
|
615
|
+
const id = idOrRelation;
|
|
616
|
+
invariant(relation !== void 0, "Relation is required.", {
|
|
617
|
+
"~LogMeta": "~LogMeta",
|
|
618
|
+
F: __dxlog_file$1,
|
|
619
|
+
L: 760,
|
|
620
|
+
S: this,
|
|
621
|
+
A: ["relation !== undefined", "'Relation is required.'"]
|
|
622
|
+
});
|
|
623
|
+
return expandImpl(graph, id, relation);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Implementation helper for sortEdges.
|
|
628
|
+
*/
|
|
629
|
+
var sortEdgesImpl = (graph, id, relation, order) => {
|
|
630
|
+
const internal = getInternal(graph);
|
|
631
|
+
const edgesAtom = internal._edges(id);
|
|
632
|
+
const edges = internal._registry.get(edgesAtom);
|
|
633
|
+
const relationId = relationKey(relation);
|
|
634
|
+
const current = edges[relationId] ?? [];
|
|
635
|
+
const unsorted = current.filter((id) => !order.includes(id));
|
|
636
|
+
const newOrder = [...order.filter((id) => current.includes(id)), ...unsorted];
|
|
637
|
+
if (newOrder.length === current.length && newOrder.every((id, i) => id === current[i])) return graph;
|
|
638
|
+
internal._registry.set(edgesAtom, {
|
|
639
|
+
...edges,
|
|
640
|
+
[relationId]: newOrder
|
|
641
|
+
});
|
|
642
|
+
return graph;
|
|
643
|
+
};
|
|
644
|
+
function sortEdges(graphOrId, idOrRelation, relationOrOrder, order) {
|
|
645
|
+
if (typeof graphOrId === "string") {
|
|
646
|
+
const id = graphOrId;
|
|
647
|
+
const relation = idOrRelation;
|
|
648
|
+
const order = relationOrOrder;
|
|
649
|
+
return (graph) => sortEdgesImpl(graph, id, relation, order);
|
|
650
|
+
} else return sortEdgesImpl(graphOrId, idOrRelation, relationOrOrder, order);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Implementation helper for addNodes.
|
|
654
|
+
*/
|
|
655
|
+
var addNodesImpl = (graph, nodes) => {
|
|
656
|
+
Atom.batch(() => {
|
|
657
|
+
nodes.map((node) => addNodeImpl(graph, node));
|
|
658
|
+
});
|
|
659
|
+
return graph;
|
|
660
|
+
};
|
|
661
|
+
function addNodes(graphOrNodes, nodes) {
|
|
662
|
+
if (nodes === void 0) {
|
|
663
|
+
const nodes = graphOrNodes;
|
|
664
|
+
return (graph) => addNodesImpl(graph, nodes);
|
|
665
|
+
} else return addNodesImpl(graphOrNodes, nodes);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Implementation helper for addNode.
|
|
669
|
+
*/
|
|
670
|
+
var addNodeImpl = (graph, nodeArg) => {
|
|
671
|
+
const internal = getInternal(graph);
|
|
672
|
+
const { nodes, actions, edges, id, type, data = null, properties = {}, ...rest } = nodeArg;
|
|
673
|
+
const nodeAtom = internal._node(id);
|
|
674
|
+
const existingNode = internal._registry.get(nodeAtom);
|
|
675
|
+
Option.match(existingNode, {
|
|
676
|
+
onSome: (existing) => {
|
|
677
|
+
const typeChanged = existing.type !== type;
|
|
678
|
+
const dataChanged = !shallowEqual(existing.data, data);
|
|
679
|
+
const propertiesChanged = Object.keys(properties).some((key) => existing.properties[key] !== properties[key]);
|
|
680
|
+
const changed = typeChanged || dataChanged || propertiesChanged;
|
|
681
|
+
log("existing node", {
|
|
682
|
+
id,
|
|
683
|
+
changed,
|
|
684
|
+
typeChanged,
|
|
685
|
+
dataChanged,
|
|
686
|
+
propertiesChanged
|
|
687
|
+
}, {
|
|
688
|
+
"~LogMeta": "~LogMeta",
|
|
689
|
+
F: __dxlog_file$1,
|
|
690
|
+
L: 886,
|
|
691
|
+
S: void 0
|
|
692
|
+
});
|
|
693
|
+
if (changed) {
|
|
694
|
+
log("updating node", {
|
|
695
|
+
id,
|
|
696
|
+
type,
|
|
697
|
+
data,
|
|
698
|
+
properties
|
|
699
|
+
}, {
|
|
700
|
+
"~LogMeta": "~LogMeta",
|
|
701
|
+
F: __dxlog_file$1,
|
|
702
|
+
L: 894,
|
|
703
|
+
S: void 0
|
|
704
|
+
});
|
|
705
|
+
const newNode = Option.some({
|
|
706
|
+
...existing,
|
|
707
|
+
...rest,
|
|
708
|
+
type,
|
|
709
|
+
data,
|
|
710
|
+
properties: {
|
|
711
|
+
...existing.properties,
|
|
712
|
+
...properties
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
internal._registry.set(nodeAtom, newNode);
|
|
716
|
+
graph.onNodeChanged.emit({
|
|
717
|
+
id,
|
|
718
|
+
node: newNode
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
onNone: () => {
|
|
723
|
+
log("new node", {
|
|
724
|
+
id,
|
|
725
|
+
type,
|
|
726
|
+
data,
|
|
727
|
+
properties
|
|
728
|
+
}, {
|
|
729
|
+
"~LogMeta": "~LogMeta",
|
|
730
|
+
F: __dxlog_file$1,
|
|
731
|
+
L: 907,
|
|
732
|
+
S: void 0
|
|
733
|
+
});
|
|
734
|
+
const newNode = internal._constructNode({
|
|
735
|
+
id,
|
|
736
|
+
type,
|
|
737
|
+
data,
|
|
738
|
+
properties,
|
|
739
|
+
...rest
|
|
740
|
+
});
|
|
741
|
+
internal._registry.set(nodeAtom, newNode);
|
|
742
|
+
graph.onNodeChanged.emit({
|
|
743
|
+
id,
|
|
744
|
+
node: newNode
|
|
745
|
+
});
|
|
746
|
+
const toApply = [...internal._pendingExpands].filter((k) => primaryParts(k)[0] === id);
|
|
747
|
+
for (const pendingKey of toApply) {
|
|
748
|
+
internal._pendingExpands.delete(pendingKey);
|
|
749
|
+
const relation = relationFromKey(primaryParts(pendingKey)[1]);
|
|
750
|
+
internal._expanded.add(pendingKey);
|
|
751
|
+
internal._onExpand?.(id, relation);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
if (nodes) {
|
|
756
|
+
addNodesImpl(graph, nodes);
|
|
757
|
+
addEdgesImpl(graph, nodes.map((node) => ({
|
|
758
|
+
source: id,
|
|
759
|
+
target: node.id,
|
|
760
|
+
relation: "child"
|
|
761
|
+
})));
|
|
762
|
+
sortEdgesImpl(graph, id, "child", nodes.map((n) => n.id));
|
|
763
|
+
}
|
|
764
|
+
if (actions) {
|
|
765
|
+
addNodesImpl(graph, actions);
|
|
766
|
+
const actionRelation$1 = actionRelation();
|
|
767
|
+
addEdgesImpl(graph, actions.map((node) => ({
|
|
768
|
+
source: id,
|
|
769
|
+
target: node.id,
|
|
770
|
+
relation: actionRelation$1
|
|
771
|
+
})));
|
|
772
|
+
sortEdgesImpl(graph, id, actionRelation$1, actions.map((node) => node.id));
|
|
773
|
+
}
|
|
774
|
+
if (edges) todo();
|
|
775
|
+
return graph;
|
|
776
|
+
};
|
|
777
|
+
function addNode(graphOrNodeArg, nodeArg) {
|
|
778
|
+
if (nodeArg === void 0) {
|
|
779
|
+
const nodeArg = graphOrNodeArg;
|
|
780
|
+
return (graph) => addNodeImpl(graph, nodeArg);
|
|
781
|
+
} else return addNodeImpl(graphOrNodeArg, nodeArg);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Implementation helper for removeNodes.
|
|
785
|
+
*/
|
|
786
|
+
var removeNodesImpl = (graph, ids, edges = false) => {
|
|
787
|
+
Atom.batch(() => {
|
|
788
|
+
ids.map((id) => removeNodeImpl(graph, id, edges));
|
|
789
|
+
});
|
|
790
|
+
return graph;
|
|
791
|
+
};
|
|
792
|
+
function removeNodes(graphOrIds, idsOrEdges, edges) {
|
|
793
|
+
if (Array.isArray(graphOrIds)) {
|
|
794
|
+
const ids = graphOrIds;
|
|
795
|
+
const edgesArg = typeof idsOrEdges === "boolean" ? idsOrEdges : false;
|
|
796
|
+
return (graph) => removeNodesImpl(graph, ids, edgesArg);
|
|
797
|
+
} else return removeNodesImpl(graphOrIds, idsOrEdges, edges ?? false);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Implementation helper for removeNode.
|
|
801
|
+
*/
|
|
802
|
+
var removeNodeImpl = (graph, id, edges = false) => {
|
|
803
|
+
const internal = getInternal(graph);
|
|
804
|
+
const nodeAtom = internal._node(id);
|
|
805
|
+
internal._registry.set(nodeAtom, Option.none());
|
|
806
|
+
graph.onNodeChanged.emit({
|
|
807
|
+
id,
|
|
808
|
+
node: Option.none()
|
|
809
|
+
});
|
|
810
|
+
if (edges) {
|
|
811
|
+
const nodeEdges = internal._registry.get(internal._edges(id));
|
|
812
|
+
const edgesToRemove = [];
|
|
813
|
+
for (const [relationKeyValue, relatedIds] of Object.entries(nodeEdges)) {
|
|
814
|
+
const relation = relationFromKey(relationKeyValue);
|
|
815
|
+
const isInboundRelation = relation.direction === "inbound";
|
|
816
|
+
for (const relatedId of relatedIds) if (isInboundRelation) edgesToRemove.push({
|
|
817
|
+
source: relatedId,
|
|
818
|
+
target: id,
|
|
819
|
+
relation: inverseRelation(relation)
|
|
820
|
+
});
|
|
821
|
+
else edgesToRemove.push({
|
|
822
|
+
source: id,
|
|
823
|
+
target: relatedId,
|
|
824
|
+
relation
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
removeEdgesImpl(graph, edgesToRemove);
|
|
828
|
+
}
|
|
829
|
+
internal._onRemoveNode?.(id);
|
|
830
|
+
return graph;
|
|
831
|
+
};
|
|
832
|
+
function removeNode(graphOrId, idOrEdges, edges) {
|
|
833
|
+
if (typeof graphOrId === "string") {
|
|
834
|
+
const id = graphOrId;
|
|
835
|
+
const edgesArg = typeof idOrEdges === "boolean" ? idOrEdges : false;
|
|
836
|
+
return (graph) => removeNodeImpl(graph, id, edgesArg);
|
|
837
|
+
} else return removeNodeImpl(graphOrId, idOrEdges, edges ?? false);
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Implementation helper for addEdges.
|
|
841
|
+
*/
|
|
842
|
+
var addEdgesImpl = (graph, edges) => {
|
|
843
|
+
Atom.batch(() => {
|
|
844
|
+
edges.map((edge) => addEdgeImpl(graph, edge));
|
|
845
|
+
});
|
|
846
|
+
return graph;
|
|
847
|
+
};
|
|
848
|
+
function addEdges(graphOrEdges, edges) {
|
|
849
|
+
if (edges === void 0) {
|
|
850
|
+
const edges = graphOrEdges;
|
|
851
|
+
return (graph) => addEdgesImpl(graph, edges);
|
|
852
|
+
} else return addEdgesImpl(graphOrEdges, edges);
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Implementation helper for addEdge.
|
|
856
|
+
*/
|
|
857
|
+
var addEdgeImpl = (graph, edgeArg) => {
|
|
858
|
+
const relation = normalizeRelation(edgeArg.relation);
|
|
859
|
+
const relationId = relationKey(relation);
|
|
860
|
+
const inverseId = relationKey(inverseRelation(relation));
|
|
861
|
+
const internal = getInternal(graph);
|
|
862
|
+
const sourceAtom = internal._edges(edgeArg.source);
|
|
863
|
+
const source = internal._registry.get(sourceAtom);
|
|
864
|
+
const sourceList = source[relationId] ?? [];
|
|
865
|
+
if (!sourceList.includes(edgeArg.target)) {
|
|
866
|
+
log("add edge", {
|
|
867
|
+
source: edgeArg.source,
|
|
868
|
+
target: edgeArg.target,
|
|
869
|
+
relation: relationId
|
|
870
|
+
}, {
|
|
871
|
+
"~LogMeta": "~LogMeta",
|
|
872
|
+
F: __dxlog_file$1,
|
|
873
|
+
L: 1109,
|
|
874
|
+
S: void 0
|
|
875
|
+
});
|
|
876
|
+
internal._registry.set(sourceAtom, {
|
|
877
|
+
...source,
|
|
878
|
+
[relationId]: [...sourceList, edgeArg.target]
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
const targetAtom = internal._edges(edgeArg.target);
|
|
882
|
+
const target = internal._registry.get(targetAtom);
|
|
883
|
+
const targetList = target[inverseId] ?? [];
|
|
884
|
+
if (!targetList.includes(edgeArg.source)) {
|
|
885
|
+
log("add inverse edge", {
|
|
886
|
+
source: edgeArg.source,
|
|
887
|
+
target: edgeArg.target,
|
|
888
|
+
relation: inverseId
|
|
889
|
+
}, {
|
|
890
|
+
"~LogMeta": "~LogMeta",
|
|
891
|
+
F: __dxlog_file$1,
|
|
892
|
+
L: 1117,
|
|
893
|
+
S: void 0
|
|
894
|
+
});
|
|
895
|
+
internal._registry.set(targetAtom, {
|
|
896
|
+
...target,
|
|
897
|
+
[inverseId]: [...targetList, edgeArg.source]
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
return graph;
|
|
901
|
+
};
|
|
902
|
+
function addEdge(graphOrEdgeArg, edgeArg) {
|
|
903
|
+
if (edgeArg === void 0) {
|
|
904
|
+
const edgeArg = graphOrEdgeArg;
|
|
905
|
+
return (graph) => addEdgeImpl(graph, edgeArg);
|
|
906
|
+
} else return addEdgeImpl(graphOrEdgeArg, edgeArg);
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Implementation helper for removeEdges.
|
|
910
|
+
*/
|
|
911
|
+
var removeEdgesImpl = (graph, edges, removeOrphans = false) => {
|
|
912
|
+
Atom.batch(() => {
|
|
913
|
+
edges.map((edge) => removeEdgeImpl(graph, edge, removeOrphans));
|
|
914
|
+
});
|
|
915
|
+
return graph;
|
|
916
|
+
};
|
|
917
|
+
function removeEdges(graphOrEdges, edgesOrRemoveOrphans, removeOrphans) {
|
|
918
|
+
if (Array.isArray(graphOrEdges)) {
|
|
919
|
+
const edges = graphOrEdges;
|
|
920
|
+
const removeOrphansArg = typeof edgesOrRemoveOrphans === "boolean" ? edgesOrRemoveOrphans : false;
|
|
921
|
+
return (graph) => removeEdgesImpl(graph, edges, removeOrphansArg);
|
|
922
|
+
} else return removeEdgesImpl(graphOrEdges, edgesOrRemoveOrphans, removeOrphans ?? false);
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Implementation helper for removeEdge.
|
|
926
|
+
*/
|
|
927
|
+
var removeEdgeImpl = (graph, edgeArg, removeOrphans = false) => {
|
|
928
|
+
const relation = normalizeRelation(edgeArg.relation);
|
|
929
|
+
const relationId = relationKey(relation);
|
|
930
|
+
const inverseId = relationKey(inverseRelation(relation));
|
|
931
|
+
const internal = getInternal(graph);
|
|
932
|
+
const sourceAtom = internal._edges(edgeArg.source);
|
|
933
|
+
const source = internal._registry.get(sourceAtom);
|
|
934
|
+
const sourceList = source[relationId] ?? [];
|
|
935
|
+
if (sourceList.includes(edgeArg.target)) internal._registry.set(sourceAtom, {
|
|
936
|
+
...source,
|
|
937
|
+
[relationId]: sourceList.filter((id) => id !== edgeArg.target)
|
|
938
|
+
});
|
|
939
|
+
const targetAtom = internal._edges(edgeArg.target);
|
|
940
|
+
const target = internal._registry.get(targetAtom);
|
|
941
|
+
const targetList = target[inverseId] ?? [];
|
|
942
|
+
if (targetList.includes(edgeArg.source)) internal._registry.set(targetAtom, {
|
|
943
|
+
...target,
|
|
944
|
+
[inverseId]: targetList.filter((id) => id !== edgeArg.source)
|
|
945
|
+
});
|
|
946
|
+
if (removeOrphans) {
|
|
947
|
+
const sourceAfter = internal._registry.get(sourceAtom);
|
|
948
|
+
const targetAfter = internal._registry.get(targetAtom);
|
|
949
|
+
const isEmpty = (edges) => Object.values(edges).every((ids) => ids.length === 0);
|
|
950
|
+
if (isEmpty(sourceAfter) && edgeArg.source !== "root") removeNodesImpl(graph, [edgeArg.source]);
|
|
951
|
+
if (isEmpty(targetAfter) && edgeArg.target !== "root") removeNodesImpl(graph, [edgeArg.target]);
|
|
952
|
+
}
|
|
953
|
+
return graph;
|
|
954
|
+
};
|
|
955
|
+
function removeEdge(graphOrEdgeArg, edgeArgOrRemoveOrphans, removeOrphans) {
|
|
956
|
+
if (edgeArgOrRemoveOrphans === void 0 || typeof edgeArgOrRemoveOrphans === "boolean" || "source" in graphOrEdgeArg) {
|
|
957
|
+
const edgeArg = graphOrEdgeArg;
|
|
958
|
+
const removeOrphansArg = typeof edgeArgOrRemoveOrphans === "boolean" ? edgeArgOrRemoveOrphans : false;
|
|
959
|
+
return (graph) => removeEdgeImpl(graph, edgeArg, removeOrphansArg);
|
|
960
|
+
} else return removeEdgeImpl(graphOrEdgeArg, edgeArgOrRemoveOrphans, removeOrphans ?? false);
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Creates a new Graph instance.
|
|
964
|
+
*/
|
|
965
|
+
var make$1 = (params) => {
|
|
966
|
+
return new GraphImpl(params);
|
|
967
|
+
};
|
|
968
|
+
var relationKey = (relation) => {
|
|
969
|
+
const normalized = normalizeRelation(relation);
|
|
970
|
+
return secondaryKey(normalized.kind, normalized.direction);
|
|
971
|
+
};
|
|
972
|
+
var relationFromKey = (encoded) => {
|
|
973
|
+
const parts = secondaryParts(encoded);
|
|
974
|
+
invariant(parts.length === 2 && parts[0].length > 0 && parts[1].length > 0, `Invalid relation key: ${encoded}`, {
|
|
975
|
+
"~LogMeta": "~LogMeta",
|
|
976
|
+
F: __dxlog_file$1,
|
|
977
|
+
L: 1262,
|
|
978
|
+
S: void 0,
|
|
979
|
+
A: ["parts.length === 2 && parts[0].length > 0 && parts[1].length > 0", "`Invalid relation key: ${encoded}`"]
|
|
980
|
+
});
|
|
981
|
+
const [kind, directionRaw] = parts;
|
|
982
|
+
invariant(directionRaw === "outbound" || directionRaw === "inbound", `Invalid relation direction: ${directionRaw}`, {
|
|
983
|
+
"~LogMeta": "~LogMeta",
|
|
984
|
+
F: __dxlog_file$1,
|
|
985
|
+
L: 1264,
|
|
986
|
+
S: void 0,
|
|
987
|
+
A: ["directionRaw === 'outbound' || directionRaw === 'inbound'", "`Invalid relation direction: ${directionRaw}`"]
|
|
988
|
+
});
|
|
989
|
+
return relation(kind, directionRaw);
|
|
990
|
+
};
|
|
991
|
+
var connectionKey = (id, relation) => primaryKey(id, relationKey(relation));
|
|
992
|
+
var relationFromConnectionKey = (key) => {
|
|
993
|
+
const [id, encodedRelation] = primaryParts(key);
|
|
994
|
+
invariant(id && encodedRelation, `Invalid connection key: ${key}`, {
|
|
995
|
+
"~LogMeta": "~LogMeta",
|
|
996
|
+
F: __dxlog_file$1,
|
|
997
|
+
L: 1272,
|
|
998
|
+
S: void 0,
|
|
999
|
+
A: ["id && encodedRelation", "`Invalid connection key: ${key}`"]
|
|
1000
|
+
});
|
|
1001
|
+
return {
|
|
1002
|
+
id,
|
|
1003
|
+
relation: relationFromKey(encodedRelation)
|
|
1004
|
+
};
|
|
1005
|
+
};
|
|
1006
|
+
var inverseRelation = (relation$1) => {
|
|
1007
|
+
const normalized = normalizeRelation(relation$1);
|
|
1008
|
+
return relation(normalized.kind, normalized.direction === "outbound" ? "inbound" : "outbound");
|
|
1009
|
+
};
|
|
1010
|
+
//#endregion
|
|
1011
|
+
//#region src/node-matcher.ts
|
|
1012
|
+
var node_matcher_exports = /* @__PURE__ */ __exportAll({
|
|
1013
|
+
whenAll: () => whenAll,
|
|
1014
|
+
whenAny: () => whenAny,
|
|
1015
|
+
whenEchoObject: () => whenEchoObject,
|
|
1016
|
+
whenEchoObjectMatches: () => whenEchoObjectMatches,
|
|
1017
|
+
whenEchoType: () => whenEchoType,
|
|
1018
|
+
whenEchoTypeMatches: () => whenEchoTypeMatches,
|
|
1019
|
+
whenId: () => whenId,
|
|
1020
|
+
whenNodeType: () => whenNodeType,
|
|
1021
|
+
whenNot: () => whenNot,
|
|
1022
|
+
whenRoot: () => whenRoot
|
|
1023
|
+
});
|
|
1024
|
+
/**
|
|
1025
|
+
* Matches the root node of the graph.
|
|
1026
|
+
*
|
|
1027
|
+
* @returns Option.some(node) if the node is the root, Option.none() otherwise.
|
|
1028
|
+
*
|
|
1029
|
+
* @example
|
|
1030
|
+
* ```ts
|
|
1031
|
+
* GraphBuilder.createExtension({
|
|
1032
|
+
* id: 'myExtension',
|
|
1033
|
+
* match: NodeMatcher.whenRoot,
|
|
1034
|
+
* connector: (node) => Effect.succeed([...]),
|
|
1035
|
+
* });
|
|
1036
|
+
* ```
|
|
1037
|
+
*/
|
|
1038
|
+
var whenRoot = (node) => node.id === "root" ? Option.some(node) : Option.none();
|
|
1039
|
+
/**
|
|
1040
|
+
* Matches a node by its exact ID.
|
|
1041
|
+
*
|
|
1042
|
+
* @param id - The node ID to match against.
|
|
1043
|
+
* @returns A matcher that returns Option.some(node) if IDs match, Option.none() otherwise.
|
|
1044
|
+
*
|
|
1045
|
+
* @example
|
|
1046
|
+
* ```ts
|
|
1047
|
+
* GraphBuilder.createExtension({
|
|
1048
|
+
* id: 'spacesExtension',
|
|
1049
|
+
* match: NodeMatcher.whenId('spaces'),
|
|
1050
|
+
* connector: (node) => Effect.succeed([...]),
|
|
1051
|
+
* });
|
|
1052
|
+
* ```
|
|
1053
|
+
*/
|
|
1054
|
+
var whenId = (id) => (node) => node.id === id ? Option.some(node) : Option.none();
|
|
1055
|
+
/**
|
|
1056
|
+
* Matches a node by its type string (the `node.type` property).
|
|
1057
|
+
*
|
|
1058
|
+
* @param type - The node type string to match against.
|
|
1059
|
+
* @returns A matcher that returns Option.some(node) if types match, Option.none() otherwise.
|
|
1060
|
+
*
|
|
1061
|
+
* @example
|
|
1062
|
+
* ```ts
|
|
1063
|
+
* GraphBuilder.createExtension({
|
|
1064
|
+
* id: 'spaceSettingsExtension',
|
|
1065
|
+
* match: NodeMatcher.whenNodeType('org.dxos.plugin.space.settings'),
|
|
1066
|
+
* connector: (node) => Effect.succeed([...]),
|
|
1067
|
+
* });
|
|
1068
|
+
* ```
|
|
1069
|
+
*/
|
|
1070
|
+
var whenNodeType = (type) => (node) => node.type === type ? Option.some(node) : Option.none();
|
|
1071
|
+
/**
|
|
1072
|
+
* Matches a node whose data is an instance of the given ECHO schema type.
|
|
1073
|
+
* Returns the **typed entity data** (not the node) for direct use in callbacks.
|
|
1074
|
+
*
|
|
1075
|
+
* Use this when you need to work directly with the typed ECHO entity in your
|
|
1076
|
+
* connector or actions callback.
|
|
1077
|
+
*
|
|
1078
|
+
* @template T - The ECHO schema type to match against.
|
|
1079
|
+
* @param type - The ECHO schema (e.g., `Collection.Collection`, `Document.Document`).
|
|
1080
|
+
* @returns A matcher that returns Option.some(entity) if the data matches, Option.none() otherwise.
|
|
1081
|
+
*
|
|
1082
|
+
* @example
|
|
1083
|
+
* ```ts
|
|
1084
|
+
* GraphBuilder.createExtension({
|
|
1085
|
+
* id: 'collectionExtension',
|
|
1086
|
+
* match: NodeMatcher.whenEchoType(Collection.Collection),
|
|
1087
|
+
* connector: (collection) => {
|
|
1088
|
+
* // `collection` is typed as Collection.Collection
|
|
1089
|
+
* return Effect.succeed(collection.objects.map(...));
|
|
1090
|
+
* },
|
|
1091
|
+
* });
|
|
1092
|
+
* ```
|
|
1093
|
+
*
|
|
1094
|
+
* Can be composed directly with {@link whenAll}/{@link whenAny}/{@link whenNot} while
|
|
1095
|
+
* preserving the typed entity data in the result.
|
|
1096
|
+
*
|
|
1097
|
+
* @see {@link whenEchoTypeMatches} - Returns the node instead of data for legacy composition.
|
|
1098
|
+
*/
|
|
1099
|
+
var whenEchoType = (type) => (node) => Entity.instanceOf(type, node.data) ? Option.some(node.data) : Option.none();
|
|
1100
|
+
/**
|
|
1101
|
+
* Matches a node whose data is any ECHO object.
|
|
1102
|
+
* Returns the **object data** (not the node) for direct use in callbacks.
|
|
1103
|
+
*
|
|
1104
|
+
* Use this when you need to work with any ECHO object regardless of its specific type.
|
|
1105
|
+
*
|
|
1106
|
+
* @returns Option.some(object) if the node's data is an ECHO object, Option.none() otherwise.
|
|
1107
|
+
*
|
|
1108
|
+
* @example
|
|
1109
|
+
* ```ts
|
|
1110
|
+
* GraphBuilder.createExtension({
|
|
1111
|
+
* id: 'objectProperties',
|
|
1112
|
+
* match: NodeMatcher.whenEchoObject,
|
|
1113
|
+
* connector: (object) => {
|
|
1114
|
+
* // `object` is typed as Obj.Unknown
|
|
1115
|
+
* const id = Obj.getURI(object);
|
|
1116
|
+
* return Effect.succeed([{ id: `${id}.settings`, ... }]);
|
|
1117
|
+
* },
|
|
1118
|
+
* });
|
|
1119
|
+
* ```
|
|
1120
|
+
*
|
|
1121
|
+
* Can be composed directly with {@link whenAll}/{@link whenAny}/{@link whenNot} while
|
|
1122
|
+
* preserving the `Obj.Unknown` data type in the result.
|
|
1123
|
+
*
|
|
1124
|
+
* @see {@link whenEchoObjectMatches} - Returns the node instead of data for legacy composition.
|
|
1125
|
+
*/
|
|
1126
|
+
var whenEchoObject = (node) => Obj.isObject(node.data) ? Option.some(node.data) : Option.none();
|
|
1127
|
+
/**
|
|
1128
|
+
* Composes multiple matchers with AND logic - all matchers must match for success.
|
|
1129
|
+
* The result data type is the intersection of all matchers' data types.
|
|
1130
|
+
* Filter matchers like {@link whenNot} return `unknown`, making them transparent
|
|
1131
|
+
* in the intersection (since `T & unknown = T`).
|
|
1132
|
+
*
|
|
1133
|
+
* @param matchers - The matchers to combine. All must return Option.some for success.
|
|
1134
|
+
* @returns A matcher whose data type is the intersection of all input matchers' data types.
|
|
1135
|
+
* Returns the first matcher's value when all match, Option.none() otherwise.
|
|
1136
|
+
*
|
|
1137
|
+
* @example
|
|
1138
|
+
* ```ts
|
|
1139
|
+
* // Match ECHO objects that are NOT Channels — result is NodeMatcher<Obj.Unknown>.
|
|
1140
|
+
* const whenCommentable = NodeMatcher.whenAll(
|
|
1141
|
+
* NodeMatcher.whenEchoObject,
|
|
1142
|
+
* NodeMatcher.whenNot(NodeMatcher.whenEchoTypeMatches(Channel.Channel)),
|
|
1143
|
+
* );
|
|
1144
|
+
* ```
|
|
1145
|
+
*/
|
|
1146
|
+
var whenAll = (...matchers) => (node, get) => {
|
|
1147
|
+
let first = Option.none();
|
|
1148
|
+
for (const candidate of matchers) {
|
|
1149
|
+
const result = candidate(node, get);
|
|
1150
|
+
if (Option.isNone(result)) return Option.none();
|
|
1151
|
+
if (Option.isNone(first)) first = result;
|
|
1152
|
+
}
|
|
1153
|
+
return first;
|
|
1154
|
+
};
|
|
1155
|
+
/**
|
|
1156
|
+
* Composes multiple matchers with OR logic - at least one matcher must match.
|
|
1157
|
+
* The result data type is the union of all matchers' data types.
|
|
1158
|
+
*
|
|
1159
|
+
* @param matchers - The matchers to combine. At least one must return Option.some.
|
|
1160
|
+
* @returns A matcher whose data type is the union of all input matchers' data types.
|
|
1161
|
+
* Returns the first matching matcher's value, or Option.none() if none match.
|
|
1162
|
+
*
|
|
1163
|
+
* @example
|
|
1164
|
+
* ```ts
|
|
1165
|
+
* // Match nodes that are either Sequences or Routines
|
|
1166
|
+
* const whenInvocable = NodeMatcher.whenAny(
|
|
1167
|
+
* NodeMatcher.whenEchoTypeMatches(Sequence),
|
|
1168
|
+
* NodeMatcher.whenEchoTypeMatches(Routine.Routine),
|
|
1169
|
+
* );
|
|
1170
|
+
* ```
|
|
1171
|
+
*/
|
|
1172
|
+
var whenAny = (...matchers) => (node, get) => {
|
|
1173
|
+
for (const candidate of matchers) {
|
|
1174
|
+
const result = candidate(node, get);
|
|
1175
|
+
if (Option.isSome(result)) return result;
|
|
1176
|
+
}
|
|
1177
|
+
return Option.none();
|
|
1178
|
+
};
|
|
1179
|
+
/**
|
|
1180
|
+
* Matches a node whose data is an instance of the given ECHO schema type.
|
|
1181
|
+
* Returns the **node** (not the data) to enable composition with whenAll/whenAny/whenNot.
|
|
1182
|
+
*
|
|
1183
|
+
* Use this instead of {@link whenEchoType} when you need to combine matchers.
|
|
1184
|
+
* The difference is what's returned:
|
|
1185
|
+
* - `whenEchoType` returns the typed entity (for direct use)
|
|
1186
|
+
* - `whenEchoTypeMatches` returns the node (for composition)
|
|
1187
|
+
*
|
|
1188
|
+
* @template T - The ECHO schema type to match against.
|
|
1189
|
+
* @param type - The ECHO schema (e.g., `Channel.Channel`, `Document.Document`).
|
|
1190
|
+
* @returns A matcher that returns Option.some(node) if the data matches, Option.none() otherwise.
|
|
1191
|
+
*
|
|
1192
|
+
* @example
|
|
1193
|
+
* ```ts
|
|
1194
|
+
* // Use with whenAny for OR logic
|
|
1195
|
+
* const whenPresentable = NodeMatcher.whenAny(
|
|
1196
|
+
* NodeMatcher.whenEchoTypeMatches(Collection.Collection),
|
|
1197
|
+
* NodeMatcher.whenEchoTypeMatches(Markdown.Document),
|
|
1198
|
+
* );
|
|
1199
|
+
*
|
|
1200
|
+
* // Use with whenNot for exclusion
|
|
1201
|
+
* const whenNotChannel = NodeMatcher.whenNot(
|
|
1202
|
+
* NodeMatcher.whenEchoTypeMatches(Channel.Channel),
|
|
1203
|
+
* );
|
|
1204
|
+
* ```
|
|
1205
|
+
*
|
|
1206
|
+
* @see {@link whenEchoType} - Use instead when you need the typed entity directly.
|
|
1207
|
+
*/
|
|
1208
|
+
var whenEchoTypeMatches = (type) => (node) => Entity.instanceOf(type, node.data) ? Option.some(node) : Option.none();
|
|
1209
|
+
/**
|
|
1210
|
+
* Matches a node whose data is any ECHO object.
|
|
1211
|
+
* Returns the **node** (not the data) to enable composition with whenAll/whenAny/whenNot.
|
|
1212
|
+
*
|
|
1213
|
+
* Use this instead of {@link whenEchoObject} when you need to combine matchers.
|
|
1214
|
+
* The difference is what's returned:
|
|
1215
|
+
* - `whenEchoObject` returns the object data (for direct use)
|
|
1216
|
+
* - `whenEchoObjectMatches` returns the node (for composition)
|
|
1217
|
+
*
|
|
1218
|
+
* @returns Option.some(node) if the node's data is an ECHO object, Option.none() otherwise.
|
|
1219
|
+
*
|
|
1220
|
+
* @example
|
|
1221
|
+
* ```ts
|
|
1222
|
+
* // Match ECHO objects that are not system types
|
|
1223
|
+
* const whenUserObject = NodeMatcher.whenAll(
|
|
1224
|
+
* NodeMatcher.whenEchoObjectMatches,
|
|
1225
|
+
* NodeMatcher.whenNot(NodeMatcher.whenEchoTypeMatches(SystemType)),
|
|
1226
|
+
* );
|
|
1227
|
+
* ```
|
|
1228
|
+
*
|
|
1229
|
+
* @see {@link whenEchoObject} - Use instead when you need the object data directly.
|
|
1230
|
+
*/
|
|
1231
|
+
var whenEchoObjectMatches = (node) => Obj.isObject(node.data) ? Option.some(node) : Option.none();
|
|
1232
|
+
/**
|
|
1233
|
+
* Negates a matcher - matches when the given matcher does NOT match.
|
|
1234
|
+
* Useful for exclusion patterns like "any object EXCEPT type X".
|
|
1235
|
+
*
|
|
1236
|
+
* Returns `NodeMatcher<unknown>` because negation is a filter — it doesn't provide
|
|
1237
|
+
* typed data. This makes it transparent in {@link whenAll} intersections
|
|
1238
|
+
* (since `T & unknown = T`).
|
|
1239
|
+
*
|
|
1240
|
+
* @param matcher - The matcher to negate.
|
|
1241
|
+
* @returns A matcher that returns Option.some(node) if the input matcher returns none,
|
|
1242
|
+
* and Option.none() if the input matcher returns some.
|
|
1243
|
+
*
|
|
1244
|
+
* @example
|
|
1245
|
+
* ```ts
|
|
1246
|
+
* // Match any ECHO object that is NOT a Channel — result is NodeMatcher<Obj.Unknown>.
|
|
1247
|
+
* const whenCommentable = NodeMatcher.whenAll(
|
|
1248
|
+
* NodeMatcher.whenEchoObject,
|
|
1249
|
+
* NodeMatcher.whenNot(NodeMatcher.whenEchoTypeMatches(Channel.Channel)),
|
|
1250
|
+
* );
|
|
1251
|
+
*
|
|
1252
|
+
* // Match any node that is NOT the root
|
|
1253
|
+
* const whenNotRoot = NodeMatcher.whenNot(NodeMatcher.whenRoot);
|
|
1254
|
+
* ```
|
|
1255
|
+
*/
|
|
1256
|
+
var whenNot = (matcher) => (node, get) => Option.isNone(matcher(node, get)) ? Option.some(node) : Option.none();
|
|
1257
|
+
//#endregion
|
|
1258
|
+
//#region src/graph-builder.ts
|
|
1259
|
+
var graph_builder_exports = /* @__PURE__ */ __exportAll({
|
|
1260
|
+
GraphBuilderTypeId: () => GraphBuilderTypeId,
|
|
1261
|
+
addExtension: () => addExtension,
|
|
1262
|
+
createConnector: () => createConnector,
|
|
1263
|
+
createExtension: () => createExtension,
|
|
1264
|
+
createExtensionRaw: () => createExtensionRaw,
|
|
1265
|
+
createTypeExtension: () => createTypeExtension,
|
|
1266
|
+
destroy: () => destroy,
|
|
1267
|
+
explore: () => explore,
|
|
1268
|
+
flattenExtensions: () => flattenExtensions,
|
|
1269
|
+
flush: () => flush,
|
|
1270
|
+
from: () => from,
|
|
1271
|
+
make: () => make,
|
|
1272
|
+
nodeUrlSegment: () => nodeUrlSegment,
|
|
1273
|
+
removeExtension: () => removeExtension,
|
|
1274
|
+
urlRepresentation: () => urlRepresentation
|
|
1275
|
+
});
|
|
1276
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/sdk/app-graph/src/graph-builder.ts";
|
|
1277
|
+
/** Default linked-segment prefix; mirrors `@dxos/react-ui-attention`'s `linkedSegment`. Internal: read
|
|
1278
|
+
* the resolved value from `builder.urlGrammar` rather than the default. */
|
|
1279
|
+
var DEFAULT_LINKED_PREFIX = "~";
|
|
1280
|
+
/** Default tail separator; never appears in an entity id or a type slug. Internal, as above. */
|
|
1281
|
+
var DEFAULT_TAIL_SEPARATOR = "+";
|
|
1282
|
+
/**
|
|
1283
|
+
* The `(key, id?)` URL representation of a node under a given {@link UrlBinding} — the reverse of forward
|
|
1284
|
+
* resolution, minus the workspace (always the node id's second segment). A singleton has no id; a
|
|
1285
|
+
* resolver-backed key keeps just the object id; a static path encodes the segments between the path and
|
|
1286
|
+
* the id, `+`-joined (empty when the node sits at the path — a container whose children are the items).
|
|
1287
|
+
*/
|
|
1288
|
+
var urlRepresentation = (nodeId, url, tailSeparator = DEFAULT_TAIL_SEPARATOR) => {
|
|
1289
|
+
if (url.kind === "singleton") return { key: url.key };
|
|
1290
|
+
const segments = nodeId.split("/");
|
|
1291
|
+
const id = typeof url.path === "function" ? segments[segments.length - 1] : segments.slice(2 + url.path.length).join(tailSeparator);
|
|
1292
|
+
return {
|
|
1293
|
+
key: url.key,
|
|
1294
|
+
id
|
|
1295
|
+
};
|
|
1296
|
+
};
|
|
1297
|
+
/**
|
|
1298
|
+
* A node's own URL pair segment — `/<key>[/<id>]`, with no workspace/anchor prefix — or `undefined` when
|
|
1299
|
+
* the node is not addressable in its own right (a container node sitting at the binding's `path`, whose
|
|
1300
|
+
* children are the addressable items). A full URL is composed by prefixing `/w/<workspace>`.
|
|
1301
|
+
*/
|
|
1302
|
+
var nodeUrlSegment = (nodeId, url, tailSeparator = DEFAULT_TAIL_SEPARATOR) => {
|
|
1303
|
+
const { key, id } = urlRepresentation(nodeId, url, tailSeparator);
|
|
1304
|
+
if (id === void 0) return `/${key}`;
|
|
1305
|
+
return id === "" ? void 0 : `/${key}/${id}`;
|
|
1306
|
+
};
|
|
1307
|
+
/**
|
|
1308
|
+
* Return a copy of `node` (and its inline descendants) with `properties.urlSegment` stamped. A linked
|
|
1309
|
+
* node (id ending in a `~<variant>` segment) is stamped from the `linked` tier key, independent of its
|
|
1310
|
+
* producing extension's binding; any other node is stamped from `url` (its producer's binding), if any.
|
|
1311
|
+
*/
|
|
1312
|
+
var stampUrlSegment = (node, url, grammar) => {
|
|
1313
|
+
const lastSegment = node.id.slice(node.id.lastIndexOf("/") + 1);
|
|
1314
|
+
const segment = lastSegment.startsWith(grammar.linkedPrefix) ? grammar.linkedKey && `/${grammar.linkedKey}/${lastSegment.slice(grammar.linkedPrefix.length)}` : url && nodeUrlSegment(node.id, url, grammar.tailSeparator);
|
|
1315
|
+
const nodes = node.nodes?.map((child) => stampUrlSegment(child, url, grammar));
|
|
1316
|
+
if (!segment && !nodes) return node;
|
|
1317
|
+
return {
|
|
1318
|
+
...node,
|
|
1319
|
+
...segment && { properties: {
|
|
1320
|
+
...node.properties,
|
|
1321
|
+
urlSegment: segment
|
|
1322
|
+
} },
|
|
1323
|
+
...nodes && { nodes }
|
|
1324
|
+
};
|
|
1325
|
+
};
|
|
1326
|
+
/**
|
|
1327
|
+
* Identifier denoting a GraphBuilder.
|
|
1328
|
+
*/
|
|
1329
|
+
var GraphBuilderTypeId = Symbol.for("@dxos/app-graph/GraphBuilder");
|
|
1330
|
+
/**
|
|
1331
|
+
* The builder provides an extensible way to compose the construction of the graph.
|
|
1332
|
+
* @internal
|
|
1333
|
+
*/
|
|
1334
|
+
var GraphBuilderImpl = class {
|
|
1335
|
+
[GraphBuilderTypeId] = GraphBuilderTypeId;
|
|
1336
|
+
pipe() {
|
|
1337
|
+
return Pipeable.pipeArguments(this, arguments);
|
|
1338
|
+
}
|
|
1339
|
+
/** Active subscriptions keyed by composite ID, cleaned up on node removal. */
|
|
1340
|
+
_subscriptions = /* @__PURE__ */ new Map();
|
|
1341
|
+
/** Connector updates pending flush, keyed by connector key. */
|
|
1342
|
+
_dirtyConnectors = /* @__PURE__ */ new Map();
|
|
1343
|
+
/** Last-flushed node IDs per connector key, used for edge removal on update. */
|
|
1344
|
+
_connectorPrevious = /* @__PURE__ */ new Map();
|
|
1345
|
+
/** All inline-descendant IDs per connector key, used to remove stale inline nodes on update. */
|
|
1346
|
+
_connectorPreviousInlineIds = /* @__PURE__ */ new Map();
|
|
1347
|
+
/** Last-flushed node args per connector key, used for change detection. */
|
|
1348
|
+
_connectorPreviousArgs = /* @__PURE__ */ new Map();
|
|
1349
|
+
/** Whether a dirty-flush task is already scheduled. */
|
|
1350
|
+
_flushScheduled = false;
|
|
1351
|
+
/** Resolves when the current flush completes. */
|
|
1352
|
+
_flushPromise = Promise.resolve();
|
|
1353
|
+
/** Registered builder extensions keyed by extension ID. */
|
|
1354
|
+
_extensions = Atom.make(Record.empty()).pipe(Atom.keepAlive, Atom.withLabel("graph-builder:extensions"));
|
|
1355
|
+
/**
|
|
1356
|
+
* Node id -> id of the extension whose connector produced it. Non-reactive: updated directly
|
|
1357
|
+
* as connectors materialize/remove nodes, so reverse (node → URL) mapping in
|
|
1358
|
+
* `path-resolution.ts` can look up the producing extension without a reactive read.
|
|
1359
|
+
*/
|
|
1360
|
+
_nodeExtensions = /* @__PURE__ */ new Map();
|
|
1361
|
+
/** Triggers signalling that a node's resolver has fired at least once. */
|
|
1362
|
+
_initialized = {};
|
|
1363
|
+
/** The URL grammar (see {@link UrlGrammar}); the keys are absent when URLs are not in play. */
|
|
1364
|
+
urlGrammar;
|
|
1365
|
+
/** Shared atom registry for reactive subscriptions. */
|
|
1366
|
+
_registry;
|
|
1367
|
+
/** Backing graph with internal accessors for node atoms and construction. */
|
|
1368
|
+
_graph;
|
|
1369
|
+
constructor({ registry, urlGrammar, ...params } = {}) {
|
|
1370
|
+
this.urlGrammar = {
|
|
1371
|
+
linkedPrefix: DEFAULT_LINKED_PREFIX,
|
|
1372
|
+
tailSeparator: DEFAULT_TAIL_SEPARATOR,
|
|
1373
|
+
...urlGrammar
|
|
1374
|
+
};
|
|
1375
|
+
this._registry = registry ?? Registry.make();
|
|
1376
|
+
const graph = make$1({
|
|
1377
|
+
...params,
|
|
1378
|
+
registry: this._registry,
|
|
1379
|
+
onExpand: (id, relation) => this._onExpand(id, relation),
|
|
1380
|
+
onInitialize: (id) => this._onInitialize(id),
|
|
1381
|
+
onRemoveNode: (id) => this._onRemoveNode(id)
|
|
1382
|
+
});
|
|
1383
|
+
this._graph = graph;
|
|
1384
|
+
}
|
|
1385
|
+
get graph() {
|
|
1386
|
+
return this._graph;
|
|
1387
|
+
}
|
|
1388
|
+
get extensions() {
|
|
1389
|
+
return this._extensions;
|
|
1390
|
+
}
|
|
1391
|
+
getExtensions() {
|
|
1392
|
+
return this._registry.get(this._extensions);
|
|
1393
|
+
}
|
|
1394
|
+
getNodeExtensionId(nodeId) {
|
|
1395
|
+
return this._nodeExtensions.get(nodeId);
|
|
1396
|
+
}
|
|
1397
|
+
/** Record `extensionId` as the producer of a qualified node and all of its inline `nodes` descendants. */
|
|
1398
|
+
_recordProvenance(node, extensionId) {
|
|
1399
|
+
this._nodeExtensions.set(node.id, extensionId);
|
|
1400
|
+
for (const child of node.nodes ?? []) this._recordProvenance(child, extensionId);
|
|
1401
|
+
}
|
|
1402
|
+
/** Apply a set of node changes for a single connector key. */
|
|
1403
|
+
_applyConnectorUpdate(key, nodes, previous) {
|
|
1404
|
+
const { id, relation } = relationFromConnectorKey(key);
|
|
1405
|
+
const ids = nodes.map((node) => node.id);
|
|
1406
|
+
const removed = previous.filter((pid) => !ids.includes(pid));
|
|
1407
|
+
this._connectorPrevious.set(key, ids);
|
|
1408
|
+
this._connectorPreviousArgs.set(key, nodes);
|
|
1409
|
+
const currentInlineIds = collectAllInlineIds(nodes);
|
|
1410
|
+
const staleInlineIds = (this._connectorPreviousInlineIds.get(key) ?? []).filter((pid) => !currentInlineIds.includes(pid));
|
|
1411
|
+
this._connectorPreviousInlineIds.set(key, currentInlineIds);
|
|
1412
|
+
removeNodes(this._graph, staleInlineIds, true);
|
|
1413
|
+
removeEdges(this._graph, removed.map((target) => ({
|
|
1414
|
+
source: id,
|
|
1415
|
+
target,
|
|
1416
|
+
relation
|
|
1417
|
+
})), true);
|
|
1418
|
+
addNodes(this._graph, nodes);
|
|
1419
|
+
addEdges(this._graph, nodes.map((node) => ({
|
|
1420
|
+
source: id,
|
|
1421
|
+
target: node.id,
|
|
1422
|
+
relation
|
|
1423
|
+
})));
|
|
1424
|
+
if (ids.length > 0) {
|
|
1425
|
+
const sortedIds = [...nodes].sort((a, b) => Position.compare({ position: a.properties?.position }, { position: b.properties?.position })).map((n) => n.id);
|
|
1426
|
+
sortEdges(this._graph, id, relation, sortedIds);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
_scheduleDirtyFlush() {
|
|
1430
|
+
if (!this._flushScheduled) {
|
|
1431
|
+
this._flushScheduled = true;
|
|
1432
|
+
this._flushPromise = scheduleTask(() => {
|
|
1433
|
+
this._flushScheduled = false;
|
|
1434
|
+
while (this._dirtyConnectors.size > 0) {
|
|
1435
|
+
const entries = [...this._dirtyConnectors.entries()];
|
|
1436
|
+
this._dirtyConnectors.clear();
|
|
1437
|
+
Atom.batch(() => {
|
|
1438
|
+
for (const [key, { nodes, previous }] of entries) this._applyConnectorUpdate(key, nodes, previous);
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
}, { strategy: "smooth" });
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
/** A connector-produced node, tagged with the id of the extension that produced it (provenance). */
|
|
1445
|
+
_resolvers = Atom.family((id) => {
|
|
1446
|
+
return Atom.make((get) => {
|
|
1447
|
+
return Function.pipe(get(this._extensions), Record.values, Array$1.sortBy(Position.compare), Array$1.map(({ resolver }) => resolver), Array$1.filter(isNonNullable), Array$1.map((resolver) => get(resolver(id))), Array$1.filter(isNonNullable), Array$1.head);
|
|
1448
|
+
});
|
|
1449
|
+
});
|
|
1450
|
+
_connectors = Atom.family((key) => {
|
|
1451
|
+
return Atom.make((get) => {
|
|
1452
|
+
const { id, relation } = relationFromConnectorKey(key);
|
|
1453
|
+
const node = this._graph.node(id);
|
|
1454
|
+
if (!Option.getOrElse(get(node), () => void 0)) return [];
|
|
1455
|
+
const extensions = Function.pipe(get(this._extensions), Record.values, Array$1.sortBy(Position.compare), Array$1.filter((ext) => relationKey(ext.relation ?? "child") === relationKey(relation) && ext.connector != null));
|
|
1456
|
+
const entries = [];
|
|
1457
|
+
for (const ext of extensions) {
|
|
1458
|
+
const result = get(ext.connector(node));
|
|
1459
|
+
for (const nodeArg of result) entries.push({
|
|
1460
|
+
extensionId: ext.id,
|
|
1461
|
+
node: nodeArg
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
return entries;
|
|
1465
|
+
}).pipe(Atom.withLabel(`graph-builder:connectors:${key}`));
|
|
1466
|
+
});
|
|
1467
|
+
_onExpand(id, relation) {
|
|
1468
|
+
log("onExpand", {
|
|
1469
|
+
id,
|
|
1470
|
+
relation,
|
|
1471
|
+
registry: getDebugName(this._registry)
|
|
1472
|
+
}, {
|
|
1473
|
+
"~LogMeta": "~LogMeta",
|
|
1474
|
+
F: __dxlog_file,
|
|
1475
|
+
L: 473,
|
|
1476
|
+
S: this
|
|
1477
|
+
});
|
|
1478
|
+
this._expandRelation(id, relation);
|
|
1479
|
+
if (relation.kind === "child" && relation.direction === "outbound") expand(this._graph, id, "action");
|
|
1480
|
+
}
|
|
1481
|
+
_expandRelation(id, relation) {
|
|
1482
|
+
const key = connectorKey(id, relation);
|
|
1483
|
+
const connectors = this._connectors(key);
|
|
1484
|
+
const cancel = this._registry.subscribe(connectors, (entries) => {
|
|
1485
|
+
const extensions = this.getExtensions();
|
|
1486
|
+
const grammar = this.urlGrammar;
|
|
1487
|
+
const nodes = qualifyNodeArgs(id)(entries.map((entry) => entry.node)).map((node, index) => stampUrlSegment(node, extensions[entries[index].extensionId]?.url, grammar));
|
|
1488
|
+
entries.forEach((entry, index) => {
|
|
1489
|
+
this._recordProvenance(nodes[index], entry.extensionId);
|
|
1490
|
+
});
|
|
1491
|
+
const previous = this._connectorPrevious.get(key) ?? [];
|
|
1492
|
+
const ids = nodes.map((n) => n.id);
|
|
1493
|
+
if (ids.length === previous.length && ids.every((nodeId, idx) => nodeId === previous[idx])) {
|
|
1494
|
+
const prevArgs = this._connectorPreviousArgs.get(key);
|
|
1495
|
+
if (prevArgs && nodeArgsUnchanged(prevArgs, nodes)) return;
|
|
1496
|
+
}
|
|
1497
|
+
log("update", {
|
|
1498
|
+
id,
|
|
1499
|
+
relation,
|
|
1500
|
+
ids
|
|
1501
|
+
}, {
|
|
1502
|
+
"~LogMeta": "~LogMeta",
|
|
1503
|
+
F: __dxlog_file,
|
|
1504
|
+
L: 515,
|
|
1505
|
+
S: this
|
|
1506
|
+
});
|
|
1507
|
+
this._dirtyConnectors.set(key, {
|
|
1508
|
+
nodes,
|
|
1509
|
+
previous
|
|
1510
|
+
});
|
|
1511
|
+
this._scheduleDirtyFlush();
|
|
1512
|
+
}, { immediate: true });
|
|
1513
|
+
this._subscriptions.set(subscriptionKey(id, "expand", key), cancel);
|
|
1514
|
+
}
|
|
1515
|
+
async _onInitialize(id) {
|
|
1516
|
+
log("onInitialize", { id }, {
|
|
1517
|
+
"~LogMeta": "~LogMeta",
|
|
1518
|
+
F: __dxlog_file,
|
|
1519
|
+
L: 526,
|
|
1520
|
+
S: this
|
|
1521
|
+
});
|
|
1522
|
+
const resolver = this._resolvers(id);
|
|
1523
|
+
const cancel = this._registry.subscribe(resolver, (node) => {
|
|
1524
|
+
const trigger = this._initialized[id];
|
|
1525
|
+
const connectorOwned = [...this._connectorPrevious.values()].some((ids) => ids.includes(id));
|
|
1526
|
+
Option.match(node, {
|
|
1527
|
+
onSome: (node) => {
|
|
1528
|
+
if (!connectorOwned) {
|
|
1529
|
+
addNodes(this._graph, [node]);
|
|
1530
|
+
const parentId = getParentId(id);
|
|
1531
|
+
if (parentId) addEdges(this._graph, [{
|
|
1532
|
+
source: parentId,
|
|
1533
|
+
target: id,
|
|
1534
|
+
relation: "child"
|
|
1535
|
+
}]);
|
|
1536
|
+
}
|
|
1537
|
+
trigger?.wake();
|
|
1538
|
+
},
|
|
1539
|
+
onNone: () => {
|
|
1540
|
+
trigger?.wake();
|
|
1541
|
+
if (!connectorOwned) removeNodes(this._graph, [id]);
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
}, { immediate: true });
|
|
1545
|
+
this._subscriptions.set(subscriptionKey(id, "init"), cancel);
|
|
1546
|
+
}
|
|
1547
|
+
_onRemoveNode(id) {
|
|
1548
|
+
this._nodeExtensions.delete(id);
|
|
1549
|
+
for (const [key, cleanup] of this._subscriptions) if (primaryParts(key)[0] === id) {
|
|
1550
|
+
cleanup();
|
|
1551
|
+
this._subscriptions.delete(key);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
/**
|
|
1556
|
+
* Creates a new GraphBuilder instance.
|
|
1557
|
+
*/
|
|
1558
|
+
var make = (params) => {
|
|
1559
|
+
return new GraphBuilderImpl(params);
|
|
1560
|
+
};
|
|
1561
|
+
/**
|
|
1562
|
+
* Creates a GraphBuilder from a serialized pickle string.
|
|
1563
|
+
*/
|
|
1564
|
+
var from = (pickle, registry, urlGrammar) => {
|
|
1565
|
+
if (!pickle) return make({
|
|
1566
|
+
registry,
|
|
1567
|
+
urlGrammar
|
|
1568
|
+
});
|
|
1569
|
+
const { nodes, edges } = JSON.parse(pickle);
|
|
1570
|
+
return make({
|
|
1571
|
+
nodes,
|
|
1572
|
+
edges,
|
|
1573
|
+
registry,
|
|
1574
|
+
urlGrammar
|
|
1575
|
+
});
|
|
1576
|
+
};
|
|
1577
|
+
/**
|
|
1578
|
+
* Implementation helper for addExtension.
|
|
1579
|
+
*/
|
|
1580
|
+
var addExtensionImpl = (builder, extensions) => {
|
|
1581
|
+
const internal = builder;
|
|
1582
|
+
flattenExtensions(extensions).forEach((extension) => {
|
|
1583
|
+
const extensions = internal._registry.get(internal._extensions);
|
|
1584
|
+
internal._registry.set(internal._extensions, Record.set(extensions, extension.id, extension));
|
|
1585
|
+
});
|
|
1586
|
+
return builder;
|
|
1587
|
+
};
|
|
1588
|
+
function addExtension(builderOrExtensions, extensions) {
|
|
1589
|
+
if (extensions === void 0) {
|
|
1590
|
+
const extensions = builderOrExtensions;
|
|
1591
|
+
return (builder) => addExtensionImpl(builder, extensions);
|
|
1592
|
+
} else return addExtensionImpl(builderOrExtensions, extensions);
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Implementation helper for removeExtension.
|
|
1596
|
+
*/
|
|
1597
|
+
var removeExtensionImpl = (builder, id) => {
|
|
1598
|
+
const internal = builder;
|
|
1599
|
+
const extensions = internal._registry.get(internal._extensions);
|
|
1600
|
+
internal._registry.set(internal._extensions, Record.remove(extensions, id));
|
|
1601
|
+
return builder;
|
|
1602
|
+
};
|
|
1603
|
+
function removeExtension(builderOrId, id) {
|
|
1604
|
+
if (typeof builderOrId === "string") {
|
|
1605
|
+
const id = builderOrId;
|
|
1606
|
+
return (builder) => removeExtensionImpl(builder, id);
|
|
1607
|
+
} else return removeExtensionImpl(builderOrId, id);
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Implementation helper for explore.
|
|
1611
|
+
*/
|
|
1612
|
+
var exploreImpl = async (builder, options, path = []) => {
|
|
1613
|
+
const internal = builder;
|
|
1614
|
+
const { registry = Registry.make(), source = RootId, relation, visitor } = options;
|
|
1615
|
+
if (path.includes(source)) return;
|
|
1616
|
+
await yieldOrContinue("idle");
|
|
1617
|
+
const node = registry.get(internal._graph.nodeOrThrow(source));
|
|
1618
|
+
if (await visitor(node, [...path, node.id]) === false) return;
|
|
1619
|
+
const nodes = Function.pipe(internal._registry.get(internal._extensions), Record.values, Array$1.map((extension) => extension.connector), Array$1.filter(isNonNullable), Array$1.flatMap((connector) => registry.get(connector(internal._graph.node(source)))), qualifyNodeArgs(source));
|
|
1620
|
+
await Promise.all(nodes.map((nodeArg) => {
|
|
1621
|
+
registry.set(internal._graph._node(nodeArg.id), internal._graph._constructNode(nodeArg));
|
|
1622
|
+
return exploreImpl(builder, {
|
|
1623
|
+
registry,
|
|
1624
|
+
source: nodeArg.id,
|
|
1625
|
+
relation,
|
|
1626
|
+
visitor
|
|
1627
|
+
}, [...path, node.id]);
|
|
1628
|
+
}));
|
|
1629
|
+
if (registry !== internal._registry) {
|
|
1630
|
+
registry.reset();
|
|
1631
|
+
registry.dispose();
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
function explore(builderOrOptions, optionsOrPath, path) {
|
|
1635
|
+
if (typeof builderOrOptions === "object" && "visitor" in builderOrOptions) {
|
|
1636
|
+
const options = builderOrOptions;
|
|
1637
|
+
const path = Array$1.isArray(optionsOrPath) ? optionsOrPath : void 0;
|
|
1638
|
+
return (builder) => exploreImpl(builder, options, path);
|
|
1639
|
+
} else return exploreImpl(builderOrOptions, optionsOrPath, path ?? (Array$1.isArray(optionsOrPath) ? optionsOrPath : void 0));
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Implementation helper for destroy.
|
|
1643
|
+
*/
|
|
1644
|
+
var destroyImpl = (builder) => {
|
|
1645
|
+
const internal = builder;
|
|
1646
|
+
internal._subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
1647
|
+
internal._subscriptions.clear();
|
|
1648
|
+
};
|
|
1649
|
+
function destroy(builder) {
|
|
1650
|
+
if (builder === void 0) return (builder) => destroyImpl(builder);
|
|
1651
|
+
else return destroyImpl(builder);
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Wait for all pending connector updates to be flushed.
|
|
1655
|
+
*/
|
|
1656
|
+
var flush = (builder) => {
|
|
1657
|
+
return builder._flushPromise;
|
|
1658
|
+
};
|
|
1659
|
+
/**
|
|
1660
|
+
* Whether a graph extension local ID follows NSID conventions: the final
|
|
1661
|
+
* dot-separated segment must be camelCase (letters and digits only, starting
|
|
1662
|
+
* with a letter — no hyphens or underscores). This mirrors the rule enforced
|
|
1663
|
+
* when the id is appended to a plugin's NSID to form a full DXN path.
|
|
1664
|
+
*
|
|
1665
|
+
* An extension with an invalid id is dropped rather than rejected, so a single
|
|
1666
|
+
* malformed contribution cannot crash plugin activation.
|
|
1667
|
+
*
|
|
1668
|
+
* @example Valid: 'about', 'devtools', 'integrationsSection'
|
|
1669
|
+
* @example Invalid: 'integration-article', 'plugin-spec'
|
|
1670
|
+
*/
|
|
1671
|
+
var isValidLocalId = (id) => /^[a-zA-Z][a-zA-Z0-9]*$/.test(id.split(".").pop() ?? "");
|
|
1672
|
+
/**
|
|
1673
|
+
* Create a graph builder extension (low-level API that works directly with Atoms).
|
|
1674
|
+
*/
|
|
1675
|
+
var createExtensionRaw = (extension) => {
|
|
1676
|
+
const { id, position, relation = "child", url, resolver: _resolver, connector: _connector, actions: _actions, actionGroups: _actionGroups } = extension;
|
|
1677
|
+
if (!isValidLocalId(id)) {
|
|
1678
|
+
log.warn("dropping graph extension with invalid id; the final segment must be camelCase (no hyphens or underscores)", { id }, {
|
|
1679
|
+
"~LogMeta": "~LogMeta",
|
|
1680
|
+
F: __dxlog_file,
|
|
1681
|
+
L: 815,
|
|
1682
|
+
S: void 0
|
|
1683
|
+
});
|
|
1684
|
+
return [];
|
|
1685
|
+
}
|
|
1686
|
+
const normalizedRelation = normalizeRelation(relation);
|
|
1687
|
+
const getId = (key) => `${id}/${key}`;
|
|
1688
|
+
const resolver = _resolver && Atom.family((id) => _resolver(id).pipe(Atom.withLabel(`graph-builder:_resolver:${id}`)));
|
|
1689
|
+
const connector = _connector && Atom.family((node) => _connector(node).pipe(Atom.withLabel(`graph-builder:_connector:${id}`)));
|
|
1690
|
+
const actionGroups = _actionGroups && Atom.family((node) => _actionGroups(node).pipe(Atom.withLabel(`graph-builder:_actionGroups:${id}`)));
|
|
1691
|
+
const actions = _actions && Atom.family((node) => _actions(node).pipe(Atom.withLabel(`graph-builder:_actions:${id}`)));
|
|
1692
|
+
const extensions = [
|
|
1693
|
+
resolver ? {
|
|
1694
|
+
id: getId("resolver"),
|
|
1695
|
+
position,
|
|
1696
|
+
resolver
|
|
1697
|
+
} : void 0,
|
|
1698
|
+
connector ? {
|
|
1699
|
+
id: getId("connector"),
|
|
1700
|
+
position,
|
|
1701
|
+
relation: normalizedRelation,
|
|
1702
|
+
url,
|
|
1703
|
+
connector: Atom.family((node) => Atom.make((get) => {
|
|
1704
|
+
try {
|
|
1705
|
+
return get(connector(node));
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
log.warn("Error in connector", {
|
|
1708
|
+
id: getId("connector"),
|
|
1709
|
+
node,
|
|
1710
|
+
error
|
|
1711
|
+
}, {
|
|
1712
|
+
"~LogMeta": "~LogMeta",
|
|
1713
|
+
F: __dxlog_file,
|
|
1714
|
+
L: 860,
|
|
1715
|
+
S: void 0
|
|
1716
|
+
});
|
|
1717
|
+
return [];
|
|
1718
|
+
}
|
|
1719
|
+
}).pipe(Atom.withLabel(`graph-builder:connector:${id}`)))
|
|
1720
|
+
} : void 0,
|
|
1721
|
+
actionGroups ? {
|
|
1722
|
+
id: getId("actionGroups"),
|
|
1723
|
+
position,
|
|
1724
|
+
relation: actionRelation(),
|
|
1725
|
+
connector: Atom.family((node) => Atom.make((get) => {
|
|
1726
|
+
try {
|
|
1727
|
+
return get(actionGroups(node)).map((arg) => ({
|
|
1728
|
+
...arg,
|
|
1729
|
+
data: actionGroupSymbol,
|
|
1730
|
+
type: ActionGroupType
|
|
1731
|
+
}));
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
log.warn("Error in actionGroups", {
|
|
1734
|
+
id: getId("actionGroups"),
|
|
1735
|
+
node,
|
|
1736
|
+
error
|
|
1737
|
+
}, {
|
|
1738
|
+
"~LogMeta": "~LogMeta",
|
|
1739
|
+
F: __dxlog_file,
|
|
1740
|
+
L: 881,
|
|
1741
|
+
S: void 0
|
|
1742
|
+
});
|
|
1743
|
+
return [];
|
|
1744
|
+
}
|
|
1745
|
+
}).pipe(Atom.withLabel(`graph-builder:connector:actionGroups:${id}`)))
|
|
1746
|
+
} : void 0,
|
|
1747
|
+
actions ? {
|
|
1748
|
+
id: getId("actions"),
|
|
1749
|
+
position,
|
|
1750
|
+
relation: actionRelation(),
|
|
1751
|
+
connector: Atom.family((node) => Atom.make((get) => {
|
|
1752
|
+
try {
|
|
1753
|
+
return get(actions(node)).map((arg) => ({
|
|
1754
|
+
...arg,
|
|
1755
|
+
type: ActionType
|
|
1756
|
+
}));
|
|
1757
|
+
} catch (error) {
|
|
1758
|
+
log.warn("Error in actions", {
|
|
1759
|
+
id: getId("actions"),
|
|
1760
|
+
node,
|
|
1761
|
+
error
|
|
1762
|
+
}, {
|
|
1763
|
+
"~LogMeta": "~LogMeta",
|
|
1764
|
+
F: __dxlog_file,
|
|
1765
|
+
L: 898,
|
|
1766
|
+
S: void 0
|
|
1767
|
+
});
|
|
1768
|
+
return [];
|
|
1769
|
+
}
|
|
1770
|
+
}).pipe(Atom.withLabel(`graph-builder:connector:actions:${id}`)))
|
|
1771
|
+
} : void 0
|
|
1772
|
+
].filter(isNonNullable);
|
|
1773
|
+
if (extensions.length === 0 && url) return [{
|
|
1774
|
+
id,
|
|
1775
|
+
position,
|
|
1776
|
+
relation: normalizedRelation,
|
|
1777
|
+
url
|
|
1778
|
+
}];
|
|
1779
|
+
return extensions;
|
|
1780
|
+
};
|
|
1781
|
+
/**
|
|
1782
|
+
* Run an Effect synchronously with the provided context.
|
|
1783
|
+
* Defects are caught, logged, and the fallback value is returned.
|
|
1784
|
+
* @internal
|
|
1785
|
+
*/
|
|
1786
|
+
var runEffectSyncWithFallback = (effect, context, extensionId, fallback) => {
|
|
1787
|
+
return Effect.runSync(effect.pipe(Effect.provide(context), Effect.catchAllDefect((defect) => {
|
|
1788
|
+
log.warn("Extension failed", {
|
|
1789
|
+
extension: extensionId,
|
|
1790
|
+
defect
|
|
1791
|
+
}, {
|
|
1792
|
+
"~LogMeta": "~LogMeta",
|
|
1793
|
+
F: __dxlog_file,
|
|
1794
|
+
L: 959,
|
|
1795
|
+
S: void 0
|
|
1796
|
+
});
|
|
1797
|
+
return Effect.succeed(fallback);
|
|
1798
|
+
})));
|
|
1799
|
+
};
|
|
1800
|
+
/**
|
|
1801
|
+
* Create a graph builder extension with simplified API.
|
|
1802
|
+
* Returns an Effect to allow callbacks to access services via dependency injection.
|
|
1803
|
+
*/
|
|
1804
|
+
var createExtension = (options) => Effect.map(Effect.context(), (context) => {
|
|
1805
|
+
const { id, match, actions, actionGroups, connector, resolver, relation, position, url } = options;
|
|
1806
|
+
const connectorExtension = connector ? createConnectorWithRuntime(id, match, connector, context) : void 0;
|
|
1807
|
+
return createExtensionRaw({
|
|
1808
|
+
id,
|
|
1809
|
+
relation,
|
|
1810
|
+
position,
|
|
1811
|
+
url,
|
|
1812
|
+
resolver: resolver ? (nodeId) => Atom.make((get) => runEffectSyncWithFallback(resolver(nodeId, get), context, id, null) ?? null) : void 0,
|
|
1813
|
+
connector: connectorExtension,
|
|
1814
|
+
actions: actions ? (node) => Atom.make((get) => Function.pipe(get(node), Option.flatMap((matchedNode) => match(matchedNode, get)), Option.map((matched) => runEffectSyncWithFallback(actions(matched, get), context, id, []).map((action) => ({
|
|
1815
|
+
...action,
|
|
1816
|
+
_actionContext: context
|
|
1817
|
+
}))), Option.getOrElse(() => []))) : void 0,
|
|
1818
|
+
actionGroups: actionGroups ? (node) => Atom.make((get) => Function.pipe(get(node), Option.flatMap((matchedNode) => match(matchedNode, get)), Option.map((matched) => runEffectSyncWithFallback(actionGroups(matched, get), context, id, []).map((group) => ({
|
|
1819
|
+
...group,
|
|
1820
|
+
actions: group.actions?.map((action) => ({
|
|
1821
|
+
...action,
|
|
1822
|
+
_actionContext: context
|
|
1823
|
+
}))
|
|
1824
|
+
}))), Option.getOrElse(() => []))) : void 0
|
|
1825
|
+
});
|
|
1826
|
+
});
|
|
1827
|
+
/**
|
|
1828
|
+
* Create a connector extension from a matcher and factory function.
|
|
1829
|
+
* The factory's data type is inferred from the matcher's return type.
|
|
1830
|
+
*/
|
|
1831
|
+
var createConnector = (matcher, factory) => {
|
|
1832
|
+
return (node) => Atom.make((get) => Function.pipe(get(node), Option.flatMap((matchedNode) => matcher(matchedNode, get)), Option.map((data) => factory(data, get)), Option.getOrElse(() => [])));
|
|
1833
|
+
};
|
|
1834
|
+
/**
|
|
1835
|
+
* Create a connector extension from a matcher and factory function with Effect support.
|
|
1836
|
+
* The factory must return an Effect. Errors are caught and logged.
|
|
1837
|
+
* @internal
|
|
1838
|
+
*/
|
|
1839
|
+
var createConnectorWithRuntime = (extensionId, matcher, factory, context) => {
|
|
1840
|
+
return (node) => Atom.make((get) => Function.pipe(get(node), Option.flatMap((matchedNode) => matcher(matchedNode, get)), Option.map((data) => runEffectSyncWithFallback(factory(data, get), context, extensionId, [])), Option.getOrElse(() => [])));
|
|
1841
|
+
};
|
|
1842
|
+
/**
|
|
1843
|
+
* Create an extension that matches nodes by schema type.
|
|
1844
|
+
* The entity type is inferred from the schema type and works for both object and relation schemas.
|
|
1845
|
+
* Returns an Effect to allow callbacks to access services via dependency injection.
|
|
1846
|
+
*/
|
|
1847
|
+
var createTypeExtension = (options) => {
|
|
1848
|
+
const { id, type, actions, actionGroups, connector, relation, position } = options;
|
|
1849
|
+
return createExtension({
|
|
1850
|
+
id,
|
|
1851
|
+
match: whenEchoType(type),
|
|
1852
|
+
actions,
|
|
1853
|
+
actionGroups,
|
|
1854
|
+
connector,
|
|
1855
|
+
relation,
|
|
1856
|
+
position
|
|
1857
|
+
});
|
|
1858
|
+
};
|
|
1859
|
+
/**
|
|
1860
|
+
* Qualify node IDs by prefixing with the parent path.
|
|
1861
|
+
* Validates that segment IDs do not contain the path separator.
|
|
1862
|
+
* Recursively qualifies inline child nodes.
|
|
1863
|
+
*/
|
|
1864
|
+
var qualifyNodeArgs = (parentId) => (nodes) => nodes.map((node) => {
|
|
1865
|
+
validateSegmentId(node.id);
|
|
1866
|
+
const qualified = qualifyId(parentId, node.id);
|
|
1867
|
+
return {
|
|
1868
|
+
...node,
|
|
1869
|
+
id: qualified,
|
|
1870
|
+
nodes: node.nodes ? qualifyNodeArgs(qualified)(node.nodes) : void 0,
|
|
1871
|
+
actions: node.actions ? qualifyNodeArgs(qualified)(node.actions) : void 0
|
|
1872
|
+
};
|
|
1873
|
+
});
|
|
1874
|
+
/**
|
|
1875
|
+
* Recursively collect all inline-descendant IDs (the `nodes` arrays at every level)
|
|
1876
|
+
* from a list of top-level NodeArgs. Top-level IDs are excluded because they are
|
|
1877
|
+
* already tracked via `_connectorPrevious`.
|
|
1878
|
+
*/
|
|
1879
|
+
var collectAllInlineIds = (nodes) => nodes.flatMap((node) => {
|
|
1880
|
+
const childNodes = node.nodes ?? [];
|
|
1881
|
+
const actionNodes = node.actions ?? [];
|
|
1882
|
+
const allInline = [...childNodes, ...actionNodes];
|
|
1883
|
+
return allInline.length > 0 ? [...allInline.map((child) => child.id), ...collectAllInlineIds(allInline)] : [];
|
|
1884
|
+
});
|
|
1885
|
+
var connectorKey = (id, relation) => primaryKey(id, relationKey(relation));
|
|
1886
|
+
var relationFromConnectorKey = (key) => {
|
|
1887
|
+
const [id, encodedRelation] = primaryParts(key);
|
|
1888
|
+
return {
|
|
1889
|
+
id,
|
|
1890
|
+
relation: relationFromKey(encodedRelation)
|
|
1891
|
+
};
|
|
1892
|
+
};
|
|
1893
|
+
var subscriptionKey = (id, kind, detail) => detail != null ? primaryKey(id, kind, detail) : primaryKey(id, kind);
|
|
1894
|
+
var flattenExtensions = (extension, acc = []) => {
|
|
1895
|
+
if (Array$1.isArray(extension)) return [...acc, ...extension.flatMap((ext) => flattenExtensions(ext, acc))];
|
|
1896
|
+
else return [...acc, extension];
|
|
1897
|
+
};
|
|
1898
|
+
//#endregion
|
|
1899
|
+
export { urlRepresentation as a, getConnections as c, getParentId as d, getSegmentId as f, __exportAll as g, node_exports as h, make as i, getNode as l, RootId as m, flush as n, node_matcher_exports as o, qualifyId as p, graph_builder_exports as r, expand as s, addExtension as t, graph_exports as u };
|
|
1900
|
+
|
|
1901
|
+
//# sourceMappingURL=chunk-graph-builder.mjs.map
|