@dxos/app-graph 0.8.4-main.ae835ea → 0.8.4-main.bc674ce
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 +1014 -553
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1013 -553
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/atoms.d.ts +8 -0
- package/dist/types/src/atoms.d.ts.map +1 -0
- package/dist/types/src/graph-builder.d.ts +108 -66
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +182 -212
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +6 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node-matcher.d.ts +218 -0
- package/dist/types/src/node-matcher.d.ts.map +1 -0
- package/dist/types/src/node-matcher.test.d.ts +2 -0
- package/dist/types/src/node-matcher.test.d.ts.map +1 -0
- package/dist/types/src/node.d.ts +32 -3
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +35 -33
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +520 -104
- package/src/graph-builder.ts +550 -255
- package/src/graph.test.ts +299 -106
- package/src/graph.ts +964 -394
- package/src/index.ts +9 -3
- package/src/node-matcher.test.ts +301 -0
- package/src/node-matcher.ts +284 -0
- package/src/node.ts +39 -6
- package/src/stories/EchoGraph.stories.tsx +104 -95
- package/src/stories/Tree.tsx +2 -2
- package/dist/types/src/experimental/graph-projections.test.d.ts +0 -25
- package/dist/types/src/experimental/graph-projections.test.d.ts.map +0 -1
- package/dist/types/src/signals-integration.test.d.ts +0 -2
- package/dist/types/src/signals-integration.test.d.ts.map +0 -1
- package/dist/types/src/testing.d.ts +0 -5
- package/dist/types/src/testing.d.ts.map +0 -1
- package/src/experimental/graph-projections.test.ts +0 -56
- package/src/signals-integration.test.ts +0 -218
- package/src/testing.ts +0 -20
package/src/graph.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Atom, Registry } from '@effect-atom/atom-react';
|
|
6
6
|
import * as Function from 'effect/Function';
|
|
7
7
|
import * as Option from 'effect/Option';
|
|
8
|
+
import * as Pipeable from 'effect/Pipeable';
|
|
8
9
|
import * as Record from 'effect/Record';
|
|
9
10
|
|
|
10
11
|
import { Event, Trigger } from '@dxos/async';
|
|
@@ -13,33 +14,32 @@ import { invariant } from '@dxos/invariant';
|
|
|
13
14
|
import { log } from '@dxos/log';
|
|
14
15
|
import { type MakeOptional, isNonNullable } from '@dxos/util';
|
|
15
16
|
|
|
16
|
-
import
|
|
17
|
+
import * as Node from './node';
|
|
17
18
|
|
|
18
19
|
const graphSymbol = Symbol('graph');
|
|
19
|
-
|
|
20
|
-
type
|
|
20
|
+
|
|
21
|
+
type DeepWriteable<T> = {
|
|
22
|
+
-readonly [K in keyof T]: T[K] extends object ? DeepWriteable<T[K]> : T[K];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type NodeInternal = DeepWriteable<Node.Node> & { [graphSymbol]: GraphImpl };
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
28
|
* Get the Graph a Node is currently associated with.
|
|
24
29
|
*/
|
|
25
|
-
export const getGraph = (node: Node): Graph => {
|
|
30
|
+
export const getGraph = (node: Node.Node): Graph => {
|
|
26
31
|
const graph = (node as NodeInternal)[graphSymbol];
|
|
27
32
|
invariant(graph, 'Node is not associated with a graph.');
|
|
28
|
-
return graph;
|
|
33
|
+
return graph as Graph;
|
|
29
34
|
};
|
|
30
35
|
|
|
31
|
-
export const ROOT_ID = 'root';
|
|
32
|
-
export const ROOT_TYPE = 'dxos.org/type/GraphRoot';
|
|
33
|
-
export const ACTION_TYPE = 'dxos.org/type/GraphAction';
|
|
34
|
-
export const ACTION_GROUP_TYPE = 'dxos.org/type/GraphActionGroup';
|
|
35
|
-
|
|
36
36
|
export type GraphTraversalOptions = {
|
|
37
37
|
/**
|
|
38
38
|
* A callback which is called for each node visited during traversal.
|
|
39
39
|
*
|
|
40
40
|
* If the callback returns `false`, traversal is stops recursing.
|
|
41
41
|
*/
|
|
42
|
-
visitor: (node: Node, path: string[]) => boolean | void;
|
|
42
|
+
visitor: (node: Node.Node, path: string[]) => boolean | void;
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* The node to start traversing from.
|
|
@@ -53,239 +53,159 @@ export type GraphTraversalOptions = {
|
|
|
53
53
|
*
|
|
54
54
|
* @default 'outbound'
|
|
55
55
|
*/
|
|
56
|
-
relation?: Relation;
|
|
56
|
+
relation?: Node.Relation;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
export type
|
|
59
|
+
export type GraphProps = {
|
|
60
60
|
registry?: Registry.Registry;
|
|
61
|
-
nodes?: MakeOptional<Node, 'data' | 'cacheable'>[];
|
|
61
|
+
nodes?: MakeOptional<Node.Node, 'data' | 'cacheable'>[];
|
|
62
62
|
edges?: Record<string, Edges>;
|
|
63
|
-
onExpand?:
|
|
64
|
-
onInitialize?:
|
|
65
|
-
onRemoveNode?:
|
|
63
|
+
onExpand?: (id: string, relation: Node.Relation) => void;
|
|
64
|
+
onInitialize?: (id: string) => Promise<void>;
|
|
65
|
+
onRemoveNode?: (id: string) => void;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
export type Edge = { source: string; target: string };
|
|
69
69
|
export type Edges = { inbound: string[]; outbound: string[] };
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Convert the graph to a JSON object.
|
|
79
|
-
*/
|
|
80
|
-
toJSON(id?: string): object;
|
|
81
|
-
|
|
82
|
-
json(id?: string): Rx.Rx<any>;
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Get the rx key for the node with the given id.
|
|
86
|
-
*/
|
|
87
|
-
node(id: string): Rx.Rx<Option.Option<Node>>;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Get the rx key for the node with the given id.
|
|
91
|
-
*/
|
|
92
|
-
nodeOrThrow(id: string): Rx.Rx<Node>;
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Get the rx key for the connections of the node with the given id.
|
|
96
|
-
*/
|
|
97
|
-
connections(id: string, relation?: Relation): Rx.Rx<Node[]>;
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Get the rx key for the actions of the node with the given id.
|
|
101
|
-
*/
|
|
102
|
-
actions(id: string): Rx.Rx<(Action | ActionGroup)[]>;
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Get the rx key for the edges of the node with the given id.
|
|
106
|
-
*/
|
|
107
|
-
edges(id: string): Rx.Rx<Edges>;
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Alias for `getNodeOrThrow(ROOT_ID)`.
|
|
111
|
-
*/
|
|
112
|
-
get root(): Node;
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Get the node with the given id from the graph's registry.
|
|
116
|
-
*/
|
|
117
|
-
getNode(id: string): Option.Option<Node>;
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Get the node with the given id from the graph's registry.
|
|
121
|
-
*
|
|
122
|
-
* @throws If the node is Option.none().
|
|
123
|
-
*/
|
|
124
|
-
getNodeOrThrow(id: string): Node;
|
|
71
|
+
/**
|
|
72
|
+
* Identifier denoting a Graph.
|
|
73
|
+
*/
|
|
74
|
+
export const GraphTypeId: unique symbol = Symbol.for('@dxos/app-graph/Graph');
|
|
75
|
+
export type GraphTypeId = typeof GraphTypeId;
|
|
125
76
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Identifier for the graph kind discriminator.
|
|
79
|
+
*/
|
|
80
|
+
export const GraphKind: unique symbol = Symbol.for('@dxos/app-graph/GraphKind');
|
|
81
|
+
export type GraphKind = typeof GraphKind;
|
|
130
82
|
|
|
131
|
-
|
|
132
|
-
* Get all actions connected to the node with the given id from the graph's registry.
|
|
133
|
-
*/
|
|
134
|
-
getActions(id: string): Node[];
|
|
83
|
+
export type GraphKindType = 'readable' | 'expandable' | 'writable';
|
|
135
84
|
|
|
85
|
+
export interface BaseGraph extends Pipeable.Pipeable {
|
|
86
|
+
readonly [GraphTypeId]: GraphTypeId;
|
|
87
|
+
readonly [GraphKind]: GraphKindType;
|
|
136
88
|
/**
|
|
137
|
-
*
|
|
89
|
+
* Event emitted when a node is changed.
|
|
138
90
|
*/
|
|
139
|
-
|
|
140
|
-
|
|
91
|
+
readonly onNodeChanged: Event<{ id: string; node: Option.Option<Node.Node> }>;
|
|
141
92
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* @param options.node The node to start traversing from.
|
|
145
|
-
* @param options.relation The relation to traverse graph edges.
|
|
146
|
-
* @param options.visitor A callback which is called for each node visited during traversal.
|
|
93
|
+
* Get the atom key for the JSON representation of the graph.
|
|
147
94
|
*/
|
|
148
|
-
|
|
149
|
-
|
|
95
|
+
json(id?: string): Atom.Atom<any>;
|
|
150
96
|
/**
|
|
151
|
-
* Get the
|
|
97
|
+
* Get the atom key for the node with the given id.
|
|
152
98
|
*/
|
|
153
|
-
|
|
154
|
-
|
|
99
|
+
node(id: string): Atom.Atom<Option.Option<Node.Node>>;
|
|
155
100
|
/**
|
|
156
|
-
*
|
|
101
|
+
* Get the atom key for the node with the given id.
|
|
157
102
|
*/
|
|
158
|
-
|
|
159
|
-
params: { source?: string; target: string },
|
|
160
|
-
options?: { timeout?: number; interval?: number },
|
|
161
|
-
): Promise<string[]>;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export interface ExpandableGraph extends ReadableGraph {
|
|
103
|
+
nodeOrThrow(id: string): Atom.Atom<Node.Node>;
|
|
165
104
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* Fires the `onInitialize` callback to provide initial data for a node.
|
|
105
|
+
* Get the atom key for the connections of the node with the given id.
|
|
169
106
|
*/
|
|
170
|
-
|
|
171
|
-
|
|
107
|
+
connections(id: string, relation?: Node.Relation): Atom.Atom<Node.Node[]>;
|
|
172
108
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* Fires the `onExpand` callback to add connections to the node.
|
|
109
|
+
* Get the atom key for the actions of the node with the given id.
|
|
176
110
|
*/
|
|
177
|
-
|
|
178
|
-
|
|
111
|
+
actions(id: string): Atom.Atom<(Node.Action | Node.ActionGroup)[]>;
|
|
179
112
|
/**
|
|
180
|
-
*
|
|
113
|
+
* Get the atom key for the edges of the node with the given id.
|
|
181
114
|
*/
|
|
182
|
-
|
|
115
|
+
edges(id: string): Atom.Atom<Edges>;
|
|
183
116
|
}
|
|
184
117
|
|
|
185
|
-
export
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
*/
|
|
189
|
-
addNodes(nodes: NodeArg<any, Record<string, any>>[]): void;
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Add a node to the graph.
|
|
193
|
-
*/
|
|
194
|
-
addNode(node: NodeArg<any, Record<string, any>>): void;
|
|
118
|
+
export type ReadableGraph = BaseGraph & { readonly [GraphKind]: 'readable' | 'expandable' | 'writable' };
|
|
119
|
+
export type ExpandableGraph = BaseGraph & { readonly [GraphKind]: 'expandable' | 'writable' };
|
|
120
|
+
export type WritableGraph = BaseGraph & { readonly [GraphKind]: 'writable' };
|
|
195
121
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Remove a node from the graph.
|
|
203
|
-
*/
|
|
204
|
-
removeNode(id: string, edges?: boolean): void;
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Add edges to the graph.
|
|
208
|
-
*/
|
|
209
|
-
addEdges(edges: Edge[]): void;
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Add an edge to the graph.
|
|
213
|
-
*/
|
|
214
|
-
addEdge(edge: Edge): void;
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Remove edges from the graph.
|
|
218
|
-
*/
|
|
219
|
-
removeEdges(edges: Edge[], removeOrphans?: boolean): void;
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Remove an edge from the graph.
|
|
223
|
-
*/
|
|
224
|
-
removeEdge(edge: Edge, removeOrphans?: boolean): void;
|
|
225
|
-
}
|
|
122
|
+
/**
|
|
123
|
+
* Graph interface.
|
|
124
|
+
*/
|
|
125
|
+
export type Graph = WritableGraph;
|
|
226
126
|
|
|
227
127
|
/**
|
|
228
128
|
* The Graph represents the user interface information architecture of the application constructed via plugins.
|
|
129
|
+
* @internal
|
|
229
130
|
*/
|
|
230
|
-
|
|
231
|
-
readonly
|
|
131
|
+
class GraphImpl implements WritableGraph {
|
|
132
|
+
readonly [GraphTypeId]: GraphTypeId = GraphTypeId;
|
|
133
|
+
readonly [GraphKind] = 'writable' as const;
|
|
232
134
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
135
|
+
pipe() {
|
|
136
|
+
// eslint-disable-next-line prefer-rest-params
|
|
137
|
+
return Pipeable.pipeArguments(this, arguments);
|
|
138
|
+
}
|
|
236
139
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
140
|
+
readonly onNodeChanged = new Event<{
|
|
141
|
+
id: string;
|
|
142
|
+
node: Option.Option<Node.Node>;
|
|
143
|
+
}>();
|
|
144
|
+
|
|
145
|
+
readonly _onExpand?: GraphProps['onExpand'];
|
|
146
|
+
readonly _onInitialize?: GraphProps['onInitialize'];
|
|
147
|
+
readonly _onRemoveNode?: GraphProps['onRemoveNode'];
|
|
148
|
+
|
|
149
|
+
readonly _registry: Registry.Registry;
|
|
150
|
+
readonly _expanded = Record.empty<string, boolean>();
|
|
151
|
+
readonly _initialized = Record.empty<string, boolean>();
|
|
152
|
+
readonly _initialEdges = Record.empty<string, Edges>();
|
|
153
|
+
readonly _initialNodes = Record.fromEntries([
|
|
154
|
+
[
|
|
155
|
+
Node.RootId,
|
|
156
|
+
this._constructNode({
|
|
157
|
+
id: Node.RootId,
|
|
158
|
+
type: Node.RootType,
|
|
159
|
+
data: null,
|
|
160
|
+
properties: {},
|
|
161
|
+
}),
|
|
162
|
+
],
|
|
243
163
|
]);
|
|
244
164
|
|
|
245
165
|
/** @internal */
|
|
246
|
-
readonly _node =
|
|
166
|
+
readonly _node = Atom.family<string, Atom.Writable<Option.Option<Node.Node>>>((id) => {
|
|
247
167
|
const initial = Option.flatten(Record.get(this._initialNodes, id));
|
|
248
|
-
return
|
|
168
|
+
return Atom.make<Option.Option<Node.Node>>(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:node:${id}`));
|
|
249
169
|
});
|
|
250
170
|
|
|
251
|
-
|
|
252
|
-
return
|
|
171
|
+
readonly _nodeOrThrow = Atom.family<string, Atom.Atom<Node.Node>>((id) => {
|
|
172
|
+
return Atom.make((get) => {
|
|
253
173
|
const node = get(this._node(id));
|
|
254
174
|
invariant(Option.isSome(node), `Node not available: ${id}`);
|
|
255
175
|
return node.value;
|
|
256
176
|
});
|
|
257
177
|
});
|
|
258
178
|
|
|
259
|
-
|
|
179
|
+
readonly _edges = Atom.family<string, Atom.Writable<Edges>>((id) => {
|
|
260
180
|
const initial = Record.get(this._initialEdges, id).pipe(Option.getOrElse(() => ({ inbound: [], outbound: [] })));
|
|
261
|
-
return
|
|
181
|
+
return Atom.make<Edges>(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:edges:${id}`));
|
|
262
182
|
});
|
|
263
183
|
|
|
264
|
-
// NOTE: Currently the argument to the family needs to be referentially stable for the
|
|
265
|
-
// TODO(wittjosiah):
|
|
266
|
-
|
|
267
|
-
return
|
|
184
|
+
// NOTE: Currently the argument to the family needs to be referentially stable for the atom to be referentially stable.
|
|
185
|
+
// TODO(wittjosiah): Atom feature request, support for something akin to `ComplexMap` to allow for complex arguments.
|
|
186
|
+
readonly _connections = Atom.family<string, Atom.Atom<Node.Node[]>>((key) => {
|
|
187
|
+
return Atom.make((get) => {
|
|
268
188
|
const [id, relation] = key.split('$');
|
|
269
189
|
const edges = get(this._edges(id));
|
|
270
|
-
return edges[relation as Relation]
|
|
190
|
+
return edges[relation as Node.Relation]
|
|
271
191
|
.map((id) => get(this._node(id)))
|
|
272
192
|
.filter(Option.isSome)
|
|
273
193
|
.map((o) => o.value);
|
|
274
|
-
}).pipe(
|
|
194
|
+
}).pipe(Atom.withLabel(`graph:connections:${key}`));
|
|
275
195
|
});
|
|
276
196
|
|
|
277
|
-
|
|
278
|
-
return
|
|
197
|
+
readonly _actions = Atom.family<string, Atom.Atom<(Node.Action | Node.ActionGroup)[]>>((id) => {
|
|
198
|
+
return Atom.make((get) => {
|
|
279
199
|
return get(this._connections(`${id}$outbound`)).filter(
|
|
280
|
-
(node) => node.type ===
|
|
200
|
+
(node) => node.type === Node.ActionType || node.type === Node.ActionGroupType,
|
|
281
201
|
);
|
|
282
|
-
}).pipe(
|
|
202
|
+
}).pipe(Atom.withLabel(`graph:actions:${id}`));
|
|
283
203
|
});
|
|
284
204
|
|
|
285
|
-
|
|
286
|
-
return
|
|
287
|
-
const toJSON = (node: Node, seen: string[] = []): any => {
|
|
288
|
-
const nodes = get(this.
|
|
205
|
+
readonly _json = Atom.family<string, Atom.Atom<any>>((id) => {
|
|
206
|
+
return Atom.make((get) => {
|
|
207
|
+
const toJSON = (node: Node.Node, seen: string[] = []): any => {
|
|
208
|
+
const nodes = get(this._connections(`${node.id}$outbound`));
|
|
289
209
|
const obj: Record<string, any> = {
|
|
290
210
|
id: node.id,
|
|
291
211
|
type: node.type,
|
|
@@ -295,7 +215,7 @@ export class Graph implements WritableGraph {
|
|
|
295
215
|
}
|
|
296
216
|
if (nodes.length) {
|
|
297
217
|
obj.nodes = nodes
|
|
298
|
-
.map((n) => {
|
|
218
|
+
.map((n: Node.Node) => {
|
|
299
219
|
// Break cycles.
|
|
300
220
|
const nextSeen = [...seen, node.id];
|
|
301
221
|
return nextSeen.includes(n.id) ? undefined : toJSON(n, nextSeen);
|
|
@@ -305,12 +225,12 @@ export class Graph implements WritableGraph {
|
|
|
305
225
|
return obj;
|
|
306
226
|
};
|
|
307
227
|
|
|
308
|
-
const root = get(this.
|
|
228
|
+
const root = get(this._nodeOrThrow(id));
|
|
309
229
|
return toJSON(root);
|
|
310
|
-
}).pipe(
|
|
230
|
+
}).pipe(Atom.withLabel(`graph:json:${id}`));
|
|
311
231
|
});
|
|
312
232
|
|
|
313
|
-
constructor({ registry, nodes, edges, onInitialize, onExpand, onRemoveNode }:
|
|
233
|
+
constructor({ registry, nodes, edges, onInitialize, onExpand, onRemoveNode }: GraphProps = {}) {
|
|
314
234
|
this._registry = registry ?? Registry.make();
|
|
315
235
|
this._onInitialize = onInitialize;
|
|
316
236
|
this._onExpand = onExpand;
|
|
@@ -329,276 +249,926 @@ export class Graph implements WritableGraph {
|
|
|
329
249
|
}
|
|
330
250
|
}
|
|
331
251
|
|
|
332
|
-
|
|
333
|
-
return
|
|
252
|
+
json(id = Node.RootId): Atom.Atom<any> {
|
|
253
|
+
return jsonImpl(this, id);
|
|
334
254
|
}
|
|
335
255
|
|
|
336
|
-
|
|
337
|
-
return this
|
|
256
|
+
node(id: string): Atom.Atom<Option.Option<Node.Node>> {
|
|
257
|
+
return nodeImpl(this, id);
|
|
338
258
|
}
|
|
339
259
|
|
|
340
|
-
|
|
341
|
-
return this
|
|
260
|
+
nodeOrThrow(id: string): Atom.Atom<Node.Node> {
|
|
261
|
+
return nodeOrThrowImpl(this, id);
|
|
342
262
|
}
|
|
343
263
|
|
|
344
|
-
|
|
345
|
-
return this
|
|
264
|
+
connections(id: string, relation: Node.Relation = 'outbound'): Atom.Atom<Node.Node[]> {
|
|
265
|
+
return connectionsImpl(this, id, relation);
|
|
346
266
|
}
|
|
347
267
|
|
|
348
|
-
|
|
349
|
-
return this
|
|
268
|
+
actions(id: string): Atom.Atom<(Node.Action | Node.ActionGroup)[]> {
|
|
269
|
+
return actionsImpl(this, id);
|
|
350
270
|
}
|
|
351
271
|
|
|
352
|
-
|
|
353
|
-
return this
|
|
272
|
+
edges(id: string): Atom.Atom<Edges> {
|
|
273
|
+
return edgesImpl(this, id);
|
|
354
274
|
}
|
|
355
275
|
|
|
356
|
-
|
|
357
|
-
|
|
276
|
+
/** @internal */
|
|
277
|
+
_constructNode(node: Node.NodeArg<any>): Option.Option<Node.Node> {
|
|
278
|
+
return Option.some({
|
|
279
|
+
[graphSymbol]: this,
|
|
280
|
+
data: null,
|
|
281
|
+
properties: {},
|
|
282
|
+
...node,
|
|
283
|
+
});
|
|
358
284
|
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Internal helper to access GraphImpl internals.
|
|
289
|
+
* @internal
|
|
290
|
+
*/
|
|
291
|
+
const getInternal = (graph: BaseGraph): GraphImpl => {
|
|
292
|
+
return graph as unknown as GraphImpl;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Convert the graph to a JSON object.
|
|
297
|
+
*/
|
|
298
|
+
export const toJSON = (graph: BaseGraph, id = Node.RootId): object => {
|
|
299
|
+
const internal = getInternal(graph);
|
|
300
|
+
return internal._registry.get(internal._json(id));
|
|
301
|
+
};
|
|
359
302
|
|
|
360
|
-
|
|
361
|
-
|
|
303
|
+
/**
|
|
304
|
+
* Implementation helper for json.
|
|
305
|
+
*/
|
|
306
|
+
const jsonImpl = (graph: BaseGraph, id = Node.RootId): Atom.Atom<any> => {
|
|
307
|
+
const internal = getInternal(graph);
|
|
308
|
+
return internal._json(id);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Implementation helper for node.
|
|
313
|
+
*/
|
|
314
|
+
const nodeImpl = (graph: BaseGraph, id: string): Atom.Atom<Option.Option<Node.Node>> => {
|
|
315
|
+
const internal = getInternal(graph);
|
|
316
|
+
return internal._node(id);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Implementation helper for nodeOrThrow.
|
|
321
|
+
*/
|
|
322
|
+
const nodeOrThrowImpl = (graph: BaseGraph, id: string): Atom.Atom<Node.Node> => {
|
|
323
|
+
const internal = getInternal(graph);
|
|
324
|
+
return internal._nodeOrThrow(id);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Implementation helper for connections.
|
|
329
|
+
*/
|
|
330
|
+
const connectionsImpl = (
|
|
331
|
+
graph: BaseGraph,
|
|
332
|
+
id: string,
|
|
333
|
+
relation: Node.Relation = 'outbound',
|
|
334
|
+
): Atom.Atom<Node.Node[]> => {
|
|
335
|
+
const internal = getInternal(graph);
|
|
336
|
+
return internal._connections(`${id}$${relation}`);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Implementation helper for actions.
|
|
341
|
+
*/
|
|
342
|
+
const actionsImpl = (graph: BaseGraph, id: string): Atom.Atom<(Node.Action | Node.ActionGroup)[]> => {
|
|
343
|
+
const internal = getInternal(graph);
|
|
344
|
+
return internal._actions(id);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Implementation helper for edges.
|
|
349
|
+
*/
|
|
350
|
+
const edgesImpl = (graph: BaseGraph, id: string): Atom.Atom<Edges> => {
|
|
351
|
+
const internal = getInternal(graph);
|
|
352
|
+
return internal._edges(id);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Implementation helper for getNode.
|
|
357
|
+
*/
|
|
358
|
+
const getNodeImpl = (graph: BaseGraph, id: string): Option.Option<Node.Node> => {
|
|
359
|
+
const internal = getInternal(graph);
|
|
360
|
+
return internal._registry.get(nodeImpl(graph, id));
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get the node with the given id from the graph's registry.
|
|
365
|
+
*/
|
|
366
|
+
export function getNode(graph: BaseGraph, id: string): Option.Option<Node.Node>;
|
|
367
|
+
export function getNode(id: string): (graph: BaseGraph) => Option.Option<Node.Node>;
|
|
368
|
+
export function getNode(
|
|
369
|
+
graphOrId: BaseGraph | string,
|
|
370
|
+
id?: string,
|
|
371
|
+
): Option.Option<Node.Node> | ((graph: BaseGraph) => Option.Option<Node.Node>) {
|
|
372
|
+
if (typeof graphOrId === 'string') {
|
|
373
|
+
// Curried: getNode(id)
|
|
374
|
+
const id = graphOrId;
|
|
375
|
+
return (graph: BaseGraph) => getNodeImpl(graph, id);
|
|
376
|
+
} else {
|
|
377
|
+
// Direct: getNode(graph, id)
|
|
378
|
+
const graph = graphOrId;
|
|
379
|
+
return getNodeImpl(graph, id!);
|
|
362
380
|
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Implementation helper for getNodeOrThrow.
|
|
385
|
+
*/
|
|
386
|
+
const getNodeOrThrowImpl = (graph: BaseGraph, id: string): Node.Node => {
|
|
387
|
+
const internal = getInternal(graph);
|
|
388
|
+
return internal._registry.get(nodeOrThrowImpl(graph, id));
|
|
389
|
+
};
|
|
363
390
|
|
|
364
|
-
|
|
365
|
-
|
|
391
|
+
/**
|
|
392
|
+
* Get the node with the given id from the graph's registry.
|
|
393
|
+
*
|
|
394
|
+
* @throws If the node is Option.none().
|
|
395
|
+
*/
|
|
396
|
+
export function getNodeOrThrow(graph: BaseGraph, id: string): Node.Node;
|
|
397
|
+
export function getNodeOrThrow(id: string): (graph: BaseGraph) => Node.Node;
|
|
398
|
+
export function getNodeOrThrow(
|
|
399
|
+
graphOrId: BaseGraph | string,
|
|
400
|
+
id?: string,
|
|
401
|
+
): Node.Node | ((graph: BaseGraph) => Node.Node) {
|
|
402
|
+
if (typeof graphOrId === 'string') {
|
|
403
|
+
// Curried: getNodeOrThrow(id)
|
|
404
|
+
const id = graphOrId;
|
|
405
|
+
return (graph: BaseGraph) => getNodeOrThrowImpl(graph, id);
|
|
406
|
+
} else {
|
|
407
|
+
// Direct: getNodeOrThrow(graph, id)
|
|
408
|
+
const graph = graphOrId;
|
|
409
|
+
return getNodeOrThrowImpl(graph, id!);
|
|
366
410
|
}
|
|
411
|
+
}
|
|
367
412
|
|
|
368
|
-
|
|
369
|
-
|
|
413
|
+
/**
|
|
414
|
+
* Get the root node of the graph.
|
|
415
|
+
* This is an alias for `getNodeOrThrow(graph, ROOT_ID)`.
|
|
416
|
+
*/
|
|
417
|
+
export function getRoot(graph: BaseGraph): Node.Node {
|
|
418
|
+
return getNodeOrThrowImpl(graph, Node.RootId);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Implementation helper for getConnections.
|
|
423
|
+
*/
|
|
424
|
+
const getConnectionsImpl = (graph: BaseGraph, id: string, relation: Node.Relation = 'outbound'): Node.Node[] => {
|
|
425
|
+
const internal = getInternal(graph);
|
|
426
|
+
return internal._registry.get(connectionsImpl(graph, id, relation));
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get all nodes connected to the node with the given id by the given relation from the graph's registry.
|
|
431
|
+
*/
|
|
432
|
+
export function getConnections(graph: BaseGraph, id: string, relation?: Node.Relation): Node.Node[];
|
|
433
|
+
export function getConnections(id: string, relation?: Node.Relation): (graph: BaseGraph) => Node.Node[];
|
|
434
|
+
export function getConnections(
|
|
435
|
+
graphOrId: BaseGraph | string,
|
|
436
|
+
idOrRelation?: string | Node.Relation,
|
|
437
|
+
relation?: Node.Relation,
|
|
438
|
+
): Node.Node[] | ((graph: BaseGraph) => Node.Node[]) {
|
|
439
|
+
if (typeof graphOrId === 'string') {
|
|
440
|
+
// Curried: getConnections(id, relation?)
|
|
441
|
+
const id = graphOrId;
|
|
442
|
+
const rel = (typeof idOrRelation === 'string' ? 'outbound' : idOrRelation) ?? 'outbound';
|
|
443
|
+
return (graph: BaseGraph) => getConnectionsImpl(graph, id, rel);
|
|
444
|
+
} else {
|
|
445
|
+
// Direct: getConnections(graph, id, relation?)
|
|
446
|
+
const graph = graphOrId;
|
|
447
|
+
const id = idOrRelation as string;
|
|
448
|
+
const rel = relation ?? 'outbound';
|
|
449
|
+
return getConnectionsImpl(graph, id, rel);
|
|
370
450
|
}
|
|
451
|
+
}
|
|
371
452
|
|
|
372
|
-
|
|
373
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Implementation helper for getActions.
|
|
455
|
+
*/
|
|
456
|
+
const getActionsImpl = (graph: BaseGraph, id: string): Node.Node[] => {
|
|
457
|
+
const internal = getInternal(graph);
|
|
458
|
+
return internal._registry.get(actionsImpl(graph, id));
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Get all actions connected to the node with the given id from the graph's registry.
|
|
463
|
+
*/
|
|
464
|
+
export function getActions(graph: BaseGraph, id: string): Node.Node[];
|
|
465
|
+
export function getActions(id: string): (graph: BaseGraph) => Node.Node[];
|
|
466
|
+
export function getActions(
|
|
467
|
+
graphOrId: BaseGraph | string,
|
|
468
|
+
id?: string,
|
|
469
|
+
): Node.Node[] | ((graph: BaseGraph) => Node.Node[]) {
|
|
470
|
+
if (typeof graphOrId === 'string') {
|
|
471
|
+
// Curried: getActions(id)
|
|
472
|
+
const id = graphOrId;
|
|
473
|
+
return (graph: BaseGraph) => getActionsImpl(graph, id);
|
|
474
|
+
} else {
|
|
475
|
+
// Direct: getActions(graph, id)
|
|
476
|
+
const graph = graphOrId;
|
|
477
|
+
return getActionsImpl(graph, id!);
|
|
374
478
|
}
|
|
479
|
+
}
|
|
375
480
|
|
|
376
|
-
|
|
377
|
-
|
|
481
|
+
/**
|
|
482
|
+
* Implementation helper for getEdges.
|
|
483
|
+
*/
|
|
484
|
+
const getEdgesImpl = (graph: BaseGraph, id: string): Edges => {
|
|
485
|
+
const internal = getInternal(graph);
|
|
486
|
+
return internal._registry.get(edgesImpl(graph, id));
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Get the edges from the node with the given id from the graph's registry.
|
|
491
|
+
*/
|
|
492
|
+
export function getEdges(graph: BaseGraph, id: string): Edges;
|
|
493
|
+
export function getEdges(id: string): (graph: BaseGraph) => Edges;
|
|
494
|
+
export function getEdges(graphOrId: BaseGraph | string, id?: string): Edges | ((graph: BaseGraph) => Edges) {
|
|
495
|
+
if (typeof graphOrId === 'string') {
|
|
496
|
+
// Curried: getEdges(id)
|
|
497
|
+
const id = graphOrId;
|
|
498
|
+
return (graph: BaseGraph) => getEdgesImpl(graph, id);
|
|
499
|
+
} else {
|
|
500
|
+
// Direct: getEdges(graph, id)
|
|
501
|
+
const graph = graphOrId;
|
|
502
|
+
return getEdgesImpl(graph, id!);
|
|
378
503
|
}
|
|
504
|
+
}
|
|
379
505
|
|
|
380
|
-
|
|
381
|
-
|
|
506
|
+
/**
|
|
507
|
+
* Recursive depth-first traversal of the graph.
|
|
508
|
+
*/
|
|
509
|
+
/**
|
|
510
|
+
* Implementation helper for traverse.
|
|
511
|
+
*/
|
|
512
|
+
const traverseImpl = (graph: BaseGraph, options: GraphTraversalOptions, path: string[] = []): void => {
|
|
513
|
+
const { visitor, source = Node.RootId, relation = 'outbound' } = options;
|
|
514
|
+
// Break cycles.
|
|
515
|
+
if (path.includes(source)) {
|
|
516
|
+
return;
|
|
382
517
|
}
|
|
383
518
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
await this._onInitialize?.(id);
|
|
389
|
-
Record.set(this._initialized, id, true);
|
|
390
|
-
}
|
|
519
|
+
const node = getNodeOrThrow(graph, source);
|
|
520
|
+
const shouldContinue = visitor(node, [...path, source]);
|
|
521
|
+
if (shouldContinue === false) {
|
|
522
|
+
return;
|
|
391
523
|
}
|
|
392
524
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
525
|
+
Object.values(getConnections(graph, source, relation)).forEach((child) =>
|
|
526
|
+
traverseImpl(graph, { source: child.id, relation, visitor }, [...path, source]),
|
|
527
|
+
);
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Traverse the graph with the given options.
|
|
532
|
+
*/
|
|
533
|
+
export function traverse(graph: BaseGraph, options: GraphTraversalOptions, path?: string[]): void;
|
|
534
|
+
export function traverse(options: GraphTraversalOptions, path?: string[]): (graph: BaseGraph) => void;
|
|
535
|
+
export function traverse(
|
|
536
|
+
graphOrOptions: BaseGraph | GraphTraversalOptions,
|
|
537
|
+
optionsOrPath?: GraphTraversalOptions | string[],
|
|
538
|
+
path?: string[],
|
|
539
|
+
): void | ((graph: BaseGraph) => void) {
|
|
540
|
+
if (typeof graphOrOptions === 'object' && 'visitor' in graphOrOptions) {
|
|
541
|
+
// Curried: traverse(options, path?)
|
|
542
|
+
const options = graphOrOptions as GraphTraversalOptions;
|
|
543
|
+
const pathArg = Array.isArray(optionsOrPath) ? optionsOrPath : undefined;
|
|
544
|
+
return (graph: BaseGraph) => traverseImpl(graph, options, pathArg);
|
|
545
|
+
} else {
|
|
546
|
+
// Direct: traverse(graph, options, path?)
|
|
547
|
+
const graph = graphOrOptions as BaseGraph;
|
|
548
|
+
const options = optionsOrPath as GraphTraversalOptions;
|
|
549
|
+
const pathArg = path ?? (Array.isArray(optionsOrPath) ? optionsOrPath : undefined);
|
|
550
|
+
return traverseImpl(graph, options, pathArg);
|
|
401
551
|
}
|
|
552
|
+
}
|
|
402
553
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
554
|
+
/**
|
|
555
|
+
* Implementation helper for getPath.
|
|
556
|
+
*/
|
|
557
|
+
const getPathImpl = (graph: BaseGraph, params: { source?: string; target: string }): Option.Option<string[]> => {
|
|
558
|
+
return Function.pipe(
|
|
559
|
+
getNode(graph, params.source ?? 'root'),
|
|
560
|
+
Option.flatMap((node) => {
|
|
561
|
+
let found: Option.Option<string[]> = Option.none();
|
|
562
|
+
traverseImpl(graph, {
|
|
563
|
+
source: node.id,
|
|
564
|
+
visitor: (node, path) => {
|
|
565
|
+
if (Option.isSome(found)) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (node.id === params.target) {
|
|
570
|
+
found = Option.some(path);
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
return found;
|
|
576
|
+
}),
|
|
577
|
+
);
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Get the path between two nodes in the graph.
|
|
582
|
+
*/
|
|
583
|
+
export function getPath(graph: BaseGraph, params: { source?: string; target: string }): Option.Option<string[]>;
|
|
584
|
+
export function getPath(params: { source?: string; target: string }): (graph: BaseGraph) => Option.Option<string[]>;
|
|
585
|
+
export function getPath(
|
|
586
|
+
graphOrParams: BaseGraph | { source?: string; target: string },
|
|
587
|
+
params?: { source?: string; target: string },
|
|
588
|
+
): Option.Option<string[]> | ((graph: BaseGraph) => Option.Option<string[]>) {
|
|
589
|
+
if (params === undefined && typeof graphOrParams === 'object' && 'target' in graphOrParams) {
|
|
590
|
+
// Curried: getPath(params)
|
|
591
|
+
const params = graphOrParams as { source?: string; target: string };
|
|
592
|
+
return (graph: BaseGraph) => getPathImpl(graph, params);
|
|
593
|
+
} else {
|
|
594
|
+
// Direct: getPath(graph, params)
|
|
595
|
+
const graph = graphOrParams as BaseGraph;
|
|
596
|
+
return getPathImpl(graph, params!);
|
|
407
597
|
}
|
|
598
|
+
}
|
|
408
599
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
this._registry.set(nodeRx, newNode);
|
|
423
|
-
this.onNodeChanged.emit({ id, node: newNode });
|
|
424
|
-
}
|
|
425
|
-
},
|
|
426
|
-
onNone: () => {
|
|
427
|
-
log('new node', { id, type, data, properties });
|
|
428
|
-
const newNode = this._constructNode({ id, type, data, properties });
|
|
429
|
-
this._registry.set(nodeRx, newNode);
|
|
430
|
-
this.onNodeChanged.emit({ id, node: newNode });
|
|
431
|
-
},
|
|
432
|
-
});
|
|
600
|
+
/**
|
|
601
|
+
* Implementation helper for waitForPath.
|
|
602
|
+
*/
|
|
603
|
+
const waitForPathImpl = (
|
|
604
|
+
graph: BaseGraph,
|
|
605
|
+
params: { source?: string; target: string },
|
|
606
|
+
options?: { timeout?: number; interval?: number },
|
|
607
|
+
): Promise<string[]> => {
|
|
608
|
+
const { timeout = 5_000, interval = 500 } = options ?? {};
|
|
609
|
+
const path = getPathImpl(graph, params);
|
|
610
|
+
if (Option.isSome(path)) {
|
|
611
|
+
return Promise.resolve(path.value);
|
|
612
|
+
}
|
|
433
613
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
// });
|
|
614
|
+
const trigger = new Trigger<string[]>();
|
|
615
|
+
const i = setInterval(() => {
|
|
616
|
+
const path = getPathImpl(graph, params);
|
|
617
|
+
if (Option.isSome(path)) {
|
|
618
|
+
trigger.wake(path.value);
|
|
440
619
|
}
|
|
620
|
+
}, interval);
|
|
441
621
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
}
|
|
622
|
+
return trigger.wait({ timeout }).finally(() => clearInterval(i));
|
|
623
|
+
};
|
|
446
624
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
625
|
+
/**
|
|
626
|
+
* Wait for the path between two nodes in the graph to be established.
|
|
627
|
+
*/
|
|
628
|
+
export function waitForPath(
|
|
629
|
+
graph: BaseGraph,
|
|
630
|
+
params: { source?: string; target: string },
|
|
631
|
+
options?: { timeout?: number; interval?: number },
|
|
632
|
+
): Promise<string[]>;
|
|
633
|
+
export function waitForPath(
|
|
634
|
+
params: { source?: string; target: string },
|
|
635
|
+
options?: { timeout?: number; interval?: number },
|
|
636
|
+
): (graph: BaseGraph) => Promise<string[]>;
|
|
637
|
+
export function waitForPath(
|
|
638
|
+
graphOrParams: BaseGraph | { source?: string; target: string },
|
|
639
|
+
paramsOrOptions?: { source?: string; target: string } | { timeout?: number; interval?: number },
|
|
640
|
+
options?: { timeout?: number; interval?: number },
|
|
641
|
+
): Promise<string[]> | ((graph: BaseGraph) => Promise<string[]>) {
|
|
642
|
+
if (typeof graphOrParams === 'object' && 'target' in graphOrParams) {
|
|
643
|
+
// Curried: waitForPath(params, options?)
|
|
644
|
+
const params = graphOrParams as { source?: string; target: string };
|
|
645
|
+
const opts = typeof paramsOrOptions === 'object' && !('target' in paramsOrOptions) ? paramsOrOptions : undefined;
|
|
646
|
+
return (graph: BaseGraph) => waitForPathImpl(graph, params, opts);
|
|
647
|
+
} else {
|
|
648
|
+
// Direct: waitForPath(graph, params, options?)
|
|
649
|
+
const graph = graphOrParams as BaseGraph;
|
|
650
|
+
const params = paramsOrOptions as { source?: string; target: string };
|
|
651
|
+
return waitForPathImpl(graph, params, options);
|
|
451
652
|
}
|
|
653
|
+
}
|
|
452
654
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
655
|
+
/**
|
|
656
|
+
* Implementation helper for initialize.
|
|
657
|
+
*/
|
|
658
|
+
const initializeImpl = async <T extends ExpandableGraph | WritableGraph>(graph: T, id: string): Promise<T> => {
|
|
659
|
+
const internal = getInternal(graph);
|
|
660
|
+
const initialized = Record.get(internal._initialized, id).pipe(Option.getOrElse(() => false));
|
|
661
|
+
log('initialize', { id, initialized });
|
|
662
|
+
if (!initialized) {
|
|
663
|
+
await internal._onInitialize?.(id);
|
|
664
|
+
Record.set(internal._initialized, id, true);
|
|
665
|
+
}
|
|
666
|
+
return graph;
|
|
667
|
+
};
|
|
459
668
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
669
|
+
/**
|
|
670
|
+
* Initialize a node in the graph.
|
|
671
|
+
*
|
|
672
|
+
* Fires the `onInitialize` callback to provide initial data for a node.
|
|
673
|
+
*/
|
|
674
|
+
export function initialize<T extends ExpandableGraph | WritableGraph>(graph: T, id: string): Promise<T>;
|
|
675
|
+
export function initialize(id: string): <T extends ExpandableGraph | WritableGraph>(graph: T) => Promise<T>;
|
|
676
|
+
export function initialize<T extends ExpandableGraph | WritableGraph>(
|
|
677
|
+
graphOrId: T | string,
|
|
678
|
+
id?: string,
|
|
679
|
+
): Promise<T> | (<T extends ExpandableGraph | WritableGraph>(graph: T) => Promise<T>) {
|
|
680
|
+
if (typeof graphOrId === 'string') {
|
|
681
|
+
// Curried: initialize(id)
|
|
682
|
+
const id = graphOrId;
|
|
683
|
+
return <T extends ExpandableGraph | WritableGraph>(graph: T) => initializeImpl(graph, id);
|
|
684
|
+
} else {
|
|
685
|
+
// Direct: initialize(graph, id)
|
|
686
|
+
const graph = graphOrId;
|
|
687
|
+
return initializeImpl(graph, id!);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
468
690
|
|
|
469
|
-
|
|
691
|
+
/**
|
|
692
|
+
* Implementation helper for expand.
|
|
693
|
+
*/
|
|
694
|
+
const expandImpl = <T extends ExpandableGraph | WritableGraph>(
|
|
695
|
+
graph: T,
|
|
696
|
+
id: string,
|
|
697
|
+
relation: Node.Relation = 'outbound',
|
|
698
|
+
): T => {
|
|
699
|
+
const internal = getInternal(graph);
|
|
700
|
+
const key = `${id}$${relation}`;
|
|
701
|
+
const expanded = Record.get(internal._expanded, key).pipe(Option.getOrElse(() => false));
|
|
702
|
+
log('expand', { key, expanded });
|
|
703
|
+
if (!expanded) {
|
|
704
|
+
internal._onExpand?.(id, relation);
|
|
705
|
+
Record.set(internal._expanded, key, true);
|
|
470
706
|
}
|
|
707
|
+
return graph;
|
|
708
|
+
};
|
|
471
709
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
710
|
+
/**
|
|
711
|
+
* Expand a node in the graph.
|
|
712
|
+
*
|
|
713
|
+
* Fires the `onExpand` callback to add connections to the node.
|
|
714
|
+
*/
|
|
715
|
+
export function expand<T extends ExpandableGraph | WritableGraph>(graph: T, id: string, relation?: Node.Relation): T;
|
|
716
|
+
export function expand(
|
|
717
|
+
id: string,
|
|
718
|
+
relation?: Node.Relation,
|
|
719
|
+
): <T extends ExpandableGraph | WritableGraph>(graph: T) => T;
|
|
720
|
+
export function expand<T extends ExpandableGraph | WritableGraph>(
|
|
721
|
+
graphOrId: T | string,
|
|
722
|
+
idOrRelation?: string | Node.Relation,
|
|
723
|
+
relation?: Node.Relation,
|
|
724
|
+
): T | (<T extends ExpandableGraph | WritableGraph>(graph: T) => T) {
|
|
725
|
+
if (typeof graphOrId === 'string') {
|
|
726
|
+
// Curried: expand(id, relation?)
|
|
727
|
+
const id = graphOrId;
|
|
728
|
+
const rel = (typeof idOrRelation === 'string' ? 'outbound' : idOrRelation) ?? 'outbound';
|
|
729
|
+
return <T extends ExpandableGraph | WritableGraph>(graph: T) => expandImpl(graph, id, rel);
|
|
730
|
+
} else {
|
|
731
|
+
// Direct: expand(graph, id, relation?)
|
|
732
|
+
const graph = graphOrId;
|
|
733
|
+
const id = idOrRelation as string;
|
|
734
|
+
const rel = relation ?? 'outbound';
|
|
735
|
+
return expandImpl(graph, id, rel);
|
|
476
736
|
}
|
|
737
|
+
}
|
|
477
738
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
739
|
+
/**
|
|
740
|
+
* Implementation helper for sortEdges.
|
|
741
|
+
*/
|
|
742
|
+
const sortEdgesImpl = <T extends ExpandableGraph | WritableGraph>(
|
|
743
|
+
graph: T,
|
|
744
|
+
id: string,
|
|
745
|
+
relation: Node.Relation,
|
|
746
|
+
order: string[],
|
|
747
|
+
): T => {
|
|
748
|
+
const internal = getInternal(graph);
|
|
749
|
+
const edgesAtom = internal._edges(id);
|
|
750
|
+
const edges = internal._registry.get(edgesAtom);
|
|
751
|
+
const unsorted = edges[relation].filter((id) => !order.includes(id)) ?? [];
|
|
752
|
+
const sorted = order.filter((id) => edges[relation].includes(id)) ?? [];
|
|
753
|
+
edges[relation].splice(0, edges[relation].length, ...[...sorted, ...unsorted]);
|
|
754
|
+
internal._registry.set(edgesAtom, edges);
|
|
755
|
+
return graph;
|
|
756
|
+
};
|
|
485
757
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
758
|
+
/**
|
|
759
|
+
* Sort the edges of the node with the given id.
|
|
760
|
+
*/
|
|
761
|
+
export function sortEdges<T extends ExpandableGraph | WritableGraph>(
|
|
762
|
+
graph: T,
|
|
763
|
+
id: string,
|
|
764
|
+
relation: Node.Relation,
|
|
765
|
+
order: string[],
|
|
766
|
+
): T;
|
|
767
|
+
export function sortEdges(
|
|
768
|
+
id: string,
|
|
769
|
+
relation: Node.Relation,
|
|
770
|
+
order: string[],
|
|
771
|
+
): <T extends ExpandableGraph | WritableGraph>(graph: T) => T;
|
|
772
|
+
export function sortEdges<T extends ExpandableGraph | WritableGraph>(
|
|
773
|
+
graphOrId: T | string,
|
|
774
|
+
idOrRelation?: string | Node.Relation,
|
|
775
|
+
relationOrOrder?: Node.Relation | string[],
|
|
776
|
+
order?: string[],
|
|
777
|
+
): T | (<T extends ExpandableGraph | WritableGraph>(graph: T) => T) {
|
|
778
|
+
if (typeof graphOrId === 'string') {
|
|
779
|
+
// Curried: sortEdges(id, relation, order)
|
|
780
|
+
const id = graphOrId;
|
|
781
|
+
const relation = idOrRelation as Node.Relation;
|
|
782
|
+
const order = relationOrOrder as string[];
|
|
783
|
+
return <T extends ExpandableGraph | WritableGraph>(graph: T) => sortEdgesImpl(graph, id, relation, order);
|
|
784
|
+
} else {
|
|
785
|
+
// Direct: sortEdges(graph, id, relation, order)
|
|
786
|
+
const graph = graphOrId;
|
|
787
|
+
const id = idOrRelation as string;
|
|
788
|
+
const relation = relationOrOrder as Node.Relation;
|
|
789
|
+
return sortEdgesImpl(graph, id, relation, order!);
|
|
492
790
|
}
|
|
791
|
+
}
|
|
493
792
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
793
|
+
/**
|
|
794
|
+
* Implementation helper for addNodes.
|
|
795
|
+
*/
|
|
796
|
+
const addNodesImpl = <T extends WritableGraph>(graph: T, nodes: Node.NodeArg<any, Record<string, any>>[]): T => {
|
|
797
|
+
Atom.batch(() => {
|
|
798
|
+
nodes.map((node) => addNodeImpl(graph, node));
|
|
799
|
+
});
|
|
800
|
+
return graph;
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Add nodes to the graph.
|
|
805
|
+
*/
|
|
806
|
+
export function addNodes<T extends WritableGraph>(graph: T, nodes: Node.NodeArg<any, Record<string, any>>[]): T;
|
|
807
|
+
export function addNodes(nodes: Node.NodeArg<any, Record<string, any>>[]): <T extends WritableGraph>(graph: T) => T;
|
|
808
|
+
export function addNodes<T extends WritableGraph>(
|
|
809
|
+
graphOrNodes: T | Node.NodeArg<any, Record<string, any>>[],
|
|
810
|
+
nodes?: Node.NodeArg<any, Record<string, any>>[],
|
|
811
|
+
): T | (<T extends WritableGraph>(graph: T) => T) {
|
|
812
|
+
if (nodes === undefined) {
|
|
813
|
+
// Curried: addNodes(nodes)
|
|
814
|
+
const nodes = graphOrNodes as Node.NodeArg<any, Record<string, any>>[];
|
|
815
|
+
return <T extends WritableGraph>(graph: T) => addNodesImpl(graph, nodes);
|
|
816
|
+
} else {
|
|
817
|
+
// Direct: addNodes(graph, nodes)
|
|
818
|
+
const graph = graphOrNodes as T;
|
|
819
|
+
return addNodesImpl(graph, nodes);
|
|
498
820
|
}
|
|
821
|
+
}
|
|
499
822
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
823
|
+
/**
|
|
824
|
+
* Implementation helper for addNode.
|
|
825
|
+
*/
|
|
826
|
+
const addNodeImpl = <T extends WritableGraph>(graph: T, nodeArg: Node.NodeArg<any, Record<string, any>>): T => {
|
|
827
|
+
const internal = getInternal(graph);
|
|
828
|
+
// Extract known NodeArg fields, preserve any extra fields (like _actionContext) in rest.
|
|
829
|
+
const {
|
|
830
|
+
nodes,
|
|
831
|
+
edges,
|
|
832
|
+
id,
|
|
833
|
+
type,
|
|
834
|
+
data = null,
|
|
835
|
+
properties = {},
|
|
836
|
+
...rest
|
|
837
|
+
} = nodeArg as Node.NodeArg<any> & {
|
|
838
|
+
_actionContext?: Node.ActionContext;
|
|
839
|
+
};
|
|
840
|
+
const nodeAtom = internal._node(id);
|
|
841
|
+
const existingNode = internal._registry.get(nodeAtom);
|
|
842
|
+
Option.match(existingNode, {
|
|
843
|
+
onSome: (existing) => {
|
|
844
|
+
const typeChanged = existing.type !== type;
|
|
845
|
+
const dataChanged = existing.data !== data;
|
|
846
|
+
const propertiesChanged = Object.keys(properties).some((key) => existing.properties[key] !== properties[key]);
|
|
847
|
+
log('existing node', {
|
|
848
|
+
id,
|
|
849
|
+
typeChanged,
|
|
850
|
+
dataChanged,
|
|
851
|
+
propertiesChanged,
|
|
507
852
|
});
|
|
508
|
-
|
|
853
|
+
if (typeChanged || dataChanged || propertiesChanged) {
|
|
854
|
+
log('updating node', { id, type, data, properties });
|
|
855
|
+
const newNode = Option.some({
|
|
856
|
+
...existing,
|
|
857
|
+
...rest,
|
|
858
|
+
type,
|
|
859
|
+
data,
|
|
860
|
+
properties: { ...existing.properties, ...properties },
|
|
861
|
+
});
|
|
862
|
+
internal._registry.set(nodeAtom, newNode);
|
|
863
|
+
graph.onNodeChanged.emit({ id, node: newNode });
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
onNone: () => {
|
|
867
|
+
log('new node', { id, type, data, properties });
|
|
868
|
+
const newNode = internal._constructNode({ id, type, data, properties, ...rest });
|
|
869
|
+
internal._registry.set(nodeAtom, newNode);
|
|
870
|
+
graph.onNodeChanged.emit({ id, node: newNode });
|
|
871
|
+
},
|
|
872
|
+
});
|
|
509
873
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
outbound: target.outbound,
|
|
516
|
-
});
|
|
517
|
-
}
|
|
874
|
+
if (nodes) {
|
|
875
|
+
addNodesImpl(graph, nodes);
|
|
876
|
+
const _edges = nodes.map((node) => ({ source: id, target: node.id }));
|
|
877
|
+
addEdgesImpl(graph, _edges);
|
|
878
|
+
}
|
|
518
879
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
880
|
+
if (edges) {
|
|
881
|
+
todo();
|
|
882
|
+
}
|
|
883
|
+
return graph;
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Add a node to the graph.
|
|
888
|
+
*/
|
|
889
|
+
export function addNode<T extends WritableGraph>(graph: T, nodeArg: Node.NodeArg<any, Record<string, any>>): T;
|
|
890
|
+
export function addNode(nodeArg: Node.NodeArg<any, Record<string, any>>): <T extends WritableGraph>(graph: T) => T;
|
|
891
|
+
export function addNode<T extends WritableGraph>(
|
|
892
|
+
graphOrNodeArg: T | Node.NodeArg<any, Record<string, any>>,
|
|
893
|
+
nodeArg?: Node.NodeArg<any, Record<string, any>>,
|
|
894
|
+
): T | (<T extends WritableGraph>(graph: T) => T) {
|
|
895
|
+
if (nodeArg === undefined) {
|
|
896
|
+
// Curried: addNode(nodeArg)
|
|
897
|
+
const nodeArg = graphOrNodeArg as Node.NodeArg<any, Record<string, any>>;
|
|
898
|
+
return <T extends WritableGraph>(graph: T) => addNodeImpl(graph, nodeArg);
|
|
899
|
+
} else {
|
|
900
|
+
// Direct: addNode(graph, nodeArg)
|
|
901
|
+
const graph = graphOrNodeArg as T;
|
|
902
|
+
return addNodeImpl(graph, nodeArg);
|
|
529
903
|
}
|
|
904
|
+
}
|
|
530
905
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
906
|
+
/**
|
|
907
|
+
* Implementation helper for removeNodes.
|
|
908
|
+
*/
|
|
909
|
+
const removeNodesImpl = <T extends WritableGraph>(graph: T, ids: string[], edges = false): T => {
|
|
910
|
+
Atom.batch(() => {
|
|
911
|
+
ids.map((id) => removeNodeImpl(graph, id, edges));
|
|
912
|
+
});
|
|
913
|
+
return graph;
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Remove nodes from the graph.
|
|
918
|
+
*/
|
|
919
|
+
export function removeNodes<T extends WritableGraph>(graph: T, ids: string[], edges?: boolean): T;
|
|
920
|
+
export function removeNodes(ids: string[], edges?: boolean): <T extends WritableGraph>(graph: T) => T;
|
|
921
|
+
export function removeNodes<T extends WritableGraph>(
|
|
922
|
+
graphOrIds: T | string[],
|
|
923
|
+
idsOrEdges?: string[] | boolean,
|
|
924
|
+
edges?: boolean,
|
|
925
|
+
): T | (<T extends WritableGraph>(graph: T) => T) {
|
|
926
|
+
if (Array.isArray(graphOrIds)) {
|
|
927
|
+
// Curried: removeNodes(ids, edges?)
|
|
928
|
+
const ids = graphOrIds;
|
|
929
|
+
const edgesArg = typeof idsOrEdges === 'boolean' ? idsOrEdges : false;
|
|
930
|
+
return <T extends WritableGraph>(graph: T) => removeNodesImpl(graph, ids, edgesArg);
|
|
931
|
+
} else {
|
|
932
|
+
// Direct: removeNodes(graph, ids, edges?)
|
|
933
|
+
const graph = graphOrIds;
|
|
934
|
+
const ids = idsOrEdges as string[];
|
|
935
|
+
const edgesArg = edges ?? false;
|
|
936
|
+
return removeNodesImpl(graph, ids, edgesArg);
|
|
538
937
|
}
|
|
938
|
+
}
|
|
539
939
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
940
|
+
/**
|
|
941
|
+
* Implementation helper for removeNode.
|
|
942
|
+
*/
|
|
943
|
+
const removeNodeImpl = <T extends WritableGraph>(graph: T, id: string, edges = false): T => {
|
|
944
|
+
const internal = getInternal(graph);
|
|
945
|
+
const nodeAtom = internal._node(id);
|
|
946
|
+
// TODO(wittjosiah): Is there a way to mark these atom values for garbage collection?
|
|
947
|
+
internal._registry.set(nodeAtom, Option.none());
|
|
948
|
+
graph.onNodeChanged.emit({ id, node: Option.none() });
|
|
949
|
+
// TODO(wittjosiah): Reset expanded and initialized flags?
|
|
950
|
+
|
|
951
|
+
if (edges) {
|
|
952
|
+
const { inbound, outbound } = internal._registry.get(internal._edges(id));
|
|
953
|
+
const edgesToRemove = [
|
|
954
|
+
...inbound.map((source) => ({ source, target: id })),
|
|
955
|
+
...outbound.map((target) => ({ source: id, target })),
|
|
956
|
+
];
|
|
957
|
+
removeEdgesImpl(graph, edgesToRemove);
|
|
958
|
+
}
|
|
545
959
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
960
|
+
internal._onRemoveNode?.(id);
|
|
961
|
+
return graph;
|
|
962
|
+
};
|
|
551
963
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
964
|
+
/**
|
|
965
|
+
* Remove a node from the graph.
|
|
966
|
+
*/
|
|
967
|
+
export function removeNode<T extends WritableGraph>(graph: T, id: string, edges?: boolean): T;
|
|
968
|
+
export function removeNode(id: string, edges?: boolean): <T extends WritableGraph>(graph: T) => T;
|
|
969
|
+
export function removeNode<T extends WritableGraph>(
|
|
970
|
+
graphOrId: T | string,
|
|
971
|
+
idOrEdges?: string | boolean,
|
|
972
|
+
edges?: boolean,
|
|
973
|
+
): T | (<T extends WritableGraph>(graph: T) => T) {
|
|
974
|
+
if (typeof graphOrId === 'string') {
|
|
975
|
+
// Curried: removeNode(id, edges?)
|
|
976
|
+
const id = graphOrId;
|
|
977
|
+
const edgesArg = typeof idOrEdges === 'boolean' ? idOrEdges : false;
|
|
978
|
+
return <T extends WritableGraph>(graph: T) => removeNodeImpl(graph, id, edgesArg);
|
|
979
|
+
} else {
|
|
980
|
+
// Direct: removeNode(graph, id, edges?)
|
|
981
|
+
const graph = graphOrId;
|
|
982
|
+
const id = idOrEdges as string;
|
|
983
|
+
const edgesArg = edges ?? false;
|
|
984
|
+
return removeNodeImpl(graph, id, edgesArg);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
574
987
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
988
|
+
/**
|
|
989
|
+
* Implementation helper for addEdges.
|
|
990
|
+
*/
|
|
991
|
+
const addEdgesImpl = <T extends WritableGraph>(graph: T, edges: Edge[]): T => {
|
|
992
|
+
Atom.batch(() => {
|
|
993
|
+
edges.map((edge) => addEdgeImpl(graph, edge));
|
|
994
|
+
});
|
|
995
|
+
return graph;
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Add edges to the graph.
|
|
1000
|
+
*/
|
|
1001
|
+
export function addEdges<T extends WritableGraph>(graph: T, edges: Edge[]): T;
|
|
1002
|
+
export function addEdges(edges: Edge[]): <T extends WritableGraph>(graph: T) => T;
|
|
1003
|
+
export function addEdges<T extends WritableGraph>(
|
|
1004
|
+
graphOrEdges: T | Edge[],
|
|
1005
|
+
edges?: Edge[],
|
|
1006
|
+
): T | (<T extends WritableGraph>(graph: T) => T) {
|
|
1007
|
+
if (edges === undefined) {
|
|
1008
|
+
// Curried: addEdges(edges)
|
|
1009
|
+
const edges = graphOrEdges as Edge[];
|
|
1010
|
+
return <T extends WritableGraph>(graph: T) => addEdgesImpl(graph, edges);
|
|
1011
|
+
} else {
|
|
1012
|
+
// Direct: addEdges(graph, edges)
|
|
1013
|
+
const graph = graphOrEdges as T;
|
|
1014
|
+
return addEdgesImpl(graph, edges);
|
|
578
1015
|
}
|
|
1016
|
+
}
|
|
579
1017
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
1018
|
+
/**
|
|
1019
|
+
* Implementation helper for addEdge.
|
|
1020
|
+
*/
|
|
1021
|
+
const addEdgeImpl = <T extends WritableGraph>(graph: T, edgeArg: Edge): T => {
|
|
1022
|
+
const internal = getInternal(graph);
|
|
1023
|
+
const sourceAtom = internal._edges(edgeArg.source);
|
|
1024
|
+
const source = internal._registry.get(sourceAtom);
|
|
1025
|
+
if (!source.outbound.includes(edgeArg.target)) {
|
|
1026
|
+
log('add outbound edge', {
|
|
1027
|
+
source: edgeArg.source,
|
|
1028
|
+
target: edgeArg.target,
|
|
1029
|
+
});
|
|
1030
|
+
internal._registry.set(sourceAtom, {
|
|
1031
|
+
inbound: source.inbound,
|
|
1032
|
+
outbound: [...source.outbound, edgeArg.target],
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
588
1035
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
1036
|
+
const targetAtom = internal._edges(edgeArg.target);
|
|
1037
|
+
const target = internal._registry.get(targetAtom);
|
|
1038
|
+
if (!target.inbound.includes(edgeArg.source)) {
|
|
1039
|
+
log('add inbound edge', {
|
|
1040
|
+
source: edgeArg.source,
|
|
1041
|
+
target: edgeArg.target,
|
|
1042
|
+
});
|
|
1043
|
+
internal._registry.set(targetAtom, {
|
|
1044
|
+
inbound: [...target.inbound, edgeArg.source],
|
|
1045
|
+
outbound: target.outbound,
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
return graph;
|
|
1049
|
+
};
|
|
596
1050
|
|
|
597
|
-
|
|
1051
|
+
/**
|
|
1052
|
+
* Add an edge to the graph.
|
|
1053
|
+
*/
|
|
1054
|
+
export function addEdge<T extends WritableGraph>(graph: T, edgeArg: Edge): T;
|
|
1055
|
+
export function addEdge(edgeArg: Edge): <T extends WritableGraph>(graph: T) => T;
|
|
1056
|
+
export function addEdge<T extends WritableGraph>(
|
|
1057
|
+
graphOrEdgeArg: T | Edge,
|
|
1058
|
+
edgeArg?: Edge,
|
|
1059
|
+
): T | (<T extends WritableGraph>(graph: T) => T) {
|
|
1060
|
+
if (edgeArg === undefined) {
|
|
1061
|
+
// Curried: addEdge(edgeArg)
|
|
1062
|
+
const edgeArg = graphOrEdgeArg as Edge;
|
|
1063
|
+
return <T extends WritableGraph>(graph: T) => addEdgeImpl(graph, edgeArg);
|
|
1064
|
+
} else {
|
|
1065
|
+
// Direct: addEdge(graph, edgeArg)
|
|
1066
|
+
const graph = graphOrEdgeArg as T;
|
|
1067
|
+
return addEdgeImpl(graph, edgeArg);
|
|
598
1068
|
}
|
|
1069
|
+
}
|
|
599
1070
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1071
|
+
/**
|
|
1072
|
+
* Implementation helper for removeEdges.
|
|
1073
|
+
*/
|
|
1074
|
+
const removeEdgesImpl = <T extends WritableGraph>(graph: T, edges: Edge[], removeOrphans = false): T => {
|
|
1075
|
+
Atom.batch(() => {
|
|
1076
|
+
edges.map((edge) => removeEdgeImpl(graph, edge, removeOrphans));
|
|
1077
|
+
});
|
|
1078
|
+
return graph;
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Remove edges from the graph.
|
|
1083
|
+
*/
|
|
1084
|
+
export function removeEdges<T extends WritableGraph>(graph: T, edges: Edge[], removeOrphans?: boolean): T;
|
|
1085
|
+
export function removeEdges(edges: Edge[], removeOrphans?: boolean): <T extends WritableGraph>(graph: T) => T;
|
|
1086
|
+
export function removeEdges<T extends WritableGraph>(
|
|
1087
|
+
graphOrEdges: T | Edge[],
|
|
1088
|
+
edgesOrRemoveOrphans?: Edge[] | boolean,
|
|
1089
|
+
removeOrphans?: boolean,
|
|
1090
|
+
): T | (<T extends WritableGraph>(graph: T) => T) {
|
|
1091
|
+
if (Array.isArray(graphOrEdges)) {
|
|
1092
|
+
// Curried: removeEdges(edges, removeOrphans?)
|
|
1093
|
+
const edges = graphOrEdges;
|
|
1094
|
+
const removeOrphansArg = typeof edgesOrRemoveOrphans === 'boolean' ? edgesOrRemoveOrphans : false;
|
|
1095
|
+
return <T extends WritableGraph>(graph: T) => removeEdgesImpl(graph, edges, removeOrphansArg);
|
|
1096
|
+
} else {
|
|
1097
|
+
// Direct: removeEdges(graph, edges, removeOrphans?)
|
|
1098
|
+
const graph = graphOrEdges;
|
|
1099
|
+
const edges = edgesOrRemoveOrphans as Edge[];
|
|
1100
|
+
const removeOrphansArg = removeOrphans ?? false;
|
|
1101
|
+
return removeEdgesImpl(graph, edges, removeOrphansArg);
|
|
603
1102
|
}
|
|
604
1103
|
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Implementation helper for removeEdge.
|
|
1107
|
+
*/
|
|
1108
|
+
const removeEdgeImpl = <T extends WritableGraph>(graph: T, edgeArg: Edge, removeOrphans = false): T => {
|
|
1109
|
+
const internal = getInternal(graph);
|
|
1110
|
+
const sourceAtom = internal._edges(edgeArg.source);
|
|
1111
|
+
const source = internal._registry.get(sourceAtom);
|
|
1112
|
+
if (source.outbound.includes(edgeArg.target)) {
|
|
1113
|
+
internal._registry.set(sourceAtom, {
|
|
1114
|
+
inbound: source.inbound,
|
|
1115
|
+
outbound: source.outbound.filter((id) => id !== edgeArg.target),
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const targetAtom = internal._edges(edgeArg.target);
|
|
1120
|
+
const target = internal._registry.get(targetAtom);
|
|
1121
|
+
if (target.inbound.includes(edgeArg.source)) {
|
|
1122
|
+
internal._registry.set(targetAtom, {
|
|
1123
|
+
inbound: target.inbound.filter((id) => id !== edgeArg.source),
|
|
1124
|
+
outbound: target.outbound,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (removeOrphans) {
|
|
1129
|
+
const source = internal._registry.get(sourceAtom);
|
|
1130
|
+
const target = internal._registry.get(targetAtom);
|
|
1131
|
+
if (source.outbound.length === 0 && source.inbound.length === 0 && edgeArg.source !== Node.RootId) {
|
|
1132
|
+
removeNodesImpl(graph, [edgeArg.source]);
|
|
1133
|
+
}
|
|
1134
|
+
if (target.outbound.length === 0 && target.inbound.length === 0 && edgeArg.target !== Node.RootId) {
|
|
1135
|
+
removeNodesImpl(graph, [edgeArg.target]);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return graph;
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Remove an edge from the graph.
|
|
1143
|
+
*/
|
|
1144
|
+
export function removeEdge<T extends WritableGraph>(graph: T, edgeArg: Edge, removeOrphans?: boolean): T;
|
|
1145
|
+
export function removeEdge(edgeArg: Edge, removeOrphans?: boolean): <T extends WritableGraph>(graph: T) => T;
|
|
1146
|
+
export function removeEdge<T extends WritableGraph>(
|
|
1147
|
+
graphOrEdgeArg: T | Edge,
|
|
1148
|
+
edgeArgOrRemoveOrphans?: Edge | boolean,
|
|
1149
|
+
removeOrphans?: boolean,
|
|
1150
|
+
): T | (<T extends WritableGraph>(graph: T) => T) {
|
|
1151
|
+
if (
|
|
1152
|
+
edgeArgOrRemoveOrphans === undefined ||
|
|
1153
|
+
typeof edgeArgOrRemoveOrphans === 'boolean' ||
|
|
1154
|
+
'source' in graphOrEdgeArg
|
|
1155
|
+
) {
|
|
1156
|
+
// Curried: removeEdge(edgeArg, removeOrphans?)
|
|
1157
|
+
const edgeArg = graphOrEdgeArg as Edge;
|
|
1158
|
+
const removeOrphansArg = typeof edgeArgOrRemoveOrphans === 'boolean' ? edgeArgOrRemoveOrphans : false;
|
|
1159
|
+
return <T extends WritableGraph>(graph: T) => removeEdgeImpl(graph, edgeArg, removeOrphansArg);
|
|
1160
|
+
} else {
|
|
1161
|
+
// Direct: removeEdge(graph, edgeArg, removeOrphans?)
|
|
1162
|
+
const graph = graphOrEdgeArg as T;
|
|
1163
|
+
const edgeArg = edgeArgOrRemoveOrphans as Edge;
|
|
1164
|
+
const removeOrphansArg = removeOrphans ?? false;
|
|
1165
|
+
return removeEdgeImpl(graph, edgeArg, removeOrphansArg);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Creates a new Graph instance.
|
|
1171
|
+
*/
|
|
1172
|
+
export const make = (params?: GraphProps): Graph => {
|
|
1173
|
+
return new GraphImpl(params);
|
|
1174
|
+
};
|