@hot-updater/react-native 0.30.11 → 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 +18 -4
- package/lib/commonjs/DefaultResolver.js.map +1 -1
- package/lib/commonjs/DefaultResolver.spec.js +136 -0
- package/lib/commonjs/DefaultResolver.spec.js.map +1 -0
- 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 +133 -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/types.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 +18 -4
- package/lib/module/DefaultResolver.js.map +1 -1
- package/lib/module/DefaultResolver.spec.js +135 -0
- package/lib/module/DefaultResolver.spec.js.map +1 -0
- 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 +133 -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/types.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 +4 -3
- 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 +37 -9
- 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/types.d.ts +1 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +48 -32
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/DefaultResolver.d.ts +4 -3
- 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 +37 -9
- 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/types.d.ts +1 -0
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +48 -32
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +17 -9
- package/src/DefaultResolver.spec.ts +178 -0
- package/src/DefaultResolver.ts +32 -6
- package/src/checkForUpdate.ts +9 -2
- package/src/index.spec.ts +170 -2
- package/src/index.ts +100 -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/types.ts +2 -0
- package/src/wrap.spec.ts +91 -0
- package/src/wrap.tsx +84 -45
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";
|
|
@@ -2,6 +2,21 @@ import type { TurboModule } from "react-native";
|
|
|
2
2
|
import { TurboModuleRegistry } from "react-native";
|
|
3
3
|
import type { UnsafeObject } from "react-native/Libraries/Types/CodegenTypes";
|
|
4
4
|
|
|
5
|
+
export interface ChangedAsset {
|
|
6
|
+
file?: {
|
|
7
|
+
compression?: "br" | null;
|
|
8
|
+
url: string;
|
|
9
|
+
} | null;
|
|
10
|
+
fileHash: string;
|
|
11
|
+
patch?: {
|
|
12
|
+
algorithm: "bsdiff";
|
|
13
|
+
baseBundleId: string;
|
|
14
|
+
baseFileHash: string;
|
|
15
|
+
patchFileHash: string;
|
|
16
|
+
patchUrl: string;
|
|
17
|
+
} | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
export interface UpdateBundleParams {
|
|
6
21
|
bundleId: string;
|
|
7
22
|
channel?: string;
|
|
@@ -16,6 +31,19 @@ export interface UpdateBundleParams {
|
|
|
16
31
|
* Native determines verification mode by checking for "sig:" prefix.
|
|
17
32
|
*/
|
|
18
33
|
fileHash: string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Optional signed manifest URL for manifest-driven installation.
|
|
36
|
+
*/
|
|
37
|
+
manifestUrl?: string | null;
|
|
38
|
+
/**
|
|
39
|
+
* File hash/signature for the manifest file itself.
|
|
40
|
+
*/
|
|
41
|
+
manifestFileHash?: string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Per-file URLs for assets that must be downloaded instead of reused from
|
|
44
|
+
* the currently active bundle.
|
|
45
|
+
*/
|
|
46
|
+
changedAssets?: UnsafeObject | null;
|
|
19
47
|
}
|
|
20
48
|
|
|
21
49
|
export interface Spec extends TurboModule {
|
package/src/store.spec.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const listeners = vi.hoisted(
|
|
4
|
+
() => new Map<string, (event: Record<string, unknown>) => void>(),
|
|
5
|
+
);
|
|
4
6
|
|
|
5
7
|
vi.mock("./native", () => ({
|
|
6
|
-
addListener:
|
|
8
|
+
addListener: vi.fn(
|
|
9
|
+
(eventName: string, listener: (event: Record<string, unknown>) => void) => {
|
|
10
|
+
listeners.set(eventName, listener);
|
|
11
|
+
return () => {
|
|
12
|
+
listeners.delete(eventName);
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
),
|
|
7
16
|
}));
|
|
8
17
|
|
|
9
18
|
const importStore = async () => {
|
|
@@ -13,8 +22,8 @@ const importStore = async () => {
|
|
|
13
22
|
|
|
14
23
|
describe("hotUpdaterStore", () => {
|
|
15
24
|
beforeEach(() => {
|
|
25
|
+
listeners.clear();
|
|
16
26
|
vi.resetModules();
|
|
17
|
-
addListenerMock.mockReset();
|
|
18
27
|
});
|
|
19
28
|
|
|
20
29
|
it("does not notify subscribers when state values are unchanged", async () => {
|
|
@@ -35,23 +44,329 @@ describe("hotUpdaterStore", () => {
|
|
|
35
44
|
it("notifies subscribers only when progress events change the snapshot", async () => {
|
|
36
45
|
const store = await importStore();
|
|
37
46
|
const listener = vi.fn();
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
const emitProgress = listeners.get("onProgress");
|
|
48
|
+
|
|
49
|
+
expect(emitProgress).toBeTypeOf("function");
|
|
41
50
|
|
|
42
51
|
const unsubscribe = store.subscribe(listener);
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
emitProgress?.({
|
|
54
|
+
artifactType: "archive",
|
|
55
|
+
progress: 0.5,
|
|
56
|
+
});
|
|
57
|
+
emitProgress?.({
|
|
58
|
+
artifactType: "archive",
|
|
59
|
+
progress: 0.5,
|
|
60
|
+
});
|
|
61
|
+
emitProgress?.({
|
|
62
|
+
artifactType: "archive",
|
|
63
|
+
progress: 1,
|
|
64
|
+
});
|
|
47
65
|
store.setState({ isUpdateDownloaded: true });
|
|
48
66
|
|
|
49
67
|
expect(listener).toHaveBeenCalledTimes(2);
|
|
50
68
|
expect(store.getSnapshot()).toEqual({
|
|
69
|
+
artifactType: "archive",
|
|
70
|
+
details: null,
|
|
71
|
+
downloadedBytes: undefined,
|
|
51
72
|
isUpdateDownloaded: true,
|
|
52
73
|
progress: 1,
|
|
74
|
+
totalBytes: undefined,
|
|
53
75
|
});
|
|
54
76
|
|
|
55
77
|
unsubscribe();
|
|
56
78
|
});
|
|
79
|
+
|
|
80
|
+
it("stores archive byte metadata and notifies when only bytes change", async () => {
|
|
81
|
+
const store = await importStore();
|
|
82
|
+
const listener = vi.fn();
|
|
83
|
+
const emitProgress = listeners.get("onProgress");
|
|
84
|
+
|
|
85
|
+
const unsubscribe = store.subscribe(listener);
|
|
86
|
+
|
|
87
|
+
emitProgress?.({
|
|
88
|
+
artifactType: "archive",
|
|
89
|
+
downloadedBytes: 100.4,
|
|
90
|
+
progress: 0.5,
|
|
91
|
+
totalBytes: 1_000,
|
|
92
|
+
});
|
|
93
|
+
emitProgress?.({
|
|
94
|
+
artifactType: "archive",
|
|
95
|
+
downloadedBytes: 250,
|
|
96
|
+
progress: 0.5,
|
|
97
|
+
totalBytes: 1_000,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(listener).toHaveBeenCalledTimes(2);
|
|
101
|
+
expect(store.getSnapshot()).toEqual({
|
|
102
|
+
artifactType: "archive",
|
|
103
|
+
details: null,
|
|
104
|
+
downloadedBytes: 250,
|
|
105
|
+
isUpdateDownloaded: false,
|
|
106
|
+
progress: 0.5,
|
|
107
|
+
totalBytes: 1_000,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
unsubscribe();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("stores manifest progress metadata while keeping overall progress", async () => {
|
|
114
|
+
const store = await importStore();
|
|
115
|
+
const emitProgress = listeners.get("onProgress");
|
|
116
|
+
|
|
117
|
+
expect(emitProgress).toBeTypeOf("function");
|
|
118
|
+
|
|
119
|
+
emitProgress?.({
|
|
120
|
+
artifactType: "diff",
|
|
121
|
+
details: {
|
|
122
|
+
completedFilesCount: 1,
|
|
123
|
+
files: [
|
|
124
|
+
{
|
|
125
|
+
downloadPath: "index.ios.bundle",
|
|
126
|
+
order: 0,
|
|
127
|
+
path: "index.ios.bundle",
|
|
128
|
+
progress: 1,
|
|
129
|
+
status: "downloaded",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
downloadPath: "assets/logo.png",
|
|
133
|
+
order: 1,
|
|
134
|
+
path: "assets/logo.png",
|
|
135
|
+
progress: 0.5,
|
|
136
|
+
status: "downloading",
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
totalFilesCount: 2,
|
|
140
|
+
},
|
|
141
|
+
progress: 0.42,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(store.getSnapshot()).toEqual({
|
|
145
|
+
artifactType: "diff",
|
|
146
|
+
details: {
|
|
147
|
+
completedFilesCount: 1,
|
|
148
|
+
files: [
|
|
149
|
+
{
|
|
150
|
+
downloadPath: "index.ios.bundle",
|
|
151
|
+
order: 0,
|
|
152
|
+
path: "index.ios.bundle",
|
|
153
|
+
progress: 1,
|
|
154
|
+
status: "downloaded",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
downloadPath: "assets/logo.png",
|
|
158
|
+
order: 1,
|
|
159
|
+
path: "assets/logo.png",
|
|
160
|
+
progress: 0.5,
|
|
161
|
+
status: "downloading",
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
totalFilesCount: 2,
|
|
165
|
+
},
|
|
166
|
+
downloadedBytes: undefined,
|
|
167
|
+
isUpdateDownloaded: false,
|
|
168
|
+
progress: 0.42,
|
|
169
|
+
totalBytes: undefined,
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("preserves patch download metadata for manifest diff files", async () => {
|
|
174
|
+
const store = await importStore();
|
|
175
|
+
const emitProgress = listeners.get("onProgress");
|
|
176
|
+
|
|
177
|
+
emitProgress?.({
|
|
178
|
+
artifactType: "diff",
|
|
179
|
+
details: {
|
|
180
|
+
completedFilesCount: 0,
|
|
181
|
+
files: [
|
|
182
|
+
{
|
|
183
|
+
downloadPath: "index.ios.bundle.bsdiff",
|
|
184
|
+
order: 0,
|
|
185
|
+
path: "index.ios.bundle",
|
|
186
|
+
progress: 0.4,
|
|
187
|
+
status: "downloading",
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
totalFilesCount: 1,
|
|
191
|
+
},
|
|
192
|
+
progress: 0.32,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(store.getSnapshot().details?.files).toEqual([
|
|
196
|
+
{
|
|
197
|
+
downloadPath: "index.ios.bundle.bsdiff",
|
|
198
|
+
downloadedBytes: undefined,
|
|
199
|
+
order: 0,
|
|
200
|
+
path: "index.ios.bundle",
|
|
201
|
+
progress: 0.4,
|
|
202
|
+
status: "downloading",
|
|
203
|
+
totalBytes: undefined,
|
|
204
|
+
},
|
|
205
|
+
]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("stores diff snapshot transitions from downloading to downloaded", async () => {
|
|
209
|
+
const store = await importStore();
|
|
210
|
+
const emitProgress = listeners.get("onProgress");
|
|
211
|
+
|
|
212
|
+
emitProgress?.({
|
|
213
|
+
artifactType: "diff",
|
|
214
|
+
details: {
|
|
215
|
+
completedFilesCount: 0,
|
|
216
|
+
files: [
|
|
217
|
+
{
|
|
218
|
+
order: 0,
|
|
219
|
+
path: "index.ios.bundle",
|
|
220
|
+
progress: 0.6,
|
|
221
|
+
status: "downloading",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
order: 1,
|
|
225
|
+
path: "assets/logo.png",
|
|
226
|
+
progress: 0,
|
|
227
|
+
status: "pending",
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
totalFilesCount: 2,
|
|
231
|
+
},
|
|
232
|
+
progress: 0.4,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
emitProgress?.({
|
|
236
|
+
artifactType: "diff",
|
|
237
|
+
details: {
|
|
238
|
+
completedFilesCount: 1,
|
|
239
|
+
files: [
|
|
240
|
+
{
|
|
241
|
+
downloadPath: "index.ios.bundle",
|
|
242
|
+
order: 0,
|
|
243
|
+
path: "index.ios.bundle",
|
|
244
|
+
progress: 1,
|
|
245
|
+
status: "downloaded",
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
downloadPath: "assets/logo.png",
|
|
249
|
+
order: 1,
|
|
250
|
+
path: "assets/logo.png",
|
|
251
|
+
progress: 0,
|
|
252
|
+
status: "pending",
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
totalFilesCount: 2,
|
|
256
|
+
},
|
|
257
|
+
progress: 0.6,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(store.getSnapshot()).toEqual({
|
|
261
|
+
artifactType: "diff",
|
|
262
|
+
details: {
|
|
263
|
+
completedFilesCount: 1,
|
|
264
|
+
files: [
|
|
265
|
+
{
|
|
266
|
+
downloadPath: "index.ios.bundle",
|
|
267
|
+
order: 0,
|
|
268
|
+
path: "index.ios.bundle",
|
|
269
|
+
progress: 1,
|
|
270
|
+
status: "downloaded",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
downloadPath: "assets/logo.png",
|
|
274
|
+
order: 1,
|
|
275
|
+
path: "assets/logo.png",
|
|
276
|
+
progress: 0,
|
|
277
|
+
status: "pending",
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
totalFilesCount: 2,
|
|
281
|
+
},
|
|
282
|
+
downloadedBytes: undefined,
|
|
283
|
+
isUpdateDownloaded: false,
|
|
284
|
+
progress: 0.6,
|
|
285
|
+
totalBytes: undefined,
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("clears manifest metadata when archive progress events arrive", async () => {
|
|
290
|
+
const store = await importStore();
|
|
291
|
+
const emitProgress = listeners.get("onProgress");
|
|
292
|
+
|
|
293
|
+
emitProgress?.({
|
|
294
|
+
artifactType: "diff",
|
|
295
|
+
details: {
|
|
296
|
+
completedFilesCount: 2,
|
|
297
|
+
files: [
|
|
298
|
+
{
|
|
299
|
+
order: 0,
|
|
300
|
+
path: "index.ios.bundle",
|
|
301
|
+
progress: 1,
|
|
302
|
+
status: "downloaded",
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
order: 1,
|
|
306
|
+
path: "assets/logo.png",
|
|
307
|
+
progress: 1,
|
|
308
|
+
status: "downloaded",
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
order: 2,
|
|
312
|
+
path: "assets/bg.png",
|
|
313
|
+
progress: 0,
|
|
314
|
+
status: "pending",
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
totalFilesCount: 3,
|
|
318
|
+
},
|
|
319
|
+
progress: 0.9,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
emitProgress?.({
|
|
323
|
+
artifactType: "archive",
|
|
324
|
+
progress: 0.25,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(store.getSnapshot()).toEqual({
|
|
328
|
+
artifactType: "archive",
|
|
329
|
+
details: null,
|
|
330
|
+
downloadedBytes: undefined,
|
|
331
|
+
isUpdateDownloaded: false,
|
|
332
|
+
progress: 0.25,
|
|
333
|
+
totalBytes: undefined,
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("clears stale manifest metadata even when archive events omit optional fields", async () => {
|
|
338
|
+
const store = await importStore();
|
|
339
|
+
const emitProgress = listeners.get("onProgress");
|
|
340
|
+
|
|
341
|
+
emitProgress?.({
|
|
342
|
+
artifactType: "diff",
|
|
343
|
+
details: {
|
|
344
|
+
completedFilesCount: 0,
|
|
345
|
+
files: [
|
|
346
|
+
{
|
|
347
|
+
order: 0,
|
|
348
|
+
path: "manifest.asset.png",
|
|
349
|
+
progress: 0,
|
|
350
|
+
status: "failed",
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
totalFilesCount: 1,
|
|
354
|
+
},
|
|
355
|
+
progress: 0.2,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
emitProgress?.({
|
|
359
|
+
artifactType: "archive",
|
|
360
|
+
progress: 0.4,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
expect(store.getSnapshot()).toEqual({
|
|
364
|
+
artifactType: "archive",
|
|
365
|
+
details: null,
|
|
366
|
+
downloadedBytes: undefined,
|
|
367
|
+
isUpdateDownloaded: false,
|
|
368
|
+
progress: 0.4,
|
|
369
|
+
totalBytes: undefined,
|
|
370
|
+
});
|
|
371
|
+
});
|
|
57
372
|
});
|