@parcel/graph 3.2.1-dev.3195 → 3.2.1-dev.3196
Sign up to get free protection for your applications and to get access to all the features.
- package/lib/AdjacencyList.js +1 -1
- package/lib/Graph.js +129 -1
- package/package.json +3 -2
- package/src/AdjacencyList.js +2 -2
- package/src/Graph.js +169 -10
- package/test/Graph.test.js +228 -2
package/lib/AdjacencyList.js
CHANGED
@@ -240,7 +240,7 @@ class AdjacencyList {
|
|
240
240
|
*
|
241
241
|
* Note that this method does not increment the node count
|
242
242
|
* (that only happens in `addEdge`), it _may_ preemptively resize
|
243
|
-
* the nodes array if it is at capacity, under the
|
243
|
+
* the nodes array if it is at capacity, under the assumption that
|
244
244
|
* at least 1 edge to or from this new node will be added.
|
245
245
|
*
|
246
246
|
* Returns the id of the added node.
|
package/lib/Graph.js
CHANGED
@@ -7,6 +7,13 @@ exports.default = exports.ALL_EDGE_TYPES = void 0;
|
|
7
7
|
exports.mapVisitor = mapVisitor;
|
8
8
|
var _types = require("./types");
|
9
9
|
var _AdjacencyList = _interopRequireDefault(require("./AdjacencyList"));
|
10
|
+
function _featureFlags() {
|
11
|
+
const data = require("@parcel/feature-flags");
|
12
|
+
_featureFlags = function () {
|
13
|
+
return data;
|
14
|
+
};
|
15
|
+
return data;
|
16
|
+
}
|
10
17
|
var _BitSet = require("./BitSet");
|
11
18
|
function _nullthrows() {
|
12
19
|
const data = _interopRequireDefault(require("nullthrows"));
|
@@ -17,6 +24,15 @@ function _nullthrows() {
|
|
17
24
|
}
|
18
25
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
19
26
|
const ALL_EDGE_TYPES = exports.ALL_EDGE_TYPES = -1;
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Internal type used for queue iterative DFS implementation.
|
30
|
+
*/
|
31
|
+
|
32
|
+
/**
|
33
|
+
* Options for DFS traversal.
|
34
|
+
*/
|
35
|
+
|
20
36
|
class Graph {
|
21
37
|
constructor(opts) {
|
22
38
|
this.nodes = (opts === null || opts === void 0 ? void 0 : opts.nodes) || [];
|
@@ -179,7 +195,11 @@ class Graph {
|
|
179
195
|
if (type === ALL_EDGE_TYPES && enter && (typeof visit === 'function' || !visit.exit)) {
|
180
196
|
return this.dfsFast(enter, startNodeId);
|
181
197
|
} else {
|
182
|
-
return this.
|
198
|
+
return (0, _featureFlags().getFeatureFlag)('dfsFasterRefactor') ? this.dfsNew({
|
199
|
+
visit,
|
200
|
+
startNodeId,
|
201
|
+
getChildren: nodeId => this.getNodeIdsConnectedFrom(nodeId, type)
|
202
|
+
}) : this.dfs({
|
183
203
|
visit,
|
184
204
|
startNodeId,
|
185
205
|
getChildren: nodeId => this.getNodeIdsConnectedFrom(nodeId, type)
|
@@ -301,6 +321,114 @@ class Graph {
|
|
301
321
|
}
|
302
322
|
this._visited = visited;
|
303
323
|
}
|
324
|
+
|
325
|
+
/**
|
326
|
+
* Iterative implementation of DFS that supports all use-cases.
|
327
|
+
*
|
328
|
+
* This replaces `dfs` and will replace `dfsFast`.
|
329
|
+
*/
|
330
|
+
dfsNew({
|
331
|
+
visit,
|
332
|
+
startNodeId,
|
333
|
+
getChildren
|
334
|
+
}) {
|
335
|
+
let traversalStartNode = (0, _nullthrows().default)(startNodeId ?? this.rootNodeId, 'A start node is required to traverse');
|
336
|
+
this._assertHasNodeId(traversalStartNode);
|
337
|
+
let visited;
|
338
|
+
if (!this._visited || this._visited.capacity < this.nodes.length) {
|
339
|
+
this._visited = new _BitSet.BitSet(this.nodes.length);
|
340
|
+
visited = this._visited;
|
341
|
+
} else {
|
342
|
+
visited = this._visited;
|
343
|
+
visited.clear();
|
344
|
+
}
|
345
|
+
// Take shared instance to avoid re-entrancy issues.
|
346
|
+
this._visited = null;
|
347
|
+
let stopped = false;
|
348
|
+
let skipped = false;
|
349
|
+
let actions = {
|
350
|
+
skipChildren() {
|
351
|
+
skipped = true;
|
352
|
+
},
|
353
|
+
stop() {
|
354
|
+
stopped = true;
|
355
|
+
}
|
356
|
+
};
|
357
|
+
const queue = [{
|
358
|
+
nodeId: traversalStartNode,
|
359
|
+
context: null
|
360
|
+
}];
|
361
|
+
const enter = typeof visit === 'function' ? visit : visit.enter;
|
362
|
+
while (queue.length !== 0) {
|
363
|
+
const command = queue.pop();
|
364
|
+
if (command.exit != null) {
|
365
|
+
let {
|
366
|
+
nodeId,
|
367
|
+
context,
|
368
|
+
exit
|
369
|
+
} = command;
|
370
|
+
let newContext = exit(nodeId, command.context, actions);
|
371
|
+
if (typeof newContext !== 'undefined') {
|
372
|
+
// $FlowFixMe[reassign-const]
|
373
|
+
context = newContext;
|
374
|
+
}
|
375
|
+
if (skipped) {
|
376
|
+
continue;
|
377
|
+
}
|
378
|
+
if (stopped) {
|
379
|
+
this._visited = visited;
|
380
|
+
return context;
|
381
|
+
}
|
382
|
+
} else {
|
383
|
+
let {
|
384
|
+
nodeId,
|
385
|
+
context
|
386
|
+
} = command;
|
387
|
+
if (!this.hasNode(nodeId) || visited.has(nodeId)) continue;
|
388
|
+
visited.add(nodeId);
|
389
|
+
skipped = false;
|
390
|
+
if (enter) {
|
391
|
+
let newContext = enter(nodeId, context, actions);
|
392
|
+
if (typeof newContext !== 'undefined') {
|
393
|
+
// $FlowFixMe[reassign-const]
|
394
|
+
context = newContext;
|
395
|
+
}
|
396
|
+
}
|
397
|
+
if (skipped) {
|
398
|
+
continue;
|
399
|
+
}
|
400
|
+
if (stopped) {
|
401
|
+
this._visited = visited;
|
402
|
+
return context;
|
403
|
+
}
|
404
|
+
if (typeof visit !== 'function' && visit.exit) {
|
405
|
+
queue.push({
|
406
|
+
nodeId,
|
407
|
+
exit: visit.exit,
|
408
|
+
context
|
409
|
+
});
|
410
|
+
}
|
411
|
+
|
412
|
+
// TODO turn into generator function
|
413
|
+
const children = getChildren(nodeId);
|
414
|
+
for (let i = children.length - 1; i > -1; i -= 1) {
|
415
|
+
const child = children[i];
|
416
|
+
if (visited.has(child)) {
|
417
|
+
continue;
|
418
|
+
}
|
419
|
+
queue.push({
|
420
|
+
nodeId: child,
|
421
|
+
context
|
422
|
+
});
|
423
|
+
}
|
424
|
+
}
|
425
|
+
}
|
426
|
+
this._visited = visited;
|
427
|
+
}
|
428
|
+
|
429
|
+
/**
|
430
|
+
* @deprecated Will be replaced by `dfsNew`
|
431
|
+
*/
|
304
432
|
dfs({
|
305
433
|
visit,
|
306
434
|
startNodeId,
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@parcel/graph",
|
3
|
-
"version": "3.2.1-dev.
|
3
|
+
"version": "3.2.1-dev.3196+c9a0ab031",
|
4
4
|
"description": "Blazing fast, zero configuration web application bundler",
|
5
5
|
"license": "MIT",
|
6
6
|
"publishConfig": {
|
@@ -20,7 +20,8 @@
|
|
20
20
|
"node": ">= 16.0.0"
|
21
21
|
},
|
22
22
|
"dependencies": {
|
23
|
+
"@parcel/feature-flags": "2.12.1-dev.3196+c9a0ab031",
|
23
24
|
"nullthrows": "^1.1.1"
|
24
25
|
},
|
25
|
-
"gitHead": "
|
26
|
+
"gitHead": "c9a0ab031986a896a5991de16420b12988d013cd"
|
26
27
|
}
|
package/src/AdjacencyList.js
CHANGED
@@ -30,7 +30,7 @@ export type AdjacencyListOptions<TEdgeType> = {|
|
|
30
30
|
minGrowFactor?: number,
|
31
31
|
/** The size after which to grow the capacity by the minimum factor. */
|
32
32
|
peakCapacity?: number,
|
33
|
-
/** The percentage of deleted edges above which the
|
33
|
+
/** The percentage of deleted edges above which the capacity should shrink. */
|
34
34
|
unloadFactor?: number,
|
35
35
|
/** The amount by which to shrink the capacity. */
|
36
36
|
shrinkFactor?: number,
|
@@ -328,7 +328,7 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
328
328
|
*
|
329
329
|
* Note that this method does not increment the node count
|
330
330
|
* (that only happens in `addEdge`), it _may_ preemptively resize
|
331
|
-
* the nodes array if it is at capacity, under the
|
331
|
+
* the nodes array if it is at capacity, under the assumption that
|
332
332
|
* at least 1 edge to or from this new node will be added.
|
333
333
|
*
|
334
334
|
* Returns the id of the added node.
|
package/src/Graph.js
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
import {fromNodeId} from './types';
|
4
4
|
import AdjacencyList, {type SerializedAdjacencyList} from './AdjacencyList';
|
5
5
|
import type {Edge, NodeId} from './types';
|
6
|
+
import {getFeatureFlag} from '@parcel/feature-flags';
|
6
7
|
import type {
|
7
8
|
TraversalActions,
|
8
9
|
GraphVisitor,
|
@@ -28,6 +29,51 @@ export type SerializedGraph<TNode, TEdgeType: number = 1> = {|
|
|
28
29
|
export type AllEdgeTypes = -1;
|
29
30
|
export const ALL_EDGE_TYPES: AllEdgeTypes = -1;
|
30
31
|
|
32
|
+
type DFSCommandVisit<TContext> = {|
|
33
|
+
nodeId: NodeId,
|
34
|
+
context: TContext | null,
|
35
|
+
|};
|
36
|
+
|
37
|
+
type DFSCommandExit<TContext> = {|
|
38
|
+
nodeId: NodeId,
|
39
|
+
exit: GraphTraversalCallback<NodeId, TContext>,
|
40
|
+
context: TContext | null,
|
41
|
+
|};
|
42
|
+
|
43
|
+
/**
|
44
|
+
* Internal type used for queue iterative DFS implementation.
|
45
|
+
*/
|
46
|
+
type DFSCommand<TContext> =
|
47
|
+
| DFSCommandVisit<TContext>
|
48
|
+
| DFSCommandExit<TContext>;
|
49
|
+
|
50
|
+
/**
|
51
|
+
* Options for DFS traversal.
|
52
|
+
*/
|
53
|
+
export type DFSParams<TContext> = {|
|
54
|
+
visit: GraphVisitor<NodeId, TContext>,
|
55
|
+
/**
|
56
|
+
* Custom function to get next entries to visit.
|
57
|
+
*
|
58
|
+
* This can be a performance bottleneck as arrays are created on every node visit.
|
59
|
+
*
|
60
|
+
* @deprecated This will be replaced by a static `traversalType` set of orders in the future
|
61
|
+
*
|
62
|
+
* Currently, this is only used in 3 ways:
|
63
|
+
*
|
64
|
+
* - Traversing down the tree (normal DFS)
|
65
|
+
* - Traversing up the tree (ancestors)
|
66
|
+
* - Filtered version of traversal; which does not need to exist at the DFS level as the visitor
|
67
|
+
* can handle filtering
|
68
|
+
* - Sorted traversal of BundleGraph entries, which does not have a clear use-case, but may
|
69
|
+
* not be safe to remove
|
70
|
+
*
|
71
|
+
* Only due to the latter we aren't replacing this.
|
72
|
+
*/
|
73
|
+
getChildren: (nodeId: NodeId) => Array<NodeId>,
|
74
|
+
startNodeId?: ?NodeId,
|
75
|
+
|};
|
76
|
+
|
31
77
|
export default class Graph<TNode, TEdgeType: number = 1> {
|
32
78
|
nodes: Array<TNode | null>;
|
33
79
|
adjacencyList: AdjacencyList<TEdgeType>;
|
@@ -289,11 +335,17 @@ export default class Graph<TNode, TEdgeType: number = 1> {
|
|
289
335
|
) {
|
290
336
|
return this.dfsFast(enter, startNodeId);
|
291
337
|
} else {
|
292
|
-
return
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
338
|
+
return getFeatureFlag('dfsFasterRefactor')
|
339
|
+
? this.dfsNew({
|
340
|
+
visit,
|
341
|
+
startNodeId,
|
342
|
+
getChildren: nodeId => this.getNodeIdsConnectedFrom(nodeId, type),
|
343
|
+
})
|
344
|
+
: this.dfs({
|
345
|
+
visit,
|
346
|
+
startNodeId,
|
347
|
+
getChildren: nodeId => this.getNodeIdsConnectedFrom(nodeId, type),
|
348
|
+
});
|
297
349
|
}
|
298
350
|
}
|
299
351
|
|
@@ -449,15 +501,122 @@ export default class Graph<TNode, TEdgeType: number = 1> {
|
|
449
501
|
return;
|
450
502
|
}
|
451
503
|
|
504
|
+
/**
|
505
|
+
* Iterative implementation of DFS that supports all use-cases.
|
506
|
+
*
|
507
|
+
* This replaces `dfs` and will replace `dfsFast`.
|
508
|
+
*/
|
509
|
+
dfsNew<TContext>({
|
510
|
+
visit,
|
511
|
+
startNodeId,
|
512
|
+
getChildren,
|
513
|
+
}: DFSParams<TContext>): ?TContext {
|
514
|
+
let traversalStartNode = nullthrows(
|
515
|
+
startNodeId ?? this.rootNodeId,
|
516
|
+
'A start node is required to traverse',
|
517
|
+
);
|
518
|
+
this._assertHasNodeId(traversalStartNode);
|
519
|
+
|
520
|
+
let visited;
|
521
|
+
if (!this._visited || this._visited.capacity < this.nodes.length) {
|
522
|
+
this._visited = new BitSet(this.nodes.length);
|
523
|
+
visited = this._visited;
|
524
|
+
} else {
|
525
|
+
visited = this._visited;
|
526
|
+
visited.clear();
|
527
|
+
}
|
528
|
+
// Take shared instance to avoid re-entrancy issues.
|
529
|
+
this._visited = null;
|
530
|
+
|
531
|
+
let stopped = false;
|
532
|
+
let skipped = false;
|
533
|
+
let actions: TraversalActions = {
|
534
|
+
skipChildren() {
|
535
|
+
skipped = true;
|
536
|
+
},
|
537
|
+
stop() {
|
538
|
+
stopped = true;
|
539
|
+
},
|
540
|
+
};
|
541
|
+
|
542
|
+
const queue: DFSCommand<TContext>[] = [
|
543
|
+
{nodeId: traversalStartNode, context: null},
|
544
|
+
];
|
545
|
+
const enter = typeof visit === 'function' ? visit : visit.enter;
|
546
|
+
while (queue.length !== 0) {
|
547
|
+
const command = queue.pop();
|
548
|
+
|
549
|
+
if (command.exit != null) {
|
550
|
+
let {nodeId, context, exit} = command;
|
551
|
+
let newContext = exit(nodeId, command.context, actions);
|
552
|
+
if (typeof newContext !== 'undefined') {
|
553
|
+
// $FlowFixMe[reassign-const]
|
554
|
+
context = newContext;
|
555
|
+
}
|
556
|
+
|
557
|
+
if (skipped) {
|
558
|
+
continue;
|
559
|
+
}
|
560
|
+
|
561
|
+
if (stopped) {
|
562
|
+
this._visited = visited;
|
563
|
+
return context;
|
564
|
+
}
|
565
|
+
} else {
|
566
|
+
let {nodeId, context} = command;
|
567
|
+
if (!this.hasNode(nodeId) || visited.has(nodeId)) continue;
|
568
|
+
visited.add(nodeId);
|
569
|
+
|
570
|
+
skipped = false;
|
571
|
+
if (enter) {
|
572
|
+
let newContext = enter(nodeId, context, actions);
|
573
|
+
if (typeof newContext !== 'undefined') {
|
574
|
+
// $FlowFixMe[reassign-const]
|
575
|
+
context = newContext;
|
576
|
+
}
|
577
|
+
}
|
578
|
+
|
579
|
+
if (skipped) {
|
580
|
+
continue;
|
581
|
+
}
|
582
|
+
|
583
|
+
if (stopped) {
|
584
|
+
this._visited = visited;
|
585
|
+
return context;
|
586
|
+
}
|
587
|
+
|
588
|
+
if (typeof visit !== 'function' && visit.exit) {
|
589
|
+
queue.push({
|
590
|
+
nodeId,
|
591
|
+
exit: visit.exit,
|
592
|
+
context,
|
593
|
+
});
|
594
|
+
}
|
595
|
+
|
596
|
+
// TODO turn into generator function
|
597
|
+
const children = getChildren(nodeId);
|
598
|
+
for (let i = children.length - 1; i > -1; i -= 1) {
|
599
|
+
const child = children[i];
|
600
|
+
if (visited.has(child)) {
|
601
|
+
continue;
|
602
|
+
}
|
603
|
+
|
604
|
+
queue.push({nodeId: child, context});
|
605
|
+
}
|
606
|
+
}
|
607
|
+
}
|
608
|
+
|
609
|
+
this._visited = visited;
|
610
|
+
}
|
611
|
+
|
612
|
+
/**
|
613
|
+
* @deprecated Will be replaced by `dfsNew`
|
614
|
+
*/
|
452
615
|
dfs<TContext>({
|
453
616
|
visit,
|
454
617
|
startNodeId,
|
455
618
|
getChildren,
|
456
|
-
}: {
|
457
|
-
visit: GraphVisitor<NodeId, TContext>,
|
458
|
-
getChildren(nodeId: NodeId): Array<NodeId>,
|
459
|
-
startNodeId?: ?NodeId,
|
460
|
-
|}): ?TContext {
|
619
|
+
}: DFSParams<TContext>): ?TContext {
|
461
620
|
let traversalStartNode = nullthrows(
|
462
621
|
startNodeId ?? this.rootNodeId,
|
463
622
|
'A start node is required to traverse',
|
package/test/Graph.test.js
CHANGED
@@ -2,9 +2,10 @@
|
|
2
2
|
|
3
3
|
import assert from 'assert';
|
4
4
|
import sinon from 'sinon';
|
5
|
+
import type {TraversalActions} from '@parcel/types-internal';
|
5
6
|
|
6
|
-
import Graph from '../src/Graph';
|
7
|
-
import {toNodeId} from '../src/types';
|
7
|
+
import Graph, {type DFSParams} from '../src/Graph';
|
8
|
+
import {toNodeId, type NodeId} from '../src/types';
|
8
9
|
|
9
10
|
describe('Graph', () => {
|
10
11
|
it('constructor should initialize an empty graph', () => {
|
@@ -340,4 +341,229 @@ describe('Graph', () => {
|
|
340
341
|
assert.deepEqual(graph.nodes.filter(Boolean), ['root']);
|
341
342
|
assert.deepStrictEqual(Array.from(graph.getAllEdges()), []);
|
342
343
|
});
|
344
|
+
|
345
|
+
describe('dfs(...)', () => {
|
346
|
+
function testSuite(
|
347
|
+
name: string,
|
348
|
+
dfsImpl: (graph: Graph<string>, DFSParams<mixed>) => mixed | null | void,
|
349
|
+
) {
|
350
|
+
it(`${name} throws if the graph is empty`, () => {
|
351
|
+
const graph = new Graph();
|
352
|
+
const visit = sinon.stub();
|
353
|
+
const getChildren = sinon.stub();
|
354
|
+
assert.throws(() => {
|
355
|
+
dfsImpl(graph, {
|
356
|
+
visit,
|
357
|
+
startNodeId: 0,
|
358
|
+
getChildren,
|
359
|
+
});
|
360
|
+
}, /Does not have node 0/);
|
361
|
+
});
|
362
|
+
|
363
|
+
it(`${name} visits a single node`, () => {
|
364
|
+
const graph = new Graph();
|
365
|
+
graph.addNode('root');
|
366
|
+
const visit = sinon.stub();
|
367
|
+
const getChildren = () => [];
|
368
|
+
dfsImpl(graph, {
|
369
|
+
visit,
|
370
|
+
startNodeId: 0,
|
371
|
+
getChildren,
|
372
|
+
});
|
373
|
+
|
374
|
+
assert(visit.calledOnce);
|
375
|
+
});
|
376
|
+
|
377
|
+
it(`${name} visits all connected nodes in DFS order`, () => {
|
378
|
+
const graph = new Graph();
|
379
|
+
graph.addNode('0');
|
380
|
+
graph.addNode('1');
|
381
|
+
graph.addNode('2');
|
382
|
+
graph.addNode('3');
|
383
|
+
graph.addNode('disconnected-1');
|
384
|
+
graph.addNode('disconnected-2');
|
385
|
+
graph.addEdge(0, 1);
|
386
|
+
graph.addEdge(0, 2);
|
387
|
+
graph.addEdge(1, 3);
|
388
|
+
graph.addEdge(2, 3);
|
389
|
+
|
390
|
+
const order = [];
|
391
|
+
const visit = (node: NodeId) => {
|
392
|
+
order.push(node);
|
393
|
+
};
|
394
|
+
const getChildren = (node: NodeId) =>
|
395
|
+
graph.getNodeIdsConnectedFrom(node);
|
396
|
+
dfsImpl(graph, {
|
397
|
+
visit,
|
398
|
+
startNodeId: 0,
|
399
|
+
getChildren,
|
400
|
+
});
|
401
|
+
|
402
|
+
assert.deepEqual(order, [0, 1, 3, 2]);
|
403
|
+
});
|
404
|
+
|
405
|
+
describe(`${name} actions tests`, () => {
|
406
|
+
it(`${name} skips children if skip is called on a node`, () => {
|
407
|
+
const graph = new Graph();
|
408
|
+
graph.addNode('0');
|
409
|
+
graph.addNode('1');
|
410
|
+
graph.addNode('2');
|
411
|
+
graph.addNode('3');
|
412
|
+
graph.addNode('disconnected-1');
|
413
|
+
graph.addNode('disconnected-2');
|
414
|
+
graph.addEdge(0, 1);
|
415
|
+
graph.addEdge(1, 2);
|
416
|
+
graph.addEdge(0, 3);
|
417
|
+
|
418
|
+
const order = [];
|
419
|
+
const visit = (
|
420
|
+
node: NodeId,
|
421
|
+
context: mixed | null,
|
422
|
+
actions: TraversalActions,
|
423
|
+
) => {
|
424
|
+
if (node === 1) actions.skipChildren();
|
425
|
+
order.push(node);
|
426
|
+
};
|
427
|
+
const getChildren = (node: NodeId) =>
|
428
|
+
graph.getNodeIdsConnectedFrom(node);
|
429
|
+
dfsImpl(graph, {
|
430
|
+
visit,
|
431
|
+
startNodeId: 0,
|
432
|
+
getChildren,
|
433
|
+
});
|
434
|
+
|
435
|
+
assert.deepEqual(order, [0, 1, 3]);
|
436
|
+
});
|
437
|
+
|
438
|
+
it(`${name} stops the traversal if stop is called`, () => {
|
439
|
+
const graph = new Graph();
|
440
|
+
graph.addNode('0');
|
441
|
+
graph.addNode('1');
|
442
|
+
graph.addNode('2');
|
443
|
+
graph.addNode('3');
|
444
|
+
graph.addNode('disconnected-1');
|
445
|
+
graph.addNode('disconnected-2');
|
446
|
+
graph.addEdge(0, 1);
|
447
|
+
graph.addEdge(1, 2);
|
448
|
+
graph.addEdge(1, 3);
|
449
|
+
graph.addEdge(0, 2);
|
450
|
+
graph.addEdge(2, 3);
|
451
|
+
|
452
|
+
const order = [];
|
453
|
+
const visit = (
|
454
|
+
node: NodeId,
|
455
|
+
context: mixed | null,
|
456
|
+
actions: TraversalActions,
|
457
|
+
) => {
|
458
|
+
order.push(node);
|
459
|
+
if (node === 1) {
|
460
|
+
actions.stop();
|
461
|
+
return 'result';
|
462
|
+
}
|
463
|
+
return 'other';
|
464
|
+
};
|
465
|
+
const getChildren = (node: NodeId) =>
|
466
|
+
graph.getNodeIdsConnectedFrom(node);
|
467
|
+
const result = dfsImpl(graph, {
|
468
|
+
visit,
|
469
|
+
startNodeId: 0,
|
470
|
+
getChildren,
|
471
|
+
});
|
472
|
+
|
473
|
+
assert.deepEqual(order, [0, 1]);
|
474
|
+
assert.equal(result, 'result');
|
475
|
+
});
|
476
|
+
});
|
477
|
+
|
478
|
+
describe(`${name} context tests`, () => {
|
479
|
+
it(`${name} passes the context between visitors`, () => {
|
480
|
+
const graph = new Graph();
|
481
|
+
graph.addNode('0');
|
482
|
+
graph.addNode('1');
|
483
|
+
graph.addNode('2');
|
484
|
+
graph.addNode('3');
|
485
|
+
graph.addNode('disconnected-1');
|
486
|
+
graph.addNode('disconnected-2');
|
487
|
+
graph.addEdge(0, 1);
|
488
|
+
graph.addEdge(1, 2);
|
489
|
+
graph.addEdge(1, 3);
|
490
|
+
graph.addEdge(0, 2);
|
491
|
+
graph.addEdge(2, 3);
|
492
|
+
|
493
|
+
const contexts = [];
|
494
|
+
const visit = (node: NodeId, context: mixed | null) => {
|
495
|
+
contexts.push([node, context]);
|
496
|
+
return `node-${node}-created-context`;
|
497
|
+
};
|
498
|
+
const getChildren = (node: NodeId) =>
|
499
|
+
graph.getNodeIdsConnectedFrom(node);
|
500
|
+
const result = dfsImpl(graph, {
|
501
|
+
visit,
|
502
|
+
startNodeId: 0,
|
503
|
+
getChildren,
|
504
|
+
});
|
505
|
+
|
506
|
+
assert.deepEqual(contexts, [
|
507
|
+
[0, undefined],
|
508
|
+
[1, 'node-0-created-context'],
|
509
|
+
[2, 'node-1-created-context'],
|
510
|
+
[3, 'node-2-created-context'],
|
511
|
+
]);
|
512
|
+
assert.equal(result, undefined);
|
513
|
+
});
|
514
|
+
});
|
515
|
+
|
516
|
+
describe(`${name} exit visitor tests`, () => {
|
517
|
+
it(`${name} calls the exit visitor`, () => {
|
518
|
+
const graph = new Graph();
|
519
|
+
graph.addNode('0');
|
520
|
+
graph.addNode('1');
|
521
|
+
graph.addNode('2');
|
522
|
+
graph.addNode('3');
|
523
|
+
graph.addNode('disconnected-1');
|
524
|
+
graph.addNode('disconnected-2');
|
525
|
+
graph.addEdge(0, 1);
|
526
|
+
graph.addEdge(1, 2);
|
527
|
+
graph.addEdge(1, 3);
|
528
|
+
graph.addEdge(0, 2);
|
529
|
+
|
530
|
+
const contexts = [];
|
531
|
+
const visit = (node: NodeId, context: mixed | null) => {
|
532
|
+
contexts.push([node, context]);
|
533
|
+
return `node-${node}-created-context`;
|
534
|
+
};
|
535
|
+
const visitExit = (node: NodeId, context: mixed | null) => {
|
536
|
+
contexts.push(['exit', node, context]);
|
537
|
+
return `node-exit-${node}-created-context`;
|
538
|
+
};
|
539
|
+
const getChildren = (node: NodeId) =>
|
540
|
+
graph.getNodeIdsConnectedFrom(node);
|
541
|
+
const result = dfsImpl(graph, {
|
542
|
+
visit: {
|
543
|
+
enter: visit,
|
544
|
+
exit: visitExit,
|
545
|
+
},
|
546
|
+
startNodeId: 0,
|
547
|
+
getChildren,
|
548
|
+
});
|
549
|
+
|
550
|
+
assert.deepEqual(contexts, [
|
551
|
+
[0, undefined],
|
552
|
+
[1, 'node-0-created-context'],
|
553
|
+
[2, 'node-1-created-context'],
|
554
|
+
['exit', 2, 'node-2-created-context'],
|
555
|
+
[3, 'node-1-created-context'],
|
556
|
+
['exit', 3, 'node-3-created-context'],
|
557
|
+
['exit', 1, 'node-1-created-context'],
|
558
|
+
['exit', 0, 'node-0-created-context'],
|
559
|
+
]);
|
560
|
+
assert.equal(result, undefined);
|
561
|
+
});
|
562
|
+
});
|
563
|
+
}
|
564
|
+
|
565
|
+
testSuite('dfs', (graph, params) => graph.dfs(params));
|
566
|
+
|
567
|
+
testSuite('dfsNew', (graph, params) => graph.dfsNew(params));
|
568
|
+
});
|
343
569
|
});
|