Make the delta protocol more robust by detecting when a client misses a delta

Reviewed By: davidaurelio

Differential Revision: D7031664

fbshipit-source-id: 22985ba45aa330780c8ddc44c07f5557e9e0d9c4
This commit is contained in:
Rafael Oleza 2018-02-24 04:08:36 -08:00 committed by Facebook Github Bot
parent a7fdc0f3c1
commit ff6f6ef1cf
16 changed files with 281 additions and 224 deletions

View File

@ -12,6 +12,7 @@
const {
initialTraverseDependencies,
reorderDependencies,
traverseDependencies,
} = require('./traverseDependencies');
const {EventEmitter} = require('events');
@ -85,7 +86,7 @@ class DeltaCalculator extends EventEmitter {
* Main method to calculate the delta of modules. It returns a DeltaResult,
* which contain the modified/added modules and the removed modules.
*/
async getDelta(): Promise<DeltaResult> {
async getDelta({reset}: {reset: boolean}): Promise<DeltaResult> {
// If there is already a build in progress, wait until it finish to start
// processing a new one (delta server doesn't support concurrent builds).
if (this._currentBuildPromise) {
@ -134,6 +135,20 @@ class DeltaCalculator extends EventEmitter {
this._currentBuildPromise = null;
}
// Return all the modules if the client requested a reset delta.
if (reset) {
return {
modified: reorderDependencies(
this._dependencyEdges.get(
this._dependencyGraph.getAbsolutePath(this._options.entryFile),
),
this._dependencyEdges,
),
deleted: new Set(),
reset: true,
};
}
return result;
}

View File

@ -27,6 +27,7 @@ class DeltaPatcher {
pre: new Map(),
post: new Map(),
modules: new Map(),
id: undefined,
};
_initialized = false;
_lastNumModifiedFiles = 0;
@ -62,6 +63,7 @@ class DeltaPatcher {
pre: new Map(),
post: new Map(),
modules: new Map(),
id: undefined,
};
}
@ -76,9 +78,15 @@ class DeltaPatcher {
this._patchMap(this._lastBundle.post, deltaBundle.post);
this._patchMap(this._lastBundle.modules, deltaBundle.delta);
this._lastBundle.id = deltaBundle.id;
return this;
}
getLastBundleId(): ?string {
return this._lastBundle.id;
}
/**
* Returns the number of modified files in the last received Delta. This is
* currently used to populate the `X-Metro-Files-Changed-Count` HTTP header

View File

@ -14,6 +14,7 @@ const DeltaCalculator = require('./DeltaCalculator');
const addParamsToDefineCall = require('../lib/addParamsToDefineCall');
const createModuleIdFactory = require('../lib/createModuleIdFactory');
const crypto = require('crypto');
const defaults = require('../defaults');
const getPreludeCode = require('../lib/getPreludeCode');
const nullthrows = require('fbjs/lib/nullthrows');
@ -24,7 +25,8 @@ import type Bundler from '../Bundler';
import type {Options as JSTransformerOptions} from '../JSTransformer/worker';
import type DependencyGraph from '../node-haste/DependencyGraph';
import type Module from '../node-haste/Module';
import type {Options as BundleOptions, MainOptions} from './';
import type {BundleOptions} from '../shared/types.flow';
import type {MainOptions} from './';
import type {DependencyEdge, DependencyEdges} from './traverseDependencies';
import type {MetroSourceMapSegmentTuple} from 'metro-source-map';
@ -48,6 +50,7 @@ export type DeltaEntry = {|
export type DeltaEntries = Map<number, ?DeltaEntry>;
export type DeltaTransformResponse = {|
+id: string,
+pre: DeltaEntries,
+post: DeltaEntries,
+delta: DeltaEntries,
@ -82,6 +85,7 @@ class DeltaTransformer extends EventEmitter {
_deltaCalculator: DeltaCalculator;
_bundleOptions: BundleOptions;
_currentBuildPromise: ?Promise<DeltaTransformResponse>;
_lastSequenceId: ?string;
constructor(
bundler: Bundler,
@ -153,7 +157,7 @@ class DeltaTransformer extends EventEmitter {
if (!this._deltaCalculator.getDependencyEdges().size) {
// If by any means the dependency graph has not been initialized, call
// getDelta() to initialize it.
await this._getDelta();
await this._getDelta({reset: false});
}
return this._getDependencies;
@ -167,7 +171,7 @@ class DeltaTransformer extends EventEmitter {
if (!this._deltaCalculator.getDependencyEdges().size) {
// If by any means the dependency graph has not been initialized, call
// getDelta() to initialize it.
await this._getDelta();
await this._getDelta({reset: false});
}
const dependencyEdges = this._deltaCalculator.getDependencyEdges();
@ -204,7 +208,11 @@ class DeltaTransformer extends EventEmitter {
* which contain the source code of the modified and added modules and the
* list of removed modules.
*/
async getDelta(): Promise<DeltaTransformResponse> {
async getDelta(sequenceId: ?string): Promise<DeltaTransformResponse> {
// If the passed sequenceId is different than the last calculated one,
// return a reset delta (since that means that the client is desynchronized)
const reset = !!this._lastSequenceId && sequenceId !== this._lastSequenceId;
// If there is already a build in progress, wait until it finish to start
// processing a new one (delta transformer doesn't support concurrent
// builds).
@ -212,7 +220,7 @@ class DeltaTransformer extends EventEmitter {
await this._currentBuildPromise;
}
this._currentBuildPromise = this._getDelta();
this._currentBuildPromise = this._getDelta({reset});
let result;
@ -225,58 +233,57 @@ class DeltaTransformer extends EventEmitter {
return result;
}
async _getDelta(): Promise<DeltaTransformResponse> {
async _getDelta({
reset: resetDelta,
}: {
reset: boolean,
}): Promise<DeltaTransformResponse> {
// Calculate the delta of modules.
const {modified, deleted, reset} = await this._deltaCalculator.getDelta();
const {modified, deleted, reset} = await this._deltaCalculator.getDelta({
reset: resetDelta,
});
const transformerOptions = await this._deltaCalculator.getTransformerOptions();
const dependencyEdges = this._deltaCalculator.getDependencyEdges();
try {
// Return the source code that gets prepended to all the modules. This
// contains polyfills and startup code (like the require() implementation).
const prependSources = reset
? await this._getPrepend(transformerOptions, dependencyEdges)
: new Map();
// Return the source code that gets prepended to all the modules. This
// contains polyfills and startup code (like the require() implementation).
const prependSources = reset
? await this._getPrepend(transformerOptions, dependencyEdges)
: new Map();
// Precalculate all module ids sequentially. We do this to be sure that the
// mapping between module -> moduleId is deterministic between runs.
const modules = Array.from(modified.values());
modules.forEach(module => this._getModuleId(module.path));
// Precalculate all module ids sequentially. We do this to be sure that the
// mapping between module -> moduleId is deterministic between runs.
const modules = Array.from(modified.values());
modules.forEach(module => this._getModuleId(module.path));
// Get the transformed source code of each modified/added module.
const modifiedDelta = await this._transformModules(
modules,
transformerOptions,
dependencyEdges,
);
// Get the transformed source code of each modified/added module.
const modifiedDelta = await this._transformModules(
modules,
transformerOptions,
dependencyEdges,
);
deleted.forEach(id => {
modifiedDelta.set(this._getModuleId(id), null);
});
deleted.forEach(id => {
modifiedDelta.set(this._getModuleId(id), null);
});
// Return the source code that gets appended to all the modules. This
// contains the require() calls to startup the execution of the modules.
const appendSources = reset
? await this._getAppend(dependencyEdges)
: new Map();
// Return the source code that gets appended to all the modules. This
// contains the require() calls to startup the execution of the modules.
const appendSources = reset
? await this._getAppend(dependencyEdges)
: new Map();
return {
pre: prependSources,
post: appendSources,
delta: modifiedDelta,
reset,
};
} catch (e) {
// If any unexpected error happens while creating the bundle, the client
// is going to lose that specific delta, while the DeltaCalulator has
// already processed the changes. This will make that change to be lost,
// which can cause the final bundle to be invalid. In order to avoid that,
// we just reset the delta calculator when this happens.
this._deltaCalculator.reset();
// generate a random
this._lastSequenceId = crypto.randomBytes(8).toString('hex');
throw e;
}
return {
pre: prependSources,
post: appendSources,
delta: modifiedDelta,
reset,
id: this._lastSequenceId,
};
}
_getDependencies = (path: string): Set<string> => {

View File

@ -12,6 +12,7 @@
const DeltaPatcher = require('./DeltaPatcher');
const stableHash = require('metro-cache/src/stableHash');
const toLocalPath = require('../node-haste/lib/toLocalPath');
const {getAssetData} = require('../Assets');
@ -19,16 +20,15 @@ const {createRamBundleGroups} = require('../Bundler/util');
const {fromRawMappings} = require('metro-source-map');
import type {AssetData} from '../Assets';
import type {BundleOptions} from '../shared/types.flow';
import type {ModuleTransportLike} from '../shared/types.flow';
import type DeltaBundler, {Options as BuildOptions} from './';
import type {BundleOptions, ModuleTransportLike} from '../shared/types.flow';
import type DeltaBundler from './';
import type DeltaTransformer, {
DeltaEntry,
DeltaTransformResponse,
} from './DeltaTransformer';
import type {BabelSourceMap} from '@babel/core';
export type Options = BundleOptions & {
export type DeltaOptions = BundleOptions & {
deltaBundleId: ?string,
};
@ -49,16 +49,17 @@ export type RamBundleInfo = {
async function deltaBundle(
deltaBundler: DeltaBundler,
options: Options,
clientId: string,
options: DeltaOptions,
): Promise<{bundle: string, numModifiedFiles: number}> {
const {id, delta} = await _build(deltaBundler, options);
const {delta} = await _build(deltaBundler, clientId, options);
function stringifyModule([id, module]) {
return [id, module ? module.code : undefined];
}
const bundle = JSON.stringify({
id,
id: delta.id,
pre: Array.from(delta.pre).map(stringifyModule),
post: Array.from(delta.post).map(stringifyModule),
delta: Array.from(delta.delta).map(stringifyModule),
@ -73,7 +74,7 @@ async function deltaBundle(
async function fullSourceMap(
deltaBundler: DeltaBundler,
options: Options,
options: BundleOptions,
): Promise<string> {
const {modules} = await _getAllModules(deltaBundler, options);
@ -84,7 +85,7 @@ async function fullSourceMap(
async function fullSourceMapObject(
deltaBundler: DeltaBundler,
options: Options,
options: BundleOptions,
): Promise<BabelSourceMap> {
const {modules} = await _getAllModules(deltaBundler, options);
@ -98,7 +99,7 @@ async function fullSourceMapObject(
*/
async function fullBundle(
deltaBundler: DeltaBundler,
options: Options,
options: BundleOptions,
): Promise<{bundle: string, numModifiedFiles: number, lastModified: Date}> {
const {modules, numModifiedFiles, lastModified} = await _getAllModules(
deltaBundler,
@ -116,7 +117,7 @@ async function fullBundle(
async function getAllModules(
deltaBundler: DeltaBundler,
options: Options,
options: BundleOptions,
): Promise<$ReadOnlyArray<DeltaEntry>> {
const {modules} = await _getAllModules(deltaBundler, options);
@ -125,16 +126,30 @@ async function getAllModules(
async function _getAllModules(
deltaBundler: DeltaBundler,
options: Options,
options: BundleOptions,
): Promise<{
modules: $ReadOnlyArray<DeltaEntry>,
numModifiedFiles: number,
lastModified: Date,
deltaTransformer: DeltaTransformer,
}> {
const {id, delta, deltaTransformer} = await _build(deltaBundler, options);
const hashedOptions = options;
delete hashedOptions.sourceMapUrl;
const deltaPatcher = DeltaPatcher.get(id);
const clientId = '__SERVER__' + stableHash(hashedOptions).toString('hex');
const deltaPatcher = DeltaPatcher.get(clientId);
options = {
...options,
deltaBundleId: deltaPatcher.getLastBundleId(),
};
const {delta, deltaTransformer} = await _build(
deltaBundler,
clientId,
options,
);
const modules = deltaPatcher
.applyDelta(delta)
@ -150,7 +165,7 @@ async function _getAllModules(
async function getRamBundleInfo(
deltaBundler: DeltaBundler,
options: Options,
options: BundleOptions,
): Promise<RamBundleInfo> {
const {modules, deltaTransformer} = await _getAllModules(
deltaBundler,
@ -226,7 +241,7 @@ async function getRamBundleInfo(
async function getAssets(
deltaBundler: DeltaBundler,
options: Options,
options: BundleOptions,
): Promise<$ReadOnlyArray<AssetData>> {
const {modules} = await _getAllModules(deltaBundler, options);
@ -254,20 +269,20 @@ async function getAssets(
async function _build(
deltaBundler: DeltaBundler,
options: BuildOptions,
clientId: string,
options: DeltaOptions,
): Promise<{
id: string,
delta: DeltaTransformResponse,
deltaTransformer: DeltaTransformer,
}> {
const {deltaTransformer, id} = await deltaBundler.getDeltaTransformer(
const deltaTransformer = await deltaBundler.getDeltaTransformer(
clientId,
options,
);
const delta = await deltaTransformer.getDelta();
const delta = await deltaTransformer.getDelta(options.deltaBundleId);
return {
id,
delta,
deltaTransformer,
};

View File

@ -43,27 +43,27 @@ describe('DeltaBundler', () => {
});
it('should create a new transformer the first time it gets called', async () => {
await deltaBundler.getDeltaTransformer({deltaBundleId: 10});
await deltaBundler.getDeltaTransformer('foo', {deltaBundleId: 10});
expect(DeltaTransformer.create.mock.calls.length).toBe(1);
});
it('should reuse the same transformer after a second call', async () => {
await deltaBundler.getDeltaTransformer({deltaBundleId: 10});
await deltaBundler.getDeltaTransformer({deltaBundleId: 10});
await deltaBundler.getDeltaTransformer('foo', {deltaBundleId: 10});
await deltaBundler.getDeltaTransformer('foo', {deltaBundleId: 20});
expect(DeltaTransformer.create.mock.calls.length).toBe(1);
});
it('should create different transformers when there is no delta bundle id', async () => {
await deltaBundler.getDeltaTransformer({});
await deltaBundler.getDeltaTransformer({});
it('should create different transformers for different clients', async () => {
await deltaBundler.getDeltaTransformer('foo', {});
await deltaBundler.getDeltaTransformer('bar', {});
expect(DeltaTransformer.create.mock.calls.length).toBe(2);
});
it('should reset everything after calling end()', async () => {
await deltaBundler.getDeltaTransformer({deltaBundleId: 10});
await deltaBundler.getDeltaTransformer('foo', {deltaBundleId: 10});
deltaBundler.end();

View File

@ -13,15 +13,16 @@
jest.mock('../../Bundler');
jest.mock('../traverseDependencies');
const {
initialTraverseDependencies,
reorderDependencies,
traverseDependencies,
} = require('../traverseDependencies');
const Bundler = require('../../Bundler');
const {EventEmitter} = require('events');
const DeltaCalculator = require('../DeltaCalculator');
const {
initialTraverseDependencies,
traverseDependencies,
} = require('../traverseDependencies');
const getTransformOptions = require('../../__fixtures__/getTransformOptions');
describe('DeltaCalculator', () => {
@ -38,8 +39,7 @@ describe('DeltaCalculator', () => {
let deltaCalculator;
let fileWatcher;
let mockedDependencies;
const bundlerMock = new Bundler();
let bundlerMock;
const options = {
assetPlugins: [],
@ -72,6 +72,8 @@ describe('DeltaCalculator', () => {
}
beforeEach(async () => {
bundlerMock = new Bundler();
mockedDependencies = [entryModule, moduleFoo, moduleBar, moduleBaz];
fileWatcher = new EventEmitter();
@ -90,10 +92,29 @@ describe('DeltaCalculator', () => {
initialTraverseDependencies.mockImplementationOnce(
async (path, dg, opt, edges) => {
edgeModule = {...entryModule};
edgeFoo = {...moduleFoo, inverseDependencies: ['/bundle']};
edgeBar = {...moduleBar, inverseDependencies: ['/bundle']};
edgeBaz = {...moduleBaz, inverseDependencies: ['/bundle']};
edgeModule = {
...entryModule,
dependencies: new Map([
['foo', '/foo'],
['bar', '/bar'],
['baz', '/baz'],
]),
};
edgeFoo = {
...moduleFoo,
dependencies: new Map(),
inverseDependencies: ['/bundle'],
};
edgeBar = {
...moduleBar,
dependencies: new Map(),
inverseDependencies: ['/bundle'],
};
edgeBaz = {
...moduleBaz,
dependencies: new Map(),
inverseDependencies: ['/bundle'],
};
edges.set('/bundle', edgeModule);
edges.set('/foo', edgeFoo);
@ -131,8 +152,10 @@ describe('DeltaCalculator', () => {
});
afterEach(() => {
initialTraverseDependencies.mockReset();
deltaCalculator.end();
traverseDependencies.mockReset();
initialTraverseDependencies.mockReset();
});
it('should start listening for file changes after being initialized', async () => {
@ -146,11 +169,44 @@ describe('DeltaCalculator', () => {
});
it('should include the entry file when calculating the initial bundle', async () => {
const result = await deltaCalculator.getDelta();
const result = await deltaCalculator.getDelta({reset: false});
expect(result).toEqual({
modified: new Map([
['/bundle', entryModule],
['/bundle', edgeModule],
['/foo', edgeFoo],
['/bar', edgeBar],
['/baz', edgeBaz],
]),
deleted: new Set(),
reset: true,
});
jest.runAllTicks();
});
it('should return an empty delta when there are no changes', async () => {
await deltaCalculator.getDelta({reset: false});
expect(await deltaCalculator.getDelta({reset: false})).toEqual({
modified: new Map(),
deleted: new Set(),
reset: false,
});
expect(traverseDependencies.mock.calls.length).toBe(0);
});
it('should return a full delta when passing reset=true', async () => {
reorderDependencies.mockImplementation((_, edges) => edges);
await deltaCalculator.getDelta({reset: false});
const result = await deltaCalculator.getDelta({reset: true});
expect(result).toEqual({
modified: new Map([
['/bundle', edgeModule],
['/foo', edgeFoo],
['/bar', edgeBar],
['/baz', edgeBaz],
@ -160,20 +216,8 @@ describe('DeltaCalculator', () => {
});
});
it('should return an empty delta when there are no changes', async () => {
await deltaCalculator.getDelta();
expect(await deltaCalculator.getDelta()).toEqual({
modified: new Map(),
deleted: new Set(),
reset: false,
});
expect(traverseDependencies.mock.calls.length).toBe(0);
});
it('should calculate a delta after a simple modification', async () => {
await deltaCalculator.getDelta();
await deltaCalculator.getDelta({reset: false});
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
@ -184,7 +228,7 @@ describe('DeltaCalculator', () => {
}),
);
const result = await deltaCalculator.getDelta();
const result = await deltaCalculator.getDelta({reset: false});
expect(result).toEqual({
modified: new Map([['/foo', edgeFoo]]),
@ -197,7 +241,7 @@ describe('DeltaCalculator', () => {
it('should calculate a delta after removing a dependency', async () => {
// Get initial delta
await deltaCalculator.getDelta();
await deltaCalculator.getDelta({reset: false});
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
@ -208,7 +252,7 @@ describe('DeltaCalculator', () => {
}),
);
const result = await deltaCalculator.getDelta();
const result = await deltaCalculator.getDelta({reset: false});
expect(result).toEqual({
modified: new Map([['/foo', edgeFoo]]),
@ -221,7 +265,7 @@ describe('DeltaCalculator', () => {
it('should calculate a delta after adding/removing dependencies', async () => {
// Get initial delta
await deltaCalculator.getDelta();
await deltaCalculator.getDelta({reset: false});
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
@ -239,7 +283,7 @@ describe('DeltaCalculator', () => {
};
});
const result = await deltaCalculator.getDelta();
const result = await deltaCalculator.getDelta({reset: false});
expect(result).toEqual({
modified: new Map([['/foo', edgeFoo], ['/qux', edgeQux]]),
deleted: new Set(['/bar', '/baz']),
@ -248,7 +292,7 @@ describe('DeltaCalculator', () => {
});
it('should emit an event when there is a relevant file change', async done => {
await deltaCalculator.getDelta();
await deltaCalculator.getDelta({reset: false});
deltaCalculator.on('change', () => done());
@ -259,7 +303,7 @@ describe('DeltaCalculator', () => {
jest.useFakeTimers();
const onChangeFile = jest.fn();
await deltaCalculator.getDelta();
await deltaCalculator.getDelta({reset: false});
deltaCalculator.on('delete', onChangeFile);
@ -273,20 +317,24 @@ describe('DeltaCalculator', () => {
});
it('should retry to build the last delta after getting an error', async () => {
await deltaCalculator.getDelta();
await deltaCalculator.getDelta({reset: false});
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
traverseDependencies.mockReturnValue(Promise.reject(new Error()));
await expect(deltaCalculator.getDelta()).rejects.toBeInstanceOf(Error);
await expect(
deltaCalculator.getDelta({reset: false}),
).rejects.toBeInstanceOf(Error);
// This second time it should still throw an error.
await expect(deltaCalculator.getDelta()).rejects.toBeInstanceOf(Error);
await expect(
deltaCalculator.getDelta({reset: false}),
).rejects.toBeInstanceOf(Error);
});
it('should never try to traverse a file after deleting it', async () => {
await deltaCalculator.getDelta();
await deltaCalculator.getDelta({reset: false});
// First modify the file
fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]});
@ -303,7 +351,7 @@ describe('DeltaCalculator', () => {
}),
);
expect(await deltaCalculator.getDelta()).toEqual({
expect(await deltaCalculator.getDelta({reset: false})).toEqual({
modified: new Map([['/bundle', edgeModule]]),
deleted: new Set(['/foo']),
reset: false,
@ -314,7 +362,7 @@ describe('DeltaCalculator', () => {
});
it('should not do unnecessary work when adding a file after deleting it', async () => {
await deltaCalculator.getDelta();
await deltaCalculator.getDelta({reset: false});
// First delete a file
fileWatcher.emit('change', {
@ -331,7 +379,7 @@ describe('DeltaCalculator', () => {
}),
);
await deltaCalculator.getDelta();
await deltaCalculator.getDelta({reset: false});
expect(traverseDependencies).toHaveBeenCalledTimes(1);
expect(traverseDependencies.mock.calls[0][0]).toEqual(['/foo']);

View File

@ -28,6 +28,7 @@ describe('Serializers', () => {
let Serializers;
const deltaResponse = {
id: '1234',
pre: new Map([[1, {type: 'script', code: 'pre;', id: 1, path: '/pre.js'}]]),
post: new Map([[2, {type: 'require', code: 'post;', id: 2, path: '/p'}]]),
delta: new Map([
@ -58,12 +59,9 @@ describe('Serializers', () => {
deltaBundler = {
async getDeltaTransformer() {
return {
id: '1234',
deltaTransformer: {
getDelta,
getDependenciesFn,
getRamOptions,
},
getDelta,
getDependenciesFn,
getRamOptions,
};
},
getPostProcessModulesFn() {
@ -91,12 +89,13 @@ describe('Serializers', () => {
it('should return the stringified delta bundle', async () => {
expect(
await Serializers.deltaBundle(deltaBundler, {deltaBundleId: 10}),
await Serializers.deltaBundle(deltaBundler, 'foo', {deltaBundleId: 10}),
).toMatchSnapshot();
// Simulate a delta with some changes now
getDelta.mockReturnValueOnce(
Promise.resolve({
id: '1234',
delta: new Map([[3, {code: 'modified module;'}], [4, null]]),
pre: new Map(),
post: new Map(),
@ -105,17 +104,16 @@ describe('Serializers', () => {
);
expect(
await Serializers.deltaBundle(deltaBundler, {deltaBundleId: 10}),
await Serializers.deltaBundle(deltaBundler, 'foo', {deltaBundleId: 10}),
).toMatchSnapshot();
});
it('should build the full JS bundle', async () => {
expect(
await Serializers.fullBundle(deltaBundler, {deltaBundleId: 10}),
).toMatchSnapshot();
expect(await Serializers.fullBundle(deltaBundler, {})).toMatchSnapshot();
getDelta.mockReturnValueOnce(
Promise.resolve({
id: '1234',
delta: new Map([[3, {code: 'modified module;'}], [4, null]]),
pre: new Map([[5, {code: 'more pre;'}]]),
post: new Map([[6, {code: 'bananas;'}], [7, {code: 'apples;'}]]),
@ -126,7 +124,6 @@ describe('Serializers', () => {
expect(
await Serializers.fullBundle(deltaBundler, {
deltaBundleId: 10,
sourceMapUrl: 'http://localhost:8081/myBundle.js',
}),
).toMatchSnapshot();
@ -135,12 +132,11 @@ describe('Serializers', () => {
// This test actually does not test the sourcemaps generation logic, which
// is already tested in the source-map file.
it('should build the full Source Maps', async () => {
expect(
await Serializers.fullSourceMap(deltaBundler, {deltaBundleId: 10}),
).toMatchSnapshot();
expect(await Serializers.fullSourceMap(deltaBundler, {})).toMatchSnapshot();
getDelta.mockReturnValueOnce(
Promise.resolve({
id: '1234',
delta: new Map([[3, {code: 'modified module;'}], [4, null]]),
pre: new Map([[5, {code: 'more pre;'}]]),
post: new Map([[6, {code: 'bananas;'}], [7, {code: 'apples;'}]]),
@ -149,15 +145,11 @@ describe('Serializers', () => {
);
setCurrentTime(CURRENT_TIME + 5000);
expect(
await Serializers.fullSourceMap(deltaBundler, {deltaBundleId: 10}),
).toMatchSnapshot();
expect(await Serializers.fullSourceMap(deltaBundler, {})).toMatchSnapshot();
});
it('should return all the bundle modules', async () => {
expect(
await Serializers.getAllModules(deltaBundler, {deltaBundleId: 10}),
).toMatchSnapshot();
expect(await Serializers.getAllModules(deltaBundler, {})).toMatchSnapshot();
getDelta.mockReturnValueOnce(
Promise.resolve({
@ -168,14 +160,12 @@ describe('Serializers', () => {
}),
);
expect(
await Serializers.getAllModules(deltaBundler, {deltaBundleId: 10}),
).toMatchSnapshot();
expect(await Serializers.getAllModules(deltaBundler, {})).toMatchSnapshot();
});
it('should return the RAM bundle info', async () => {
expect(
await Serializers.getRamBundleInfo(deltaBundler, {deltaBundleId: 10}),
await Serializers.getRamBundleInfo(deltaBundler, {}),
).toMatchSnapshot();
getDelta.mockReturnValueOnce(
@ -198,7 +188,7 @@ describe('Serializers', () => {
);
expect(
await Serializers.getRamBundleInfo(deltaBundler, {deltaBundleId: 10}),
await Serializers.getRamBundleInfo(deltaBundler, {}),
).toMatchSnapshot();
});
@ -241,14 +231,12 @@ describe('Serializers', () => {
);
expect(
await Serializers.getRamBundleInfo(deltaBundler, {deltaBundleId: 10}),
await Serializers.getRamBundleInfo(deltaBundler, {}),
).toMatchSnapshot();
});
it('should return the bundle assets', async () => {
expect(
await Serializers.getAllModules(deltaBundler, {deltaBundleId: 10}),
).toMatchSnapshot();
expect(await Serializers.getAllModules(deltaBundler, {})).toMatchSnapshot();
getDelta.mockReturnValueOnce(
Promise.resolve({
@ -261,12 +249,12 @@ describe('Serializers', () => {
pre: new Map([[5, {code: 'more pre;'}]]),
post: new Map([[6, {code: 'bananas;'}]]),
inverseDependencies: [],
reset: true,
}),
);
expect(
await Serializers.getAssets(deltaBundler, {
deltaBundleId: 10,
platform: 'ios',
}),
).toMatchSnapshot();
@ -279,10 +267,6 @@ describe('Serializers', () => {
);
});
expect(
await Serializers.getAllModules(deltaBundler, {
deltaBundleId: 10,
}),
).toMatchSnapshot();
expect(await Serializers.getAllModules(deltaBundler, {})).toMatchSnapshot();
});
});

View File

@ -27,16 +27,11 @@ export type MainOptions = {|
postProcessModules?: PostProcessModules,
|};
export type Options = BundleOptions & {
+deltaBundleId: ?string,
};
/**
* `DeltaBundler` uses the `DeltaTransformer` to build bundle deltas. This
* module handles all the transformer instances so it can support multiple
* concurrent clients requesting their own deltas. This is done through the
* `deltaBundleId` options param (which maps a client to a specific delta
* transformer).
* `clientId` param (which maps a client to a specific delta transformer).
*/
class DeltaBundler {
_bundler: Bundler;
@ -59,18 +54,10 @@ class DeltaBundler {
}
async getDeltaTransformer(
options: Options,
): Promise<{deltaTransformer: DeltaTransformer, id: string}> {
let bundleId = options.deltaBundleId;
// If no bundle id is passed, generate a new one (which is going to be
// returned as part of the bundle, so the client can later ask for an actual
// delta).
if (!bundleId) {
bundleId = String(this._currentId++);
}
let deltaTransformer = this._deltaTransformers.get(bundleId);
clientId: string,
options: BundleOptions,
): Promise<DeltaTransformer> {
let deltaTransformer = this._deltaTransformers.get(clientId);
if (!deltaTransformer) {
deltaTransformer = await DeltaTransformer.create(
@ -79,13 +66,10 @@ class DeltaBundler {
options,
);
this._deltaTransformers.set(bundleId, deltaTransformer);
this._deltaTransformers.set(clientId, deltaTransformer);
}
return {
deltaTransformer,
id: bundleId,
};
return deltaTransformer;
}
getPostProcessModulesFn(

View File

@ -431,4 +431,5 @@ function flatten<T>(input: Iterable<Iterable<T>>): Set<T> {
module.exports = {
initialTraverseDependencies,
traverseDependencies,
reorderDependencies,
};

View File

@ -22,14 +22,12 @@ describe('HmrServer', () => {
beforeEach(() => {
deltaTransformerMock = new EventEmitter();
deltaTransformerMock.getDelta = jest.fn();
deltaTransformerMock.getDelta = jest.fn().mockReturnValue({id: '1234'});
deltaTransformerMock.getInverseDependencies = jest.fn();
getDeltaTransformerMock = jest
.fn()
.mockReturnValue(
Promise.resolve({deltaTransformer: deltaTransformerMock}),
);
.mockReturnValue(Promise.resolve(deltaTransformerMock));
deltaBundlerMock = {
getDeltaTransformer: getDeltaTransformerMock,
@ -55,6 +53,7 @@ describe('HmrServer', () => {
);
expect(getDeltaTransformerMock).toBeCalledWith(
'/hot?bundleEntry=EntryPoint.js&platform=ios',
expect.objectContaining({
deltaBundleId: null,
dev: true,

View File

@ -10,8 +10,8 @@
'use strict';
import type {Options as BundleOptions} from '../DeltaBundler';
import type {CustomTransformOptions} from '../JSTransformer/worker';
import type {BundleOptions} from '../shared/types.flow';
/**
* Module to easily create the needed configuration parameters needed for the

View File

@ -42,6 +42,7 @@ type Client = {|
class HmrServer<TClient: Client> {
_packagerServer: PackagerServer;
_reporter: Reporter;
_lastSequenceId: ?string;
constructor(packagerServer: PackagerServer) {
this._packagerServer = packagerServer;
@ -62,12 +63,15 @@ class HmrServer<TClient: Client> {
// DeltaBundleId param through the WS connection and we'll be able to share
// the same DeltaTransformer between the WS connection and the HTTP one.
const deltaBundler = this._packagerServer.getDeltaBundler();
const {deltaTransformer} = await deltaBundler.getDeltaTransformer(
const deltaTransformer = await deltaBundler.getDeltaTransformer(
clientUrl,
getBundlingOptionsForHmr(bundleEntry, platform, customTransformOptions),
);
// Trigger an initial build to start up the DeltaTransformer.
await deltaTransformer.getDelta();
const {id} = await deltaTransformer.getDelta();
this._lastSequenceId = id;
// Listen to file changes.
const client = {sendFn, deltaTransformer};
@ -113,7 +117,7 @@ class HmrServer<TClient: Client> {
let result;
try {
result = await client.deltaTransformer.getDelta();
result = await client.deltaTransformer.getDelta(this._lastSequenceId);
} catch (error) {
const formattedError = formatBundlingError(error);
@ -138,6 +142,8 @@ class HmrServer<TClient: Client> {
}
}
this._lastSequenceId = result.id;
return {
type: 'update',
body: {

View File

@ -182,7 +182,6 @@ describe('processRequest', () => {
assetPlugins: [],
bundleType: 'bundle',
customTransformOptions: {},
deltaBundleId: expect.any(String),
dev: true,
entryFile: 'index.ios.js',
entryModuleOnly: false,
@ -214,7 +213,6 @@ describe('processRequest', () => {
assetPlugins: [],
bundleType: 'bundle',
customTransformOptions: {},
deltaBundleId: expect.any(String),
dev: true,
entryFile: 'index.js',
entryModuleOnly: false,
@ -246,7 +244,6 @@ describe('processRequest', () => {
assetPlugins: ['assetPlugin1', 'assetPlugin2'],
bundleType: 'bundle',
customTransformOptions: {},
deltaBundleId: expect.any(String),
dev: true,
entryFile: 'index.js',
entryModuleOnly: false,
@ -286,14 +283,17 @@ describe('processRequest', () => {
});
it('should send the correct deltaBundlerId to the bundler', () => {
Serializers.deltaBundle.mockImplementation(async (_, options) => {
expect(options.deltaBundleId).toBe('1234');
Serializers.deltaBundle.mockImplementation(
async (_, clientId, options) => {
expect(clientId).toMatchSnapshot();
expect(options.deltaBundleId).toBe('1234');
return {
bundle: '{"delta": "bundle"}',
numModifiedFiles: 3,
};
});
return {
bundle: '{"delta": "bundle"}',
numModifiedFiles: 3,
};
},
);
return makeRequest(
requestHandler,
@ -449,7 +449,6 @@ describe('processRequest', () => {
{
assetPlugins: [],
customTransformOptions: {},
deltaBundleId: null,
dev: true,
entryFile: 'foo file',
entryModuleOnly: false,

View File

@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`processRequest Generate delta bundle endpoint should send the correct deltaBundlerId to the bundler 1`] = `"{\\"sourceMapUrl\\":null,\\"bundleType\\":\\"delta\\",\\"customTransformOptions\\":{},\\"entryFile\\":\\"index.js\\",\\"deltaBundleId\\":null,\\"dev\\":true,\\"minify\\":false,\\"excludeSource\\":null,\\"hot\\":true,\\"runBeforeMainModule\\":[\\"InitializeCore\\"],\\"runModule\\":true,\\"inlineSourceMap\\":false,\\"isolateModuleIDs\\":false,\\"platform\\":\\"ios\\",\\"resolutionResponse\\":null,\\"entryModuleOnly\\":false,\\"assetPlugins\\":[],\\"onProgress\\":null,\\"unbundle\\":false}"`;

View File

@ -33,7 +33,7 @@ const resolveSync: ResolveSync = require('resolve').sync;
import type {CustomError} from '../lib/formatBundlingError';
import type {IncomingMessage, ServerResponse} from 'http';
import type {Reporter} from '../lib/reporting';
import type {Options as DeltaBundlerOptions} from '../DeltaBundler/Serializers';
import type {DeltaOptions} from '../DeltaBundler/Serializers';
import type {BundleOptions, Options} from '../shared/types.flow';
import type {
GetTransformOptions,
@ -229,7 +229,6 @@ class Server {
async build(options: BundleOptions): Promise<{code: string, map: string}> {
options = {
...options,
deltaBundleId: null,
runBeforeMainModule: this._opts.getModulesRunBeforeMainModule(
options.entryFile,
),
@ -252,16 +251,11 @@ class Server {
}
async getRamBundleInfo(options: BundleOptions): Promise<RamBundleInfo> {
options = {...options, deltaBundleId: null};
return await Serializers.getRamBundleInfo(this._deltaBundler, options);
}
async getAssets(options: BundleOptions): Promise<$ReadOnlyArray<AssetData>> {
return await Serializers.getAssets(this._deltaBundler, {
...options,
deltaBundleId: null,
});
return await Serializers.getAssets(this._deltaBundler, options);
}
async getOrderedDependencyPaths(options: {
@ -274,7 +268,6 @@ class Server {
...Server.DEFAULT_BUNDLE_OPTIONS,
...options,
bundleType: 'delta',
deltaBundleId: null,
};
if (!bundleOptions.platform) {
@ -401,6 +394,7 @@ class Server {
// can be ignored to calculate the options hash.
const ignoredParams = {
onProgress: null,
deltaBundleId: null,
excludeSource: null,
sourceMapUrl: null,
};
@ -444,7 +438,7 @@ class Server {
_prepareDeltaBundler(
req: IncomingMessage,
mres: MultipartResponse,
): {options: DeltaBundlerOptions, buildID: string} {
): {options: DeltaOptions, buildID: string} {
const options = this._getOptionsFromUrl(
url.format({
...url.parse(req.url),
@ -503,11 +497,14 @@ class Server {
let output;
const clientId = this._optionsHash(options);
try {
output = await Serializers.deltaBundle(this._deltaBundler, {
...options,
deltaBundleId: options.deltaBundleId,
});
output = await Serializers.deltaBundle(
this._deltaBundler,
clientId,
options,
);
} catch (error) {
this._handleError(mres, this._optionsHash(options), error);
@ -552,10 +549,7 @@ class Server {
let result;
try {
result = await Serializers.fullBundle(this._deltaBundler, {
...options,
deltaBundleId: this._optionsHash(options),
});
result = await Serializers.fullBundle(this._deltaBundler, options);
} catch (error) {
this._handleError(mres, this._optionsHash(options), error);
@ -619,10 +613,7 @@ class Server {
let sourceMap;
try {
sourceMap = await Serializers.fullSourceMap(this._deltaBundler, {
...options,
deltaBundleId: this._optionsHash(options),
});
sourceMap = await Serializers.fullSourceMap(this._deltaBundler, options);
} catch (error) {
this._handleError(mres, this._optionsHash(options), error);
@ -751,12 +742,9 @@ class Server {
}
async _sourceMapForURL(reqUrl: string): Promise<MetroSourceMap> {
const options: DeltaBundlerOptions = this._getOptionsFromUrl(reqUrl);
const options: DeltaOptions = this._getOptionsFromUrl(reqUrl);
return await Serializers.fullSourceMapObject(this._deltaBundler, {
...options,
deltaBundleId: this._optionsHash(options),
});
return await Serializers.fullSourceMapObject(this._deltaBundler, options);
}
_handleError(res: ServerResponse, bundleID: string, error: CustomError) {
@ -776,7 +764,7 @@ class Server {
});
}
_getOptionsFromUrl(reqUrl: string): BundleOptions & DeltaBundlerOptions {
_getOptionsFromUrl(reqUrl: string): BundleOptions & DeltaOptions {
// `true` to parse the query param as an object.
const urlObj = nullthrows(url.parse(reqUrl, true));
const urlQuery = nullthrows(urlObj.query);

View File

@ -14,12 +14,12 @@ const Serializers = require('../DeltaBundler/Serializers');
const {getAssetFiles} = require('../Assets');
import type {Options} from '../DeltaBundler/Serializers';
import type DeltaBundler from '../DeltaBundler';
import type {BundleOptions} from '../shared/types.flow';
async function getOrderedDependencyPaths(
deltaBundler: DeltaBundler,
options: Options,
options: BundleOptions,
): Promise<Array<string>> {
const modules = await Serializers.getAllModules(deltaBundler, options);