Skip to content

Commit 021b313

Browse files
davidaurelioFacebook Github Bot
authored and
Facebook Github Bot
committed
Fix double callback invocation in ModuleGraph/Graph
Summary: The logic in `ModuleGraph/Graph` allowed the callback to be invoked twice, if two invocations of `resolve` call back with errors asynchronously. This fixes that problem by always calling `queue.kill()` on the asynchronous queue, and only invoke the main callback from the `drain` and `error` queue callbacks. Reviewed By: jeanlauliac Differential Revision: D4236797 fbshipit-source-id: c30da7bf7707e13b11270bb2c6117997fd35b029
1 parent c76f5e1 commit 021b313

File tree

3 files changed

+123
-42
lines changed

3 files changed

+123
-42
lines changed

packager/react-packager/src/ModuleGraph/Graph.js

+98-39
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,57 @@
1212

1313
const invariant = require('fbjs/lib/invariant');
1414
const memoize = require('async/memoize');
15+
const nullthrows = require('fbjs/lib/nullthrows');
1516
const queue = require('async/queue');
1617
const seq = require('async/seq');
1718

18-
import type {GraphFn, LoadFn, ResolveFn, File, Module} from './types.flow';
19+
import type {
20+
Callback,
21+
File,
22+
GraphFn,
23+
LoadFn,
24+
Module,
25+
ResolveFn,
26+
} from './types.flow';
27+
28+
type Async$Queue<T, C> = {
29+
buffer: number,
30+
concurrency: number,
31+
drain: () => mixed,
32+
empty: () => mixed,
33+
error: (Error, T) => mixed,
34+
idle(): boolean,
35+
kill(): void,
36+
length(): number,
37+
pause(): void,
38+
paused: boolean,
39+
push(T | Array<T>, void | C): void,
40+
resume(): void,
41+
running(): number,
42+
saturated: () => mixed,
43+
started: boolean,
44+
unsaturated: () => mixed,
45+
unshift(T, void | C): void,
46+
workersList(): Array<T>,
47+
};
48+
49+
type LoadQueue =
50+
Async$Queue<{id: string, parent: string}, Callback<File, Array<string>>>;
1951

2052
const createParentModule =
21-
() => ({file: {path: '', ast: {}}, dependencies: []});
53+
() => ({file: {code: '', isPolyfill: false, path: ''}, dependencies: []});
2254

2355
const noop = () => {};
56+
const NO_OPTIONS = {};
2457

