From 1e8e4dc2235401d95f8c84e71cb3c4737034dee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Tue, 8 Dec 2015 12:23:41 -0800 Subject: [PATCH] Introduce `loadBundles` Summary: public Introduce a new Polyfill module to load bundles. This polyfill contains the function to which System.import gets transformed into. It works similarly to require in the way it's registered. It keeps track of the bundles that have ever been requested using promises, either fulfilled or not. The use of promises makes the implementation quite easy as we don't need to differenciate whether the request has been started or not. There're a couple of follow up steps that still need to get done: - Included this polyfill on the ones we automatically include on the bundle. - Modify the transform to include the modules the user is actually requesting and pipe that through loadBundles so that the promise, once resolved, has the ordered list of modules the user requested. - Implement the actual native code that loads a bundle. This shouldn't be that taught as native will be able to assume it will never receive the same request twice. Reviewed By: davidaurelio Differential Revision: D2727241 fb-gh-sync-id: 317d80754783caf43f10c71a34a4558a4d298d45 --- .../polyfills/__tests__/loadBundles-test.js | 64 +++++++++++++++++++ .../src/Resolver/polyfills/loadBundles.js | 50 +++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 react-packager/src/Resolver/polyfills/__tests__/loadBundles-test.js create mode 100644 react-packager/src/Resolver/polyfills/loadBundles.js 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); + }); + }; +}