@hot-updater/react-native 1.0.0-rc.16 → 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 +18 -165
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/httpClient.spec.js +26 -199
- package/lib/commonjs/httpClient.spec.js.map +1 -1
- package/lib/commonjs/index.js +49 -36
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +108 -123
- 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 +18 -165
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/httpClient.spec.js +27 -200
- package/lib/module/httpClient.spec.js.map +1 -1
- package/lib/module/index.js +44 -38
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +108 -123
- 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 -36
- 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 -10
- 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 -36
- 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 -10
- 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 +30 -227
- package/src/httpClient.ts +20 -256
- package/src/index.spec.ts +117 -93
- package/src/index.ts +67 -52
- 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 -12
- 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 -171
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/commonjs/uuidv7.js +0 -41
- package/lib/commonjs/uuidv7.js.map +0 -1
- package/lib/commonjs/uuidv7.spec.js +0 -30
- package/lib/commonjs/uuidv7.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 -170
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/module/uuidv7.js +0 -36
- package/lib/module/uuidv7.js.map +0 -1
- package/lib/module/uuidv7.spec.js +0 -30
- package/lib/module/uuidv7.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/commonjs/uuidv7.d.ts +0 -10
- package/lib/typescript/commonjs/uuidv7.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/lib/typescript/module/uuidv7.d.ts +0 -10
- package/lib/typescript/module/uuidv7.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 -196
- package/src/notifyAppReadyInsights.ts +0 -293
- package/src/uuidv7.spec.ts +0 -25
- package/src/uuidv7.ts +0 -38
package/src/httpClient.spec.ts
CHANGED
|
@@ -1,22 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type ArtifactInfo,
|
|
4
|
-
type ReleaseCatalog,
|
|
5
|
-
} from "@hot-updater/core";
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
1
|
+
import type { ArtifactInfo, ReleaseCatalog } from "@hot-updater/core";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
3
|
|
|
8
4
|
import { FetchJSONResponseError } from "./fetchJSON";
|
|
9
|
-
import { createHttpClient
|
|
5
|
+
import { createHttpClient } from "./httpClient";
|
|
6
|
+
import { InvalidUpdateResponseError } from "./updateError";
|
|
10
7
|
|
|
11
|
-
const mocks = vi.hoisted(() => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return {
|
|
16
|
-
fetchCatalog: vi.fn(),
|
|
17
|
-
fetchJSON: vi.fn(),
|
|
18
|
-
};
|
|
19
|
-
});
|
|
8
|
+
const mocks = vi.hoisted(() => ({
|
|
9
|
+
fetchCatalog: vi.fn(),
|
|
10
|
+
fetchJSON: vi.fn(),
|
|
11
|
+
}));
|
|
20
12
|
|
|
21
13
|
vi.mock("./releaseCatalogCache", () => ({
|
|
22
14
|
fetchReleaseCatalogWithCache: mocks.fetchCatalog,
|
|
@@ -193,221 +185,32 @@ describe("private HotUpdater HTTP client", () => {
|
|
|
193
185
|
).rejects.toThrow("does not support artifact protocol 1");
|
|
194
186
|
});
|
|
195
187
|
|
|
196
|
-
it(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
platform: "ios",
|
|
211
|
-
toBundleId: "bundle-id",
|
|
212
|
-
type: "UNCHANGED",
|
|
213
|
-
updateStrategy: null,
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
/** Answers each POST /events with the next response, then 204. */
|
|
217
|
-
const stubEventsEndpoint = (...responses: (number | Response | Error)[]) => {
|
|
218
|
-
const fetchMock = vi.fn<typeof fetch>(async () => {
|
|
219
|
-
const next = responses.shift() ?? 204;
|
|
220
|
-
if (next instanceof Error) throw next;
|
|
221
|
-
return typeof next === "number"
|
|
222
|
-
? new Response(null, { status: next })
|
|
223
|
-
: next;
|
|
224
|
-
});
|
|
225
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
226
|
-
const sentEvents = () =>
|
|
227
|
-
fetchMock.mock.calls.map(
|
|
228
|
-
([, request]) =>
|
|
229
|
-
JSON.parse(String(request?.body)) as {
|
|
230
|
-
eventId: string;
|
|
231
|
-
type: string;
|
|
232
|
-
},
|
|
233
|
-
);
|
|
234
|
-
return { fetchMock, sentEvents };
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
describe("Insights event delivery", () => {
|
|
238
|
-
beforeEach(() => {
|
|
239
|
-
vi.useFakeTimers();
|
|
240
|
-
// No jitter: the first retry waits exactly 1 s and the second 2 s.
|
|
241
|
-
vi.spyOn(Math, "random").mockReturnValue(0.5);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
afterEach(() => {
|
|
245
|
-
vi.useRealTimers();
|
|
246
|
-
vi.unstubAllGlobals();
|
|
247
|
-
vi.restoreAllMocks();
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
const createSession = () =>
|
|
251
|
-
createHttpClient("https://updates.example.com").createSession();
|
|
252
|
-
|
|
253
|
-
it("retries a 503 in the background under the same UUIDv7 eventId", async () => {
|
|
254
|
-
const { fetchMock, sentEvents } = stubEventsEndpoint(503, 204);
|
|
255
|
-
const session = await createSession();
|
|
256
|
-
|
|
257
|
-
// Settles after the failed first attempt, without waiting for the retry.
|
|
258
|
-
await session.sendInsightsEvent(unchangedEvent);
|
|
259
|
-
expect(fetchMock).toHaveBeenCalledOnce();
|
|
260
|
-
|
|
261
|
-
await vi.advanceTimersByTimeAsync(1_000);
|
|
262
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
263
|
-
const [first, second] = sentEvents();
|
|
264
|
-
expect(isUUIDv7(first?.eventId)).toBe(true);
|
|
265
|
-
expect(second?.eventId).toBe(first?.eventId);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it("waits for a 429's Retry-After before the next attempt", async () => {
|
|
269
|
-
const { fetchMock } = stubEventsEndpoint(
|
|
270
|
-
new Response(null, { headers: { "Retry-After": "7" }, status: 429 }),
|
|
271
|
-
);
|
|
272
|
-
const session = await createSession();
|
|
273
|
-
|
|
274
|
-
await session.sendInsightsEvent(unchangedEvent);
|
|
275
|
-
await vi.advanceTimersByTimeAsync(6_999);
|
|
276
|
-
expect(fetchMock).toHaveBeenCalledOnce();
|
|
277
|
-
await vi.advanceTimersByTimeAsync(1);
|
|
278
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it("caps a server's Retry-After at 30 seconds", async () => {
|
|
282
|
-
const { fetchMock } = stubEventsEndpoint(
|
|
283
|
-
new Response(null, { headers: { "Retry-After": "3600" }, status: 503 }),
|
|
284
|
-
);
|
|
285
|
-
const session = await createSession();
|
|
286
|
-
|
|
287
|
-
await session.sendInsightsEvent(unchangedEvent);
|
|
288
|
-
await vi.advanceTimersByTimeAsync(29_999);
|
|
289
|
-
expect(fetchMock).toHaveBeenCalledOnce();
|
|
290
|
-
await vi.advanceTimersByTimeAsync(1);
|
|
291
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it("does not retry a 400", async () => {
|
|
295
|
-
const { fetchMock } = stubEventsEndpoint(400);
|
|
296
|
-
const session = await createSession();
|
|
297
|
-
|
|
298
|
-
await expect(session.sendInsightsEvent(unchangedEvent)).rejects.toThrow(
|
|
299
|
-
"Expected HTTP 204 from /events, received 400",
|
|
300
|
-
);
|
|
301
|
-
await vi.runAllTimersAsync();
|
|
302
|
-
expect(fetchMock).toHaveBeenCalledOnce();
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it("stops after three attempts and warns once", async () => {
|
|
306
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
307
|
-
const { fetchMock } = stubEventsEndpoint(
|
|
308
|
-
500,
|
|
309
|
-
new Error("network unavailable"),
|
|
310
|
-
503,
|
|
311
|
-
);
|
|
312
|
-
const session = await createSession();
|
|
313
|
-
|
|
314
|
-
await session.sendInsightsEvent(unchangedEvent);
|
|
315
|
-
await vi.runAllTimersAsync();
|
|
188
|
+
it.each([
|
|
189
|
+
{ label: "a legacy response", response: { fileUrl: "/storage/a.zip" } },
|
|
190
|
+
{
|
|
191
|
+
label: "an invalid absolute URL",
|
|
192
|
+
response: {
|
|
193
|
+
...artifact,
|
|
194
|
+
manifestUrl: "https://",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
])("marks $label as an invalid update response", async ({ response }) => {
|
|
198
|
+
mocks.fetchJSON.mockResolvedValue(response);
|
|
199
|
+
const session = await createHttpClient(
|
|
200
|
+
"https://updates.example.com",
|
|
201
|
+
).createSession();
|
|
316
202
|
|
|
317
|
-
expect(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
message: "Expected HTTP 204 from /events, received 503",
|
|
203
|
+
await expect(
|
|
204
|
+
session.resolveArtifact({
|
|
205
|
+
currentBundleId: "current",
|
|
206
|
+
targetBundleId: "target",
|
|
322
207
|
}),
|
|
323
|
-
);
|
|
208
|
+
).rejects.toBeInstanceOf(InvalidUpdateResponseError);
|
|
324
209
|
});
|
|
325
210
|
|
|
326
|
-
it("
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
(_input, request) =>
|
|
330
|
-
new Promise((_resolve, reject) => {
|
|
331
|
-
request?.signal?.addEventListener("abort", () => {
|
|
332
|
-
reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
|
|
333
|
-
});
|
|
334
|
-
}),
|
|
335
|
-
);
|
|
336
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
337
|
-
const session = await createSession();
|
|
338
|
-
|
|
339
|
-
const sent = session.sendInsightsEvent({
|
|
340
|
-
...unchangedEvent,
|
|
341
|
-
requestTimeout: 2_000,
|
|
342
|
-
});
|
|
343
|
-
await vi.advanceTimersByTimeAsync(2_000);
|
|
344
|
-
await sent;
|
|
345
|
-
// The second attempt starts after a 1 s backoff and gets a full 2 s.
|
|
346
|
-
await vi.advanceTimersByTimeAsync(1_000);
|
|
347
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
348
|
-
const secondSignal = fetchMock.mock.calls[1]?.[1]?.signal;
|
|
349
|
-
await vi.advanceTimersByTimeAsync(1_999);
|
|
350
|
-
expect(secondSignal?.aborted).toBe(false);
|
|
351
|
-
await vi.advanceTimersByTimeAsync(1);
|
|
352
|
-
expect(secondSignal?.aborted).toBe(true);
|
|
353
|
-
await vi.runAllTimersAsync();
|
|
354
|
-
|
|
355
|
-
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
356
|
-
expect(warn).toHaveBeenCalledExactlyOnceWith(
|
|
357
|
-
"[HotUpdater] Insights UNCHANGED event was not delivered:",
|
|
358
|
-
expect.objectContaining({ message: "Request timed out" }),
|
|
211
|
+
it("requires a functional baseURL to resolve to a non-empty string", async () => {
|
|
212
|
+
await expect(createHttpClient(() => "").createSession()).rejects.toThrow(
|
|
213
|
+
"baseURL function must return a non-empty string",
|
|
359
214
|
);
|
|
360
215
|
});
|
|
361
|
-
|
|
362
|
-
it("sends a later event after an earlier event's retries without holding its caller", async () => {
|
|
363
|
-
const { fetchMock, sentEvents } = stubEventsEndpoint(503);
|
|
364
|
-
const session = await createSession();
|
|
365
|
-
|
|
366
|
-
await session.sendInsightsEvent(unchangedEvent);
|
|
367
|
-
await session.sendInsightsEvent({
|
|
368
|
-
...unchangedEvent,
|
|
369
|
-
fromBundleId: "bundle-id",
|
|
370
|
-
toBundleId: "next-bundle-id",
|
|
371
|
-
type: "UPDATE_DOWNLOADED",
|
|
372
|
-
updateStrategy: "appVersion",
|
|
373
|
-
});
|
|
374
|
-
expect(fetchMock).toHaveBeenCalledOnce();
|
|
375
|
-
|
|
376
|
-
await vi.runAllTimersAsync();
|
|
377
|
-
const [first, retry, later] = sentEvents();
|
|
378
|
-
expect([first?.type, retry?.type, later?.type]).toEqual([
|
|
379
|
-
"UNCHANGED",
|
|
380
|
-
"UNCHANGED",
|
|
381
|
-
"UPDATE_DOWNLOADED",
|
|
382
|
-
]);
|
|
383
|
-
expect(retry?.eventId).toBe(first?.eventId);
|
|
384
|
-
expect(later?.eventId).not.toBe(first?.eventId);
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it("keeps sending events after one fails outside its requests", async () => {
|
|
388
|
-
vi.spyOn(console, "warn").mockImplementation(() => {
|
|
389
|
-
throw new Error("console unavailable");
|
|
390
|
-
});
|
|
391
|
-
const { fetchMock, sentEvents } = stubEventsEndpoint(500, 500, 500);
|
|
392
|
-
const session = await createSession();
|
|
393
|
-
|
|
394
|
-
await session.sendInsightsEvent(unchangedEvent);
|
|
395
|
-
await vi.runAllTimersAsync();
|
|
396
|
-
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
397
|
-
|
|
398
|
-
// A queue left rejected would never send this event or settle its caller.
|
|
399
|
-
await session.sendInsightsEvent({
|
|
400
|
-
...unchangedEvent,
|
|
401
|
-
fromBundleId: "bundle-id",
|
|
402
|
-
toBundleId: "next-bundle-id",
|
|
403
|
-
type: "UPDATE_DOWNLOADED",
|
|
404
|
-
updateStrategy: "appVersion",
|
|
405
|
-
});
|
|
406
|
-
expect(sentEvents().map(({ type }) => type)).toEqual([
|
|
407
|
-
"UNCHANGED",
|
|
408
|
-
"UNCHANGED",
|
|
409
|
-
"UNCHANGED",
|
|
410
|
-
"UPDATE_DOWNLOADED",
|
|
411
|
-
]);
|
|
412
|
-
});
|
|
413
216
|
});
|
package/src/httpClient.ts
CHANGED
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
} from "@hot-updater/core";
|
|
7
7
|
import { canonicalizeAppVersion } from "@hot-updater/plugin-core";
|
|
8
8
|
|
|
9
|
+
import { resolveBaseURL } from "./baseURL";
|
|
9
10
|
import { fetchJSON, FetchJSONResponseError } from "./fetchJSON";
|
|
10
11
|
import { fetchReleaseCatalogWithCache } from "./releaseCatalogCache";
|
|
11
|
-
import { HOT_UPDATER_SDK_VERSION } from "./sdkVersion";
|
|
12
12
|
import type { HotUpdaterBaseURL } from "./types";
|
|
13
|
-
import {
|
|
13
|
+
import { InvalidUpdateResponseError } from "./updateError";
|
|
14
14
|
|
|
15
15
|
export interface ReleaseCatalogRequest {
|
|
16
16
|
readonly platform: "ios" | "android";
|
|
@@ -29,75 +29,31 @@ export interface ArtifactRequest {
|
|
|
29
29
|
readonly requestTimeout?: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
interface InsightsEventCommonParams {
|
|
33
|
-
readonly installId: string;
|
|
34
|
-
readonly userId?: string;
|
|
35
|
-
readonly username?: string;
|
|
36
|
-
readonly platform: "ios" | "android";
|
|
37
|
-
readonly appVersion: string;
|
|
38
|
-
readonly channel: string;
|
|
39
|
-
readonly cohort: string;
|
|
40
|
-
readonly fingerprintHash: string | null;
|
|
41
|
-
readonly fromReleaseId?: string | null;
|
|
42
|
-
readonly toReleaseId?: string | null;
|
|
43
|
-
readonly requestHeaders?: Record<string, string>;
|
|
44
|
-
readonly requestTimeout?: number;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
type InsightsTransitionEventParams = InsightsEventCommonParams & {
|
|
48
|
-
readonly type: "UPDATE_DOWNLOADED" | "UPDATE_APPLIED" | "RECOVERED";
|
|
49
|
-
readonly fromBundleId: string;
|
|
50
|
-
readonly toBundleId: string;
|
|
51
|
-
readonly updateStrategy: "fingerprint" | "appVersion";
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
type InsightsUnchangedEventParams = InsightsEventCommonParams & {
|
|
55
|
-
readonly type: "UNCHANGED";
|
|
56
|
-
readonly fromBundleId: null;
|
|
57
|
-
readonly toBundleId: string;
|
|
58
|
-
readonly updateStrategy: null;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export type InsightsEventParams =
|
|
62
|
-
| InsightsTransitionEventParams
|
|
63
|
-
| InsightsUnchangedEventParams;
|
|
64
|
-
|
|
65
32
|
export interface HotUpdaterHttpSession {
|
|
33
|
+
/** Resolves to null when the server says the scope has no catalog, so no update. */
|
|
66
34
|
fetchReleaseCatalog: (
|
|
67
35
|
params: ReleaseCatalogRequest,
|
|
68
|
-
) => Promise<ReleaseCatalog>;
|
|
36
|
+
) => Promise<ReleaseCatalog | null>;
|
|
69
37
|
resolveArtifact: (params: ArtifactRequest) => Promise<ArtifactInfo>;
|
|
70
|
-
/**
|
|
71
|
-
* Posts an Insights event under a client `eventId`, retrying a network
|
|
72
|
-
* error, timeout, 429 or 5xx for up to three attempts in all. Resolves once
|
|
73
|
-
* the event is delivered or left to background retries; rejects when its
|
|
74
|
-
* first attempt fails for good, such as with a 400.
|
|
75
|
-
*/
|
|
76
|
-
sendInsightsEvent: (params: InsightsEventParams) => Promise<void>;
|
|
77
38
|
}
|
|
78
39
|
|
|
79
40
|
export interface HotUpdaterHttpClient {
|
|
80
41
|
createSession: () => Promise<HotUpdaterHttpSession>;
|
|
81
42
|
}
|
|
82
43
|
|
|
83
|
-
const resolveBaseURL = async (baseURL: HotUpdaterBaseURL): Promise<string> => {
|
|
84
|
-
const resolvedBaseURL =
|
|
85
|
-
typeof baseURL === "function" ? await baseURL() : baseURL;
|
|
86
|
-
|
|
87
|
-
if (!resolvedBaseURL) {
|
|
88
|
-
throw new Error("baseURL function must return a non-empty string");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return resolvedBaseURL.replace(/\/+$/, "");
|
|
92
|
-
};
|
|
93
|
-
|
|
94
44
|
const resolveArtifactUrl = (baseURL: string, value: string): string => {
|
|
95
45
|
if (/^https?:\/\//i.test(value)) {
|
|
96
|
-
|
|
46
|
+
try {
|
|
47
|
+
new URL(value);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new InvalidUpdateResponseError(`Invalid artifact URL: ${value}`, {
|
|
50
|
+
cause: error,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
97
53
|
return value;
|
|
98
54
|
}
|
|
99
55
|
if (!value.startsWith("/storage/")) {
|
|
100
|
-
throw new
|
|
56
|
+
throw new InvalidUpdateResponseError(
|
|
101
57
|
"Artifact URLs must be absolute HTTP(S) URLs or client-relative storage paths.",
|
|
102
58
|
);
|
|
103
59
|
}
|
|
@@ -143,13 +99,13 @@ const requireArtifactProtocolV1 = (info: ArtifactInfo): ArtifactInfo => {
|
|
|
143
99
|
!info.manifestFileHash ||
|
|
144
100
|
(info.archiveUrl !== undefined && typeof info.archiveUrl !== "string")
|
|
145
101
|
) {
|
|
146
|
-
throw new
|
|
102
|
+
throw new InvalidUpdateResponseError(
|
|
147
103
|
`Server does not support artifact protocol ${ARTIFACT_PROTOCOL_VERSION}.`,
|
|
148
104
|
);
|
|
149
105
|
}
|
|
150
106
|
for (const asset of Object.values(info.assets)) {
|
|
151
107
|
if (!asset.file?.url || !asset.fileHash) {
|
|
152
|
-
throw new
|
|
108
|
+
throw new InvalidUpdateResponseError(
|
|
153
109
|
"Artifact protocol 1 requires an original file for every asset.",
|
|
154
110
|
);
|
|
155
111
|
}
|
|
@@ -157,194 +113,8 @@ const requireArtifactProtocolV1 = (info: ArtifactInfo): ArtifactInfo => {
|
|
|
157
113
|
return info;
|
|
158
114
|
};
|
|
159
115
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
/** Wait before the second attempt; it doubles for each later attempt. */
|
|
163
|
-
const INSIGHTS_RETRY_BASE_DELAY_MS = 1000;
|
|
164
|
-
/** Longest wait between attempts, a server's `Retry-After` included. */
|
|
165
|
-
const INSIGHTS_RETRY_MAX_DELAY_MS = 30000;
|
|
166
|
-
|
|
167
|
-
type InsightsAttemptFailure = {
|
|
168
|
-
readonly error: Error;
|
|
169
|
-
/** A network error, timeout, 429 or 5xx may pass on a later attempt. */
|
|
170
|
-
readonly retryable: boolean;
|
|
171
|
-
readonly retryAfterMs: number | null;
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Reads `Retry-After` as delay-seconds, the form a server under load sends;
|
|
176
|
-
* an HTTP-date falls back to the backoff.
|
|
177
|
-
*/
|
|
178
|
-
const parseRetryAfterMs = (value: string | null): number | null => {
|
|
179
|
-
const seconds = value?.trim();
|
|
180
|
-
return seconds && /^\d+$/.test(seconds) ? Number(seconds) * 1000 : null;
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const getRetryDelayMs = (
|
|
184
|
-
failedAttempt: number,
|
|
185
|
-
retryAfterMs: number | null,
|
|
186
|
-
): number =>
|
|
187
|
-
Math.min(
|
|
188
|
-
retryAfterMs ??
|
|
189
|
-
// Jitter spreads the retries of installations that failed together.
|
|
190
|
-
INSIGHTS_RETRY_BASE_DELAY_MS *
|
|
191
|
-
2 ** (failedAttempt - 1) *
|
|
192
|
-
(0.5 + Math.random()),
|
|
193
|
-
INSIGHTS_RETRY_MAX_DELAY_MS,
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
const wait = (ms: number) =>
|
|
197
|
-
new Promise<void>((resolve) => {
|
|
198
|
-
setTimeout(resolve, ms);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
/** Makes one POST, with its own timeout, and settles with its failure. */
|
|
202
|
-
const postInsightsEvent = async (
|
|
203
|
-
baseURL: string,
|
|
204
|
-
params: InsightsEventParams,
|
|
205
|
-
eventId: string,
|
|
206
|
-
): Promise<InsightsAttemptFailure | null> => {
|
|
207
|
-
const controller = new AbortController();
|
|
208
|
-
const timeoutId = setTimeout(() => {
|
|
209
|
-
controller.abort();
|
|
210
|
-
}, params.requestTimeout ?? 5000);
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
const response = await fetch(`${baseURL}/events`, {
|
|
214
|
-
body: JSON.stringify({
|
|
215
|
-
appVersion: params.appVersion,
|
|
216
|
-
channel: params.channel,
|
|
217
|
-
cohort: params.cohort,
|
|
218
|
-
eventId,
|
|
219
|
-
fingerprintHash: params.fingerprintHash,
|
|
220
|
-
fromBundleId: params.fromBundleId,
|
|
221
|
-
...(params.fromReleaseId === undefined
|
|
222
|
-
? {}
|
|
223
|
-
: { fromReleaseId: params.fromReleaseId }),
|
|
224
|
-
installId: params.installId,
|
|
225
|
-
platform: params.platform,
|
|
226
|
-
sdkVersion: HOT_UPDATER_SDK_VERSION,
|
|
227
|
-
toBundleId: params.toBundleId,
|
|
228
|
-
...(params.toReleaseId === undefined
|
|
229
|
-
? {}
|
|
230
|
-
: { toReleaseId: params.toReleaseId }),
|
|
231
|
-
type: params.type,
|
|
232
|
-
updateStrategy: params.updateStrategy,
|
|
233
|
-
...(params.userId != null ? { userId: params.userId } : {}),
|
|
234
|
-
...(params.username != null ? { username: params.username } : {}),
|
|
235
|
-
}),
|
|
236
|
-
headers: {
|
|
237
|
-
"Content-Type": "application/json",
|
|
238
|
-
...params.requestHeaders,
|
|
239
|
-
},
|
|
240
|
-
method: "POST",
|
|
241
|
-
signal: controller.signal,
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
if (response.status === 204) return null;
|
|
245
|
-
return {
|
|
246
|
-
error: new Error(
|
|
247
|
-
`Expected HTTP 204 from /events, received ${response.status}`,
|
|
248
|
-
),
|
|
249
|
-
retryable: response.status === 429 || response.status >= 500,
|
|
250
|
-
retryAfterMs: parseRetryAfterMs(response.headers.get("Retry-After")),
|
|
251
|
-
};
|
|
252
|
-
} catch (error: unknown) {
|
|
253
|
-
// fetch rejects only when no response arrived (a timeout or a network
|
|
254
|
-
// error), so a later attempt may still get through.
|
|
255
|
-
return {
|
|
256
|
-
error:
|
|
257
|
-
error instanceof Error && error.name === "AbortError"
|
|
258
|
-
? new Error("Request timed out")
|
|
259
|
-
: error instanceof Error
|
|
260
|
-
? error
|
|
261
|
-
: new Error(String(error)),
|
|
262
|
-
retryable: true,
|
|
263
|
-
retryAfterMs: null,
|
|
264
|
-
};
|
|
265
|
-
} finally {
|
|
266
|
-
clearTimeout(timeoutId);
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
type InsightsEventSender = (
|
|
271
|
-
baseURL: string,
|
|
272
|
-
params: InsightsEventParams,
|
|
273
|
-
) => Promise<void>;
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Sends Insights events one at a time and retries each in the background.
|
|
277
|
-
*
|
|
278
|
-
* The server keeps an installation's latest report by arrival, so a later
|
|
279
|
-
* event waits behind an earlier one's retries; otherwise a retried
|
|
280
|
-
* UPDATE_APPLIED could land after the UPDATE_DOWNLOADED that followed it.
|
|
281
|
-
* Callers still wait only for first attempts, as they did before retries, so
|
|
282
|
-
* startup, readiness and `updateBundle()` never wait out a backoff: once an
|
|
283
|
-
* event backs off, every waiting caller is released, and a failure after that
|
|
284
|
-
* only warns.
|
|
285
|
-
*/
|
|
286
|
-
const createInsightsEventSender = (): InsightsEventSender => {
|
|
287
|
-
let queue = Promise.resolve();
|
|
288
|
-
let retrying = false;
|
|
289
|
-
const waiting = new Set<() => void>();
|
|
290
|
-
|
|
291
|
-
return (baseURL, params) => {
|
|
292
|
-
// One ID for every attempt, so the server counts a retried POST once.
|
|
293
|
-
const eventId = createUUIDv7();
|
|
294
|
-
|
|
295
|
-
return new Promise<void>((resolve, reject) => {
|
|
296
|
-
let released = false;
|
|
297
|
-
const release = () => {
|
|
298
|
-
released = true;
|
|
299
|
-
waiting.delete(release);
|
|
300
|
-
resolve();
|
|
301
|
-
};
|
|
302
|
-
if (retrying) release();
|
|
303
|
-
else waiting.add(release);
|
|
304
|
-
|
|
305
|
-
queue = queue.then(async () => {
|
|
306
|
-
try {
|
|
307
|
-
for (let attempt = 1; ; attempt += 1) {
|
|
308
|
-
const failure = await postInsightsEvent(baseURL, params, eventId);
|
|
309
|
-
if (failure === null) {
|
|
310
|
-
release();
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
313
|
-
if (!failure.retryable || attempt === INSIGHTS_MAX_ATTEMPTS) {
|
|
314
|
-
waiting.delete(release);
|
|
315
|
-
if (released) {
|
|
316
|
-
console.warn(
|
|
317
|
-
`[HotUpdater] Insights ${params.type} event was not delivered:`,
|
|
318
|
-
failure.error,
|
|
319
|
-
);
|
|
320
|
-
} else {
|
|
321
|
-
reject(failure.error);
|
|
322
|
-
}
|
|
323
|
-
break;
|
|
324
|
-
}
|
|
325
|
-
retrying = true;
|
|
326
|
-
for (const releaseWaiting of waiting) releaseWaiting();
|
|
327
|
-
await wait(getRetryDelayMs(attempt, failure.retryAfterMs));
|
|
328
|
-
}
|
|
329
|
-
} catch (error: unknown) {
|
|
330
|
-
// A failed request settles in postInsightsEvent, so only a bug gets
|
|
331
|
-
// here. A rejected queue would leave every later event, and a caller
|
|
332
|
-
// such as `updateBundle()` waiting on one, pending forever.
|
|
333
|
-
waiting.delete(release);
|
|
334
|
-
if (!released) reject(error);
|
|
335
|
-
} finally {
|
|
336
|
-
retrying = false;
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
};
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
const createSession = (
|
|
344
|
-
baseURL: string,
|
|
345
|
-
sendInsightsEvent: InsightsEventSender,
|
|
346
|
-
): HotUpdaterHttpSession => ({
|
|
347
|
-
fetchReleaseCatalog: async (params): Promise<ReleaseCatalog> => {
|
|
116
|
+
const createSession = (baseURL: string): HotUpdaterHttpSession => ({
|
|
117
|
+
fetchReleaseCatalog: async (params): Promise<ReleaseCatalog | null> => {
|
|
348
118
|
const channelKey = encodeChannelKey(params.channel);
|
|
349
119
|
let strategyValue: string;
|
|
350
120
|
if (params.updateStrategy === "fingerprint") {
|
|
@@ -393,7 +163,7 @@ const createSession = (
|
|
|
393
163
|
});
|
|
394
164
|
} catch (error) {
|
|
395
165
|
if (error instanceof FetchJSONResponseError && error.status === 404) {
|
|
396
|
-
throw new
|
|
166
|
+
throw new InvalidUpdateResponseError(
|
|
397
167
|
`Server does not support artifact protocol ${ARTIFACT_PROTOCOL_VERSION}.`,
|
|
398
168
|
{ cause: error },
|
|
399
169
|
);
|
|
@@ -402,17 +172,11 @@ const createSession = (
|
|
|
402
172
|
}
|
|
403
173
|
return resolveArtifactUrls(baseURL, requireArtifactProtocolV1(info));
|
|
404
174
|
},
|
|
405
|
-
sendInsightsEvent: (params) => sendInsightsEvent(baseURL, params),
|
|
406
175
|
});
|
|
407
176
|
|
|
408
177
|
/** Creates the private HTTP client used by HotUpdater.init and HotUpdater.wrap. */
|
|
409
178
|
export const createHttpClient = (
|
|
410
179
|
baseURL: HotUpdaterBaseURL,
|
|
411
|
-
): HotUpdaterHttpClient => {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return {
|
|
415
|
-
createSession: async () =>
|
|
416
|
-
createSession(await resolveBaseURL(baseURL), sendInsightsEvent),
|
|
417
|
-
};
|
|
418
|
-
};
|
|
180
|
+
): HotUpdaterHttpClient => ({
|
|
181
|
+
createSession: async () => createSession(await resolveBaseURL(baseURL)),
|
|
182
|
+
});
|