diff --git a/packages/react-native/Libraries/Core/Devtools/__tests__/loadBundleFromServer-test.js b/packages/react-native/Libraries/Core/Devtools/__tests__/loadBundleFromServer-test.js index 42f986e1807..3f755fbf5e8 100644 --- a/packages/react-native/Libraries/Core/Devtools/__tests__/loadBundleFromServer-test.js +++ b/packages/react-native/Libraries/Core/Devtools/__tests__/loadBundleFromServer-test.js @@ -170,3 +170,46 @@ test('shows and hides the loading view around concurrent requests', async () => await promise2; expect(loadingViewMock.hide).toHaveBeenCalledTimes(1); }); + +test('loadBundleFromServer does not cache errors', async () => { + mockHeaders = {'Content-Type': 'application/json'}; + mockDataResponse = JSON.stringify({message: 'Error thrown from Metro'}); + + await expect( + loadBundleFromServer('/Fail.bundle?platform=ios'), + ).rejects.toThrow(); + + mockDataResponse = '"code";'; + mockHeaders = {'Content-Type': 'application/javascript'}; + + await expect( + loadBundleFromServer('/Fail.bundle?platform=ios'), + ).resolves.not.toThrow(); +}); + +test('loadBundleFromServer caches successful fetches', async () => { + mockDataResponse = '"code";'; + mockHeaders = {'Content-Type': 'application/javascript'}; + + const promise1 = loadBundleFromServer( + '/Banana.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false', + ); + + // Request again in the same tick = same promise + const promise2 = loadBundleFromServer( + '/Banana.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false', + ); + expect(promise2).toBe(promise1); + + await promise1; + + // Request again once resolved = still the same promise + const promise3 = loadBundleFromServer( + '/Banana.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false', + ); + expect(promise3).toBe(promise1); + + await promise2; + + expect(sendRequest).toBeCalledTimes(1); +}); diff --git a/packages/react-native/Libraries/Core/Devtools/loadBundleFromServer.js b/packages/react-native/Libraries/Core/Devtools/loadBundleFromServer.js index 443b316fd33..222320b22bd 100644 --- a/packages/react-native/Libraries/Core/Devtools/loadBundleFromServer.js +++ b/packages/react-native/Libraries/Core/Devtools/loadBundleFromServer.js @@ -18,6 +18,8 @@ declare var global: {globalEvalWithSourceUrl?: (string, string) => mixed, ...}; let pendingRequests = 0; +const cachedPromisesByUrl = new Map>(); + function asyncRequest( url: string, ): Promise<{body: string, headers: {[string]: string}}> { @@ -90,9 +92,15 @@ function buildUrlForBundle(bundlePathAndQuery: string) { module.exports = function (bundlePathAndQuery: string): Promise { const requestUrl = buildUrlForBundle(bundlePathAndQuery); + let loadPromise = cachedPromisesByUrl.get(requestUrl); + + if (loadPromise) { + return loadPromise; + } LoadingView.showMessage('Downloading...', 'load'); ++pendingRequests; - return asyncRequest(requestUrl) + + loadPromise = asyncRequest(requestUrl) .then(({body, headers}) => { if ( headers['Content-Type'] != null && @@ -116,9 +124,16 @@ module.exports = function (bundlePathAndQuery: string): Promise { eval(body); } }) + .catch(e => { + cachedPromisesByUrl.delete(requestUrl); + throw e; + }) .finally(() => { if (!--pendingRequests) { LoadingView.hide(); } }); + + cachedPromisesByUrl.set(requestUrl, loadPromise); + return loadPromise; };