@hot-updater/react-native 1.0.0-rc.15 → 1.0.0-rc.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +491 -388
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +5 -11
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +290 -12
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +35 -19
- package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
- package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
- package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
- package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +110 -52
- package/android/src/main/java/com/hotupdater/PreviousProcessExit.kt +89 -0
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
- package/android/src/newarch/HotUpdaterModule.kt +74 -69
- package/android/src/oldarch/HotUpdaterModule.kt +63 -59
- package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
- package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +1 -1
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +446 -61
- package/android/src/test/java/com/hotupdater/HotUpdaterExceptionTest.kt +220 -0
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +19 -27
- package/android/src/test/java/com/hotupdater/InstallIdentityServiceTest.kt +71 -0
- package/android/src/test/java/com/hotupdater/KeyValueStorageServiceTest.kt +106 -0
- package/android/src/test/java/com/hotupdater/NoBackupContext.kt +11 -0
- package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +309 -23
- package/android/src/test/java/com/hotupdater/PreviousProcessExitTest.kt +57 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +238 -157
- package/ios/HotUpdater/Internal/BundleMetadata.swift +3 -53
- package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +8 -18
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +48 -29
- package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +108 -1
- package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
- package/lib/commonjs/appReady.js +111 -0
- package/lib/commonjs/appReady.js.map +1 -0
- package/lib/commonjs/appReady.spec.js +360 -0
- package/lib/commonjs/appReady.spec.js.map +1 -0
- package/lib/commonjs/{notifyAppReadyInsights.test-utils.js → appReady.test-utils.js} +4 -3
- package/lib/commonjs/appReady.test-utils.js.map +1 -0
- package/lib/commonjs/baseURL.js +16 -0
- package/lib/commonjs/baseURL.js.map +1 -0
- package/lib/commonjs/checkForUpdate.js +104 -56
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +337 -74
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/commonjs/clientPlugin.js +49 -0
- package/lib/commonjs/clientPlugin.js.map +1 -0
- package/lib/commonjs/entryImportGraph.spec.js +50 -0
- package/lib/commonjs/entryImportGraph.spec.js.map +1 -0
- package/lib/commonjs/fetchJSON.js +2 -6
- package/lib/commonjs/fetchJSON.js.map +1 -1
- package/lib/commonjs/httpClient.js +15 -67
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/httpClient.spec.js +26 -9
- package/lib/commonjs/httpClient.spec.js.map +1 -1
- package/lib/commonjs/index.js +49 -25
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +126 -40
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +67 -48
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +80 -58
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/pluginHost.js +208 -0
- package/lib/commonjs/pluginHost.js.map +1 -0
- package/lib/commonjs/pluginHost.spec.js +292 -0
- package/lib/commonjs/pluginHost.spec.js.map +1 -0
- package/lib/commonjs/plugins/insights/eventId.js +129 -0
- package/lib/commonjs/plugins/insights/eventId.js.map +1 -0
- package/lib/commonjs/plugins/insights/eventId.spec.js +47 -0
- package/lib/commonjs/plugins/insights/eventId.spec.js.map +1 -0
- package/lib/commonjs/plugins/insights/index.js +272 -0
- package/lib/commonjs/plugins/insights/index.js.map +1 -0
- package/lib/commonjs/plugins/insights/insights.spec.js +640 -0
- package/lib/commonjs/plugins/insights/insights.spec.js.map +1 -0
- package/lib/commonjs/plugins/insights/sender.js +128 -0
- package/lib/commonjs/plugins/insights/sender.js.map +1 -0
- package/lib/commonjs/plugins/insights/sender.spec.js +265 -0
- package/lib/commonjs/plugins/insights/sender.spec.js.map +1 -0
- package/lib/commonjs/plugins/insights/state.js +145 -0
- package/lib/commonjs/plugins/insights/state.js.map +1 -0
- package/lib/commonjs/releaseCatalogCache.js +13 -2
- package/lib/commonjs/releaseCatalogCache.js.map +1 -1
- package/lib/commonjs/releaseCatalogCache.spec.js +18 -0
- package/lib/commonjs/releaseCatalogCache.spec.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/updateError.js +132 -0
- package/lib/commonjs/updateError.js.map +1 -0
- package/lib/commonjs/updateError.spec.js +149 -0
- package/lib/commonjs/updateError.spec.js.map +1 -0
- package/lib/commonjs/wrap.js +43 -25
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.progress.spec.js +195 -0
- package/lib/commonjs/wrap.progress.spec.js.map +1 -0
- package/lib/commonjs/wrap.spec.js +94 -230
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/appReady.js +103 -0
- package/lib/module/appReady.js.map +1 -0
- package/lib/module/appReady.spec.js +359 -0
- package/lib/module/appReady.spec.js.map +1 -0
- package/lib/module/{notifyAppReadyInsights.test-utils.js → appReady.test-utils.js} +4 -3
- package/lib/module/appReady.test-utils.js.map +1 -0
- package/lib/module/baseURL.js +11 -0
- package/lib/module/baseURL.js.map +1 -0
- package/lib/module/checkForUpdate.js +103 -57
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +337 -74
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/module/clientPlugin.js +44 -0
- package/lib/module/clientPlugin.js.map +1 -0
- package/lib/module/entryImportGraph.spec.js +50 -0
- package/lib/module/entryImportGraph.spec.js.map +1 -0
- package/lib/module/fetchJSON.js +2 -6
- package/lib/module/fetchJSON.js.map +1 -1
- package/lib/module/httpClient.js +14 -66
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/httpClient.spec.js +26 -9
- package/lib/module/httpClient.spec.js.map +1 -1
- package/lib/module/index.js +44 -27
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +127 -41
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +63 -44
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +80 -58
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/pluginHost.js +200 -0
- package/lib/module/pluginHost.js.map +1 -0
- package/lib/module/pluginHost.spec.js +291 -0
- package/lib/module/pluginHost.spec.js.map +1 -0
- package/lib/module/plugins/insights/eventId.js +122 -0
- package/lib/module/plugins/insights/eventId.js.map +1 -0
- package/lib/module/plugins/insights/eventId.spec.js +47 -0
- package/lib/module/plugins/insights/eventId.spec.js.map +1 -0
- package/lib/module/plugins/insights/index.js +268 -0
- package/lib/module/plugins/insights/index.js.map +1 -0
- package/lib/module/plugins/insights/insights.spec.js +639 -0
- package/lib/module/plugins/insights/insights.spec.js.map +1 -0
- package/lib/module/plugins/insights/sender.js +123 -0
- package/lib/module/plugins/insights/sender.js.map +1 -0
- package/lib/module/plugins/insights/sender.spec.js +265 -0
- package/lib/module/plugins/insights/sender.spec.js.map +1 -0
- package/lib/module/plugins/insights/state.js +139 -0
- package/lib/module/plugins/insights/state.js.map +1 -0
- package/lib/module/releaseCatalogCache.js +13 -2
- package/lib/module/releaseCatalogCache.js.map +1 -1
- package/lib/module/releaseCatalogCache.spec.js +18 -0
- package/lib/module/releaseCatalogCache.spec.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/updateError.js +126 -0
- package/lib/module/updateError.js.map +1 -0
- package/lib/module/updateError.spec.js +149 -0
- package/lib/module/updateError.spec.js.map +1 -0
- package/lib/module/wrap.js +43 -25
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.progress.spec.js +196 -0
- package/lib/module/wrap.progress.spec.js.map +1 -0
- package/lib/module/wrap.spec.js +93 -229
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/appReady.d.ts +23 -0
- package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
- package/lib/typescript/commonjs/appReady.test-utils.d.ts +9 -0
- package/lib/typescript/commonjs/appReady.test-utils.d.ts.map +1 -0
- package/lib/typescript/commonjs/baseURL.d.ts +4 -0
- package/lib/typescript/commonjs/baseURL.d.ts.map +1 -0
- package/lib/typescript/commonjs/checkForUpdate.d.ts +15 -2
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/clientPlugin.d.ts +185 -0
- package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
- package/lib/typescript/commonjs/fetchJSON.d.ts +2 -3
- package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +2 -30
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -6
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +29 -15
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/pluginHost.d.ts +29 -0
- package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
- package/lib/typescript/commonjs/plugins/insights/eventId.d.ts +18 -0
- package/lib/typescript/commonjs/plugins/insights/eventId.d.ts.map +1 -0
- package/lib/typescript/commonjs/plugins/insights/index.d.ts +40 -0
- package/lib/typescript/commonjs/plugins/insights/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/plugins/insights/sender.d.ts +74 -0
- package/lib/typescript/commonjs/plugins/insights/sender.d.ts.map +1 -0
- package/lib/typescript/commonjs/plugins/insights/state.d.ts +46 -0
- package/lib/typescript/commonjs/plugins/insights/state.d.ts.map +1 -0
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts +5 -1
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +34 -12
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/updateError.d.ts +26 -0
- package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +6 -5
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
- package/lib/typescript/module/appReady.d.ts +23 -0
- package/lib/typescript/module/appReady.d.ts.map +1 -0
- package/lib/typescript/module/appReady.test-utils.d.ts +9 -0
- package/lib/typescript/module/appReady.test-utils.d.ts.map +1 -0
- package/lib/typescript/module/baseURL.d.ts +4 -0
- package/lib/typescript/module/baseURL.d.ts.map +1 -0
- package/lib/typescript/module/checkForUpdate.d.ts +15 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/clientPlugin.d.ts +185 -0
- package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
- package/lib/typescript/module/fetchJSON.d.ts +2 -3
- package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +2 -30
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -6
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +29 -15
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/pluginHost.d.ts +29 -0
- package/lib/typescript/module/pluginHost.d.ts.map +1 -0
- package/lib/typescript/module/plugins/insights/eventId.d.ts +18 -0
- package/lib/typescript/module/plugins/insights/eventId.d.ts.map +1 -0
- package/lib/typescript/module/plugins/insights/index.d.ts +40 -0
- package/lib/typescript/module/plugins/insights/index.d.ts.map +1 -0
- package/lib/typescript/module/plugins/insights/sender.d.ts +74 -0
- package/lib/typescript/module/plugins/insights/sender.d.ts.map +1 -0
- package/lib/typescript/module/plugins/insights/state.d.ts +46 -0
- package/lib/typescript/module/plugins/insights/state.d.ts.map +1 -0
- package/lib/typescript/module/releaseCatalogCache.d.ts +5 -1
- package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +34 -12
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/updateError.d.ts +26 -0
- package/lib/typescript/module/updateError.d.ts.map +1 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +6 -5
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +14 -2
- package/plugins/insights/package.json +8 -0
- package/src/appReady.spec.ts +393 -0
- package/src/{notifyAppReadyInsights.test-utils.ts → appReady.test-utils.ts} +7 -7
- package/src/appReady.ts +149 -0
- package/src/baseURL.ts +15 -0
- package/src/checkForUpdate.releaseCatalog.spec.ts +370 -76
- package/src/checkForUpdate.ts +135 -80
- package/src/clientPlugin.ts +231 -0
- package/src/entryImportGraph.spec.ts +65 -0
- package/src/fetchJSON.ts +3 -10
- package/src/httpClient.spec.ts +28 -9
- package/src/httpClient.ts +16 -109
- package/src/index.spec.ts +129 -36
- package/src/index.ts +67 -37
- package/src/native.spec.ts +98 -72
- package/src/native.ts +93 -77
- package/src/pluginHost.spec.ts +297 -0
- package/src/pluginHost.ts +262 -0
- package/src/plugins/insights/eventId.spec.ts +60 -0
- package/src/plugins/insights/eventId.ts +171 -0
- package/src/plugins/insights/index.ts +367 -0
- package/src/plugins/insights/insights.spec.ts +732 -0
- package/src/plugins/insights/sender.spec.ts +287 -0
- package/src/plugins/insights/sender.ts +221 -0
- package/src/plugins/insights/state.ts +184 -0
- package/src/releaseCatalogCache.spec.ts +22 -0
- package/src/releaseCatalogCache.ts +19 -4
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +34 -13
- package/src/updateError.spec.ts +144 -0
- package/src/updateError.ts +153 -0
- package/src/wrap.progress.spec.ts +232 -0
- package/src/wrap.spec.ts +100 -283
- package/src/wrap.tsx +57 -26
- package/src/wrap.types.ts +6 -5
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -225
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.js +0 -188
- package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -378
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -124
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -224
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.js +0 -181
- package/lib/module/notifyAppReadyInsights.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.spec.js +0 -377
- package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -123
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -25
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -25
- package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
- package/src/notifyAppReadyInsights.boundary.spec.ts +0 -242
- package/src/notifyAppReadyInsights.spec.ts +0 -362
- package/src/notifyAppReadyInsights.transport.spec.ts +0 -138
- package/src/notifyAppReadyInsights.ts +0 -293
|
@@ -199,6 +199,28 @@ describe("Release Catalog persistent cache", () => {
|
|
|
199
199
|
},
|
|
200
200
|
);
|
|
201
201
|
|
|
202
|
+
it("reads a 404 the server marks x-hot-updater-catalog: none as no catalog", async () => {
|
|
203
|
+
const fetchMock = vi.fn().mockResolvedValueOnce(
|
|
204
|
+
new Response(null, {
|
|
205
|
+
headers: { "x-hot-updater-catalog": "none" },
|
|
206
|
+
status: 404,
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
210
|
+
|
|
211
|
+
await expect(fetchReleaseCatalogWithCache(input())).resolves.toBeNull();
|
|
212
|
+
expect(nativeMocks.write).not.toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("throws the HTTP status of any other 404", async () => {
|
|
216
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValueOnce(response(404)));
|
|
217
|
+
|
|
218
|
+
await expect(fetchReleaseCatalogWithCache(input())).rejects.toMatchObject({
|
|
219
|
+
name: "UpdateHttpError",
|
|
220
|
+
status: 404,
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
202
224
|
it("does not use cached data as a fallback on network failure", async () => {
|
|
203
225
|
const fetchMock = vi
|
|
204
226
|
.fn()
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
type ReleaseCatalog,
|
|
9
9
|
} from "@hot-updater/core";
|
|
10
10
|
|
|
11
|
+
import { InvalidUpdateResponseError, UpdateHttpError } from "./updateError";
|
|
12
|
+
|
|
11
13
|
const CACHE_FORMAT_VERSION = "1";
|
|
12
14
|
const MAX_ETAG_BYTES = 1024;
|
|
13
15
|
export const MAX_RELEASE_CATALOG_WIRE_BYTES =
|
|
@@ -290,16 +292,25 @@ const consumeSuccessfulResponse = async (
|
|
|
290
292
|
response: Response,
|
|
291
293
|
input: FetchReleaseCatalogInput,
|
|
292
294
|
partition: string,
|
|
293
|
-
): Promise<ReleaseCatalog> => {
|
|
295
|
+
): Promise<ReleaseCatalog | null> => {
|
|
296
|
+
// The server marks the 404 of a scope that has no catalog yet: no update,
|
|
297
|
+
// not a failure. Any other 404 is a wrong baseURL or route.
|
|
298
|
+
if (
|
|
299
|
+
response.status === 404 &&
|
|
300
|
+
response.headers.get("x-hot-updater-catalog")?.trim().toLowerCase() ===
|
|
301
|
+
"none"
|
|
302
|
+
) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
294
305
|
if (response.status !== 200) {
|
|
295
|
-
throw new
|
|
306
|
+
throw new UpdateHttpError(response.status, response.statusText);
|
|
296
307
|
}
|
|
297
308
|
|
|
298
309
|
const body = await response.text();
|
|
299
310
|
const catalog = parseValidatedCatalog(body, input.expectedScope);
|
|
300
311
|
if (catalog === null) {
|
|
301
312
|
await removeNativeReleaseCatalogCache(partition);
|
|
302
|
-
throw new
|
|
313
|
+
throw new InvalidUpdateResponseError("Received an invalid Release catalog");
|
|
303
314
|
}
|
|
304
315
|
|
|
305
316
|
const etag = response.headers.get("etag");
|
|
@@ -323,9 +334,13 @@ const consumeSuccessfulResponse = async (
|
|
|
323
334
|
return catalog;
|
|
324
335
|
};
|
|
325
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Fetches a scope's catalog, or null when the server answers that the scope
|
|
339
|
+
* has none (a 404 with `x-hot-updater-catalog: none`).
|
|
340
|
+
*/
|
|
326
341
|
export const fetchReleaseCatalogWithCache = async (
|
|
327
342
|
input: FetchReleaseCatalogInput,
|
|
328
|
-
): Promise<ReleaseCatalog> => {
|
|
343
|
+
): Promise<ReleaseCatalog | null> => {
|
|
329
344
|
const partition = createReleaseCatalogCachePartition(input);
|
|
330
345
|
const cached = await readValidatedCache(partition, input.expectedScope);
|
|
331
346
|
const response = await fetchCatalogResponse(input, cached?.etag);
|
package/src/sdkVersion.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.
|
|
1
|
+
export const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.17";
|
|
@@ -34,7 +34,11 @@ export interface Spec extends TurboModule {
|
|
|
34
34
|
* Downloads and applies a bundle update.
|
|
35
35
|
*
|
|
36
36
|
* @param params - Update bundle parameters
|
|
37
|
-
* @returns Promise that resolves to
|
|
37
|
+
* @returns Promise that resolves, once the bundle is staged, to how it
|
|
38
|
+
* arrived: `{ delivery, patchFallback }`. `delivery` is "patch" when a
|
|
39
|
+
* bsdiff patch produced a file, "manifest" when only changed files were
|
|
40
|
+
* downloaded, or "archive" for the full archive; `patchFallback` is true
|
|
41
|
+
* when a patch was tried but the file or archive was downloaded instead.
|
|
38
42
|
* @throws {HotUpdaterErrorCode} Rejects with one of the following error codes:
|
|
39
43
|
*
|
|
40
44
|
* Parameter validation:
|
|
@@ -59,8 +63,20 @@ export interface Spec extends TurboModule {
|
|
|
59
63
|
*
|
|
60
64
|
* Note: iOS normalizes rare signature/storage errors to SIGNATURE_VERIFICATION_FAILED
|
|
61
65
|
* or UNKNOWN_ERROR to keep the JS error surface small.
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
*
|
|
67
|
+
* A rejection from the download or install also carries `userInfo`
|
|
68
|
+
* `{ stage, reason, resource?, httpStatus?, transport?, originCode? }`:
|
|
69
|
+
* `stage` is "download" (transfer and verification) or "install" (patch,
|
|
70
|
+
* extract, move into place); `reason` is one of "network", "http",
|
|
71
|
+
* "invalid_response", "hash_mismatch", "signature", "patch", "extract",
|
|
72
|
+
* "storage", or "unknown"; `resource` is what was being fetched or
|
|
73
|
+
* applied: "manifest", "file", "patch", or "archive". `httpStatus` is
|
|
74
|
+
* present when `reason` is "http", with `originCode`, the storage
|
|
75
|
+
* origin's XML error `<Code>` when the body names one. `transport` is
|
|
76
|
+
* present when no response arrived: "timeout", "dns", "tls",
|
|
77
|
+
* "connection", "offline", or "cancelled".
|
|
78
|
+
*/
|
|
79
|
+
updateBundle(params: UpdateBundleParams): Promise<UnsafeObject>;
|
|
64
80
|
|
|
65
81
|
/** Accepts and durably advances the catalog high-water for a scope. */
|
|
66
82
|
acceptReleaseCatalog(params: UnsafeObject): boolean;
|
|
@@ -98,6 +114,13 @@ export interface Spec extends TurboModule {
|
|
|
98
114
|
toBundleId?: string;
|
|
99
115
|
updateStrategy?: "fingerprint" | "appVersion";
|
|
100
116
|
crashedBundleId?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Android 11+: why the app's previous main process exited, the
|
|
119
|
+
* `ApplicationExitInfo` reason without its `REASON_` prefix (for
|
|
120
|
+
* example "CRASH", "ANR", "LOW_MEMORY", "USER_REQUESTED"), read once
|
|
121
|
+
* when the process starts. Absent on iOS and older Android.
|
|
122
|
+
*/
|
|
123
|
+
previousProcessExit?: string;
|
|
101
124
|
};
|
|
102
125
|
|
|
103
126
|
/**
|
|
@@ -164,24 +187,22 @@ export interface Spec extends TurboModule {
|
|
|
164
187
|
getCohort: () => string;
|
|
165
188
|
|
|
166
189
|
/**
|
|
167
|
-
* Gets the
|
|
190
|
+
* Gets the install id: a random id native creates once per app
|
|
191
|
+
* installation and keeps out of device backups.
|
|
168
192
|
*/
|
|
169
193
|
getInstallId: () => string;
|
|
170
194
|
|
|
171
195
|
/**
|
|
172
|
-
*
|
|
173
|
-
|
|
174
|
-
getUserId: () => string | null;
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Gets the persisted nullable username associated with this installation.
|
|
196
|
+
* Reads a value from the SDK's persistent key-value store, which native
|
|
197
|
+
* keeps out of device backups. Returns null when the key has no value.
|
|
178
198
|
*/
|
|
179
|
-
|
|
199
|
+
getStorageItem: (key: string) => string | null;
|
|
180
200
|
|
|
181
201
|
/**
|
|
182
|
-
*
|
|
202
|
+
* Writes a value to the SDK's persistent key-value store; null removes the
|
|
203
|
+
* key.
|
|
183
204
|
*/
|
|
184
|
-
|
|
205
|
+
setStorageItem: (key: string, value: string | null) => void;
|
|
185
206
|
|
|
186
207
|
// EventEmitter
|
|
187
208
|
addListener(eventName: string): void;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { StaleReleaseCatalogError } from "./error";
|
|
4
|
+
import { FetchJSONResponseError } from "./fetchJSON";
|
|
5
|
+
import {
|
|
6
|
+
classifyUpdateError,
|
|
7
|
+
InvalidUpdateResponseError,
|
|
8
|
+
UpdateHttpError,
|
|
9
|
+
} from "./updateError";
|
|
10
|
+
|
|
11
|
+
const nativeRejection = (userInfo?: Record<string, unknown>) =>
|
|
12
|
+
Object.assign(new Error("native failure"), {
|
|
13
|
+
code: "DOWNLOAD_FAILED",
|
|
14
|
+
...(userInfo === undefined ? {} : { userInfo }),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("classifyUpdateError", () => {
|
|
18
|
+
it.each([
|
|
19
|
+
{
|
|
20
|
+
label: "an HTTP status",
|
|
21
|
+
error: new UpdateHttpError(503, "Service Unavailable"),
|
|
22
|
+
expected: { reason: "http", httpStatus: 503 },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
label: "a JSON request's HTTP status",
|
|
26
|
+
error: new FetchJSONResponseError(404, "Not Found"),
|
|
27
|
+
expected: { reason: "http", httpStatus: 404 },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: "fetch without a response",
|
|
31
|
+
error: new TypeError("Network request failed"),
|
|
32
|
+
expected: { reason: "network" },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: "a timeout",
|
|
36
|
+
error: new Error("Request timed out"),
|
|
37
|
+
expected: { reason: "network", transport: "timeout" },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: "a cancelled request",
|
|
41
|
+
error: Object.assign(new Error("aborted"), { name: "AbortError" }),
|
|
42
|
+
expected: { reason: "network", transport: "cancelled" },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: "an invalid response",
|
|
46
|
+
error: new InvalidUpdateResponseError(
|
|
47
|
+
"Received an invalid Release catalog",
|
|
48
|
+
),
|
|
49
|
+
expected: { reason: "invalid_response" },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: "unparsable JSON",
|
|
53
|
+
error: new SyntaxError("Unexpected token"),
|
|
54
|
+
expected: { reason: "invalid_response" },
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
label: "anything else",
|
|
58
|
+
error: new Error("Fingerprint hash is required"),
|
|
59
|
+
expected: { reason: "unknown" },
|
|
60
|
+
},
|
|
61
|
+
])("classifies $label in the stage it happened", ({ error, expected }) => {
|
|
62
|
+
expect(classifyUpdateError(error, "check", "catalog")).toEqual({
|
|
63
|
+
stage: "check",
|
|
64
|
+
resource: "catalog",
|
|
65
|
+
...expected,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("leaves out the resource when the caller names none", () => {
|
|
70
|
+
expect(classifyUpdateError(new Error("unexpected"), "download")).toEqual({
|
|
71
|
+
stage: "download",
|
|
72
|
+
reason: "unknown",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("takes a native rejection's own classification", () => {
|
|
77
|
+
expect(
|
|
78
|
+
classifyUpdateError(
|
|
79
|
+
nativeRejection({
|
|
80
|
+
reason: "patch",
|
|
81
|
+
resource: "patch",
|
|
82
|
+
stage: "install",
|
|
83
|
+
}),
|
|
84
|
+
"download",
|
|
85
|
+
"artifact",
|
|
86
|
+
),
|
|
87
|
+
).toEqual({ stage: "install", reason: "patch", resource: "patch" });
|
|
88
|
+
expect(
|
|
89
|
+
classifyUpdateError(
|
|
90
|
+
nativeRejection({
|
|
91
|
+
httpStatus: 403,
|
|
92
|
+
originCode: "AccessDenied",
|
|
93
|
+
reason: "http",
|
|
94
|
+
resource: "file",
|
|
95
|
+
stage: "download",
|
|
96
|
+
}),
|
|
97
|
+
"download",
|
|
98
|
+
),
|
|
99
|
+
).toEqual({
|
|
100
|
+
stage: "download",
|
|
101
|
+
reason: "http",
|
|
102
|
+
resource: "file",
|
|
103
|
+
httpStatus: 403,
|
|
104
|
+
originCode: "AccessDenied",
|
|
105
|
+
});
|
|
106
|
+
expect(
|
|
107
|
+
classifyUpdateError(
|
|
108
|
+
nativeRejection({
|
|
109
|
+
reason: "network",
|
|
110
|
+
stage: "download",
|
|
111
|
+
transport: "dns",
|
|
112
|
+
}),
|
|
113
|
+
"download",
|
|
114
|
+
),
|
|
115
|
+
).toEqual({ stage: "download", reason: "network", transport: "dns" });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("drops native details that do not fit their field", () => {
|
|
119
|
+
expect(
|
|
120
|
+
classifyUpdateError(
|
|
121
|
+
nativeRejection({
|
|
122
|
+
httpStatus: 403,
|
|
123
|
+
originCode: "Access Denied <Key>secret</Key>",
|
|
124
|
+
reason: "http",
|
|
125
|
+
resource: "bucket",
|
|
126
|
+
stage: "download",
|
|
127
|
+
transport: "timeout",
|
|
128
|
+
}),
|
|
129
|
+
"download",
|
|
130
|
+
),
|
|
131
|
+
).toEqual({ stage: "download", reason: "http", httpStatus: 403 });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it.each([
|
|
135
|
+
{ label: "a stale selection", error: new StaleReleaseCatalogError() },
|
|
136
|
+
{ label: "an unclassified native rejection", error: nativeRejection() },
|
|
137
|
+
{
|
|
138
|
+
label: "a native rejection with an unknown class",
|
|
139
|
+
error: nativeRejection({ reason: "cosmic", stage: "download" }),
|
|
140
|
+
},
|
|
141
|
+
])("reports $label as no update failure", ({ error }) => {
|
|
142
|
+
expect(classifyUpdateError(error, "download")).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
UpdateError,
|
|
3
|
+
UpdateErrorReason,
|
|
4
|
+
UpdateErrorResource,
|
|
5
|
+
UpdateErrorStage,
|
|
6
|
+
UpdateErrorTransport,
|
|
7
|
+
} from "./clientPlugin";
|
|
8
|
+
import { HotUpdaterError } from "./error";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A response the SDK cannot use: a malformed or stale catalog, an artifact
|
|
12
|
+
* in another protocol, or an invalid URL.
|
|
13
|
+
*/
|
|
14
|
+
export class InvalidUpdateResponseError extends HotUpdaterError {
|
|
15
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "InvalidUpdateResponseError";
|
|
18
|
+
if (options?.cause !== undefined) this.cause = options.cause;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A request the server answered with a status the SDK does not accept. */
|
|
23
|
+
export class UpdateHttpError extends HotUpdaterError {
|
|
24
|
+
constructor(
|
|
25
|
+
readonly status: number,
|
|
26
|
+
statusText: string,
|
|
27
|
+
) {
|
|
28
|
+
super(
|
|
29
|
+
`Request failed with HTTP ${status}${statusText ? ` ${statusText}` : ""}`,
|
|
30
|
+
);
|
|
31
|
+
this.name = "UpdateHttpError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const STAGES: readonly UpdateErrorStage[] = ["check", "download", "install"];
|
|
36
|
+
const REASONS: readonly UpdateErrorReason[] = [
|
|
37
|
+
"network",
|
|
38
|
+
"http",
|
|
39
|
+
"invalid_response",
|
|
40
|
+
"hash_mismatch",
|
|
41
|
+
"signature",
|
|
42
|
+
"patch",
|
|
43
|
+
"extract",
|
|
44
|
+
"storage",
|
|
45
|
+
"unknown",
|
|
46
|
+
];
|
|
47
|
+
const RESOURCES: readonly UpdateErrorResource[] = [
|
|
48
|
+
"catalog",
|
|
49
|
+
"artifact",
|
|
50
|
+
"manifest",
|
|
51
|
+
"file",
|
|
52
|
+
"patch",
|
|
53
|
+
"archive",
|
|
54
|
+
];
|
|
55
|
+
const TRANSPORTS: readonly UpdateErrorTransport[] = [
|
|
56
|
+
"timeout",
|
|
57
|
+
"dns",
|
|
58
|
+
"tls",
|
|
59
|
+
"connection",
|
|
60
|
+
"offline",
|
|
61
|
+
"cancelled",
|
|
62
|
+
];
|
|
63
|
+
/** An S3, R2, or GCS error code: letters, digits, and punctuation only. */
|
|
64
|
+
const ORIGIN_CODE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
65
|
+
|
|
66
|
+
type Classification = Pick<
|
|
67
|
+
UpdateError,
|
|
68
|
+
"stage" | "reason" | "resource" | "httpStatus" | "originCode" | "transport"
|
|
69
|
+
>;
|
|
70
|
+
|
|
71
|
+
const oneOf = <T extends string>(
|
|
72
|
+
values: readonly T[],
|
|
73
|
+
value: unknown,
|
|
74
|
+
): value is T => values.includes(value as T);
|
|
75
|
+
|
|
76
|
+
const readNativeClassification = (
|
|
77
|
+
error: object,
|
|
78
|
+
): Classification | null | undefined => {
|
|
79
|
+
if (!("code" in error) || typeof error.code !== "string") return undefined;
|
|
80
|
+
const userInfo =
|
|
81
|
+
"userInfo" in error && typeof error.userInfo === "object"
|
|
82
|
+
? (error.userInfo as Record<string, unknown> | null)
|
|
83
|
+
: null;
|
|
84
|
+
const { stage, reason, resource, transport, originCode } = userInfo ?? {};
|
|
85
|
+
// Native classifies every failure of its download and install; a rejection
|
|
86
|
+
// without it, such as a stale selection, is not an update failure.
|
|
87
|
+
if (!oneOf(STAGES, stage) || !oneOf(REASONS, reason)) return null;
|
|
88
|
+
const httpStatus = Number(userInfo?.httpStatus);
|
|
89
|
+
return {
|
|
90
|
+
stage,
|
|
91
|
+
reason,
|
|
92
|
+
...(oneOf(RESOURCES, resource) ? { resource } : {}),
|
|
93
|
+
...(reason === "http" && Number.isSafeInteger(httpStatus)
|
|
94
|
+
? { httpStatus }
|
|
95
|
+
: {}),
|
|
96
|
+
...(reason === "http" &&
|
|
97
|
+
typeof originCode === "string" &&
|
|
98
|
+
ORIGIN_CODE.test(originCode)
|
|
99
|
+
? { originCode }
|
|
100
|
+
: {}),
|
|
101
|
+
...(reason === "network" && oneOf(TRANSPORTS, transport)
|
|
102
|
+
? { transport }
|
|
103
|
+
: {}),
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Where and why an update failed, or null when the error is not an update
|
|
109
|
+
* failure. Native rejections carry their own classification; an error the
|
|
110
|
+
* SDK's JavaScript raised happened in `stage`, fetching `resource` when one
|
|
111
|
+
* is given.
|
|
112
|
+
*/
|
|
113
|
+
export const classifyUpdateError = (
|
|
114
|
+
error: unknown,
|
|
115
|
+
stage: UpdateErrorStage,
|
|
116
|
+
resource?: UpdateErrorResource,
|
|
117
|
+
): Classification | null => {
|
|
118
|
+
if (typeof error === "object" && error !== null) {
|
|
119
|
+
const native = readNativeClassification(error);
|
|
120
|
+
if (native !== undefined) return native;
|
|
121
|
+
}
|
|
122
|
+
const scope = {
|
|
123
|
+
stage,
|
|
124
|
+
...(resource === undefined ? {} : { resource }),
|
|
125
|
+
};
|
|
126
|
+
// Names, not classes, so an error from another copy of a module still
|
|
127
|
+
// classifies, as it does across test module resets.
|
|
128
|
+
const name = error instanceof Error ? error.name : null;
|
|
129
|
+
// A selection that became stale is a race between checks, not a failure.
|
|
130
|
+
if (name === "StaleReleaseCatalogError") return null;
|
|
131
|
+
if (name === "UpdateHttpError") {
|
|
132
|
+
return {
|
|
133
|
+
...scope,
|
|
134
|
+
reason: "http",
|
|
135
|
+
httpStatus: (error as UpdateHttpError).status,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (name === "InvalidUpdateResponseError" || name === "SyntaxError") {
|
|
139
|
+
return { ...scope, reason: "invalid_response" };
|
|
140
|
+
}
|
|
141
|
+
if (error instanceof Error && error.message === "Request timed out") {
|
|
142
|
+
return { ...scope, reason: "network", transport: "timeout" };
|
|
143
|
+
}
|
|
144
|
+
if (name === "AbortError") {
|
|
145
|
+
return { ...scope, reason: "network", transport: "cancelled" };
|
|
146
|
+
}
|
|
147
|
+
if (name === "TypeError") {
|
|
148
|
+
// fetch rejects with a TypeError when no response arrives, without
|
|
149
|
+
// saying why.
|
|
150
|
+
return { ...scope, reason: "network" };
|
|
151
|
+
}
|
|
152
|
+
return { ...scope, reason: "unknown" };
|
|
153
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, cleanup, render, screen } from "@testing-library/react";
|
|
4
|
+
import { createElement } from "react";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import type { HotUpdaterProgressEvent } from "./native";
|
|
8
|
+
import type {
|
|
9
|
+
HotUpdaterFallbackComponentProps,
|
|
10
|
+
InternalWrapOptions,
|
|
11
|
+
} from "./wrap";
|
|
12
|
+
|
|
13
|
+
const mocks = vi.hoisted(() => ({
|
|
14
|
+
listeners: new Map<string, (event: HotUpdaterProgressEvent) => void>(),
|
|
15
|
+
checkForUpdate: vi.fn(),
|
|
16
|
+
reload: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("./checkForUpdate", () => ({
|
|
20
|
+
checkForUpdate: mocks.checkForUpdate,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock("./appReady", () => ({
|
|
24
|
+
handleNotifyAppReady: vi.fn().mockResolvedValue({ status: "UNCHANGED" }),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("./native", () => ({
|
|
28
|
+
addListener: vi.fn(
|
|
29
|
+
(eventName: string, listener: (event: HotUpdaterProgressEvent) => void) => {
|
|
30
|
+
mocks.listeners.set(eventName, listener);
|
|
31
|
+
return () => {
|
|
32
|
+
mocks.listeners.delete(eventName);
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
),
|
|
36
|
+
getUpdateId: vi.fn(() => "release-id"),
|
|
37
|
+
reload: mocks.reload,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
describe("HotUpdater wrap progress subscription", () => {
|
|
41
|
+
let frameCallbacks: ((timestamp: number) => void)[];
|
|
42
|
+
|
|
43
|
+
const flushFrames = async () => {
|
|
44
|
+
await act(async () => {
|
|
45
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
46
|
+
while (frameCallbacks.length > 0) {
|
|
47
|
+
frameCallbacks.shift()?.(0);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const emitProgress = async (progress: number) => {
|
|
53
|
+
const event: HotUpdaterProgressEvent = {
|
|
54
|
+
artifactType: "diff",
|
|
55
|
+
progress,
|
|
56
|
+
details: {
|
|
57
|
+
totalFilesCount: 1,
|
|
58
|
+
completedFilesCount: progress === 1 ? 1 : 0,
|
|
59
|
+
files: [
|
|
60
|
+
{
|
|
61
|
+
order: 0,
|
|
62
|
+
path: "index.bundle",
|
|
63
|
+
downloadPath: "index.bundle",
|
|
64
|
+
downloadedBytes: progress * 1_000,
|
|
65
|
+
totalBytes: 1_000,
|
|
66
|
+
progress,
|
|
67
|
+
status: progress === 1 ? "downloaded" : "downloading",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
await act(async () => {
|
|
73
|
+
mocks.listeners.get("onProgress")?.(event);
|
|
74
|
+
});
|
|
75
|
+
await flushFrames();
|
|
76
|
+
return event;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const renderWrapped = async (
|
|
80
|
+
options: Pick<
|
|
81
|
+
InternalWrapOptions,
|
|
82
|
+
"fallbackComponent" | "onProgress" | "onUpdateProcessCompleted"
|
|
83
|
+
>,
|
|
84
|
+
onRender: () => void,
|
|
85
|
+
) => {
|
|
86
|
+
const { wrap } = await import("./wrap");
|
|
87
|
+
const App = () => {
|
|
88
|
+
onRender();
|
|
89
|
+
return createElement("div", null, "app");
|
|
90
|
+
};
|
|
91
|
+
const Wrapped = wrap({
|
|
92
|
+
client: { createSession: vi.fn() },
|
|
93
|
+
updateStrategy: "appVersion",
|
|
94
|
+
...options,
|
|
95
|
+
})(App);
|
|
96
|
+
|
|
97
|
+
render(createElement(Wrapped));
|
|
98
|
+
await flushFrames();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
vi.resetModules();
|
|
103
|
+
mocks.listeners.clear();
|
|
104
|
+
mocks.checkForUpdate.mockReset();
|
|
105
|
+
mocks.checkForUpdate.mockResolvedValue(null);
|
|
106
|
+
mocks.reload.mockReset();
|
|
107
|
+
frameCallbacks = [];
|
|
108
|
+
vi.stubGlobal(
|
|
109
|
+
"requestAnimationFrame",
|
|
110
|
+
(callback: (timestamp: number) => void) => {
|
|
111
|
+
frameCallbacks.push(callback);
|
|
112
|
+
return frameCallbacks.length;
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
afterEach(() => {
|
|
118
|
+
cleanup();
|
|
119
|
+
vi.unstubAllGlobals();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("does not re-render the wrapped app on progress updates without fallbackComponent or onProgress", async () => {
|
|
123
|
+
const onRender = vi.fn();
|
|
124
|
+
await renderWrapped({}, onRender);
|
|
125
|
+
const rendersBeforeDownload = onRender.mock.calls.length;
|
|
126
|
+
|
|
127
|
+
for (const progress of [0.1, 0.2, 0.3, 0.4, 0.5]) {
|
|
128
|
+
await emitProgress(progress);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
expect(onRender).toHaveBeenCalledTimes(rendersBeforeDownload);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("keeps reporting progress to onProgress without re-rendering the wrapped app", async () => {
|
|
135
|
+
const onRender = vi.fn();
|
|
136
|
+
const onProgress = vi.fn();
|
|
137
|
+
await renderWrapped({ onProgress }, onRender);
|
|
138
|
+
const rendersBeforeDownload = onRender.mock.calls.length;
|
|
139
|
+
|
|
140
|
+
await emitProgress(0.25);
|
|
141
|
+
await emitProgress(0.75);
|
|
142
|
+
|
|
143
|
+
expect(onProgress.mock.calls).toEqual([[0], [0.25], [0.75]]);
|
|
144
|
+
expect(onRender).toHaveBeenCalledTimes(rendersBeforeDownload);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("keeps passing progress details to fallbackComponent during an update check", async () => {
|
|
148
|
+
mocks.checkForUpdate.mockReturnValue(new Promise(() => {}));
|
|
149
|
+
const fallbackProps = vi.fn();
|
|
150
|
+
const Fallback = (props: HotUpdaterFallbackComponentProps) => {
|
|
151
|
+
fallbackProps(props);
|
|
152
|
+
return createElement("div", null, `progress:${props.progress}`);
|
|
153
|
+
};
|
|
154
|
+
await renderWrapped({ fallbackComponent: Fallback }, vi.fn());
|
|
155
|
+
|
|
156
|
+
const event = await emitProgress(0.5);
|
|
157
|
+
|
|
158
|
+
expect(screen.getByText("progress:0.5")).toBeTruthy();
|
|
159
|
+
expect(fallbackProps).toHaveBeenLastCalledWith({
|
|
160
|
+
...event,
|
|
161
|
+
status: "CHECK_FOR_UPDATE",
|
|
162
|
+
message: null,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it.each([false, true])(
|
|
167
|
+
"preserves progress reporting across the fallback transition with shouldForceUpdate=%s",
|
|
168
|
+
async (shouldForceUpdate) => {
|
|
169
|
+
let resolveCheck!: (update: unknown) => void;
|
|
170
|
+
mocks.checkForUpdate.mockReturnValue(
|
|
171
|
+
new Promise((resolve) => {
|
|
172
|
+
resolveCheck = resolve;
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
let finishDownload!: (success: boolean) => void;
|
|
176
|
+
const download = new Promise<boolean>((resolve) => {
|
|
177
|
+
finishDownload = resolve;
|
|
178
|
+
});
|
|
179
|
+
const updateBundle = vi.fn(() => download);
|
|
180
|
+
const onProgress = vi.fn();
|
|
181
|
+
const onRender = vi.fn();
|
|
182
|
+
const onUpdateProcessCompleted = vi.fn();
|
|
183
|
+
const Fallback = ({
|
|
184
|
+
status,
|
|
185
|
+
progress,
|
|
186
|
+
}: HotUpdaterFallbackComponentProps) =>
|
|
187
|
+
createElement("div", null, `${status}:${progress}`);
|
|
188
|
+
await renderWrapped(
|
|
189
|
+
{ fallbackComponent: Fallback, onProgress, onUpdateProcessCompleted },
|
|
190
|
+
onRender,
|
|
191
|
+
);
|
|
192
|
+
expect(screen.getByText("CHECK_FOR_UPDATE:0")).toBeTruthy();
|
|
193
|
+
expect(onRender).not.toHaveBeenCalled();
|
|
194
|
+
|
|
195
|
+
const update = {
|
|
196
|
+
id: "new-release",
|
|
197
|
+
status: "UPDATE",
|
|
198
|
+
shouldForceUpdate,
|
|
199
|
+
message: "New release",
|
|
200
|
+
};
|
|
201
|
+
await act(async () => {
|
|
202
|
+
resolveCheck({ ...update, updateBundle });
|
|
203
|
+
});
|
|
204
|
+
expect(updateBundle).toHaveBeenCalledOnce();
|
|
205
|
+
expect(onUpdateProcessCompleted).toHaveBeenCalledTimes(
|
|
206
|
+
shouldForceUpdate ? 0 : 1,
|
|
207
|
+
);
|
|
208
|
+
expect(
|
|
209
|
+
screen.getByText(shouldForceUpdate ? "UPDATING:0" : "app"),
|
|
210
|
+
).toBeTruthy();
|
|
211
|
+
const rendersDuringDownload = onRender.mock.calls.length;
|
|
212
|
+
|
|
213
|
+
for (const progress of [0.25, 0.75, 1]) {
|
|
214
|
+
await emitProgress(progress);
|
|
215
|
+
}
|
|
216
|
+
expect(onRender).toHaveBeenCalledTimes(rendersDuringDownload);
|
|
217
|
+
if (shouldForceUpdate) {
|
|
218
|
+
expect(screen.getByText("UPDATING:1")).toBeTruthy();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await act(async () => {
|
|
222
|
+
finishDownload(true);
|
|
223
|
+
});
|
|
224
|
+
expect(screen.getByText("app")).toBeTruthy();
|
|
225
|
+
expect(onRender).toHaveBeenCalledOnce();
|
|
226
|
+
expect(onUpdateProcessCompleted).toHaveBeenCalledOnce();
|
|
227
|
+
expect(onUpdateProcessCompleted).toHaveBeenCalledWith(update);
|
|
228
|
+
expect(mocks.reload).toHaveBeenCalledTimes(shouldForceUpdate ? 1 : 0);
|
|
229
|
+
expect(onProgress.mock.calls).toEqual([[0], [0.25], [0.75], [1]]);
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
});
|