-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathindex.js
182 lines (150 loc) · 5.49 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// @flow
'use strict';
const assert = require('assert');
const arrayFlatten = require('array-flatten');
const arrayIncludes = require('array-includes');
/*::
type Graph<T> = Map<T, Array<T>>;
type Groups<T> = Array<Array<T>>;
type Options<T> = {
graph: Graph<T>,
groups: Groups<T>,
};
type Result<T> = {
safe: boolean,
chunks: Groups<T>,
cycles: Groups<T>,
};
*/
// A function for finding cycles in a remaining graph.
function getCycles/*::<T>*/(currDepsMap /*: Graph<T> */, visited /*: Graph<T> */) /*: Groups<T> */ {
// Get a list of all the remaining items in the dep map.
let items = Array.from(currDepsMap.keys());
// Create an array for cycles to push to.
let cycles /*: Groups<T> */ = [];
// Create a function to call recursively in a depth-first search.
function visit(item, cycle) {
let visitedDeps = visited.get(item);
// Create an object for the item to mark visited deps.
if (!visitedDeps) {
visitedDeps = [];
visited.set(item, visitedDeps);
}
// Get the current deps for the item.
let deps = currDepsMap.get(item);
if (typeof deps === 'undefined') return;
// For each dep,
for (let dep of deps) {
// Check if this dep creates a cycle. We know it's a cycle if the first
// item is the same as our dep.
if (cycle[0] === dep) {
cycles.push(cycle);
}
// If an item hasn't been visited, visit it (and pass an updated
// potential cycle)
if (!arrayIncludes(visitedDeps, dep)) {
visitedDeps.push(dep);
visit(dep, cycle.concat(dep));
}
}
}
// Iterate through each item in our current deps map and search it for cycles.
for (let item of items) {
visit(item, [item]);
}
// Return any cycles we found.
return cycles;
}
function graphSequencer/*::<T>*/(opts /*: Options<T> */) /*: Result<T> */ {
let graph = opts.graph;
let groups = opts.groups;
let graphItems = Array.from(graph.keys());
// Ensure that we have the same set of items in both graph and groups.
assert.deepStrictEqual(
graphItems.sort(),
arrayFlatten(groups).sort(),
'items in graph must be the same as items in groups'
);
// We'll push to these with the results.
let chunks /*: Groups<T> */ = [];
let cycles /*: Groups<T> */ = [];
let safe = true;
// We'll keep replacing this queue as we unload items into chunks.
let queue = graphItems;
// A set of items already put into chunks.
let chunked /*: Set<T> */ = new Set();
// A cache of visited connection in the graph when finding cycles.
let visited /*: Graph<T> */ = new Map();
// Keep running this loop while we have items left to unload into chunks.
while (queue.length) {
// Init the next `queue` after this iteration.
let nextQueue = [];
// Create a chunk for the cycle which we'll unload items into.
let chunk = [];
// A map of remaining deps for each of the remaining items in the current
// iteration.
let currDepsMap /*: Graph<T> */ = new Map();
for (let i = 0; i < queue.length; i++) {
// Get the current `item` in the `queue`.
let item = queue[i];
// Get all the deps of the `item`.
let deps = graph.get(item);
if (typeof deps === 'undefined') continue;
// Find the index of the group that the `item` belongs to.
let itemGroup = groups.findIndex(group => arrayIncludes(group, item));
// Filter down a set of deps which need to be run first.
let currDeps = deps.filter(dep => {
// Find the index of the group that the `dep` belongs to.
let depGroup = groups.findIndex(group => arrayIncludes(group, dep));
if (depGroup > itemGroup) {
return false;
} else {
return !chunked.has(dep);
}
});
// Store the remaining deps for this `item` for later.
currDepsMap.set(item, currDeps);
// If there are deps remaining,
if (currDeps.length) {
// Add the `item` to the next `queue` for the next iteration.
nextQueue.push(item);
} else {
// Otherwise, add it to the current `chunk`.
chunk.push(item);
}
}
// If we got to this point and nothing was added to the chunk, it's because
// we have a cycle.
if (!chunk.length) {
// First we'll identify all the cycles in the `currDepsMap` so we can
// return them at the end.
cycles = cycles.concat(getCycles(currDepsMap, visited));
// We'll sort the remaining items in the queue by the number of deps they
// have remaining.
let sorted = queue.sort((a, b) => {
let aDeps = currDepsMap.get(a) || [];
let bDeps = currDepsMap.get(b) || [];
return aDeps.length - bDeps.length;
});
// We'll push the item with the fewest deps remaining to the chunk.
chunk.push(sorted[0]);
// And set the next `queue` to the rest of the items.
nextQueue = sorted.slice(1);
// This will ensure that something gets unloaded every cycle, putting in
// a best effort to keep a working order... However, this does mean that
// the resulting order is unsafe.
safe = false;
}
// Mark all of the unloaded items in the chunk as being chunked already.
for (let item of chunk) {
chunked.add(item);
}
// Add the current cycle's chunk to the resulting chunks array, ensuring
// that there is a consistent order.
chunks.push(chunk.sort());
// And move on to the next `queue`.
queue = nextQueue;
}
return { safe, chunks, cycles };
}
module.exports = graphSequencer;