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