Implement caching in __loadBundleAsync (#36809)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/36809

Changelog: [General][Added] [2/n] Support lazy bundling in development

Implements caching in `__loadBundleAsync` in line with https://github.com/react-native-community/discussions-and-proposals/pull/628.

Reviewed By: jacdebug

Differential Revision: D44630077

fbshipit-source-id: f1a19217f54493e5cf0c8bba910a6a6e14c10c03
This commit is contained in:
Moti Zilberman
2023-04-12 14:21:48 -07:00
committed by Facebook GitHub Bot
parent 799b0f4be8
commit b66ab690fe
2 changed files with 59 additions and 1 deletions
@@ -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);
});
@@ -18,6 +18,8 @@ declare var global: {globalEvalWithSourceUrl?: (string, string) => mixed, ...};
let pendingRequests = 0;
const cachedPromisesByUrl = new Map<string, Promise<void>>();
function asyncRequest(
url: string,
): Promise<{body: string, headers: {[string]: string}}> {
@@ -90,9 +92,15 @@ function buildUrlForBundle(bundlePathAndQuery: string) {
module.exports = function (bundlePathAndQuery: string): Promise<void> {
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<void>(({body, headers}) => {
if (
headers['Content-Type'] != null &&
@@ -116,9 +124,16 @@ module.exports = function (bundlePathAndQuery: string): Promise<void> {
eval(body);
}
})
.catch<void>(e => {
cachedPromisesByUrl.delete(requestUrl);
throw e;
})
.finally(() => {
if (!--pendingRequests) {
LoadingView.hide();
}
});
cachedPromisesByUrl.set(requestUrl, loadPromise);
return loadPromise;
};