diff --git a/react-packager/src/Resolver/polyfills/__tests__/loadBundles-test.js b/react-packager/src/Resolver/polyfills/__tests__/loadBundles-test.js new file mode 100644 index 00000000..ad0dcf27 --- /dev/null +++ b/react-packager/src/Resolver/polyfills/__tests__/loadBundles-test.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.dontMock('../loadBundles'); +jest.mock('NativeModules'); + +let loadBundles; +let loadBundlesCalls; + +describe('loadBundles', () => { + beforeEach(() => { + loadBundles = jest.genMockFunction(); + loadBundlesCalls = loadBundles.mock.calls; + require('NativeModules').RCTBundlesLoader = {loadBundles}; + + require('../loadBundles'); + }); + + it('should set `global.__loadBundles` function when polyfill is initialized', () => { + expect(typeof global.__loadBundles).toBe('function'); + }); + + it('should return a promise', () => { + loadBundles.mockImpl((bundles, callback) => callback()); + expect(global.__loadBundles(['bundle.0']) instanceof Promise).toBeTruthy(); + }); + + pit('shouldn\'t request already loaded bundles', () => { + loadBundles.mockImpl((bundles, callback) => callback()); + return global + .__loadBundles(['bundle.0']) + .then(() => global.__loadBundles(['bundle.0'])) + .then(() => expect(loadBundlesCalls.length).toBe(1)); + }); + + pit('shouldn\'n request inflight bundles', () => { + loadBundles.mockImpl((bundles, callback) => { + if (bundles.length === 1 && bundles[0] === 'bundle.0') { + setTimeout(callback, 1000); + } else if (bundles.length === 1 && bundles[0] === 'bundle.1') { + setTimeout(callback, 500); + } + }); + + const promises = Promise.all([ + global.__loadBundles(['bundle.0']), + global.__loadBundles(['bundle.0', 'bundle.1']), + ]).then(() => { + expect(loadBundlesCalls.length).toBe(2); + expect(loadBundlesCalls[0][0][0]).toBe('bundle.0'); + expect(loadBundlesCalls[1][0][0]).toBe('bundle.1'); + }); + + jest.runAllTimers(); + return promises; + }); +}); diff --git a/react-packager/src/Resolver/polyfills/loadBundles.js b/react-packager/src/Resolver/polyfills/loadBundles.js new file mode 100644 index 00000000..551d4cac --- /dev/null +++ b/react-packager/src/Resolver/polyfills/loadBundles.js @@ -0,0 +1,50 @@ +/* eslint global-strict:0 */ +(global => { + let loadBundlesOnNative = (bundles) => + new Promise((_, resolve) => + require('NativeModules').RCTBundlesLoader.loadBundles(bundles, resolve)); + + let requestedBundles = Object.create(null); + + /** + * Returns a promise that is fulfilled once all the indicated bundles are + * loaded into memory and injected into the JS engine. + * This invokation might need to go through the bridge + * and run native code to load some, if not all, the requested bundles. + * If all the bundles have already been loaded, the promise is resolved + * immediately. Otherwise, we'll determine which bundles still need to get + * loaded considering both, the ones already loaded, and the ones being + * currently asynchronously loaded by other invocations to `__loadBundles`, + * and return a promise that will get fulfilled once all these are finally + * loaded. + * + * Note this function should only be invoked by generated code. + */ + global.__loadBundles = function(bundles) { + // split bundles by whether they've already been requested or not + const bundlesToRequest = bundles.filter(b => !requestedBundles[b]); + const bundlesAlreadyRequested = bundles.filter(b => !!requestedBundles[b]); + + // keep a reference to the promise loading each bundle + if (bundlesToRequest.length > 0) { + const nativePromise = loadBundlesOnNative(bundlesToRequest); + bundlesToRequest.forEach(b => requestedBundles[b] = nativePromise); + } + + return Promise.all(bundles.map(bundle => requestedBundles[bundle])); + }; +})(global); + + +// turns a callback async based function into a promised based one +function promisify(fn) { + return function() { + var self = this; + var args = Array.prototype.slice.call(arguments); + + return new Promise((resolve, reject) => { + args.push(resolve); + fn.apply(self, args); + }); + }; +}