2558
exports.create = function create(resolve: ResolveFn, load: LoadFn): GraphFn {
26-
function Graph(entryPoints, platform, options = {}, callback = noop) {
27-
const {cwd = '', log = (console: any), optimize = false, skip} = options;
59+
function Graph(entryPoints, platform, options, callback = noop) {
60+
const {
61+
cwd = '',
62+
log = (console: any),
63+
optimize = false,
64+
skip,
65+
} = options || NO_OPTIONS;
2866

2967
if (typeof platform !== 'string') {
3068
log.error('`Graph`, called without a platform');
@@ -35,58 +73,76 @@ exports.create = function create(resolve: ResolveFn, load: LoadFn): GraphFn {
3573
const modules: Map<string | null, Module> = new Map();
3674
modules.set(null, createParentModule());
3775

38-
const loadQueue = queue(seq(
39-
({id, parent}, cb) => resolve(id, parent, platform, options, cb),
76+
const loadQueue: LoadQueue = queue(seq(
77+
({id, parent}, cb) => resolve(id, parent, platform, options || NO_OPTIONS, cb),
4078
memoize((file, cb) => load(file, {log, optimize}, cb)),
4179
), Number.MAX_SAFE_INTEGER);
4280

43-
const cleanup = () => (loadQueue.drain = noop);
4481
loadQueue.drain = () => {
45-
cleanup();
82+
loadQueue.kill();
4683
callback(null, collect(null, modules));
4784
};
48-
49-
function loadModule(id: string, parent: string | null, parentDependencyIndex) {
50-
function onFileLoaded(
51-
error: ?Error,
52-
file: File,
53-
dependencyIDs: Array<string>,
54-
) {
55-
if (error) {
56-
cleanup();
57-
callback(error);
58-
return;
59-
}
60-
61-
const parentModule = modules.get(parent);
62-
invariant(parentModule, 'Invalid parent module: ' + String(parent));
63-
parentModule.dependencies[parentDependencyIndex] = {id, path: file.path};
64-
65-
if ((!skip || !skip.has(file.path)) && !modules.has(file.path)) {
66-
const dependencies = Array(dependencyIDs.length);
67-
modules.set(file.path, {file, dependencies});
68-
dependencyIDs.forEach(
69-
(dependencyID, j) => loadModule(dependencyID, file.path, j));
70-
}
71-
}
72-
loadQueue.push({id, parent: parent != null ? parent : cwd}, onFileLoaded);
73-
}
85+
loadQueue.error = error => {
86+
loadQueue.error = noop;
87+
loadQueue.kill();
88+
callback(error);
89+
};
7490

7591
let i = 0;
7692
for (const entryPoint of entryPoints) {
77-
loadModule(entryPoint, null, i++);
93+
loadModule(entryPoint, null, i++, loadQueue, modules, skip, cwd, callback);
7894
}
7995

80-
if (loadQueue.idle()) {
96+
if (i === 0) {
8197
log.error('`Graph` called without any entry points');
82-
cleanup();
98+
loadQueue.kill();
8399
callback(Error('At least one entry point has to be passed.'));
84100
}
85101
}
86102

87103
return Graph;
88104
};
89105

106+
function loadModule(
107+
id: string,
108+
parent: string | null,
109+
parentDependencyIndex: number,
110+
loadQueue: LoadQueue,
111+
modules: Map<string | null, Module>,
112+
skip?: Set<string>,
113+
cwd: string,
114+
) {
115+
function onFileLoaded(
116+
error?: ?Error,
117+
file?: File,
118+
dependencyIDs?: Array<string>,
119+
) {
120+
if (error) {
121+
return;
122+
}
123+
124+
const {path} = nullthrows(file);
125+
dependencyIDs = nullthrows(dependencyIDs);
126+
127+
const parentModule = modules.get(parent);
128+
invariant(parentModule, 'Invalid parent module: ' + String(parent));
129+
parentModule.dependencies[parentDependencyIndex] = {id, path};
130+
131+
if ((!skip || !skip.has(path)) && !modules.has(path)) {
132+
const dependencies = Array(dependencyIDs.length);
133+
modules.set(path, {dependencies, file: nullthrows(file)});
134+
for (let i = 0; i < dependencyIDs.length; ++i) {
135+
loadModule(dependencyIDs[i], path, i, loadQueue, modules, skip, cwd);
136+
}
137+
}
138+
}
139+
140+
loadQueue.push(
141+
{id, parent: parent != null ? parent : cwd},
142+
onFileLoaded,
143+
);
144+
}
145+
90146
function collect(
91147
path,
92148
modules,
@@ -100,8 +156,11 @@ function collect(
100156
serialized.push(module);
101157
seen.add(path);
102158
}
103-
module.dependencies.forEach(
104-
dependency => collect(dependency.path, modules, serialized, seen));
159+
160+
const {dependencies} = module;
161+
for (var i = 0; i < dependencies.length; i++) {
162+
collect(dependencies[i].path, modules, serialized, seen);
163+
}
105164

106165
return serialized;
107166
}

packager/react-packager/src/ModuleGraph/__tests__/Graph-test.js

+21
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
jest
1212
.disableAutomock()
13+
.useRealTimers()
1314
.mock('console');
1415

1516
const {Console} = require('console');
@@ -96,6 +97,26 @@ describe('Graph:', () => {
9697
});
9798
});
9899

100+
it('only calls back once if two parallel invocations of `resolve` fail', done => {
101+
load.stub.yields(null, createFile('with two deps'), ['depA', 'depB']);
102+
resolve.stub
103+
.withArgs('depA').yieldsAsync(new Error())
104+
.withArgs('depB').yieldsAsync(new Error());
105+
106+
let calls = 0;
107+
function callback() {
108+
if (calls === 0) {
109+
process.nextTick(() => {
110+
expect(calls).toEqual(1);
111+
done();
112+
});
113+
}
114+
++calls;
115+
}
116+
117+
graph(['entryA', 'entryB'], anyPlatform, noOpts, callback);
118+
});
119+
99120
it('passes the files returned by `resolve` on to the `load` function', done => {
100121
const modules = new Map([
101122
['Arbitrary', '/absolute/path/to/Arbitrary.js'],

packager/react-packager/src/ModuleGraph/types.flow.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ type Dependency = {|
3939
|};
4040

4141
export type File = {|
42-
ast: Object,
43-
code?: string,
42+
code: string,
43+
isPolyfill: boolean,
44+
map?: ?Object,
4445
path: string,
4546
|};
4647

@@ -52,7 +53,7 @@ export type Module = {|
5253
export type GraphFn = (
5354
entryPoints: Iterable<string>,
5455
platform: string,
55-
options?: GraphOptions,
56+
options?: ?GraphOptions,
5657
callback?: Callback<Array<Module>>,
5758
) => void;
5859

0 commit comments

Comments
 (0)