@hot-updater/react-native 0.36.7 → 1.0.0-rc.0
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 +357 -6
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +268 -34
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +75 -9
- package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +2 -16
- package/android/src/main/java/com/hotupdater/ReleaseCatalogCacheService.kt +166 -0
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +1 -2
- package/android/src/newarch/HotUpdater.kt +1 -1
- package/android/src/newarch/HotUpdaterModule.kt +116 -0
- package/android/src/oldarch/HotUpdater.kt +1 -1
- package/android/src/oldarch/HotUpdaterModule.kt +128 -0
- package/android/src/oldarch/HotUpdaterSpec.kt +38 -0
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +280 -2
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +57 -2
- package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +118 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +491 -30
- package/ios/HotUpdater/Internal/BundleMetadata.swift +229 -6
- package/ios/HotUpdater/Internal/HotUpdater.mm +122 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +176 -7
- package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +213 -0
- package/lib/commonjs/catalogCacheNative.js +44 -0
- package/lib/commonjs/catalogCacheNative.js.map +1 -0
- package/lib/commonjs/catalogCacheNative.spec.js +31 -0
- package/lib/commonjs/catalogCacheNative.spec.js.map +1 -0
- package/lib/commonjs/checkForUpdate.js +231 -60
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +604 -0
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -0
- package/lib/commonjs/error.js +8 -1
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/{fetchUpdateInfo.js → fetchJSON.js} +11 -14
- package/lib/commonjs/fetchJSON.js.map +1 -0
- package/lib/commonjs/httpClient.js +153 -0
- package/lib/commonjs/httpClient.js.map +1 -0
- package/lib/commonjs/httpClient.spec.js +102 -0
- package/lib/commonjs/httpClient.spec.js.map +1 -0
- package/lib/commonjs/index.js +48 -71
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +117 -161
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/iosInterop.spec.js +18 -0
- package/lib/commonjs/iosInterop.spec.js.map +1 -0
- package/lib/commonjs/native.js +222 -55
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +226 -12
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js +218 -0
- package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js.map +1 -0
- package/lib/commonjs/notifyAppReadyAnalytics.js +133 -0
- package/lib/commonjs/notifyAppReadyAnalytics.js.map +1 -0
- package/lib/commonjs/notifyAppReadyAnalytics.spec.js +246 -0
- package/lib/commonjs/notifyAppReadyAnalytics.spec.js.map +1 -0
- package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js +24 -0
- package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js.map +1 -0
- package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js +117 -0
- package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js.map +1 -0
- package/lib/commonjs/releaseCatalogCache.js +179 -0
- package/lib/commonjs/releaseCatalogCache.js.map +1 -0
- package/lib/commonjs/releaseCatalogCache.spec.js +195 -0
- package/lib/commonjs/releaseCatalogCache.spec.js.map +1 -0
- package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +64 -0
- package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +1 -0
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/sdkVersion.js.map +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/types.js +0 -12
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/wrap.js +7 -82
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +203 -56
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/commonjs/wrap.types.js +6 -0
- package/lib/commonjs/wrap.types.js.map +1 -0
- package/lib/module/catalogCacheNative.js +36 -0
- package/lib/module/catalogCacheNative.js.map +1 -0
- package/lib/module/catalogCacheNative.spec.js +31 -0
- package/lib/module/catalogCacheNative.spec.js.map +1 -0
- package/lib/module/checkForUpdate.js +233 -62
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +603 -0
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -0
- package/lib/module/error.js +6 -0
- package/lib/module/error.js.map +1 -1
- package/lib/module/{fetchUpdateInfo.js → fetchJSON.js} +9 -12
- package/lib/module/fetchJSON.js.map +1 -0
- package/lib/module/httpClient.js +148 -0
- package/lib/module/httpClient.js.map +1 -0
- package/lib/module/httpClient.spec.js +102 -0
- package/lib/module/httpClient.spec.js.map +1 -0
- package/lib/module/index.js +49 -66
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +117 -161
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/iosInterop.spec.js +18 -0
- package/lib/module/iosInterop.spec.js.map +1 -0
- package/lib/module/native.js +206 -51
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +226 -12
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/notifyAppReadyAnalytics.boundary.spec.js +217 -0
- package/lib/module/notifyAppReadyAnalytics.boundary.spec.js.map +1 -0
- package/lib/module/notifyAppReadyAnalytics.js +128 -0
- package/lib/module/notifyAppReadyAnalytics.js.map +1 -0
- package/lib/module/notifyAppReadyAnalytics.spec.js +245 -0
- package/lib/module/notifyAppReadyAnalytics.spec.js.map +1 -0
- package/lib/module/notifyAppReadyAnalytics.test-utils.js +18 -0
- package/lib/module/notifyAppReadyAnalytics.test-utils.js.map +1 -0
- package/lib/module/notifyAppReadyAnalytics.transport.spec.js +116 -0
- package/lib/module/notifyAppReadyAnalytics.transport.spec.js.map +1 -0
- package/lib/module/releaseCatalogCache.js +171 -0
- package/lib/module/releaseCatalogCache.js.map +1 -0
- package/lib/module/releaseCatalogCache.spec.js +195 -0
- package/lib/module/releaseCatalogCache.spec.js.map +1 -0
- package/lib/module/releaseCatalogCacheNativeContract.spec.js +64 -0
- package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +1 -0
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/sdkVersion.js.map +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/types.js +0 -12
- package/lib/module/types.js.map +1 -1
- package/lib/module/wrap.js +6 -81
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +203 -56
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/module/wrap.types.js +4 -0
- package/lib/module/wrap.types.js.map +1 -0
- package/lib/typescript/commonjs/catalogCacheNative.d.ts +4 -0
- package/lib/typescript/commonjs/catalogCacheNative.d.ts.map +1 -0
- package/lib/typescript/commonjs/checkForUpdate.d.ts +14 -4
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/error.d.ts +3 -0
- package/lib/typescript/commonjs/error.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchJSON.d.ts +6 -0
- package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -0
- package/lib/typescript/commonjs/httpClient.d.ts +56 -0
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +32 -41
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +83 -18
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts +13 -0
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts.map +1 -0
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts +8 -0
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts.map +1 -0
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts +25 -0
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +40 -5
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +0 -118
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +2 -191
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +65 -0
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -0
- package/lib/typescript/module/catalogCacheNative.d.ts +4 -0
- package/lib/typescript/module/catalogCacheNative.d.ts.map +1 -0
- package/lib/typescript/module/checkForUpdate.d.ts +14 -4
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/error.d.ts +3 -0
- package/lib/typescript/module/error.d.ts.map +1 -1
- package/lib/typescript/module/fetchJSON.d.ts +6 -0
- package/lib/typescript/module/fetchJSON.d.ts.map +1 -0
- package/lib/typescript/module/httpClient.d.ts +56 -0
- package/lib/typescript/module/httpClient.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +32 -41
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +83 -18
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/notifyAppReadyAnalytics.d.ts +13 -0
- package/lib/typescript/module/notifyAppReadyAnalytics.d.ts.map +1 -0
- package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts +8 -0
- package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts.map +1 -0
- package/lib/typescript/module/releaseCatalogCache.d.ts +25 -0
- package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -0
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +40 -5
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +0 -118
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +2 -191
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +65 -0
- package/lib/typescript/module/wrap.types.d.ts.map +1 -0
- package/package.json +6 -24
- package/src/catalogCacheNative.spec.ts +63 -0
- package/src/catalogCacheNative.ts +74 -0
- package/src/checkForUpdate.releaseCatalog.spec.ts +596 -0
- package/src/checkForUpdate.ts +368 -97
- package/src/error.ts +7 -0
- package/src/{fetchUpdateInfo.ts → fetchJSON.ts} +8 -15
- package/src/httpClient.spec.ts +132 -0
- package/src/httpClient.ts +261 -0
- package/src/index.spec.ts +110 -203
- package/src/index.ts +70 -93
- package/src/iosInterop.spec.ts +30 -0
- package/src/native.spec.ts +226 -10
- package/src/native.ts +385 -83
- package/src/notifyAppReadyAnalytics.boundary.spec.ts +234 -0
- package/src/notifyAppReadyAnalytics.spec.ts +243 -0
- package/src/notifyAppReadyAnalytics.test-utils.ts +31 -0
- package/src/notifyAppReadyAnalytics.transport.spec.ts +130 -0
- package/src/notifyAppReadyAnalytics.ts +194 -0
- package/src/releaseCatalogCache.spec.ts +282 -0
- package/src/releaseCatalogCache.ts +341 -0
- package/src/releaseCatalogCacheNativeContract.spec.ts +80 -0
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +51 -5
- package/src/types.ts +0 -140
- package/src/wrap.spec.ts +251 -56
- package/src/wrap.tsx +23 -318
- package/src/wrap.types.ts +78 -0
- package/android/src/main/java/com/hotupdater/StringResourceUtils.kt +0 -45
- package/app.plugin.js +0 -1
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.h +0 -7
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.mm +0 -4
- package/lib/commonjs/DefaultResolver.js +0 -50
- package/lib/commonjs/DefaultResolver.js.map +0 -1
- package/lib/commonjs/DefaultResolver.spec.js +0 -144
- package/lib/commonjs/DefaultResolver.spec.js.map +0 -1
- package/lib/commonjs/fetchUpdateInfo.js.map +0 -1
- package/lib/module/DefaultResolver.js +0 -46
- package/lib/module/DefaultResolver.js.map +0 -1
- package/lib/module/DefaultResolver.spec.js +0 -144
- package/lib/module/DefaultResolver.spec.js.map +0 -1
- package/lib/module/fetchUpdateInfo.js.map +0 -1
- package/lib/typescript/commonjs/DefaultResolver.d.ts +0 -11
- package/lib/typescript/commonjs/DefaultResolver.d.ts.map +0 -1
- package/lib/typescript/commonjs/fetchUpdateInfo.d.ts +0 -7
- package/lib/typescript/commonjs/fetchUpdateInfo.d.ts.map +0 -1
- package/lib/typescript/module/DefaultResolver.d.ts +0 -11
- package/lib/typescript/module/DefaultResolver.d.ts.map +0 -1
- package/lib/typescript/module/fetchUpdateInfo.d.ts +0 -7
- package/lib/typescript/module/fetchUpdateInfo.d.ts.map +0 -1
- package/plugin/build/transformers.js +0 -314
- package/plugin/build/withHotUpdater.js +0 -228
- package/src/DefaultResolver.spec.ts +0 -193
- package/src/DefaultResolver.ts +0 -63
package/src/checkForUpdate.ts
CHANGED
|
@@ -1,23 +1,40 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
authorizeReleaseTransition,
|
|
3
|
+
createReleaseSelectionContextHash,
|
|
4
|
+
encodeChannelKey,
|
|
5
|
+
selectDesiredRelease,
|
|
6
|
+
type ArtifactInfo,
|
|
7
|
+
type PersistedSelectionReceipt,
|
|
8
|
+
type ReleaseCatalog,
|
|
9
|
+
} from "@hot-updater/core";
|
|
10
|
+
import { canonicalizeAppVersion } from "@hot-updater/plugin-core";
|
|
2
11
|
import { Platform } from "react-native";
|
|
3
12
|
|
|
4
|
-
import { HotUpdaterError } from "./error";
|
|
13
|
+
import { HotUpdaterError, StaleReleaseCatalogError } from "./error";
|
|
14
|
+
import type { HotUpdaterHttpClient, HotUpdaterHttpSession } from "./httpClient";
|
|
5
15
|
import {
|
|
16
|
+
acceptReleaseCatalog,
|
|
17
|
+
commitReleaseSelection,
|
|
18
|
+
getActiveUpdateState,
|
|
6
19
|
getAppVersion,
|
|
7
20
|
getBundleId,
|
|
8
21
|
getChannel,
|
|
9
22
|
getCohort,
|
|
10
23
|
getDefaultChannel,
|
|
11
24
|
getFingerprintHash,
|
|
12
|
-
|
|
25
|
+
getInstallId,
|
|
26
|
+
getMinimumReleaseId,
|
|
27
|
+
getCrashHistory,
|
|
28
|
+
getPersistedUserIdentity,
|
|
29
|
+
isReleaseSelectionCurrent,
|
|
13
30
|
isChannelSwitched,
|
|
14
|
-
resetChannel,
|
|
15
31
|
updateBundle,
|
|
16
32
|
} from "./native";
|
|
33
|
+
import {
|
|
34
|
+
hasExpectedReleaseCatalogScope,
|
|
35
|
+
type ExpectedReleaseCatalogScope,
|
|
36
|
+
} from "./releaseCatalogCache";
|
|
17
37
|
import { hotUpdaterStore } from "./store";
|
|
18
|
-
import type { HotUpdaterResolver } from "./types";
|
|
19
|
-
|
|
20
|
-
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
21
38
|
|
|
22
39
|
export interface CheckForUpdateOptions {
|
|
23
40
|
/**
|
|
@@ -43,7 +60,15 @@ export interface CheckForUpdateOptions {
|
|
|
43
60
|
requestTimeout?: number;
|
|
44
61
|
}
|
|
45
62
|
|
|
46
|
-
export type CheckForUpdateResult =
|
|
63
|
+
export type CheckForUpdateResult = ArtifactInfo & {
|
|
64
|
+
readonly id: string;
|
|
65
|
+
readonly message: string | null;
|
|
66
|
+
readonly rolloutCohortCount: number;
|
|
67
|
+
readonly shouldForceUpdate: boolean;
|
|
68
|
+
readonly status: "ROLLBACK" | "UPDATE";
|
|
69
|
+
readonly targetCohorts: string[];
|
|
70
|
+
readonly releaseId?: string | null;
|
|
71
|
+
readonly transitionKind?: ReleaseTransitionKind;
|
|
47
72
|
/**
|
|
48
73
|
* Updates the bundle.
|
|
49
74
|
* This method is equivalent to `HotUpdater.updateBundle()` but with all required arguments pre-filled.
|
|
@@ -51,19 +76,334 @@ export type CheckForUpdateResult = AppUpdateAvailableInfo & {
|
|
|
51
76
|
updateBundle: () => Promise<boolean>;
|
|
52
77
|
};
|
|
53
78
|
|
|
54
|
-
|
|
79
|
+
export type ReleaseTransitionKind =
|
|
80
|
+
| "INSTALL"
|
|
81
|
+
| "ADOPT_RELEASE"
|
|
82
|
+
| "USE_EMBEDDED"
|
|
83
|
+
| "USE_BUILTIN";
|
|
84
|
+
|
|
55
85
|
export interface InternalCheckForUpdateOptions extends CheckForUpdateOptions {
|
|
56
|
-
|
|
86
|
+
analytics?: boolean;
|
|
87
|
+
client: HotUpdaterHttpClient;
|
|
57
88
|
}
|
|
58
89
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
const sameReceipt = (
|
|
91
|
+
first: PersistedSelectionReceipt | null,
|
|
92
|
+
second: PersistedSelectionReceipt,
|
|
93
|
+
): boolean =>
|
|
94
|
+
first !== null &&
|
|
95
|
+
first.kind === second.kind &&
|
|
96
|
+
first.releaseId === second.releaseId &&
|
|
97
|
+
first.bundleId === second.bundleId &&
|
|
98
|
+
first.catalogId === second.catalogId &&
|
|
99
|
+
first.scopeKey === second.scopeKey &&
|
|
100
|
+
first.generation === second.generation &&
|
|
101
|
+
first.catalogHash === second.catalogHash &&
|
|
102
|
+
first.channel === second.channel &&
|
|
103
|
+
first.selectionContextHash === second.selectionContextHash;
|
|
104
|
+
|
|
105
|
+
const validateCatalog = (
|
|
106
|
+
catalog: ReleaseCatalog,
|
|
107
|
+
expectedScope: ExpectedReleaseCatalogScope,
|
|
108
|
+
) => {
|
|
109
|
+
if (
|
|
110
|
+
catalog.schemaVersion !== 1 ||
|
|
111
|
+
!Number.isSafeInteger(catalog.generation) ||
|
|
112
|
+
catalog.generation < 1 ||
|
|
113
|
+
!/^sha256:[0-9a-f]{64}$/.test(catalog.catalogHash) ||
|
|
114
|
+
!hasExpectedReleaseCatalogScope(catalog, expectedScope)
|
|
115
|
+
) {
|
|
116
|
+
throw new HotUpdaterError("Received an invalid Release catalog");
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const resetProgress = () => {
|
|
121
|
+
hotUpdaterStore.setState({
|
|
122
|
+
artifactType: null,
|
|
123
|
+
details: null,
|
|
124
|
+
downloadedBytes: undefined,
|
|
125
|
+
isUpdateDownloaded: false,
|
|
126
|
+
progress: 0,
|
|
127
|
+
totalBytes: undefined,
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const notifyReleaseAdoption = async (input: {
|
|
132
|
+
readonly active: PersistedSelectionReceipt | null;
|
|
133
|
+
readonly desired: PersistedSelectionReceipt;
|
|
134
|
+
readonly appVersion: string;
|
|
135
|
+
readonly cohort: string;
|
|
136
|
+
readonly fingerprintHash: string | null;
|
|
137
|
+
readonly platform: "ios" | "android";
|
|
138
|
+
readonly session: HotUpdaterHttpSession;
|
|
139
|
+
readonly requestHeaders?: Record<string, string>;
|
|
140
|
+
readonly requestTimeout?: number;
|
|
141
|
+
readonly updateStrategy: "appVersion" | "fingerprint";
|
|
142
|
+
}): Promise<void> => {
|
|
143
|
+
const { userId, username } = getPersistedUserIdentity();
|
|
144
|
+
try {
|
|
145
|
+
await input.session.sendAnalyticsEvent({
|
|
146
|
+
appVersion: input.appVersion,
|
|
147
|
+
channel: input.desired.channel,
|
|
148
|
+
cohort: input.cohort,
|
|
149
|
+
fingerprintHash: input.fingerprintHash,
|
|
150
|
+
fromBundleId: input.desired.bundleId,
|
|
151
|
+
fromReleaseId: input.active?.releaseId ?? null,
|
|
152
|
+
installId: getInstallId(),
|
|
153
|
+
platform: input.platform,
|
|
154
|
+
requestHeaders: input.requestHeaders,
|
|
155
|
+
requestTimeout: input.requestTimeout,
|
|
156
|
+
toBundleId: input.desired.bundleId,
|
|
157
|
+
toReleaseId: input.desired.releaseId,
|
|
158
|
+
type: "RELEASE_ADOPTED",
|
|
159
|
+
updateStrategy: input.updateStrategy,
|
|
160
|
+
...(userId === undefined ? {} : { userId }),
|
|
161
|
+
...(username === undefined ? {} : { username }),
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.warn("[HotUpdater] Release adoption analytics failed:", error);
|
|
165
|
+
}
|
|
65
166
|
};
|
|
66
167
|
|
|
168
|
+
async function checkForReleaseCatalogUpdate(input: {
|
|
169
|
+
readonly options: InternalCheckForUpdateOptions;
|
|
170
|
+
readonly platform: "ios" | "android";
|
|
171
|
+
readonly currentAppVersion: string;
|
|
172
|
+
readonly currentBundleId: string;
|
|
173
|
+
readonly minimumReleaseId: string;
|
|
174
|
+
readonly defaultChannel: string;
|
|
175
|
+
readonly currentChannel: string;
|
|
176
|
+
readonly targetChannel: string;
|
|
177
|
+
readonly explicitChannel: string | undefined;
|
|
178
|
+
readonly isSwitched: boolean;
|
|
179
|
+
readonly cohort: string;
|
|
180
|
+
readonly fingerprintHash: string | null;
|
|
181
|
+
}): Promise<CheckForUpdateResult | null> {
|
|
182
|
+
const { options } = input;
|
|
183
|
+
const session = await options.client.createSession();
|
|
184
|
+
const channelKey = encodeChannelKey(input.targetChannel);
|
|
185
|
+
const strategy =
|
|
186
|
+
options.updateStrategy === "appVersion" ? "APP_VERSION" : "FINGERPRINT";
|
|
187
|
+
const canonicalAppVersion = canonicalizeAppVersion(input.currentAppVersion);
|
|
188
|
+
if (strategy === "APP_VERSION" && canonicalAppVersion === null) {
|
|
189
|
+
throw new HotUpdaterError("Failed to canonicalize app version");
|
|
190
|
+
}
|
|
191
|
+
if (strategy === "FINGERPRINT" && !input.fingerprintHash) {
|
|
192
|
+
throw new HotUpdaterError("Fingerprint hash is required");
|
|
193
|
+
}
|
|
194
|
+
const expectedScope: ExpectedReleaseCatalogScope =
|
|
195
|
+
strategy === "APP_VERSION"
|
|
196
|
+
? {
|
|
197
|
+
channelKey,
|
|
198
|
+
platform: input.platform,
|
|
199
|
+
strategy,
|
|
200
|
+
}
|
|
201
|
+
: {
|
|
202
|
+
channelKey,
|
|
203
|
+
fingerprintHash: input.fingerprintHash!,
|
|
204
|
+
platform: input.platform,
|
|
205
|
+
strategy,
|
|
206
|
+
};
|
|
207
|
+
const catalog = await session.fetchReleaseCatalog({
|
|
208
|
+
appVersion: canonicalAppVersion ?? input.currentAppVersion,
|
|
209
|
+
channel: input.targetChannel,
|
|
210
|
+
fingerprintHash: input.fingerprintHash,
|
|
211
|
+
platform: input.platform,
|
|
212
|
+
requestHeaders: options.requestHeaders,
|
|
213
|
+
requestTimeout: options.requestTimeout,
|
|
214
|
+
updateStrategy: options.updateStrategy,
|
|
215
|
+
});
|
|
216
|
+
validateCatalog(catalog, expectedScope);
|
|
217
|
+
const catalogId = catalog.catalogId;
|
|
218
|
+
const scopeKey = catalog.scopeKey;
|
|
219
|
+
|
|
220
|
+
const crashedBundleIds = getCrashHistory();
|
|
221
|
+
const active = getActiveUpdateState().activeSelection;
|
|
222
|
+
const explicitScopeSwitch =
|
|
223
|
+
input.explicitChannel !== undefined &&
|
|
224
|
+
input.explicitChannel !== input.currentChannel;
|
|
225
|
+
const activeInTargetScope =
|
|
226
|
+
active?.catalogId === catalogId && active.scopeKey === scopeKey
|
|
227
|
+
? active
|
|
228
|
+
: null;
|
|
229
|
+
const hasAuthenticatedActive =
|
|
230
|
+
active !== null &&
|
|
231
|
+
active.catalogId !== null &&
|
|
232
|
+
active.scopeKey !== null &&
|
|
233
|
+
active.generation !== null;
|
|
234
|
+
if (
|
|
235
|
+
hasAuthenticatedActive &&
|
|
236
|
+
activeInTargetScope === null &&
|
|
237
|
+
!explicitScopeSwitch
|
|
238
|
+
) {
|
|
239
|
+
throw new HotUpdaterError("Release transition rejected: UNSOLICITED_SCOPE");
|
|
240
|
+
}
|
|
241
|
+
const selectorCurrentBundleId =
|
|
242
|
+
activeInTargetScope !== null ||
|
|
243
|
+
(!hasAuthenticatedActive && !explicitScopeSwitch)
|
|
244
|
+
? input.currentBundleId
|
|
245
|
+
: input.minimumReleaseId;
|
|
246
|
+
const selectionContextHash = createReleaseSelectionContextHash({
|
|
247
|
+
activeBundleId: selectorCurrentBundleId,
|
|
248
|
+
activeReleaseId: activeInTargetScope?.releaseId ?? null,
|
|
249
|
+
cohort: input.cohort,
|
|
250
|
+
crashedBundleIds,
|
|
251
|
+
minimumReleaseId: input.minimumReleaseId,
|
|
252
|
+
strategy,
|
|
253
|
+
strategyValue:
|
|
254
|
+
strategy === "APP_VERSION"
|
|
255
|
+
? canonicalAppVersion!
|
|
256
|
+
: input.fingerprintHash!,
|
|
257
|
+
});
|
|
258
|
+
if (
|
|
259
|
+
!acceptReleaseCatalog({
|
|
260
|
+
catalogId,
|
|
261
|
+
catalogHash: catalog.catalogHash,
|
|
262
|
+
channel: input.targetChannel,
|
|
263
|
+
generation: catalog.generation,
|
|
264
|
+
selectionContextHash,
|
|
265
|
+
scopeKey,
|
|
266
|
+
})
|
|
267
|
+
) {
|
|
268
|
+
throw new HotUpdaterError(
|
|
269
|
+
"Rejected a stale or inconsistent Release catalog",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const desired = selectDesiredRelease(catalog, {
|
|
273
|
+
activeReleaseId: activeInTargetScope?.releaseId ?? null,
|
|
274
|
+
builtInBundleId: input.minimumReleaseId,
|
|
275
|
+
cohort: input.cohort,
|
|
276
|
+
crashedBundleIds,
|
|
277
|
+
currentBundleId: selectorCurrentBundleId,
|
|
278
|
+
minimumReleaseId: input.minimumReleaseId,
|
|
279
|
+
});
|
|
280
|
+
if (desired === null) return null;
|
|
281
|
+
|
|
282
|
+
if (
|
|
283
|
+
desired.kind === "BUILTIN" &&
|
|
284
|
+
input.currentBundleId === input.minimumReleaseId &&
|
|
285
|
+
(active === null || active.kind === "BUILTIN")
|
|
286
|
+
) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const receipt: PersistedSelectionReceipt = {
|
|
291
|
+
catalogId,
|
|
292
|
+
bundleId: desired.bundleId,
|
|
293
|
+
catalogHash: catalog.catalogHash,
|
|
294
|
+
channel: input.targetChannel,
|
|
295
|
+
generation: catalog.generation,
|
|
296
|
+
kind: desired.kind,
|
|
297
|
+
releaseId: desired.releaseId,
|
|
298
|
+
scopeKey,
|
|
299
|
+
selectionContextHash,
|
|
300
|
+
};
|
|
301
|
+
if (sameReceipt(active, receipt)) return null;
|
|
302
|
+
|
|
303
|
+
const authorization = authorizeReleaseTransition({
|
|
304
|
+
active,
|
|
305
|
+
desired: receipt,
|
|
306
|
+
explicitScopeSwitch,
|
|
307
|
+
});
|
|
308
|
+
if (!authorization.authorized) {
|
|
309
|
+
if (authorization.reason === "EMPTY_TARGET_SCOPE") return null;
|
|
310
|
+
throw new HotUpdaterError(
|
|
311
|
+
`Release transition rejected: ${authorization.reason}`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const release = desired.release;
|
|
316
|
+
const transitionKind: ReleaseTransitionKind =
|
|
317
|
+
desired.kind === "BUILTIN"
|
|
318
|
+
? "USE_BUILTIN"
|
|
319
|
+
: desired.kind === "EMBEDDED"
|
|
320
|
+
? "USE_EMBEDDED"
|
|
321
|
+
: active?.bundleId === desired.bundleId
|
|
322
|
+
? "ADOPT_RELEASE"
|
|
323
|
+
: "INSTALL";
|
|
324
|
+
const guard = {
|
|
325
|
+
catalogId,
|
|
326
|
+
catalogHash: catalog.catalogHash,
|
|
327
|
+
channel: input.targetChannel,
|
|
328
|
+
generation: catalog.generation,
|
|
329
|
+
scopeKey,
|
|
330
|
+
selectionContextHash,
|
|
331
|
+
};
|
|
332
|
+
const updateBundleForSelection = async (): Promise<boolean> => {
|
|
333
|
+
resetProgress();
|
|
334
|
+
if (!isReleaseSelectionCurrent(guard)) {
|
|
335
|
+
throw new StaleReleaseCatalogError();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (transitionKind !== "INSTALL") {
|
|
339
|
+
const committed = await commitReleaseSelection({
|
|
340
|
+
guard,
|
|
341
|
+
selection: receipt,
|
|
342
|
+
});
|
|
343
|
+
if (
|
|
344
|
+
committed &&
|
|
345
|
+
transitionKind === "ADOPT_RELEASE" &&
|
|
346
|
+
options.analytics
|
|
347
|
+
) {
|
|
348
|
+
await notifyReleaseAdoption({
|
|
349
|
+
active,
|
|
350
|
+
appVersion: input.currentAppVersion,
|
|
351
|
+
cohort: input.cohort,
|
|
352
|
+
desired: receipt,
|
|
353
|
+
fingerprintHash: input.fingerprintHash,
|
|
354
|
+
platform: input.platform,
|
|
355
|
+
requestHeaders: options.requestHeaders,
|
|
356
|
+
requestTimeout: options.requestTimeout,
|
|
357
|
+
session,
|
|
358
|
+
updateStrategy: options.updateStrategy,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return committed;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const artifact = await session.resolveArtifact({
|
|
365
|
+
currentBundleId: input.currentBundleId,
|
|
366
|
+
requestHeaders: options.requestHeaders,
|
|
367
|
+
requestTimeout: options.requestTimeout,
|
|
368
|
+
targetBundleId: desired.bundleId,
|
|
369
|
+
});
|
|
370
|
+
if (!isReleaseSelectionCurrent(guard)) {
|
|
371
|
+
throw new StaleReleaseCatalogError();
|
|
372
|
+
}
|
|
373
|
+
return updateBundle({
|
|
374
|
+
bundleId: desired.bundleId,
|
|
375
|
+
changedAssets: artifact.changedAssets ?? null,
|
|
376
|
+
channel: input.targetChannel,
|
|
377
|
+
fileHash: artifact.fileHash,
|
|
378
|
+
fileUrl: artifact.fileUrl,
|
|
379
|
+
manifestFileHash: artifact.manifestFileHash ?? null,
|
|
380
|
+
manifestUrl: artifact.manifestUrl ?? null,
|
|
381
|
+
selection: receipt,
|
|
382
|
+
shouldSkipCurrentBundleIdCheck: true,
|
|
383
|
+
status: desired.status,
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
fileHash: null,
|
|
389
|
+
fileUrl: null,
|
|
390
|
+
id: desired.bundleId,
|
|
391
|
+
message: release?.message ?? null,
|
|
392
|
+
releaseId: desired.releaseId,
|
|
393
|
+
rolloutCohortCount: release?.rolloutCohortCount ?? 1000,
|
|
394
|
+
shouldForceUpdate:
|
|
395
|
+
transitionKind === "ADOPT_RELEASE"
|
|
396
|
+
? false
|
|
397
|
+
: desired.status === "ROLLBACK"
|
|
398
|
+
? true
|
|
399
|
+
: (release?.shouldForceUpdate ?? false),
|
|
400
|
+
status: desired.status,
|
|
401
|
+
targetCohorts: release?.targetCohorts ? [...release.targetCohorts] : [],
|
|
402
|
+
transitionKind,
|
|
403
|
+
updateBundle: updateBundleForSelection,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
67
407
|
export async function checkForUpdate(
|
|
68
408
|
options: InternalCheckForUpdateOptions,
|
|
69
409
|
): Promise<CheckForUpdateResult | null> {
|
|
@@ -81,20 +421,12 @@ export async function checkForUpdate(
|
|
|
81
421
|
const currentAppVersion = getAppVersion();
|
|
82
422
|
const platform = Platform.OS as "ios" | "android";
|
|
83
423
|
const currentBundleId = getBundleId();
|
|
84
|
-
const
|
|
424
|
+
const minimumReleaseId = getMinimumReleaseId();
|
|
85
425
|
const defaultChannel = getDefaultChannel();
|
|
86
426
|
const isSwitched = isChannelSwitched();
|
|
87
427
|
const currentChannel = isSwitched ? getChannel() : defaultChannel;
|
|
88
428
|
const explicitChannel = options.channel || undefined;
|
|
89
429
|
const targetChannel = explicitChannel || currentChannel;
|
|
90
|
-
const isFirstRuntimeChannelSwitchAttempt =
|
|
91
|
-
!isSwitched &&
|
|
92
|
-
explicitChannel !== undefined &&
|
|
93
|
-
explicitChannel !== defaultChannel;
|
|
94
|
-
const requestBundleId = isFirstRuntimeChannelSwitchAttempt
|
|
95
|
-
? minBundleId
|
|
96
|
-
: currentBundleId;
|
|
97
|
-
|
|
98
430
|
const cohort = getCohort();
|
|
99
431
|
|
|
100
432
|
if (!currentAppVersion) {
|
|
@@ -112,84 +444,23 @@ export async function checkForUpdate(
|
|
|
112
444
|
|
|
113
445
|
const fingerprintHash = getFingerprintHash();
|
|
114
446
|
|
|
115
|
-
if (!options.resolver?.checkUpdate) {
|
|
116
|
-
options.onError?.(
|
|
117
|
-
new HotUpdaterError("Resolver is required but not configured"),
|
|
118
|
-
);
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
let updateInfo: AppUpdateInfo | null = null;
|
|
123
|
-
|
|
124
447
|
try {
|
|
125
|
-
|
|
126
|
-
platform,
|
|
127
|
-
appVersion: currentAppVersion,
|
|
128
|
-
bundleId: requestBundleId,
|
|
129
|
-
minBundleId,
|
|
448
|
+
return await checkForReleaseCatalogUpdate({
|
|
130
449
|
cohort,
|
|
131
|
-
|
|
132
|
-
|
|
450
|
+
currentAppVersion,
|
|
451
|
+
currentBundleId,
|
|
452
|
+
currentChannel,
|
|
453
|
+
defaultChannel,
|
|
454
|
+
explicitChannel,
|
|
133
455
|
fingerprintHash,
|
|
134
|
-
|
|
135
|
-
|
|
456
|
+
isSwitched,
|
|
457
|
+
minimumReleaseId,
|
|
458
|
+
options,
|
|
459
|
+
platform,
|
|
460
|
+
targetChannel,
|
|
136
461
|
});
|
|
137
462
|
} catch (error) {
|
|
138
463
|
options.onError?.(error as Error);
|
|
139
464
|
return null;
|
|
140
465
|
}
|
|
141
|
-
|
|
142
|
-
if (!updateInfo) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (updateInfo.status === "UP_TO_DATE") {
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
explicitChannel &&
|
|
152
|
-
explicitChannel !== defaultChannel &&
|
|
153
|
-
!isSwitched &&
|
|
154
|
-
updateInfo.status === "ROLLBACK"
|
|
155
|
-
) {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
...updateInfo,
|
|
161
|
-
updateBundle: async () => {
|
|
162
|
-
if (
|
|
163
|
-
explicitChannel &&
|
|
164
|
-
isSwitched &&
|
|
165
|
-
isResetToBuiltInResponse(updateInfo)
|
|
166
|
-
) {
|
|
167
|
-
return resetChannel();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const runtimeChannel =
|
|
171
|
-
updateInfo.fileUrl !== null ? targetChannel : undefined;
|
|
172
|
-
|
|
173
|
-
hotUpdaterStore.setState({
|
|
174
|
-
artifactType: null,
|
|
175
|
-
details: null,
|
|
176
|
-
downloadedBytes: undefined,
|
|
177
|
-
isUpdateDownloaded: false,
|
|
178
|
-
progress: 0,
|
|
179
|
-
totalBytes: undefined,
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
return updateBundle({
|
|
183
|
-
bundleId: updateInfo.id,
|
|
184
|
-
channel: runtimeChannel,
|
|
185
|
-
changedAssets: updateInfo.changedAssets ?? null,
|
|
186
|
-
fileUrl: updateInfo.fileUrl,
|
|
187
|
-
fileHash: updateInfo.fileHash,
|
|
188
|
-
manifestFileHash: updateInfo.manifestFileHash ?? null,
|
|
189
|
-
manifestUrl: updateInfo.manifestUrl ?? null,
|
|
190
|
-
status: updateInfo.status,
|
|
191
|
-
shouldSkipCurrentBundleIdCheck: isFirstRuntimeChannelSwitchAttempt,
|
|
192
|
-
});
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
466
|
}
|
package/src/error.ts
CHANGED
|
@@ -157,3 +157,10 @@ export class HotUpdaterError extends Error {
|
|
|
157
157
|
this.name = "HotUpdaterError";
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
+
|
|
161
|
+
export class StaleReleaseCatalogError extends HotUpdaterError {
|
|
162
|
+
constructor() {
|
|
163
|
+
super("Release catalog selection became stale before it was committed");
|
|
164
|
+
this.name = "StaleReleaseCatalogError";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export const fetchUpdateInfo = async ({
|
|
1
|
+
export const fetchJSON = async <T>({
|
|
4
2
|
url,
|
|
5
3
|
requestHeaders,
|
|
6
4
|
requestTimeout = 5000,
|
|
@@ -8,27 +6,22 @@ export const fetchUpdateInfo = async ({
|
|
|
8
6
|
url: string;
|
|
9
7
|
requestHeaders?: Record<string, string>;
|
|
10
8
|
requestTimeout?: number;
|
|
11
|
-
}): Promise<
|
|
9
|
+
}): Promise<T> => {
|
|
12
10
|
const controller = new AbortController();
|
|
13
|
-
const timeoutId = setTimeout(() =>
|
|
14
|
-
controller.abort();
|
|
15
|
-
}, requestTimeout);
|
|
11
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
16
12
|
|
|
17
13
|
try {
|
|
18
|
-
const headers = {
|
|
19
|
-
"Content-Type": "application/json",
|
|
20
|
-
...requestHeaders,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
14
|
const response = await fetch(url, {
|
|
15
|
+
headers: {
|
|
16
|
+
Accept: "application/json",
|
|
17
|
+
...requestHeaders,
|
|
18
|
+
},
|
|
24
19
|
signal: controller.signal,
|
|
25
|
-
headers,
|
|
26
20
|
});
|
|
27
|
-
|
|
28
21
|
if (response.status !== 200) {
|
|
29
22
|
throw new Error(response.statusText);
|
|
30
23
|
}
|
|
31
|
-
return response.json();
|
|
24
|
+
return (await response.json()) as T;
|
|
32
25
|
} catch (error: unknown) {
|
|
33
26
|
if (error instanceof Error && error.name === "AbortError") {
|
|
34
27
|
throw new Error("Request timed out");
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { ArtifactInfo, ReleaseCatalog } from "@hot-updater/core";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { createHttpClient } from "./httpClient";
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => {
|
|
7
|
+
Reflect.set(globalThis, "HotUpdater", {
|
|
8
|
+
SDK_VERSION: "test-sdk-version",
|
|
9
|
+
});
|
|
10
|
+
return {
|
|
11
|
+
fetchCatalog: vi.fn(),
|
|
12
|
+
fetchJSON: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
vi.mock("./releaseCatalogCache", () => ({
|
|
17
|
+
fetchReleaseCatalogWithCache: mocks.fetchCatalog,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("./fetchJSON", () => ({
|
|
21
|
+
fetchJSON: mocks.fetchJSON,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const artifact: ArtifactInfo = {
|
|
25
|
+
fileHash: "bundle-hash",
|
|
26
|
+
fileUrl: "/storage/bundle.zip",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const catalog: ReleaseCatalog = {
|
|
30
|
+
catalogId: "server-owned-project",
|
|
31
|
+
catalogHash: `sha256:${"a".repeat(64)}`,
|
|
32
|
+
fallbackPolicy: "BUILTIN_IF_ACTIVE_INELIGIBLE",
|
|
33
|
+
generation: 1,
|
|
34
|
+
releases: [],
|
|
35
|
+
schemaVersion: 1,
|
|
36
|
+
scopeKey: "v1:app-version:server-owned-project:ios:cHJvZHVjdGlvbg",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe("private HotUpdater HTTP client", () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
mocks.fetchCatalog.mockResolvedValue(catalog);
|
|
43
|
+
mocks.fetchJSON.mockResolvedValue(artifact);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("fetches a catalog without putting its internal identity in the route", async () => {
|
|
47
|
+
const session = await createHttpClient(
|
|
48
|
+
"https://updates.example.com/hot-updater/",
|
|
49
|
+
).createSession();
|
|
50
|
+
|
|
51
|
+
await expect(
|
|
52
|
+
session.fetchReleaseCatalog({
|
|
53
|
+
appVersion: "1.2.0",
|
|
54
|
+
channel: "production",
|
|
55
|
+
fingerprintHash: null,
|
|
56
|
+
platform: "ios",
|
|
57
|
+
updateStrategy: "appVersion",
|
|
58
|
+
}),
|
|
59
|
+
).resolves.toBe(catalog);
|
|
60
|
+
|
|
61
|
+
expect(mocks.fetchCatalog).toHaveBeenCalledWith({
|
|
62
|
+
baseURL: "https://updates.example.com/hot-updater",
|
|
63
|
+
expectedScope: {
|
|
64
|
+
channelKey: "cHJvZHVjdGlvbg",
|
|
65
|
+
platform: "ios",
|
|
66
|
+
strategy: "APP_VERSION",
|
|
67
|
+
},
|
|
68
|
+
requestHeaders: undefined,
|
|
69
|
+
requestTimeout: undefined,
|
|
70
|
+
url: "https://updates.example.com/hot-updater/release-catalogs/app-version/ios/cHJvZHVjdGlvbg/1.2.0",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("resolves a functional baseURL once per session and captures it for a deferred artifact", async () => {
|
|
75
|
+
const resolveBaseURL = vi
|
|
76
|
+
.fn()
|
|
77
|
+
.mockResolvedValueOnce("https://first.example.com/hot-updater")
|
|
78
|
+
.mockResolvedValueOnce("https://second.example.com/hot-updater");
|
|
79
|
+
const client = createHttpClient(resolveBaseURL);
|
|
80
|
+
const firstSession = await client.createSession();
|
|
81
|
+
|
|
82
|
+
await firstSession.fetchReleaseCatalog({
|
|
83
|
+
appVersion: "1.2.0",
|
|
84
|
+
channel: "production",
|
|
85
|
+
fingerprintHash: null,
|
|
86
|
+
platform: "ios",
|
|
87
|
+
updateStrategy: "appVersion",
|
|
88
|
+
});
|
|
89
|
+
await expect(
|
|
90
|
+
firstSession.resolveArtifact({
|
|
91
|
+
currentBundleId: "current",
|
|
92
|
+
targetBundleId: "target",
|
|
93
|
+
}),
|
|
94
|
+
).resolves.toEqual({
|
|
95
|
+
...artifact,
|
|
96
|
+
fileUrl: "https://first.example.com/hot-updater/storage/bundle.zip",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(resolveBaseURL).toHaveBeenCalledOnce();
|
|
100
|
+
expect(mocks.fetchJSON).toHaveBeenCalledWith({
|
|
101
|
+
requestHeaders: undefined,
|
|
102
|
+
requestTimeout: undefined,
|
|
103
|
+
url: "https://first.example.com/hot-updater/artifacts/target/from/current",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await client.createSession();
|
|
107
|
+
expect(resolveBaseURL).toHaveBeenCalledTimes(2);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("rejects non-storage relative artifact URLs", async () => {
|
|
111
|
+
mocks.fetchJSON.mockResolvedValue({
|
|
112
|
+
...artifact,
|
|
113
|
+
fileUrl: "/private/bundle.zip",
|
|
114
|
+
});
|
|
115
|
+
const session = await createHttpClient(
|
|
116
|
+
"https://updates.example.com",
|
|
117
|
+
).createSession();
|
|
118
|
+
|
|
119
|
+
await expect(
|
|
120
|
+
session.resolveArtifact({
|
|
121
|
+
currentBundleId: "current",
|
|
122
|
+
targetBundleId: "target",
|
|
123
|
+
}),
|
|
124
|
+
).rejects.toThrow("client-relative storage paths");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("requires a functional baseURL to resolve to a non-empty string", async () => {
|
|
128
|
+
await expect(createHttpClient(() => "").createSession()).rejects.toThrow(
|
|
129
|
+
"baseURL function must return a non-empty string",
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|