@hot-updater/react-native 1.0.0-rc.14 → 1.0.0-rc.16
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/proguard-rules.pro +2 -2
- package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +650 -636
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +3 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +0 -13
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +10 -14
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +19 -0
- package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
- package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
- package/android/src/newarch/HotUpdater.kt +7 -9
- package/android/src/newarch/HotUpdaterModule.kt +40 -20
- package/android/src/oldarch/HotUpdater.kt +7 -9
- package/android/src/oldarch/HotUpdaterModule.kt +40 -20
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +1126 -74
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -5
- package/android/src/test/java/com/hotupdater/TarBrArchiveExtractorTest.kt +206 -0
- package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +773 -1016
- package/ios/HotUpdater/Internal/BundleMetadata.swift +5 -0
- package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +5 -27
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +47 -31
- package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
- package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +13 -72
- package/lib/commonjs/checkForUpdate.js +7 -10
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +9 -4
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/commonjs/error.js +4 -13
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/fetchJSON.js +9 -2
- package/lib/commonjs/fetchJSON.js.map +1 -1
- package/lib/commonjs/httpClient.js +151 -39
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/httpClient.spec.js +247 -5
- package/lib/commonjs/httpClient.spec.js.map +1 -1
- package/lib/commonjs/index.js +15 -8
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +101 -0
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +7 -8
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +21 -16
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +7 -0
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.js +4 -1
- package/lib/commonjs/notifyAppReadyInsights.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.spec.js +59 -0
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +55 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/store.js +3 -10
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/store.notification.spec.js +12 -4
- package/lib/commonjs/store.notification.spec.js.map +1 -1
- package/lib/commonjs/store.spec.js +2 -136
- package/lib/commonjs/store.spec.js.map +1 -1
- package/lib/commonjs/uuidv7.js +41 -0
- package/lib/commonjs/uuidv7.js.map +1 -0
- package/lib/commonjs/uuidv7.spec.js +30 -0
- package/lib/commonjs/uuidv7.spec.js.map +1 -0
- package/lib/commonjs/wrap.js +1 -3
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +7 -0
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/checkForUpdate.js +7 -10
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +9 -4
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/module/error.js +4 -13
- package/lib/module/error.js.map +1 -1
- package/lib/module/fetchJSON.js +7 -1
- package/lib/module/fetchJSON.js.map +1 -1
- package/lib/module/httpClient.js +153 -41
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/httpClient.spec.js +248 -6
- package/lib/module/httpClient.spec.js.map +1 -1
- package/lib/module/index.js +15 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +102 -1
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +7 -8
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +21 -16
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.boundary.spec.js +7 -0
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.js +5 -2
- package/lib/module/notifyAppReadyInsights.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.spec.js +59 -0
- package/lib/module/notifyAppReadyInsights.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js +55 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/store.js +3 -10
- package/lib/module/store.js.map +1 -1
- package/lib/module/store.notification.spec.js +12 -4
- package/lib/module/store.notification.spec.js.map +1 -1
- package/lib/module/store.spec.js +2 -136
- package/lib/module/store.spec.js.map +1 -1
- package/lib/module/uuidv7.js +36 -0
- package/lib/module/uuidv7.js.map +1 -0
- package/lib/module/uuidv7.spec.js +30 -0
- package/lib/module/uuidv7.spec.js.map +1 -0
- package/lib/module/wrap.js +1 -3
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +7 -0
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +1 -2
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/error.d.ts +4 -13
- package/lib/typescript/commonjs/error.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchJSON.d.ts +4 -0
- package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +6 -0
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +0 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +1 -6
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +1 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +8 -35
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts +1 -3
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/uuidv7.d.ts +10 -0
- package/lib/typescript/commonjs/uuidv7.d.ts.map +1 -0
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +7 -4
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +1 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/error.d.ts +4 -13
- package/lib/typescript/module/error.d.ts.map +1 -1
- package/lib/typescript/module/fetchJSON.d.ts +4 -0
- package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +6 -0
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +0 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +1 -6
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +1 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +8 -35
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts +1 -3
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/uuidv7.d.ts +10 -0
- package/lib/typescript/module/uuidv7.d.ts.map +1 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +7 -4
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/checkForUpdate.releaseCatalog.spec.ts +10 -4
- package/src/checkForUpdate.ts +5 -11
- package/src/error.ts +4 -14
- package/src/fetchJSON.ts +12 -1
- package/src/httpClient.spec.ts +289 -8
- package/src/httpClient.ts +224 -67
- package/src/index.spec.ts +70 -1
- package/src/index.ts +18 -7
- package/src/native.spec.ts +23 -16
- package/src/native.ts +12 -24
- package/src/notifyAppReadyInsights.boundary.spec.ts +8 -0
- package/src/notifyAppReadyInsights.spec.ts +60 -0
- package/src/notifyAppReadyInsights.transport.spec.ts +66 -0
- package/src/notifyAppReadyInsights.ts +13 -1
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +8 -36
- package/src/store.notification.spec.ts +13 -3
- package/src/store.spec.ts +0 -157
- package/src/store.ts +2 -23
- package/src/uuidv7.spec.ts +25 -0
- package/src/uuidv7.ts +38 -0
- package/src/wrap.spec.ts +8 -0
- package/src/wrap.tsx +0 -2
- package/src/wrap.types.ts +9 -4
- package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
- package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
- package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
- package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
- package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
- package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
- package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
- package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
- package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
|
@@ -2,48 +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
|
-
|
|
20
5
|
export interface UpdateBundleParams {
|
|
21
6
|
bundleId: string;
|
|
22
7
|
channel?: string;
|
|
23
|
-
fileUrl: string | null;
|
|
24
|
-
/**
|
|
25
|
-
* File hash for integrity/signature verification.
|
|
26
|
-
*
|
|
27
|
-
* Format depends on signing configuration:
|
|
28
|
-
* - Signed: `sig:<base64_signature>` - Native will verify signature (and implicitly hash)
|
|
29
|
-
* - Unsigned: `<hex_hash>` - Native will verify SHA256 hash only
|
|
30
|
-
*
|
|
31
|
-
* Native determines verification mode by checking for "sig:" prefix.
|
|
32
|
-
*/
|
|
33
|
-
fileHash: string | null;
|
|
34
8
|
/**
|
|
35
|
-
*
|
|
9
|
+
* Signed manifest URL for installation.
|
|
36
10
|
*/
|
|
37
|
-
manifestUrl
|
|
11
|
+
manifestUrl: string;
|
|
38
12
|
/**
|
|
39
13
|
* File hash/signature for the manifest file itself.
|
|
40
14
|
*/
|
|
41
|
-
manifestFileHash
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
changedAssets?: UnsafeObject | null;
|
|
15
|
+
manifestFileHash: string;
|
|
16
|
+
/** Optional tar.br URL; integrity and sizes come from the verified manifest. */
|
|
17
|
+
archiveUrl?: string | null;
|
|
18
|
+
/** Full protocol v1 target file descriptor map. */
|
|
19
|
+
assets: UnsafeObject;
|
|
47
20
|
/** Full Release Catalog selection receipt committed with the staged Bundle. */
|
|
48
21
|
selection?: UnsafeObject | null;
|
|
49
22
|
}
|
|
@@ -66,13 +39,12 @@ export interface Spec extends TurboModule {
|
|
|
66
39
|
*
|
|
67
40
|
* Parameter validation:
|
|
68
41
|
* - MISSING_BUNDLE_ID: Missing or empty bundleId
|
|
69
|
-
* - INVALID_FILE_URL: Invalid
|
|
42
|
+
* - INVALID_FILE_URL: Invalid manifest or asset URL provided
|
|
70
43
|
*
|
|
71
44
|
* Bundle storage:
|
|
72
45
|
* - DIRECTORY_CREATION_FAILED: Failed to create bundle directory
|
|
73
46
|
* - DOWNLOAD_FAILED: Failed to download bundle
|
|
74
47
|
* - INCOMPLETE_DOWNLOAD: Download incomplete (size mismatch)
|
|
75
|
-
* - EXTRACTION_FORMAT_ERROR: Invalid or corrupted archive format
|
|
76
48
|
* - INVALID_BUNDLE: Bundle missing required platform files
|
|
77
49
|
* - INSUFFICIENT_DISK_SPACE: Insufficient disk space
|
|
78
50
|
* - MOVE_OPERATION_FAILED: Failed to move bundle files
|
|
@@ -61,10 +61,20 @@ describe("hotUpdaterStore notification scheduling", () => {
|
|
|
61
61
|
|
|
62
62
|
const nextProgress = Math.min(1, progress + 0.01);
|
|
63
63
|
emitProgress?.({
|
|
64
|
-
artifactType: "
|
|
65
|
-
|
|
64
|
+
artifactType: "diff",
|
|
65
|
+
details: {
|
|
66
|
+
completedFilesCount: nextProgress === 1 ? 1 : 0,
|
|
67
|
+
files: [
|
|
68
|
+
{
|
|
69
|
+
order: 0,
|
|
70
|
+
path: "index.ios.bundle",
|
|
71
|
+
progress: nextProgress,
|
|
72
|
+
status: nextProgress === 1 ? "downloaded" : "downloading",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
totalFilesCount: 1,
|
|
76
|
+
},
|
|
66
77
|
progress: nextProgress,
|
|
67
|
-
totalBytes: 1_000_000,
|
|
68
78
|
});
|
|
69
79
|
}, [progress]);
|
|
70
80
|
|
package/src/store.spec.ts
CHANGED
|
@@ -41,75 +41,6 @@ describe("hotUpdaterStore", () => {
|
|
|
41
41
|
unsubscribe();
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
it("notifies subscribers only when progress events change the snapshot", async () => {
|
|
45
|
-
const store = await importStore();
|
|
46
|
-
const listener = vi.fn();
|
|
47
|
-
const emitProgress = listeners.get("onProgress");
|
|
48
|
-
|
|
49
|
-
expect(emitProgress).toBeTypeOf("function");
|
|
50
|
-
|
|
51
|
-
const unsubscribe = store.subscribe(listener);
|
|
52
|
-
|
|
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
|
-
});
|
|
65
|
-
store.setState({ isUpdateDownloaded: true });
|
|
66
|
-
|
|
67
|
-
expect(listener).toHaveBeenCalledTimes(2);
|
|
68
|
-
expect(store.getSnapshot()).toEqual({
|
|
69
|
-
artifactType: "archive",
|
|
70
|
-
details: null,
|
|
71
|
-
downloadedBytes: undefined,
|
|
72
|
-
isUpdateDownloaded: true,
|
|
73
|
-
progress: 1,
|
|
74
|
-
totalBytes: undefined,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
unsubscribe();
|
|
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
44
|
it("stores manifest progress metadata while keeping overall progress", async () => {
|
|
114
45
|
const store = await importStore();
|
|
115
46
|
const emitProgress = listeners.get("onProgress");
|
|
@@ -163,10 +94,8 @@ describe("hotUpdaterStore", () => {
|
|
|
163
94
|
],
|
|
164
95
|
totalFilesCount: 2,
|
|
165
96
|
},
|
|
166
|
-
downloadedBytes: undefined,
|
|
167
97
|
isUpdateDownloaded: false,
|
|
168
98
|
progress: 0.42,
|
|
169
|
-
totalBytes: undefined,
|
|
170
99
|
});
|
|
171
100
|
});
|
|
172
101
|
|
|
@@ -279,94 +208,8 @@ describe("hotUpdaterStore", () => {
|
|
|
279
208
|
],
|
|
280
209
|
totalFilesCount: 2,
|
|
281
210
|
},
|
|
282
|
-
downloadedBytes: undefined,
|
|
283
211
|
isUpdateDownloaded: false,
|
|
284
212
|
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
213
|
});
|
|
371
214
|
});
|
|
372
215
|
});
|
package/src/store.ts
CHANGED
|
@@ -9,10 +9,8 @@ import { addListener } from "./native";
|
|
|
9
9
|
|
|
10
10
|
export type HotUpdaterState = {
|
|
11
11
|
progress: number;
|
|
12
|
-
downloadedBytes: number | undefined;
|
|
13
|
-
totalBytes: number | undefined;
|
|
14
12
|
isUpdateDownloaded: boolean;
|
|
15
|
-
artifactType: "
|
|
13
|
+
artifactType: "diff" | null;
|
|
16
14
|
details: HotUpdaterDiffProgressDetails | null;
|
|
17
15
|
};
|
|
18
16
|
|
|
@@ -56,8 +54,6 @@ const areDiffDetailsEqual = (
|
|
|
56
54
|
const areStatesEqual = (left: HotUpdaterState, right: HotUpdaterState) => {
|
|
57
55
|
return (
|
|
58
56
|
left.progress === right.progress &&
|
|
59
|
-
left.downloadedBytes === right.downloadedBytes &&
|
|
60
|
-
left.totalBytes === right.totalBytes &&
|
|
61
57
|
left.isUpdateDownloaded === right.isUpdateDownloaded &&
|
|
62
58
|
left.artifactType === right.artifactType &&
|
|
63
59
|
areDiffDetailsEqual(left.details, right.details)
|
|
@@ -78,8 +74,6 @@ const normalizeDownloadPath = (value: string | undefined) => {
|
|
|
78
74
|
const createHotUpdaterStore = () => {
|
|
79
75
|
let state: HotUpdaterState = {
|
|
80
76
|
progress: 0,
|
|
81
|
-
downloadedBytes: undefined,
|
|
82
|
-
totalBytes: undefined,
|
|
83
77
|
isUpdateDownloaded: false,
|
|
84
78
|
artifactType: null,
|
|
85
79
|
details: null,
|
|
@@ -165,10 +159,6 @@ const createHotUpdaterStore = () => {
|
|
|
165
159
|
nextState.details = state.details;
|
|
166
160
|
}
|
|
167
161
|
|
|
168
|
-
if (nextState.artifactType !== "diff") {
|
|
169
|
-
nextState.details = null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
162
|
if (areStatesEqual(state, nextState)) {
|
|
173
163
|
return;
|
|
174
164
|
}
|
|
@@ -180,19 +170,8 @@ const createHotUpdaterStore = () => {
|
|
|
180
170
|
const applyProgressEvent = (event: HotUpdaterProgressEvent) => {
|
|
181
171
|
setState({
|
|
182
172
|
artifactType: event.artifactType,
|
|
183
|
-
details:
|
|
184
|
-
event.artifactType === "diff"
|
|
185
|
-
? normalizeDiffDetails(event.details)
|
|
186
|
-
: null,
|
|
187
|
-
downloadedBytes:
|
|
188
|
-
event.artifactType === "archive"
|
|
189
|
-
? normalizeByteCount(event.downloadedBytes)
|
|
190
|
-
: undefined,
|
|
173
|
+
details: normalizeDiffDetails(event.details),
|
|
191
174
|
progress: event.progress,
|
|
192
|
-
totalBytes:
|
|
193
|
-
event.artifactType === "archive"
|
|
194
|
-
? normalizeByteCount(event.totalBytes)
|
|
195
|
-
: undefined,
|
|
196
175
|
});
|
|
197
176
|
};
|
|
198
177
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isUUIDv7 } from "@hot-updater/core";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { createUUIDv7 } from "./uuidv7";
|
|
5
|
+
|
|
6
|
+
describe("createUUIDv7", () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.useRealTimers();
|
|
9
|
+
vi.unstubAllGlobals();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it.each([
|
|
13
|
+
{ label: "Web Crypto", crypto: globalThis.crypto },
|
|
14
|
+
{ label: "no Web Crypto, as in React Native", crypto: undefined },
|
|
15
|
+
])("leads with the 48-bit millisecond time using $label", ({ crypto }) => {
|
|
16
|
+
vi.useFakeTimers({ now: 0x0192_3456_789a });
|
|
17
|
+
vi.stubGlobal("crypto", crypto);
|
|
18
|
+
|
|
19
|
+
const id = createUUIDv7();
|
|
20
|
+
|
|
21
|
+
expect(isUUIDv7(id)).toBe(true);
|
|
22
|
+
expect(id.startsWith("01923456-789a-7")).toBe(true);
|
|
23
|
+
expect(createUUIDv7()).not.toBe(id);
|
|
24
|
+
});
|
|
25
|
+
});
|
package/src/uuidv7.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type RandomValuesSource = {
|
|
2
|
+
readonly getRandomValues?: (array: Uint8Array) => Uint8Array;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates an RFC 9562 UUIDv7: a 48-bit millisecond timestamp, then random bits.
|
|
7
|
+
*
|
|
8
|
+
* `createUUIDv7` from `@hot-updater/plugin-core` calls `crypto.getRandomValues`
|
|
9
|
+
* unconditionally, but React Native has no Web Crypto unless the app installs a
|
|
10
|
+
* polyfill. An Insights event ID only has to tell one event from another so a
|
|
11
|
+
* retried POST is counted once, so `Math.random` is enough without one.
|
|
12
|
+
*/
|
|
13
|
+
export const createUUIDv7 = (): string => {
|
|
14
|
+
const bytes = new Uint8Array(16);
|
|
15
|
+
const { crypto } = globalThis as { crypto?: RandomValuesSource };
|
|
16
|
+
if (typeof crypto?.getRandomValues === "function") {
|
|
17
|
+
crypto.getRandomValues(bytes);
|
|
18
|
+
} else {
|
|
19
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
20
|
+
bytes[index] = Math.floor(Math.random() * 256);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Division, not bit shifts: shifts truncate to 32 bits, and a millisecond
|
|
25
|
+
// timestamp already needs 41.
|
|
26
|
+
let timestamp = Date.now();
|
|
27
|
+
for (let index = 5; index >= 0; index -= 1) {
|
|
28
|
+
bytes[index] = timestamp % 256;
|
|
29
|
+
timestamp = Math.floor(timestamp / 256);
|
|
30
|
+
}
|
|
31
|
+
bytes[6] = 0x70 | (bytes[6]! & 0x0f);
|
|
32
|
+
bytes[8] = 0x80 | (bytes[8]! & 0x3f);
|
|
33
|
+
|
|
34
|
+
const hex = Array.from(bytes, (byte) =>
|
|
35
|
+
byte.toString(16).padStart(2, "0"),
|
|
36
|
+
).join("");
|
|
37
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
38
|
+
};
|
package/src/wrap.spec.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { createElement, type ComponentType } from "react";
|
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
7
|
import type {
|
|
8
|
+
ActiveUpdateState,
|
|
8
9
|
NotifyAppReadyInsightsEvent,
|
|
9
10
|
NotifyAppReadyResult,
|
|
10
11
|
} from "./native";
|
|
@@ -37,6 +38,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
37
38
|
getBundleId: vi.fn(() => "bundle-id"),
|
|
38
39
|
getUpdateId: vi.fn(() => "release-id"),
|
|
39
40
|
getChannel: vi.fn(() => "production"),
|
|
41
|
+
getActiveUpdateState: vi.fn<() => ActiveUpdateState>(),
|
|
40
42
|
getCohort: vi.fn(() => "123"),
|
|
41
43
|
getFingerprintHash: vi.fn(() => "fingerprint-hash"),
|
|
42
44
|
getInstallId: vi.fn(() => "install-id"),
|
|
@@ -61,6 +63,7 @@ vi.mock("./native", () => ({
|
|
|
61
63
|
getBundleId: mocks.getBundleId,
|
|
62
64
|
getUpdateId: mocks.getUpdateId,
|
|
63
65
|
getChannel: mocks.getChannel,
|
|
66
|
+
getActiveUpdateState: mocks.getActiveUpdateState,
|
|
64
67
|
getCohort: mocks.getCohort,
|
|
65
68
|
getFingerprintHash: mocks.getFingerprintHash,
|
|
66
69
|
getInstallId: mocks.getInstallId,
|
|
@@ -99,6 +102,11 @@ describe("HotUpdater wrap initialization", () => {
|
|
|
99
102
|
mocks.getBundleId.mockReturnValue("bundle-id");
|
|
100
103
|
mocks.getUpdateId.mockReturnValue("release-id");
|
|
101
104
|
mocks.getChannel.mockReturnValue("production");
|
|
105
|
+
mocks.getActiveUpdateState.mockReturnValue({
|
|
106
|
+
activeSelection: null,
|
|
107
|
+
stableSelection: null,
|
|
108
|
+
verificationPending: false,
|
|
109
|
+
});
|
|
102
110
|
mocks.getCohort.mockReturnValue("123");
|
|
103
111
|
mocks.getFingerprintHash.mockReturnValue("fingerprint-hash");
|
|
104
112
|
mocks.getInstallId.mockReturnValue("install-id");
|
package/src/wrap.tsx
CHANGED
|
@@ -136,11 +136,9 @@ export function wrap(
|
|
|
136
136
|
<Fallback
|
|
137
137
|
artifactType={progressState.artifactType}
|
|
138
138
|
details={progressState.details}
|
|
139
|
-
downloadedBytes={progressState.downloadedBytes}
|
|
140
139
|
progress={progress}
|
|
141
140
|
status={updateStatus}
|
|
142
141
|
message={message}
|
|
143
|
-
totalBytes={progressState.totalBytes}
|
|
144
142
|
/>
|
|
145
143
|
);
|
|
146
144
|
}
|
package/src/wrap.types.ts
CHANGED
|
@@ -21,8 +21,6 @@ export type UpdateStatus =
|
|
|
21
21
|
export type HotUpdaterFallbackComponentProps = {
|
|
22
22
|
status: Exclude<UpdateStatus, "UPDATE_PROCESS_COMPLETED">;
|
|
23
23
|
progress: number;
|
|
24
|
-
downloadedBytes: HotUpdaterState["downloadedBytes"];
|
|
25
|
-
totalBytes: HotUpdaterState["totalBytes"];
|
|
26
24
|
message: string | null;
|
|
27
25
|
artifactType: HotUpdaterState["artifactType"];
|
|
28
26
|
details: HotUpdaterState["details"];
|
|
@@ -32,9 +30,15 @@ interface CommonHotUpdaterOptions {
|
|
|
32
30
|
/**
|
|
33
31
|
* Sends app-ready transitions and same-Bundle Release adoptions to the
|
|
34
32
|
* configured server. Defaults to `true`; set to `false` to disable client
|
|
35
|
-
* reporting.
|
|
33
|
+
* reporting. Debug builds (`__DEV__`) send nothing unless you pass
|
|
34
|
+
* `{ debug: true }`, so development sessions stay out of production data.
|
|
36
35
|
*/
|
|
37
|
-
insights?:
|
|
36
|
+
insights?:
|
|
37
|
+
| boolean
|
|
38
|
+
| {
|
|
39
|
+
/** Also report from debug builds. Defaults to `false`. */
|
|
40
|
+
debug?: boolean;
|
|
41
|
+
};
|
|
38
42
|
/** Base URL of a server exposing the Hot Updater v1 client HTTP protocol. */
|
|
39
43
|
baseURL: HotUpdaterBaseURL;
|
|
40
44
|
requestHeaders?: Record<string, string>;
|
|
@@ -56,6 +60,7 @@ export type HotUpdaterInitOptions = CommonHotUpdaterOptions;
|
|
|
56
60
|
export type HotUpdaterOptions = AutoUpdateOptions;
|
|
57
61
|
|
|
58
62
|
type InternalCommonOptions = {
|
|
63
|
+
/** Whether this runtime reports, with the debug-build default applied. */
|
|
59
64
|
insights?: boolean;
|
|
60
65
|
client: HotUpdaterHttpClient;
|
|
61
66
|
requestHeaders?: Record<string, string>;
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
package com.hotupdater
|
|
2
|
-
|
|
3
|
-
import android.util.Log
|
|
4
|
-
import java.io.File
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Unified decompression service that uses Strategy pattern to handle multiple compression formats.
|
|
8
|
-
* Automatically detects format by trying each strategy's validation and delegates to appropriate decompression strategy.
|
|
9
|
-
*/
|
|
10
|
-
class DecompressService {
|
|
11
|
-
companion object {
|
|
12
|
-
private const val TAG = "DecompressService"
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Strategies with reliable file signatures that can be validated cheaply.
|
|
16
|
-
// TAR.BR is attempted only as the final fallback because Brotli has no reliable magic bytes.
|
|
17
|
-
private val signatureStrategies =
|
|
18
|
-
listOf(
|
|
19
|
-
ZipDecompressionStrategy(),
|
|
20
|
-
TarGzDecompressionStrategy(),
|
|
21
|
-
)
|
|
22
|
-
private val tarBrStrategy = TarBrDecompressionStrategy()
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Extracts a compressed file to the destination directory.
|
|
26
|
-
* Automatically detects compression format by trying each strategy's validation.
|
|
27
|
-
* @param filePath Path to the compressed file
|
|
28
|
-
* @param destinationPath Path to the destination directory
|
|
29
|
-
* @param progressCallback Callback for progress updates (0.0 - 1.0)
|
|
30
|
-
* @return true if extraction was successful, false otherwise
|
|
31
|
-
*/
|
|
32
|
-
fun extractZipFile(
|
|
33
|
-
filePath: String,
|
|
34
|
-
destinationPath: String,
|
|
35
|
-
progressCallback: (Double) -> Unit,
|
|
36
|
-
): Boolean {
|
|
37
|
-
// Collect file information for better error messages
|
|
38
|
-
val file = File(filePath)
|
|
39
|
-
val fileName = file.name
|
|
40
|
-
val fileSize = if (file.exists()) file.length() else 0L
|
|
41
|
-
|
|
42
|
-
// Try each signature-based strategy first.
|
|
43
|
-
for (strategy in signatureStrategies) {
|
|
44
|
-
if (strategy.isValid(filePath)) {
|
|
45
|
-
Log.d(TAG, "Using strategy for $fileName")
|
|
46
|
-
return strategy.decompress(filePath, destinationPath, progressCallback)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
Log.d(TAG, "No ZIP/TAR.GZ signature matched for $fileName, trying TAR.BR fallback")
|
|
51
|
-
|
|
52
|
-
if (tarBrStrategy.decompress(filePath, destinationPath, progressCallback)) {
|
|
53
|
-
Log.d(TAG, "Using TAR.BR fallback for $fileName")
|
|
54
|
-
return true
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
val errorMessage = createInvalidArchiveMessage(fileName, fileSize)
|
|
58
|
-
Log.e(TAG, errorMessage)
|
|
59
|
-
return false
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Validates if a file matches one of the signature-based archive formats.
|
|
64
|
-
* @param filePath Path to the file to validate
|
|
65
|
-
* @return true if the file is a valid compressed archive
|
|
66
|
-
*/
|
|
67
|
-
fun isValidZipFile(filePath: String): Boolean {
|
|
68
|
-
for (strategy in signatureStrategies) {
|
|
69
|
-
if (strategy.isValid(filePath)) {
|
|
70
|
-
return true
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
Log.d(TAG, "No ZIP/TAR.GZ signature matched for file: $filePath. TAR.BR is handled during extraction fallback.")
|
|
74
|
-
return false
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private fun createInvalidArchiveMessage(
|
|
78
|
-
fileName: String,
|
|
79
|
-
fileSize: Long,
|
|
80
|
-
): String =
|
|
81
|
-
"""
|
|
82
|
-
The downloaded bundle file is not a valid compressed archive: $fileName ($fileSize bytes)
|
|
83
|
-
|
|
84
|
-
Supported formats:
|
|
85
|
-
- ZIP archives (.zip)
|
|
86
|
-
- GZIP compressed TAR archives (.tar.gz)
|
|
87
|
-
- Brotli compressed TAR archives (.tar.br)
|
|
88
|
-
""".trimIndent()
|
|
89
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
package com.hotupdater
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Interface for decompression strategies
|
|
5
|
-
*/
|
|
6
|
-
interface DecompressionStrategy {
|
|
7
|
-
/**
|
|
8
|
-
* Validates if a file can be decompressed by this strategy
|
|
9
|
-
* @param filePath Path to the file to validate
|
|
10
|
-
* @return true if the file is valid for this strategy
|
|
11
|
-
*/
|
|
12
|
-
fun isValid(filePath: String): Boolean
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Decompresses a file to the destination directory
|
|
16
|
-
* @param filePath Path to the compressed file
|
|
17
|
-
* @param destinationPath Path to the destination directory
|
|
18
|
-
* @param progressCallback Callback for progress updates (0.0 - 1.0)
|
|
19
|
-
* @return true if decompression was successful, false otherwise
|
|
20
|
-
*/
|
|
21
|
-
fun decompress(
|
|
22
|
-
filePath: String,
|
|
23
|
-
destinationPath: String,
|
|
24
|
-
progressCallback: (Double) -> Unit,
|
|
25
|
-
): Boolean
|
|
26
|
-
}
|