@hot-updater/react-native 0.30.12 → 0.31.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/HotUpdater.podspec +1 -0
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/hotupdater/BsdiffPatch.kt +284 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +885 -5
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +13 -2
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +37 -8
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +48 -12
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +60 -22
- package/android/src/newarch/HotUpdater.kt +14 -2
- package/android/src/newarch/HotUpdaterModule.kt +112 -1
- package/android/src/oldarch/HotUpdater.kt +14 -2
- package/android/src/oldarch/HotUpdaterModule.kt +107 -3
- package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +46 -0
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +29 -1
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -1
- package/ios/HotUpdater/Internal/BsdiffPatchBridge.h +16 -0
- package/ios/HotUpdater/Internal/BsdiffPatchBridge.mm +343 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +1093 -47
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +2 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +12 -2
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +64 -5
- package/ios/HotUpdater/Internal/NotificationExtension.swift +2 -1
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +28 -5
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +11 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +11 -5
- package/lib/commonjs/DefaultResolver.js +6 -2
- package/lib/commonjs/DefaultResolver.js.map +1 -1
- package/lib/commonjs/DefaultResolver.spec.js +79 -3
- package/lib/commonjs/DefaultResolver.spec.js.map +1 -1
- package/lib/commonjs/checkForUpdate.js +6 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +53 -18
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +117 -1
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +40 -16
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +119 -3
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +8 -0
- package/lib/commonjs/sdkVersion.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/store.js +72 -5
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/store.spec.js +265 -8
- package/lib/commonjs/store.spec.js.map +1 -1
- package/lib/commonjs/wrap.js +19 -5
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +74 -0
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/DefaultResolver.js +6 -2
- package/lib/module/DefaultResolver.js.map +1 -1
- package/lib/module/DefaultResolver.spec.js +78 -3
- package/lib/module/DefaultResolver.spec.js.map +1 -1
- package/lib/module/checkForUpdate.js +6 -0
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +53 -18
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +117 -1
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +40 -16
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +119 -3
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/sdkVersion.js +4 -0
- package/lib/module/sdkVersion.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/store.js +72 -5
- package/lib/module/store.js.map +1 -1
- package/lib/module/store.spec.js +265 -8
- package/lib/module/store.spec.js.map +1 -1
- package/lib/module/wrap.js +19 -5
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +74 -0
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/DefaultResolver.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +2 -2
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +36 -8
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +56 -3
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +2 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +27 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts +5 -0
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +43 -29
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/DefaultResolver.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +2 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +36 -8
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +56 -3
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +2 -0
- package/lib/typescript/module/sdkVersion.d.ts.map +1 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +27 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts +5 -0
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +43 -29
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +17 -9
- package/src/DefaultResolver.spec.ts +103 -3
- package/src/DefaultResolver.ts +9 -2
- package/src/checkForUpdate.ts +9 -2
- package/src/index.spec.ts +149 -1
- package/src/index.ts +99 -40
- package/src/native.spec.ts +139 -1
- package/src/native.ts +145 -31
- package/src/sdkVersion.ts +1 -0
- package/src/specs/NativeHotUpdater.ts +28 -0
- package/src/store.spec.ts +324 -9
- package/src/store.ts +133 -7
- package/src/wrap.spec.ts +91 -0
- package/src/wrap.tsx +79 -42
package/src/index.ts
CHANGED
|
@@ -28,17 +28,21 @@ import {
|
|
|
28
28
|
import { hotUpdaterStore } from "./store";
|
|
29
29
|
import type { HotUpdaterResolver } from "./types";
|
|
30
30
|
import {
|
|
31
|
+
type AutoUpdateOptions,
|
|
31
32
|
type HotUpdaterInitOptions,
|
|
32
33
|
type HotUpdaterOptions,
|
|
33
34
|
init,
|
|
34
35
|
type InternalInitOptions,
|
|
35
36
|
type InternalWrapOptions,
|
|
37
|
+
type ManualUpdateOptions,
|
|
36
38
|
wrap,
|
|
37
39
|
} from "./wrap";
|
|
38
40
|
|
|
39
41
|
export type {
|
|
40
42
|
CustomReloadHandler,
|
|
41
43
|
HotUpdaterEvent,
|
|
44
|
+
HotUpdaterProgressArtifactType,
|
|
45
|
+
HotUpdaterProgressEvent,
|
|
42
46
|
Manifest,
|
|
43
47
|
ManifestAsset,
|
|
44
48
|
NotifyAppReadyResult,
|
|
@@ -56,8 +60,10 @@ export {
|
|
|
56
60
|
type SignatureVerificationFailure,
|
|
57
61
|
} from "./types";
|
|
58
62
|
export type {
|
|
63
|
+
HotUpdaterFallbackComponentProps,
|
|
59
64
|
HotUpdaterInitOptions,
|
|
60
65
|
HotUpdaterOptions,
|
|
66
|
+
ManualUpdateOptions,
|
|
61
67
|
RunUpdateProcessResponse,
|
|
62
68
|
} from "./wrap";
|
|
63
69
|
|
|
@@ -79,6 +85,33 @@ const registerGlobalGetBaseURL = () => {
|
|
|
79
85
|
// Call registration immediately on module load
|
|
80
86
|
registerGlobalGetBaseURL();
|
|
81
87
|
|
|
88
|
+
type HotUpdaterWrap = {
|
|
89
|
+
(options: AutoUpdateOptions): ReturnType<typeof wrap>;
|
|
90
|
+
/**
|
|
91
|
+
* @deprecated Replace manual `HotUpdater.wrap` with `HotUpdater.init`.
|
|
92
|
+
*
|
|
93
|
+
* ```tsx
|
|
94
|
+
* import { HotUpdater } from "@hot-updater/react-native";
|
|
95
|
+
*
|
|
96
|
+
* HotUpdater.init({
|
|
97
|
+
* baseURL: "<your-update-server-url>",
|
|
98
|
+
* });
|
|
99
|
+
*
|
|
100
|
+
* export default App;
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* Then call `HotUpdater.checkForUpdate(...)` from your manual update flow.
|
|
104
|
+
* See https://hot-updater.dev/docs/guides/custom-update
|
|
105
|
+
*/
|
|
106
|
+
(options: ManualUpdateOptions): ReturnType<typeof wrap>;
|
|
107
|
+
(options: HotUpdaterOptions): ReturnType<typeof wrap>;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const isManualWrapOptions = (
|
|
111
|
+
options: HotUpdaterOptions,
|
|
112
|
+
): options is ManualUpdateOptions =>
|
|
113
|
+
"updateMode" in options && options.updateMode === "manual";
|
|
114
|
+
|
|
82
115
|
/**
|
|
83
116
|
* Creates a HotUpdater client instance with all update management methods.
|
|
84
117
|
* This function is called once on module initialization to create a singleton instance.
|
|
@@ -89,6 +122,7 @@ function createHotUpdaterClient() {
|
|
|
89
122
|
resolver: HotUpdaterResolver | null;
|
|
90
123
|
requestHeaders?: Record<string, string>;
|
|
91
124
|
requestTimeout?: number;
|
|
125
|
+
onError?: (error: unknown) => void;
|
|
92
126
|
} = {
|
|
93
127
|
resolver: null,
|
|
94
128
|
};
|
|
@@ -96,54 +130,67 @@ function createHotUpdaterClient() {
|
|
|
96
130
|
const createMissingNetworkConfigError = (apiName: "init" | "wrap") => {
|
|
97
131
|
if (apiName === "init") {
|
|
98
132
|
return new Error(
|
|
99
|
-
`[HotUpdater] baseURL must be provided.\n\n` +
|
|
100
|
-
`Configure HotUpdater before calling update APIs:\n\n` +
|
|
133
|
+
`[HotUpdater] Either baseURL or resolver must be provided.\n\n` +
|
|
134
|
+
`Configure HotUpdater before calling update APIs with the standard baseURL setup:\n\n` +
|
|
101
135
|
` HotUpdater.init({\n` +
|
|
102
136
|
` baseURL: "<your-update-server-url>",\n` +
|
|
103
137
|
` });\n\n` +
|
|
104
|
-
`For
|
|
138
|
+
`For manual update flows, visit: https://hot-updater.dev/docs/guides/custom-update`,
|
|
105
139
|
);
|
|
106
140
|
}
|
|
107
141
|
|
|
108
142
|
const baseURLExample =
|
|
109
143
|
` export default HotUpdater.wrap({\n` +
|
|
110
144
|
` baseURL: "<your-update-server-url>",\n` +
|
|
111
|
-
` updateStrategy: "appVersion"
|
|
112
|
-
` updateMode: "auto"\n` +
|
|
113
|
-
` })(App);\n\n`;
|
|
114
|
-
|
|
115
|
-
const resolverExample =
|
|
116
|
-
` export default HotUpdater.wrap({\n` +
|
|
117
|
-
` resolver: {\n` +
|
|
118
|
-
` checkUpdate: async (params) => { /* custom logic */ },\n` +
|
|
119
|
-
` notifyAppReady: async (params) => { /* custom logic */ }\n` +
|
|
120
|
-
` },\n` +
|
|
121
|
-
` updateMode: "manual"\n` +
|
|
145
|
+
` updateStrategy: "appVersion"\n` +
|
|
122
146
|
` })(App);\n\n`;
|
|
123
147
|
|
|
124
148
|
return new Error(
|
|
125
149
|
`[HotUpdater] Either baseURL or resolver must be provided.\n\n` +
|
|
126
|
-
`
|
|
150
|
+
`Configure HotUpdater.wrap with the standard baseURL setup:\n\n` +
|
|
127
151
|
baseURLExample +
|
|
128
|
-
`
|
|
129
|
-
|
|
130
|
-
`For more information, visit: https://hot-updater.dev/docs/react-native-api/${apiName}`,
|
|
152
|
+
`For manual update flows, use HotUpdater.init() and visit: ` +
|
|
153
|
+
`https://hot-updater.dev/docs/guides/custom-update`,
|
|
131
154
|
);
|
|
132
155
|
};
|
|
133
156
|
|
|
134
157
|
const normalizeOptions = (
|
|
135
158
|
options: HotUpdaterOptions,
|
|
136
159
|
): InternalWrapOptions => {
|
|
137
|
-
if (
|
|
138
|
-
|
|
160
|
+
if (isManualWrapOptions(options)) {
|
|
161
|
+
if ("baseURL" in options && options.baseURL) {
|
|
162
|
+
const { baseURL, ...rest } = options;
|
|
163
|
+
return {
|
|
164
|
+
...rest,
|
|
165
|
+
resolver: createDefaultResolver(baseURL),
|
|
166
|
+
updateMode: "manual",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if ("resolver" in options && options.resolver) {
|
|
171
|
+
return {
|
|
172
|
+
...options,
|
|
173
|
+
updateMode: "manual",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const autoOptions = options as AutoUpdateOptions;
|
|
179
|
+
|
|
180
|
+
if ("baseURL" in autoOptions && autoOptions.baseURL) {
|
|
181
|
+
const { baseURL, ...rest } = autoOptions;
|
|
139
182
|
return {
|
|
140
183
|
...rest,
|
|
141
184
|
resolver: createDefaultResolver(baseURL),
|
|
185
|
+
updateMode: "auto",
|
|
142
186
|
};
|
|
143
187
|
}
|
|
144
188
|
|
|
145
|
-
if ("resolver" in
|
|
146
|
-
return
|
|
189
|
+
if ("resolver" in autoOptions && autoOptions.resolver) {
|
|
190
|
+
return {
|
|
191
|
+
...autoOptions,
|
|
192
|
+
updateMode: "auto",
|
|
193
|
+
};
|
|
147
194
|
}
|
|
148
195
|
|
|
149
196
|
throw createMissingNetworkConfigError("wrap");
|
|
@@ -152,21 +199,23 @@ function createHotUpdaterClient() {
|
|
|
152
199
|
const normalizeInitOptions = (
|
|
153
200
|
options: HotUpdaterInitOptions,
|
|
154
201
|
): InternalInitOptions => {
|
|
155
|
-
const {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (baseURL) {
|
|
202
|
+
const { updateMode: _updateMode, ...rest } =
|
|
203
|
+
options as HotUpdaterInitOptions & {
|
|
204
|
+
updateMode?: unknown;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
if ("baseURL" in rest && rest.baseURL) {
|
|
208
|
+
const { baseURL, ...baseURLRest } = rest;
|
|
164
209
|
return {
|
|
165
|
-
...
|
|
210
|
+
...baseURLRest,
|
|
166
211
|
resolver: createDefaultResolver(baseURL),
|
|
167
212
|
};
|
|
168
213
|
}
|
|
169
214
|
|
|
215
|
+
if ("resolver" in rest && rest.resolver) {
|
|
216
|
+
return rest;
|
|
217
|
+
}
|
|
218
|
+
|
|
170
219
|
throw createMissingNetworkConfigError("init");
|
|
171
220
|
};
|
|
172
221
|
|
|
@@ -177,6 +226,7 @@ function createHotUpdaterClient() {
|
|
|
177
226
|
globalConfig.resolver = normalizedOptions.resolver;
|
|
178
227
|
globalConfig.requestHeaders = options.requestHeaders;
|
|
179
228
|
globalConfig.requestTimeout = options.requestTimeout;
|
|
229
|
+
globalConfig.onError = options.onError;
|
|
180
230
|
};
|
|
181
231
|
|
|
182
232
|
const ensureGlobalResolver = (methodName: string) => {
|
|
@@ -187,15 +237,13 @@ function createHotUpdaterClient() {
|
|
|
187
237
|
`Option 1: With HotUpdater.wrap()\n` +
|
|
188
238
|
` export default HotUpdater.wrap({\n` +
|
|
189
239
|
` baseURL: "<your-update-server-url>",\n` +
|
|
190
|
-
` updateStrategy: "appVersion"
|
|
191
|
-
` updateMode: "auto"\n` +
|
|
240
|
+
` updateStrategy: "appVersion"\n` +
|
|
192
241
|
` })(App);\n\n` +
|
|
193
242
|
`Option 2: With HotUpdater.init() for custom runtimes\n` +
|
|
194
243
|
` HotUpdater.init({\n` +
|
|
195
244
|
` baseURL: "<your-update-server-url>",\n` +
|
|
196
245
|
` });\n\n` +
|
|
197
|
-
`For
|
|
198
|
-
`or https://hot-updater.dev/docs/react-native-api/init`,
|
|
246
|
+
`For manual update flows, visit: https://hot-updater.dev/docs/guides/custom-update`,
|
|
199
247
|
);
|
|
200
248
|
}
|
|
201
249
|
return globalConfig.resolver;
|
|
@@ -219,7 +267,6 @@ function createHotUpdaterClient() {
|
|
|
219
267
|
* export default HotUpdater.wrap({
|
|
220
268
|
* baseURL: "<your-update-server-url>",
|
|
221
269
|
* updateStrategy: "appVersion",
|
|
222
|
-
* updateMode: "auto",
|
|
223
270
|
* requestHeaders: {
|
|
224
271
|
* "Authorization": "Bearer <your-access-token>",
|
|
225
272
|
* },
|
|
@@ -237,8 +284,8 @@ function createHotUpdaterClient() {
|
|
|
237
284
|
* Initializes HotUpdater without wrapping a React component.
|
|
238
285
|
*
|
|
239
286
|
* Use this for manual update flows in runtimes where a root component HOC
|
|
240
|
-
* is not convenient.
|
|
241
|
-
* `HotUpdater.wrap({ updateMode: "manual" })(App)
|
|
287
|
+
* is not convenient. Replace deprecated manual
|
|
288
|
+
* `HotUpdater.wrap({ updateMode: "manual" })(App)` usage with this API.
|
|
242
289
|
*
|
|
243
290
|
* @example
|
|
244
291
|
* ```tsx
|
|
@@ -420,6 +467,7 @@ function createHotUpdaterClient() {
|
|
|
420
467
|
...config.requestHeaders,
|
|
421
468
|
},
|
|
422
469
|
requestTimeout: config.requestTimeout ?? globalConfig.requestTimeout,
|
|
470
|
+
onError: config.onError ?? globalConfig.onError,
|
|
423
471
|
};
|
|
424
472
|
|
|
425
473
|
return checkForUpdate(mergedConfig);
|
|
@@ -472,8 +520,12 @@ function createHotUpdaterClient() {
|
|
|
472
520
|
const ok = await resetChannel();
|
|
473
521
|
if (ok) {
|
|
474
522
|
hotUpdaterStore.setState({
|
|
523
|
+
artifactType: null,
|
|
524
|
+
details: null,
|
|
525
|
+
downloadedBytes: undefined,
|
|
475
526
|
isUpdateDownloaded: false,
|
|
476
527
|
progress: 0,
|
|
528
|
+
totalBytes: undefined,
|
|
477
529
|
});
|
|
478
530
|
}
|
|
479
531
|
return ok;
|
|
@@ -522,4 +574,11 @@ function createHotUpdaterClient() {
|
|
|
522
574
|
};
|
|
523
575
|
}
|
|
524
576
|
|
|
525
|
-
|
|
577
|
+
type HotUpdaterClient = Omit<
|
|
578
|
+
ReturnType<typeof createHotUpdaterClient>,
|
|
579
|
+
"wrap"
|
|
580
|
+
> & {
|
|
581
|
+
wrap: HotUpdaterWrap;
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
export const HotUpdater: HotUpdaterClient = createHotUpdaterClient();
|
package/src/native.spec.ts
CHANGED
|
@@ -167,6 +167,7 @@ describe("notifyAppReady", () => {
|
|
|
167
167
|
assets: {
|
|
168
168
|
"assets/logo.png": {
|
|
169
169
|
fileHash: "hash-logo",
|
|
170
|
+
signature: "sig-logo",
|
|
170
171
|
},
|
|
171
172
|
"index.android.bundle": {
|
|
172
173
|
fileHash: "hash-bundle",
|
|
@@ -181,6 +182,7 @@ describe("notifyAppReady", () => {
|
|
|
181
182
|
assets: {
|
|
182
183
|
"assets/logo.png": {
|
|
183
184
|
fileHash: "hash-logo",
|
|
185
|
+
signature: "sig-logo",
|
|
184
186
|
},
|
|
185
187
|
"index.android.bundle": {
|
|
186
188
|
fileHash: "hash-bundle",
|
|
@@ -337,11 +339,147 @@ describe("notifyAppReady", () => {
|
|
|
337
339
|
bundleId: "bundle-123",
|
|
338
340
|
});
|
|
339
341
|
expect(getBaseURL()).toBe("file:///bundle-123");
|
|
340
|
-
expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(
|
|
342
|
+
expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(3);
|
|
341
343
|
expect(nativeModuleMock.getManifest).toHaveBeenCalledTimes(2);
|
|
342
344
|
expect(nativeModuleMock.getBaseURL).toHaveBeenCalledTimes(2);
|
|
343
345
|
});
|
|
344
346
|
|
|
347
|
+
it("reinstalls a session-installed bundle after native resets to built-in", async () => {
|
|
348
|
+
nativeModuleMock.getConstants.mockReturnValue({
|
|
349
|
+
APP_VERSION: null,
|
|
350
|
+
CHANNEL: "production",
|
|
351
|
+
DEFAULT_CHANNEL: "production",
|
|
352
|
+
FINGERPRINT_HASH: null,
|
|
353
|
+
MIN_BUNDLE_ID: "00000000-0000-0000-0000-000000000001",
|
|
354
|
+
});
|
|
355
|
+
nativeModuleMock.getBundleId.mockReturnValue(
|
|
356
|
+
"00000000-0000-0000-0000-000000000002",
|
|
357
|
+
);
|
|
358
|
+
nativeModuleMock.updateBundle.mockResolvedValue(true);
|
|
359
|
+
|
|
360
|
+
const { getBundleId, updateBundle } = await import("./native");
|
|
361
|
+
|
|
362
|
+
await updateBundle({
|
|
363
|
+
bundleId: "00000000-0000-0000-0000-000000000003",
|
|
364
|
+
fileHash: null,
|
|
365
|
+
fileUrl: "https://example.com/bundle.zip",
|
|
366
|
+
status: "UPDATE",
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
nativeModuleMock.getBundleId.mockReturnValue(
|
|
370
|
+
"00000000-0000-0000-0000-000000000003",
|
|
371
|
+
);
|
|
372
|
+
expect(getBundleId()).toBe("00000000-0000-0000-0000-000000000003");
|
|
373
|
+
|
|
374
|
+
nativeModuleMock.getBundleId.mockReturnValue(null);
|
|
375
|
+
|
|
376
|
+
await expect(
|
|
377
|
+
updateBundle({
|
|
378
|
+
bundleId: "00000000-0000-0000-0000-000000000003",
|
|
379
|
+
fileHash: null,
|
|
380
|
+
fileUrl: "https://example.com/bundle.zip",
|
|
381
|
+
status: "UPDATE",
|
|
382
|
+
}),
|
|
383
|
+
).resolves.toBe(true);
|
|
384
|
+
|
|
385
|
+
expect(nativeModuleMock.updateBundle).toHaveBeenCalledTimes(2);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("skips a session-installed bundle when native still reports it active", async () => {
|
|
389
|
+
nativeModuleMock.getConstants.mockReturnValue({
|
|
390
|
+
APP_VERSION: null,
|
|
391
|
+
CHANNEL: "production",
|
|
392
|
+
DEFAULT_CHANNEL: "production",
|
|
393
|
+
FINGERPRINT_HASH: null,
|
|
394
|
+
MIN_BUNDLE_ID: "00000000-0000-0000-0000-000000000001",
|
|
395
|
+
});
|
|
396
|
+
nativeModuleMock.getBundleId.mockReturnValue(
|
|
397
|
+
"00000000-0000-0000-0000-000000000002",
|
|
398
|
+
);
|
|
399
|
+
nativeModuleMock.updateBundle.mockResolvedValue(true);
|
|
400
|
+
|
|
401
|
+
const { updateBundle } = await import("./native");
|
|
402
|
+
|
|
403
|
+
await updateBundle({
|
|
404
|
+
bundleId: "00000000-0000-0000-0000-000000000003",
|
|
405
|
+
fileHash: null,
|
|
406
|
+
fileUrl: "https://example.com/bundle.zip",
|
|
407
|
+
status: "UPDATE",
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
nativeModuleMock.getBundleId.mockReturnValue(
|
|
411
|
+
"00000000-0000-0000-0000-000000000003",
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
await expect(
|
|
415
|
+
updateBundle({
|
|
416
|
+
bundleId: "00000000-0000-0000-0000-000000000003",
|
|
417
|
+
fileHash: null,
|
|
418
|
+
fileUrl: "https://example.com/bundle.zip",
|
|
419
|
+
status: "UPDATE",
|
|
420
|
+
}),
|
|
421
|
+
).resolves.toBe(true);
|
|
422
|
+
|
|
423
|
+
expect(nativeModuleMock.updateBundle).toHaveBeenCalledTimes(1);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("forwards manifest artifact parameters to native updateBundle", async () => {
|
|
427
|
+
nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
|
|
428
|
+
nativeModuleMock.updateBundle.mockResolvedValue(true);
|
|
429
|
+
|
|
430
|
+
const { updateBundle } = await import("./native");
|
|
431
|
+
|
|
432
|
+
await updateBundle({
|
|
433
|
+
bundleId: "bundle-789",
|
|
434
|
+
changedAssets: {
|
|
435
|
+
"index.ios.bundle": {
|
|
436
|
+
file: {
|
|
437
|
+
compression: "br",
|
|
438
|
+
url: "https://example.com/files/index.ios.bundle.br",
|
|
439
|
+
},
|
|
440
|
+
fileHash: "hash-next",
|
|
441
|
+
patch: {
|
|
442
|
+
algorithm: "bsdiff",
|
|
443
|
+
baseBundleId: "bundle-123",
|
|
444
|
+
baseFileHash: "hash-prev",
|
|
445
|
+
patchFileHash: "hash-patch",
|
|
446
|
+
patchUrl: "https://example.com/files/index.ios.bundle.bsdiff",
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
fileHash: "sig:archive",
|
|
451
|
+
fileUrl: "https://example.com/bundle.zip",
|
|
452
|
+
manifestFileHash: "sig:manifest",
|
|
453
|
+
manifestUrl: "https://example.com/manifest.json",
|
|
454
|
+
status: "UPDATE",
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
expect(nativeModuleMock.updateBundle).toHaveBeenCalledWith({
|
|
458
|
+
bundleId: "bundle-789",
|
|
459
|
+
changedAssets: {
|
|
460
|
+
"index.ios.bundle": {
|
|
461
|
+
file: {
|
|
462
|
+
compression: "br",
|
|
463
|
+
url: "https://example.com/files/index.ios.bundle.br",
|
|
464
|
+
},
|
|
465
|
+
fileHash: "hash-next",
|
|
466
|
+
patch: {
|
|
467
|
+
algorithm: "bsdiff",
|
|
468
|
+
baseBundleId: "bundle-123",
|
|
469
|
+
baseFileHash: "hash-prev",
|
|
470
|
+
patchFileHash: "hash-patch",
|
|
471
|
+
patchUrl: "https://example.com/files/index.ios.bundle.bsdiff",
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
channel: undefined,
|
|
476
|
+
fileHash: "sig:archive",
|
|
477
|
+
fileUrl: "https://example.com/bundle.zip",
|
|
478
|
+
manifestFileHash: "sig:manifest",
|
|
479
|
+
manifestUrl: "https://example.com/manifest.json",
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
345
483
|
it("invalidates cached bundle getters after resetChannel succeeds", async () => {
|
|
346
484
|
nativeModuleMock.getConstants.mockReturnValue({
|
|
347
485
|
APP_VERSION: null,
|
package/src/native.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type ChangedAsset,
|
|
2
3
|
INVALID_COHORT_ERROR_MESSAGE,
|
|
3
4
|
isValidCohort,
|
|
4
5
|
normalizeCohortValue,
|
|
@@ -24,6 +25,7 @@ const normalizeAndValidateCohort = (cohort: string): string => {
|
|
|
24
25
|
|
|
25
26
|
export interface ManifestAsset {
|
|
26
27
|
fileHash: string;
|
|
28
|
+
signature?: string;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface Manifest {
|
|
@@ -186,11 +188,38 @@ const cloneManifest = (manifest: Manifest): Manifest => ({
|
|
|
186
188
|
assets: Object.fromEntries(
|
|
187
189
|
Object.entries(manifest.assets).map(([key, asset]) => [
|
|
188
190
|
key,
|
|
189
|
-
{
|
|
191
|
+
{
|
|
192
|
+
fileHash: asset.fileHash,
|
|
193
|
+
...(asset.signature ? { signature: asset.signature } : {}),
|
|
194
|
+
},
|
|
190
195
|
]),
|
|
191
196
|
),
|
|
192
197
|
});
|
|
193
198
|
|
|
199
|
+
const getNativeBundleId = (): string | null => {
|
|
200
|
+
const nativeModule = HotUpdaterNative as typeof HotUpdaterNative & {
|
|
201
|
+
getBundleId?: () => string | null;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (typeof nativeModule.getBundleId !== "function") {
|
|
205
|
+
throw new Error(
|
|
206
|
+
"[HotUpdater] Native module is missing 'getBundleId()'. This JS bundle requires a newer native @hot-updater/react-native SDK. Rebuild and release a new app version before delivering this OTA update.",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return nativeModule.getBundleId();
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const resolveBundleId = (bundleId: string | null): string => {
|
|
214
|
+
return !bundleId || bundleId === NIL_UUID ? getMinBundleId() : bundleId;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const getFreshBundleId = (): string => {
|
|
218
|
+
const resolvedBundleId = resolveBundleId(getNativeBundleId());
|
|
219
|
+
sessionState.cacheBundleId(resolvedBundleId);
|
|
220
|
+
return resolvedBundleId;
|
|
221
|
+
};
|
|
222
|
+
|
|
194
223
|
const getReloadProcess = (): (() => Promise<void>) | null => {
|
|
195
224
|
const nativeModule = HotUpdaterNative as typeof HotUpdaterNative & {
|
|
196
225
|
reloadProcess?: () => Promise<void>;
|
|
@@ -201,10 +230,73 @@ const getReloadProcess = (): (() => Promise<void>) | null => {
|
|
|
201
230
|
: null;
|
|
202
231
|
};
|
|
203
232
|
|
|
233
|
+
export type HotUpdaterProgressArtifactType = "archive" | "diff";
|
|
234
|
+
|
|
235
|
+
export type HotUpdaterDiffFileStatus =
|
|
236
|
+
| "pending"
|
|
237
|
+
| "downloading"
|
|
238
|
+
| "downloaded"
|
|
239
|
+
| "failed";
|
|
240
|
+
|
|
241
|
+
export interface HotUpdaterDiffFileSnapshot {
|
|
242
|
+
/**
|
|
243
|
+
* Manifest asset path for this progress entry.
|
|
244
|
+
*
|
|
245
|
+
* This is the stable identity of the asset in the installed bundle. Use this
|
|
246
|
+
* when matching progress back to the manifest or to the final file location.
|
|
247
|
+
*/
|
|
248
|
+
path: string;
|
|
249
|
+
/**
|
|
250
|
+
* Artifact path currently being transferred for this progress entry.
|
|
251
|
+
*
|
|
252
|
+
* Usually this is the same as `path`. It can differ when the updater
|
|
253
|
+
* downloads an intermediate artifact that will be applied to produce the
|
|
254
|
+
* final asset.
|
|
255
|
+
*/
|
|
256
|
+
downloadPath: string;
|
|
257
|
+
/**
|
|
258
|
+
* Current download state for this asset within the manifest diff update.
|
|
259
|
+
*/
|
|
260
|
+
status: HotUpdaterDiffFileStatus;
|
|
261
|
+
/**
|
|
262
|
+
* Download progress for this file, normalized from 0 to 1.
|
|
263
|
+
*/
|
|
264
|
+
progress: number;
|
|
265
|
+
/**
|
|
266
|
+
* Stable display order among diff files.
|
|
267
|
+
*/
|
|
268
|
+
order: number;
|
|
269
|
+
/**
|
|
270
|
+
* Bytes downloaded for `downloadPath`, when the native downloader reports it.
|
|
271
|
+
*/
|
|
272
|
+
downloadedBytes?: number;
|
|
273
|
+
/**
|
|
274
|
+
* Total expected bytes for `downloadPath`, when known.
|
|
275
|
+
*/
|
|
276
|
+
totalBytes?: number;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface HotUpdaterDiffProgressDetails {
|
|
280
|
+
totalFilesCount: number;
|
|
281
|
+
completedFilesCount: number;
|
|
282
|
+
files: HotUpdaterDiffFileSnapshot[];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export type HotUpdaterProgressEvent =
|
|
286
|
+
| {
|
|
287
|
+
progress: number;
|
|
288
|
+
artifactType: "archive";
|
|
289
|
+
downloadedBytes?: number;
|
|
290
|
+
totalBytes?: number;
|
|
291
|
+
}
|
|
292
|
+
| {
|
|
293
|
+
progress: number;
|
|
294
|
+
artifactType: "diff";
|
|
295
|
+
details: HotUpdaterDiffProgressDetails;
|
|
296
|
+
};
|
|
297
|
+
|
|
204
298
|
export type HotUpdaterEvent = {
|
|
205
|
-
onProgress:
|
|
206
|
-
progress: number;
|
|
207
|
-
};
|
|
299
|
+
onProgress: HotUpdaterProgressEvent;
|
|
208
300
|
};
|
|
209
301
|
|
|
210
302
|
const eventEmitter = new NativeEventEmitter(HotUpdaterNative);
|
|
@@ -252,8 +344,21 @@ export async function updateBundle(
|
|
|
252
344
|
const status =
|
|
253
345
|
typeof paramsOrBundleId === "string" ? "UPDATE" : paramsOrBundleId.status;
|
|
254
346
|
|
|
255
|
-
|
|
256
|
-
|
|
347
|
+
const targetFileUrl =
|
|
348
|
+
typeof paramsOrBundleId === "string"
|
|
349
|
+
? (fileUrl ?? null)
|
|
350
|
+
: paramsOrBundleId.fileUrl;
|
|
351
|
+
|
|
352
|
+
const currentBundleId = status === "UPDATE" ? getFreshBundleId() : undefined;
|
|
353
|
+
|
|
354
|
+
// If native is still on the same bundle we installed in this session,
|
|
355
|
+
// skip re-download. Native state can move back to the built-in bundle after
|
|
356
|
+
// rollback/reset, so check a fresh native bundle id before using this guard.
|
|
357
|
+
if (
|
|
358
|
+
status === "UPDATE" &&
|
|
359
|
+
sessionState.hasInstalledBundle(updateBundleId) &&
|
|
360
|
+
currentBundleId === updateBundleId
|
|
361
|
+
) {
|
|
257
362
|
return true;
|
|
258
363
|
}
|
|
259
364
|
|
|
@@ -265,7 +370,8 @@ export async function updateBundle(
|
|
|
265
370
|
if (
|
|
266
371
|
!shouldSkipCurrentBundleIdCheck &&
|
|
267
372
|
status === "UPDATE" &&
|
|
268
|
-
|
|
373
|
+
currentBundleId !== undefined &&
|
|
374
|
+
updateBundleId.localeCompare(currentBundleId) <= 0
|
|
269
375
|
) {
|
|
270
376
|
throw new Error(
|
|
271
377
|
"Update bundle id is not newer than the current bundle id. Preventing infinite update loop.",
|
|
@@ -276,11 +382,6 @@ export async function updateBundle(
|
|
|
276
382
|
const existing = sessionState.getInflightUpdate(updateBundleId);
|
|
277
383
|
if (existing) return existing;
|
|
278
384
|
|
|
279
|
-
const targetFileUrl =
|
|
280
|
-
typeof paramsOrBundleId === "string"
|
|
281
|
-
? (fileUrl ?? null)
|
|
282
|
-
: paramsOrBundleId.fileUrl;
|
|
283
|
-
|
|
284
385
|
const targetFileHash =
|
|
285
386
|
typeof paramsOrBundleId === "string"
|
|
286
387
|
? undefined
|
|
@@ -288,14 +389,30 @@ export async function updateBundle(
|
|
|
288
389
|
|
|
289
390
|
const targetChannel =
|
|
290
391
|
typeof paramsOrBundleId === "string" ? undefined : paramsOrBundleId.channel;
|
|
392
|
+
const targetManifestUrl =
|
|
393
|
+
typeof paramsOrBundleId === "string"
|
|
394
|
+
? undefined
|
|
395
|
+
: paramsOrBundleId.manifestUrl;
|
|
396
|
+
const targetManifestFileHash =
|
|
397
|
+
typeof paramsOrBundleId === "string"
|
|
398
|
+
? undefined
|
|
399
|
+
: paramsOrBundleId.manifestFileHash;
|
|
400
|
+
const targetChangedAssets =
|
|
401
|
+
typeof paramsOrBundleId === "string"
|
|
402
|
+
? undefined
|
|
403
|
+
: paramsOrBundleId.changedAssets;
|
|
291
404
|
|
|
292
405
|
const promise = (async () => {
|
|
293
406
|
try {
|
|
294
407
|
const ok = await HotUpdaterNative.updateBundle({
|
|
295
408
|
bundleId: updateBundleId,
|
|
296
409
|
channel: targetChannel,
|
|
410
|
+
changedAssets:
|
|
411
|
+
(targetChangedAssets as Record<string, ChangedAsset> | null) ?? null,
|
|
297
412
|
fileUrl: targetFileUrl,
|
|
298
413
|
fileHash: targetFileHash ?? null,
|
|
414
|
+
manifestFileHash: targetManifestFileHash ?? null,
|
|
415
|
+
manifestUrl: targetManifestUrl ?? null,
|
|
299
416
|
});
|
|
300
417
|
if (ok) {
|
|
301
418
|
sessionState.markBundleInstalled(updateBundleId, targetChannel);
|
|
@@ -424,23 +541,7 @@ export const getBundleId = (): string => {
|
|
|
424
541
|
return cachedBundleId;
|
|
425
542
|
}
|
|
426
543
|
|
|
427
|
-
|
|
428
|
-
getBundleId?: () => string | null;
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
if (typeof nativeModule.getBundleId !== "function") {
|
|
432
|
-
throw new Error(
|
|
433
|
-
"[HotUpdater] Native module is missing 'getBundleId()'. This JS bundle requires a newer native @hot-updater/react-native SDK. Rebuild and release a new app version before delivering this OTA update.",
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const bundleId = nativeModule.getBundleId();
|
|
438
|
-
|
|
439
|
-
const resolvedBundleId =
|
|
440
|
-
!bundleId || bundleId === NIL_UUID ? getMinBundleId() : bundleId;
|
|
441
|
-
|
|
442
|
-
sessionState.cacheBundleId(resolvedBundleId);
|
|
443
|
-
return resolvedBundleId;
|
|
544
|
+
return getFreshBundleId();
|
|
444
545
|
};
|
|
445
546
|
|
|
446
547
|
/**
|
|
@@ -617,12 +718,25 @@ const normalizeManifestAssets = (value: unknown): Manifest["assets"] => {
|
|
|
617
718
|
return [];
|
|
618
719
|
}
|
|
619
720
|
|
|
620
|
-
const { fileHash } = entry as {
|
|
721
|
+
const { fileHash, signature } = entry as {
|
|
722
|
+
fileHash?: unknown;
|
|
723
|
+
signature?: unknown;
|
|
724
|
+
};
|
|
621
725
|
if (typeof fileHash !== "string" || !fileHash.trim()) {
|
|
622
726
|
return [];
|
|
623
727
|
}
|
|
624
728
|
|
|
625
|
-
return [
|
|
729
|
+
return [
|
|
730
|
+
[
|
|
731
|
+
trimmedKey,
|
|
732
|
+
{
|
|
733
|
+
fileHash: fileHash.trim(),
|
|
734
|
+
...(typeof signature === "string" && signature.trim()
|
|
735
|
+
? { signature: signature.trim() }
|
|
736
|
+
: {}),
|
|
737
|
+
},
|
|
738
|
+
] as const,
|
|
739
|
+
];
|
|
626
740
|
}),
|
|
627
741
|
);
|
|
628
742
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const HOT_UPDATER_SDK_VERSION = "0.30.12";
|