@atlaspack/graph 3.2.1-canary.3354
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/LICENSE +201 -0
- package/lib/AdjacencyList.js +1415 -0
- package/lib/BitSet.js +80 -0
- package/lib/ContentGraph.js +80 -0
- package/lib/Graph.js +521 -0
- package/lib/index.js +54 -0
- package/lib/shared-buffer.js +28 -0
- package/lib/types.js +14 -0
- package/package.json +23 -0
- package/src/AdjacencyList.js +1655 -0
- package/src/BitSet.js +98 -0
- package/src/ContentGraph.js +96 -0
- package/src/Graph.js +740 -0
- package/src/index.js +9 -0
- package/src/shared-buffer.js +23 -0
- package/src/types.js +18 -0
- package/test/AdjacencyList.test.js +322 -0
- package/test/BitSet.test.js +110 -0
- package/test/ContentGraph.test.js +42 -0
- package/test/Graph.test.js +559 -0
- package/test/integration/adjacency-list-shared-array.js +20 -0
package/src/Graph.js
ADDED
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
// @flow strict-local
|
|
2
|
+
|
|
3
|
+
import {fromNodeId} from './types';
|
|
4
|
+
import AdjacencyList, {type SerializedAdjacencyList} from './AdjacencyList';
|
|
5
|
+
import type {Edge, NodeId} from './types';
|
|
6
|
+
import type {
|
|
7
|
+
TraversalActions,
|
|
8
|
+
GraphVisitor,
|
|
9
|
+
GraphTraversalCallback,
|
|
10
|
+
} from '@atlaspack/types';
|
|
11
|
+
import {BitSet} from './BitSet';
|
|
12
|
+
|
|
13
|
+
import nullthrows from 'nullthrows';
|
|
14
|
+
|
|
15
|
+
export type NullEdgeType = 1;
|
|
16
|
+
export type GraphOpts<TNode, TEdgeType: number = 1> = {|
|
|
17
|
+
nodes?: Array<TNode | null>,
|
|
18
|
+
adjacencyList?: SerializedAdjacencyList<TEdgeType>,
|
|
19
|
+
rootNodeId?: ?NodeId,
|
|
20
|
+
|};
|
|
21
|
+
|
|
22
|
+
export type SerializedGraph<TNode, TEdgeType: number = 1> = {|
|
|
23
|
+
nodes: Array<TNode | null>,
|
|
24
|
+
adjacencyList: SerializedAdjacencyList<TEdgeType>,
|
|
25
|
+
rootNodeId: ?NodeId,
|
|
26
|
+
|};
|
|
27
|
+
|
|
28
|
+
export type AllEdgeTypes = -1;
|
|
29
|
+
export const ALL_EDGE_TYPES: AllEdgeTypes = -1;
|
|
30
|
+
|
|
31
|
+
type DFSCommandVisit<TContext> = {|
|
|
32
|
+
nodeId: NodeId,
|
|
33
|
+
context: TContext | null,
|
|
34
|
+
|};
|
|
35
|
+
|
|
36
|
+
type DFSCommandExit<TContext> = {|
|
|
37
|
+
nodeId: NodeId,
|
|
38
|
+
exit: GraphTraversalCallback<NodeId, TContext>,
|
|
39
|
+
context: TContext | null,
|
|
40
|
+
|};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Internal type used for queue iterative DFS implementation.
|
|
44
|
+
*/
|
|
45
|
+
type DFSCommand<TContext> =
|
|
46
|
+
| DFSCommandVisit<TContext>
|
|
47
|
+
| DFSCommandExit<TContext>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Options for DFS traversal.
|
|
51
|
+
*/
|
|
52
|
+
export type DFSParams<TContext> = {|
|
|
53
|
+
visit: GraphVisitor<NodeId, TContext>,
|
|
54
|
+
/**
|
|
55
|
+
* Custom function to get next entries to visit.
|
|
56
|
+
*
|
|
57
|
+
* This can be a performance bottleneck as arrays are created on every node visit.
|
|
58
|
+
*
|
|
59
|
+
* @deprecated This will be replaced by a static `traversalType` set of orders in the future
|
|
60
|
+
*
|
|
61
|
+
* Currently, this is only used in 3 ways:
|
|
62
|
+
*
|
|
63
|
+
* - Traversing down the tree (normal DFS)
|
|
64
|
+
* - Traversing up the tree (ancestors)
|
|
65
|
+
* - Filtered version of traversal; which does not need to exist at the DFS level as the visitor
|
|
66
|
+
* can handle filtering
|
|
67
|
+
* - Sorted traversal of BundleGraph entries, which does not have a clear use-case, but may
|
|
68
|
+
* not be safe to remove
|
|
69
|
+
*
|
|
70
|
+
* Only due to the latter we aren't replacing this.
|
|
71
|
+
*/
|
|
72
|
+
getChildren: (nodeId: NodeId) => Array<NodeId>,
|
|
73
|
+
startNodeId?: ?NodeId,
|
|
74
|
+
|};
|
|
75
|
+
|
|
76
|
+
export default class Graph<TNode, TEdgeType: number = 1> {
|
|
77
|
+
nodes: Array<TNode | null>;
|
|
78
|
+
adjacencyList: AdjacencyList<TEdgeType>;
|
|
79
|
+
rootNodeId: ?NodeId;
|
|
80
|
+
_visited: ?BitSet;
|
|
81
|
+
|
|
82
|
+
constructor(opts: ?GraphOpts<TNode, TEdgeType>) {
|
|
83
|
+
this.nodes = opts?.nodes || [];
|
|
84
|
+
this.setRootNodeId(opts?.rootNodeId);
|
|
85
|
+
|
|
86
|
+
let adjacencyList = opts?.adjacencyList;
|
|
87
|
+
this.adjacencyList = adjacencyList
|
|
88
|
+
? AdjacencyList.deserialize(adjacencyList)
|
|
89
|
+
: new AdjacencyList<TEdgeType>();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setRootNodeId(id: ?NodeId) {
|
|
93
|
+
this.rootNodeId = id;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static deserialize(
|
|
97
|
+
opts: GraphOpts<TNode, TEdgeType>,
|
|
98
|
+
): Graph<TNode, TEdgeType> {
|
|
99
|
+
return new this({
|
|
100
|
+
nodes: opts.nodes,
|
|
101
|
+
adjacencyList: opts.adjacencyList,
|
|
102
|
+
rootNodeId: opts.rootNodeId,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
serialize(): SerializedGraph<TNode, TEdgeType> {
|
|
107
|
+
return {
|
|
108
|
+
nodes: this.nodes,
|
|
109
|
+
adjacencyList: this.adjacencyList.serialize(),
|
|
110
|
+
rootNodeId: this.rootNodeId,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Returns an iterator of all edges in the graph. This can be large, so iterating
|
|
115
|
+
// the complete list can be costly in large graphs. Used when merging graphs.
|
|
116
|
+
getAllEdges(): Iterator<Edge<TEdgeType | NullEdgeType>> {
|
|
117
|
+
return this.adjacencyList.getAllEdges();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
addNode(node: TNode): NodeId {
|
|
121
|
+
let id = this.adjacencyList.addNode();
|
|
122
|
+
this.nodes.push(node);
|
|
123
|
+
return id;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
hasNode(id: NodeId): boolean {
|
|
127
|
+
return this.nodes[id] != null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getNode(id: NodeId): ?TNode {
|
|
131
|
+
return this.nodes[id];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
addEdge(
|
|
135
|
+
from: NodeId,
|
|
136
|
+
to: NodeId,
|
|
137
|
+
type: TEdgeType | NullEdgeType = 1,
|
|
138
|
+
): boolean {
|
|
139
|
+
if (Number(type) === 0) {
|
|
140
|
+
throw new Error(`Edge type "${type}" not allowed`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (this.getNode(from) == null) {
|
|
144
|
+
throw new Error(`"from" node '${fromNodeId(from)}' not found`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (this.getNode(to) == null) {
|
|
148
|
+
throw new Error(`"to" node '${fromNodeId(to)}' not found`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return this.adjacencyList.addEdge(from, to, type);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
hasEdge(
|
|
155
|
+
from: NodeId,
|
|
156
|
+
to: NodeId,
|
|
157
|
+
type?: TEdgeType | NullEdgeType | Array<TEdgeType | NullEdgeType> = 1,
|
|
158
|
+
): boolean {
|
|
159
|
+
return this.adjacencyList.hasEdge(from, to, type);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getNodeIdsConnectedTo(
|
|
163
|
+
nodeId: NodeId,
|
|
164
|
+
type:
|
|
165
|
+
| TEdgeType
|
|
166
|
+
| NullEdgeType
|
|
167
|
+
| Array<TEdgeType | NullEdgeType>
|
|
168
|
+
| AllEdgeTypes = 1,
|
|
169
|
+
): Array<NodeId> {
|
|
170
|
+
this._assertHasNodeId(nodeId);
|
|
171
|
+
|
|
172
|
+
return this.adjacencyList.getNodeIdsConnectedTo(nodeId, type);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
getNodeIdsConnectedFrom(
|
|
176
|
+
nodeId: NodeId,
|
|
177
|
+
type:
|
|
178
|
+
| TEdgeType
|
|
179
|
+
| NullEdgeType
|
|
180
|
+
| Array<TEdgeType | NullEdgeType>
|
|
181
|
+
| AllEdgeTypes = 1,
|
|
182
|
+
): Array<NodeId> {
|
|
183
|
+
this._assertHasNodeId(nodeId);
|
|
184
|
+
|
|
185
|
+
return this.adjacencyList.getNodeIdsConnectedFrom(nodeId, type);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Removes node and any edges coming from or to that node
|
|
189
|
+
removeNode(nodeId: NodeId) {
|
|
190
|
+
if (!this.hasNode(nodeId)) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (let {type, from} of this.adjacencyList.getInboundEdgesByType(nodeId)) {
|
|
195
|
+
this._removeEdge(
|
|
196
|
+
from,
|
|
197
|
+
nodeId,
|
|
198
|
+
type,
|
|
199
|
+
// Do not allow orphans to be removed as this node could be one
|
|
200
|
+
// and is already being removed.
|
|
201
|
+
false,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (let {type, to} of this.adjacencyList.getOutboundEdgesByType(nodeId)) {
|
|
206
|
+
this._removeEdge(nodeId, to, type);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.nodes[nodeId] = null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
removeEdges(nodeId: NodeId, type: TEdgeType | NullEdgeType = 1) {
|
|
213
|
+
if (!this.hasNode(nodeId)) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (let to of this.getNodeIdsConnectedFrom(nodeId, type)) {
|
|
218
|
+
this._removeEdge(nodeId, to, type);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
removeEdge(
|
|
223
|
+
from: NodeId,
|
|
224
|
+
to: NodeId,
|
|
225
|
+
type: TEdgeType | NullEdgeType = 1,
|
|
226
|
+
removeOrphans: boolean = true,
|
|
227
|
+
) {
|
|
228
|
+
if (!this.adjacencyList.hasEdge(from, to, type)) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Edge from ${fromNodeId(from)} to ${fromNodeId(to)} not found!`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this._removeEdge(from, to, type, removeOrphans);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Removes edge and node the edge is to if the node is orphaned
|
|
238
|
+
_removeEdge(
|
|
239
|
+
from: NodeId,
|
|
240
|
+
to: NodeId,
|
|
241
|
+
type: TEdgeType | NullEdgeType = 1,
|
|
242
|
+
removeOrphans: boolean = true,
|
|
243
|
+
) {
|
|
244
|
+
if (!this.adjacencyList.hasEdge(from, to, type)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this.adjacencyList.removeEdge(from, to, type);
|
|
249
|
+
if (removeOrphans && this.isOrphanedNode(to)) {
|
|
250
|
+
this.removeNode(to);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
isOrphanedNode(nodeId: NodeId): boolean {
|
|
255
|
+
if (!this.hasNode(nodeId)) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (this.rootNodeId == null) {
|
|
260
|
+
// If the graph does not have a root, and there are inbound edges,
|
|
261
|
+
// this node should not be considered orphaned.
|
|
262
|
+
return !this.adjacencyList.hasInboundEdges(nodeId);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Otherwise, attempt to traverse backwards to the root. If there is a path,
|
|
266
|
+
// then this is not an orphaned node.
|
|
267
|
+
let hasPathToRoot = false;
|
|
268
|
+
// go back to traverseAncestors
|
|
269
|
+
this.traverseAncestors(
|
|
270
|
+
nodeId,
|
|
271
|
+
(ancestorId, _, actions) => {
|
|
272
|
+
if (ancestorId === this.rootNodeId) {
|
|
273
|
+
hasPathToRoot = true;
|
|
274
|
+
actions.stop();
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
ALL_EDGE_TYPES,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (hasPathToRoot) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
updateNode(nodeId: NodeId, node: TNode): void {
|
|
288
|
+
this._assertHasNodeId(nodeId);
|
|
289
|
+
this.nodes[nodeId] = node;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Update a node's downstream nodes making sure to prune any orphaned branches
|
|
293
|
+
replaceNodeIdsConnectedTo(
|
|
294
|
+
fromNodeId: NodeId,
|
|
295
|
+
toNodeIds: $ReadOnlyArray<NodeId>,
|
|
296
|
+
replaceFilter?: null | (NodeId => boolean),
|
|
297
|
+
type?: TEdgeType | NullEdgeType = 1,
|
|
298
|
+
): void {
|
|
299
|
+
this._assertHasNodeId(fromNodeId);
|
|
300
|
+
|
|
301
|
+
let outboundEdges = this.getNodeIdsConnectedFrom(fromNodeId, type);
|
|
302
|
+
let childrenToRemove = new Set(
|
|
303
|
+
replaceFilter
|
|
304
|
+
? outboundEdges.filter(toNodeId => replaceFilter(toNodeId))
|
|
305
|
+
: outboundEdges,
|
|
306
|
+
);
|
|
307
|
+
for (let toNodeId of toNodeIds) {
|
|
308
|
+
childrenToRemove.delete(toNodeId);
|
|
309
|
+
|
|
310
|
+
if (!this.hasEdge(fromNodeId, toNodeId, type)) {
|
|
311
|
+
this.addEdge(fromNodeId, toNodeId, type);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (let child of childrenToRemove) {
|
|
316
|
+
this._removeEdge(fromNodeId, child, type);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
traverse<TContext>(
|
|
321
|
+
visit: GraphVisitor<NodeId, TContext>,
|
|
322
|
+
startNodeId: ?NodeId,
|
|
323
|
+
type:
|
|
324
|
+
| TEdgeType
|
|
325
|
+
| NullEdgeType
|
|
326
|
+
| Array<TEdgeType | NullEdgeType>
|
|
327
|
+
| AllEdgeTypes = 1,
|
|
328
|
+
): ?TContext {
|
|
329
|
+
let enter = typeof visit === 'function' ? visit : visit.enter;
|
|
330
|
+
if (
|
|
331
|
+
type === ALL_EDGE_TYPES &&
|
|
332
|
+
enter &&
|
|
333
|
+
(typeof visit === 'function' || !visit.exit)
|
|
334
|
+
) {
|
|
335
|
+
return this.dfsFast(enter, startNodeId);
|
|
336
|
+
} else {
|
|
337
|
+
return this.dfs({
|
|
338
|
+
visit,
|
|
339
|
+
startNodeId,
|
|
340
|
+
getChildren: nodeId => this.getNodeIdsConnectedFrom(nodeId, type),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
filteredTraverse<TValue, TContext>(
|
|
346
|
+
filter: (NodeId, TraversalActions) => ?TValue,
|
|
347
|
+
visit: GraphVisitor<TValue, TContext>,
|
|
348
|
+
startNodeId: ?NodeId,
|
|
349
|
+
type?: TEdgeType | Array<TEdgeType | NullEdgeType> | AllEdgeTypes,
|
|
350
|
+
): ?TContext {
|
|
351
|
+
return this.traverse(mapVisitor(filter, visit), startNodeId, type);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
traverseAncestors<TContext>(
|
|
355
|
+
startNodeId: ?NodeId,
|
|
356
|
+
visit: GraphVisitor<NodeId, TContext>,
|
|
357
|
+
type:
|
|
358
|
+
| TEdgeType
|
|
359
|
+
| NullEdgeType
|
|
360
|
+
| Array<TEdgeType | NullEdgeType>
|
|
361
|
+
| AllEdgeTypes = 1,
|
|
362
|
+
): ?TContext {
|
|
363
|
+
return this.dfs({
|
|
364
|
+
visit,
|
|
365
|
+
startNodeId,
|
|
366
|
+
getChildren: nodeId => this.getNodeIdsConnectedTo(nodeId, type),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
dfsFast<TContext>(
|
|
371
|
+
visit: GraphTraversalCallback<NodeId, TContext>,
|
|
372
|
+
startNodeId: ?NodeId,
|
|
373
|
+
): ?TContext {
|
|
374
|
+
let traversalStartNode = nullthrows(
|
|
375
|
+
startNodeId ?? this.rootNodeId,
|
|
376
|
+
'A start node is required to traverse',
|
|
377
|
+
);
|
|
378
|
+
this._assertHasNodeId(traversalStartNode);
|
|
379
|
+
|
|
380
|
+
let visited;
|
|
381
|
+
if (!this._visited || this._visited.capacity < this.nodes.length) {
|
|
382
|
+
this._visited = new BitSet(this.nodes.length);
|
|
383
|
+
visited = this._visited;
|
|
384
|
+
} else {
|
|
385
|
+
visited = this._visited;
|
|
386
|
+
visited.clear();
|
|
387
|
+
}
|
|
388
|
+
// Take shared instance to avoid re-entrancy issues.
|
|
389
|
+
this._visited = null;
|
|
390
|
+
|
|
391
|
+
let stopped = false;
|
|
392
|
+
let skipped = false;
|
|
393
|
+
let actions: TraversalActions = {
|
|
394
|
+
skipChildren() {
|
|
395
|
+
skipped = true;
|
|
396
|
+
},
|
|
397
|
+
stop() {
|
|
398
|
+
stopped = true;
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
let queue = [{nodeId: traversalStartNode, context: null}];
|
|
403
|
+
while (queue.length !== 0) {
|
|
404
|
+
let {nodeId, context} = queue.pop();
|
|
405
|
+
if (!this.hasNode(nodeId) || visited.has(nodeId)) continue;
|
|
406
|
+
visited.add(nodeId);
|
|
407
|
+
|
|
408
|
+
skipped = false;
|
|
409
|
+
let newContext = visit(nodeId, context, actions);
|
|
410
|
+
if (typeof newContext !== 'undefined') {
|
|
411
|
+
// $FlowFixMe[reassign-const]
|
|
412
|
+
context = newContext;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (skipped) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (stopped) {
|
|
420
|
+
this._visited = visited;
|
|
421
|
+
return context;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
this.adjacencyList.forEachNodeIdConnectedFromReverse(nodeId, child => {
|
|
425
|
+
if (!visited.has(child)) {
|
|
426
|
+
queue.push({nodeId: child, context});
|
|
427
|
+
}
|
|
428
|
+
return false;
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
this._visited = visited;
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// A post-order implementation of dfsFast
|
|
437
|
+
postOrderDfsFast(
|
|
438
|
+
visit: GraphTraversalCallback<NodeId, TraversalActions>,
|
|
439
|
+
startNodeId: ?NodeId,
|
|
440
|
+
): void {
|
|
441
|
+
let traversalStartNode = nullthrows(
|
|
442
|
+
startNodeId ?? this.rootNodeId,
|
|
443
|
+
'A start node is required to traverse',
|
|
444
|
+
);
|
|
445
|
+
this._assertHasNodeId(traversalStartNode);
|
|
446
|
+
|
|
447
|
+
let visited;
|
|
448
|
+
if (!this._visited || this._visited.capacity < this.nodes.length) {
|
|
449
|
+
this._visited = new BitSet(this.nodes.length);
|
|
450
|
+
visited = this._visited;
|
|
451
|
+
} else {
|
|
452
|
+
visited = this._visited;
|
|
453
|
+
visited.clear();
|
|
454
|
+
}
|
|
455
|
+
this._visited = null;
|
|
456
|
+
|
|
457
|
+
let stopped = false;
|
|
458
|
+
let actions: TraversalActions = {
|
|
459
|
+
stop() {
|
|
460
|
+
stopped = true;
|
|
461
|
+
},
|
|
462
|
+
skipChildren() {
|
|
463
|
+
throw new Error(
|
|
464
|
+
'Calling skipChildren inside a post-order traversal is not allowed',
|
|
465
|
+
);
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
let queue = [traversalStartNode];
|
|
470
|
+
while (queue.length !== 0) {
|
|
471
|
+
let nodeId = queue[queue.length - 1];
|
|
472
|
+
|
|
473
|
+
if (!visited.has(nodeId)) {
|
|
474
|
+
visited.add(nodeId);
|
|
475
|
+
|
|
476
|
+
this.adjacencyList.forEachNodeIdConnectedFromReverse(nodeId, child => {
|
|
477
|
+
if (!visited.has(child)) {
|
|
478
|
+
queue.push(child);
|
|
479
|
+
}
|
|
480
|
+
return false;
|
|
481
|
+
});
|
|
482
|
+
} else {
|
|
483
|
+
queue.pop();
|
|
484
|
+
visit(nodeId, null, actions);
|
|
485
|
+
|
|
486
|
+
if (stopped) {
|
|
487
|
+
this._visited = visited;
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
this._visited = visited;
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Iterative implementation of DFS that supports all use-cases.
|
|
499
|
+
*
|
|
500
|
+
* This replaces `dfs` and will replace `dfsFast`.
|
|
501
|
+
*/
|
|
502
|
+
dfs<TContext>({
|
|
503
|
+
visit,
|
|
504
|
+
startNodeId,
|
|
505
|
+
getChildren,
|
|
506
|
+
}: DFSParams<TContext>): ?TContext {
|
|
507
|
+
let traversalStartNode = nullthrows(
|
|
508
|
+
startNodeId ?? this.rootNodeId,
|
|
509
|
+
'A start node is required to traverse',
|
|
510
|
+
);
|
|
511
|
+
this._assertHasNodeId(traversalStartNode);
|
|
512
|
+
|
|
513
|
+
let visited;
|
|
514
|
+
if (!this._visited || this._visited.capacity < this.nodes.length) {
|
|
515
|
+
this._visited = new BitSet(this.nodes.length);
|
|
516
|
+
visited = this._visited;
|
|
517
|
+
} else {
|
|
518
|
+
visited = this._visited;
|
|
519
|
+
visited.clear();
|
|
520
|
+
}
|
|
521
|
+
// Take shared instance to avoid re-entrancy issues.
|
|
522
|
+
this._visited = null;
|
|
523
|
+
|
|
524
|
+
let stopped = false;
|
|
525
|
+
let skipped = false;
|
|
526
|
+
let actions: TraversalActions = {
|
|
527
|
+
skipChildren() {
|
|
528
|
+
skipped = true;
|
|
529
|
+
},
|
|
530
|
+
stop() {
|
|
531
|
+
stopped = true;
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const queue: DFSCommand<TContext>[] = [
|
|
536
|
+
{nodeId: traversalStartNode, context: null},
|
|
537
|
+
];
|
|
538
|
+
const enter = typeof visit === 'function' ? visit : visit.enter;
|
|
539
|
+
while (queue.length !== 0) {
|
|
540
|
+
const command = queue.pop();
|
|
541
|
+
|
|
542
|
+
if (command.exit != null) {
|
|
543
|
+
let {nodeId, context, exit} = command;
|
|
544
|
+
let newContext = exit(nodeId, command.context, actions);
|
|
545
|
+
if (typeof newContext !== 'undefined') {
|
|
546
|
+
// $FlowFixMe[reassign-const]
|
|
547
|
+
context = newContext;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (skipped) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (stopped) {
|
|
555
|
+
this._visited = visited;
|
|
556
|
+
return context;
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
let {nodeId, context} = command;
|
|
560
|
+
if (!this.hasNode(nodeId) || visited.has(nodeId)) continue;
|
|
561
|
+
visited.add(nodeId);
|
|
562
|
+
|
|
563
|
+
skipped = false;
|
|
564
|
+
if (enter) {
|
|
565
|
+
let newContext = enter(nodeId, context, actions);
|
|
566
|
+
if (typeof newContext !== 'undefined') {
|
|
567
|
+
// $FlowFixMe[reassign-const]
|
|
568
|
+
context = newContext;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (skipped) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (stopped) {
|
|
577
|
+
this._visited = visited;
|
|
578
|
+
return context;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (typeof visit !== 'function' && visit.exit) {
|
|
582
|
+
queue.push({
|
|
583
|
+
nodeId,
|
|
584
|
+
exit: visit.exit,
|
|
585
|
+
context,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// TODO turn into generator function
|
|
590
|
+
const children = getChildren(nodeId);
|
|
591
|
+
for (let i = children.length - 1; i > -1; i -= 1) {
|
|
592
|
+
const child = children[i];
|
|
593
|
+
if (visited.has(child)) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
queue.push({nodeId: child, context});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
this._visited = visited;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
bfs(visit: (nodeId: NodeId) => ?boolean): ?NodeId {
|
|
606
|
+
let rootNodeId = nullthrows(
|
|
607
|
+
this.rootNodeId,
|
|
608
|
+
'A root node is required to traverse',
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
let queue: Array<NodeId> = [rootNodeId];
|
|
612
|
+
let visited = new Set<NodeId>([rootNodeId]);
|
|
613
|
+
|
|
614
|
+
while (queue.length > 0) {
|
|
615
|
+
let node = queue.shift();
|
|
616
|
+
let stop = visit(rootNodeId);
|
|
617
|
+
if (stop === true) {
|
|
618
|
+
return node;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
for (let child of this.getNodeIdsConnectedFrom(node)) {
|
|
622
|
+
if (!visited.has(child)) {
|
|
623
|
+
visited.add(child);
|
|
624
|
+
queue.push(child);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
topoSort(type?: TEdgeType): Array<NodeId> {
|
|
633
|
+
let sorted: Array<NodeId> = [];
|
|
634
|
+
this.traverse(
|
|
635
|
+
{
|
|
636
|
+
exit: nodeId => {
|
|
637
|
+
sorted.push(nodeId);
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
null,
|
|
641
|
+
type,
|
|
642
|
+
);
|
|
643
|
+
return sorted.reverse();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
findAncestor(nodeId: NodeId, fn: (nodeId: NodeId) => boolean): ?NodeId {
|
|
647
|
+
let res = null;
|
|
648
|
+
this.traverseAncestors(nodeId, (nodeId, ctx, traversal) => {
|
|
649
|
+
if (fn(nodeId)) {
|
|
650
|
+
res = nodeId;
|
|
651
|
+
traversal.stop();
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
return res;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
findAncestors(
|
|
658
|
+
nodeId: NodeId,
|
|
659
|
+
fn: (nodeId: NodeId) => boolean,
|
|
660
|
+
): Array<NodeId> {
|
|
661
|
+
let res = [];
|
|
662
|
+
this.traverseAncestors(nodeId, (nodeId, ctx, traversal) => {
|
|
663
|
+
if (fn(nodeId)) {
|
|
664
|
+
res.push(nodeId);
|
|
665
|
+
traversal.skipChildren();
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
return res;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
findDescendant(nodeId: NodeId, fn: (nodeId: NodeId) => boolean): ?NodeId {
|
|
672
|
+
let res = null;
|
|
673
|
+
this.traverse((nodeId, ctx, traversal) => {
|
|
674
|
+
if (fn(nodeId)) {
|
|
675
|
+
res = nodeId;
|
|
676
|
+
traversal.stop();
|
|
677
|
+
}
|
|
678
|
+
}, nodeId);
|
|
679
|
+
return res;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
findDescendants(
|
|
683
|
+
nodeId: NodeId,
|
|
684
|
+
fn: (nodeId: NodeId) => boolean,
|
|
685
|
+
): Array<NodeId> {
|
|
686
|
+
let res = [];
|
|
687
|
+
this.traverse((nodeId, ctx, traversal) => {
|
|
688
|
+
if (fn(nodeId)) {
|
|
689
|
+
res.push(nodeId);
|
|
690
|
+
traversal.skipChildren();
|
|
691
|
+
}
|
|
692
|
+
}, nodeId);
|
|
693
|
+
return res;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
_assertHasNodeId(nodeId: NodeId) {
|
|
697
|
+
if (!this.hasNode(nodeId)) {
|
|
698
|
+
throw new Error('Does not have node ' + fromNodeId(nodeId));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export function mapVisitor<NodeId, TValue, TContext>(
|
|
704
|
+
filter: (NodeId, TraversalActions) => ?TValue,
|
|
705
|
+
visit: GraphVisitor<TValue, TContext>,
|
|
706
|
+
): GraphVisitor<NodeId, TContext> {
|
|
707
|
+
function makeEnter(visit) {
|
|
708
|
+
return function mappedEnter(nodeId, context, actions) {
|
|
709
|
+
let value = filter(nodeId, actions);
|
|
710
|
+
if (value != null) {
|
|
711
|
+
return visit(value, context, actions);
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (typeof visit === 'function') {
|
|
717
|
+
return makeEnter(visit);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
let mapped = {};
|
|
721
|
+
if (visit.enter != null) {
|
|
722
|
+
mapped.enter = makeEnter(visit.enter);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (visit.exit != null) {
|
|
726
|
+
mapped.exit = function mappedExit(nodeId, context, actions) {
|
|
727
|
+
let exit = visit.exit;
|
|
728
|
+
if (!exit) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
let value = filter(nodeId, actions);
|
|
733
|
+
if (value != null) {
|
|
734
|
+
return exit(value, context, actions);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return mapped;
|
|
740
|
+
}
|