@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.
Files changed (202) hide show
  1. package/android/proguard-rules.pro +2 -2
  2. package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
  3. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +650 -636
  4. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +3 -0
  5. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +0 -13
  6. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +10 -14
  7. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +19 -0
  8. package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
  9. package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
  10. package/android/src/newarch/HotUpdater.kt +7 -9
  11. package/android/src/newarch/HotUpdaterModule.kt +40 -20
  12. package/android/src/oldarch/HotUpdater.kt +7 -9
  13. package/android/src/oldarch/HotUpdaterModule.kt +40 -20
  14. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +1126 -74
  15. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -5
  16. package/android/src/test/java/com/hotupdater/TarBrArchiveExtractorTest.kt +206 -0
  17. package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
  18. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
  19. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +773 -1016
  20. package/ios/HotUpdater/Internal/BundleMetadata.swift +5 -0
  21. package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
  22. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
  23. package/ios/HotUpdater/Internal/HotUpdater.mm +5 -27
  24. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +47 -31
  25. package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
  26. package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
  27. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +13 -72
  28. package/lib/commonjs/checkForUpdate.js +7 -10
  29. package/lib/commonjs/checkForUpdate.js.map +1 -1
  30. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +9 -4
  31. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  32. package/lib/commonjs/error.js +4 -13
  33. package/lib/commonjs/error.js.map +1 -1
  34. package/lib/commonjs/fetchJSON.js +9 -2
  35. package/lib/commonjs/fetchJSON.js.map +1 -1
  36. package/lib/commonjs/httpClient.js +151 -39
  37. package/lib/commonjs/httpClient.js.map +1 -1
  38. package/lib/commonjs/httpClient.spec.js +247 -5
  39. package/lib/commonjs/httpClient.spec.js.map +1 -1
  40. package/lib/commonjs/index.js +15 -8
  41. package/lib/commonjs/index.js.map +1 -1
  42. package/lib/commonjs/index.spec.js +101 -0
  43. package/lib/commonjs/index.spec.js.map +1 -1
  44. package/lib/commonjs/native.js +7 -8
  45. package/lib/commonjs/native.js.map +1 -1
  46. package/lib/commonjs/native.spec.js +21 -16
  47. package/lib/commonjs/native.spec.js.map +1 -1
  48. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +7 -0
  49. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +1 -1
  50. package/lib/commonjs/notifyAppReadyInsights.js +4 -1
  51. package/lib/commonjs/notifyAppReadyInsights.js.map +1 -1
  52. package/lib/commonjs/notifyAppReadyInsights.spec.js +59 -0
  53. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +1 -1
  54. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +55 -1
  55. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +1 -1
  56. package/lib/commonjs/sdkVersion.js +1 -1
  57. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  58. package/lib/commonjs/store.js +3 -10
  59. package/lib/commonjs/store.js.map +1 -1
  60. package/lib/commonjs/store.notification.spec.js +12 -4
  61. package/lib/commonjs/store.notification.spec.js.map +1 -1
  62. package/lib/commonjs/store.spec.js +2 -136
  63. package/lib/commonjs/store.spec.js.map +1 -1
  64. package/lib/commonjs/uuidv7.js +41 -0
  65. package/lib/commonjs/uuidv7.js.map +1 -0
  66. package/lib/commonjs/uuidv7.spec.js +30 -0
  67. package/lib/commonjs/uuidv7.spec.js.map +1 -0
  68. package/lib/commonjs/wrap.js +1 -3
  69. package/lib/commonjs/wrap.js.map +1 -1
  70. package/lib/commonjs/wrap.spec.js +7 -0
  71. package/lib/commonjs/wrap.spec.js.map +1 -1
  72. package/lib/module/checkForUpdate.js +7 -10
  73. package/lib/module/checkForUpdate.js.map +1 -1
  74. package/lib/module/checkForUpdate.releaseCatalog.spec.js +9 -4
  75. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  76. package/lib/module/error.js +4 -13
  77. package/lib/module/error.js.map +1 -1
  78. package/lib/module/fetchJSON.js +7 -1
  79. package/lib/module/fetchJSON.js.map +1 -1
  80. package/lib/module/httpClient.js +153 -41
  81. package/lib/module/httpClient.js.map +1 -1
  82. package/lib/module/httpClient.spec.js +248 -6
  83. package/lib/module/httpClient.spec.js.map +1 -1
  84. package/lib/module/index.js +15 -8
  85. package/lib/module/index.js.map +1 -1
  86. package/lib/module/index.spec.js +102 -1
  87. package/lib/module/index.spec.js.map +1 -1
  88. package/lib/module/native.js +7 -8
  89. package/lib/module/native.js.map +1 -1
  90. package/lib/module/native.spec.js +21 -16
  91. package/lib/module/native.spec.js.map +1 -1
  92. package/lib/module/notifyAppReadyInsights.boundary.spec.js +7 -0
  93. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +1 -1
  94. package/lib/module/notifyAppReadyInsights.js +5 -2
  95. package/lib/module/notifyAppReadyInsights.js.map +1 -1
  96. package/lib/module/notifyAppReadyInsights.spec.js +59 -0
  97. package/lib/module/notifyAppReadyInsights.spec.js.map +1 -1
  98. package/lib/module/notifyAppReadyInsights.transport.spec.js +55 -1
  99. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +1 -1
  100. package/lib/module/sdkVersion.js +1 -1
  101. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  102. package/lib/module/store.js +3 -10
  103. package/lib/module/store.js.map +1 -1
  104. package/lib/module/store.notification.spec.js +12 -4
  105. package/lib/module/store.notification.spec.js.map +1 -1
  106. package/lib/module/store.spec.js +2 -136
  107. package/lib/module/store.spec.js.map +1 -1
  108. package/lib/module/uuidv7.js +36 -0
  109. package/lib/module/uuidv7.js.map +1 -0
  110. package/lib/module/uuidv7.spec.js +30 -0
  111. package/lib/module/uuidv7.spec.js.map +1 -0
  112. package/lib/module/wrap.js +1 -3
  113. package/lib/module/wrap.js.map +1 -1
  114. package/lib/module/wrap.spec.js +7 -0
  115. package/lib/module/wrap.spec.js.map +1 -1
  116. package/lib/typescript/commonjs/checkForUpdate.d.ts +1 -2
  117. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  118. package/lib/typescript/commonjs/error.d.ts +4 -13
  119. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/fetchJSON.d.ts +4 -0
  121. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/httpClient.d.ts +6 -0
  123. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/index.d.ts +0 -2
  125. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/native.d.ts +1 -6
  127. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  128. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +1 -1
  129. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  131. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +8 -35
  132. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/store.d.ts +1 -3
  134. package/lib/typescript/commonjs/store.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/uuidv7.d.ts +10 -0
  136. package/lib/typescript/commonjs/uuidv7.d.ts.map +1 -0
  137. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/wrap.types.d.ts +7 -4
  139. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  140. package/lib/typescript/module/checkForUpdate.d.ts +1 -2
  141. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  142. package/lib/typescript/module/error.d.ts +4 -13
  143. package/lib/typescript/module/error.d.ts.map +1 -1
  144. package/lib/typescript/module/fetchJSON.d.ts +4 -0
  145. package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
  146. package/lib/typescript/module/httpClient.d.ts +6 -0
  147. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  148. package/lib/typescript/module/index.d.ts +0 -2
  149. package/lib/typescript/module/index.d.ts.map +1 -1
  150. package/lib/typescript/module/native.d.ts +1 -6
  151. package/lib/typescript/module/native.d.ts.map +1 -1
  152. package/lib/typescript/module/notifyAppReadyInsights.d.ts +1 -1
  153. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +1 -1
  154. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  155. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +8 -35
  156. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  157. package/lib/typescript/module/store.d.ts +1 -3
  158. package/lib/typescript/module/store.d.ts.map +1 -1
  159. package/lib/typescript/module/uuidv7.d.ts +10 -0
  160. package/lib/typescript/module/uuidv7.d.ts.map +1 -0
  161. package/lib/typescript/module/wrap.d.ts.map +1 -1
  162. package/lib/typescript/module/wrap.types.d.ts +7 -4
  163. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  164. package/package.json +3 -3
  165. package/src/checkForUpdate.releaseCatalog.spec.ts +10 -4
  166. package/src/checkForUpdate.ts +5 -11
  167. package/src/error.ts +4 -14
  168. package/src/fetchJSON.ts +12 -1
  169. package/src/httpClient.spec.ts +289 -8
  170. package/src/httpClient.ts +224 -67
  171. package/src/index.spec.ts +70 -1
  172. package/src/index.ts +18 -7
  173. package/src/native.spec.ts +23 -16
  174. package/src/native.ts +12 -24
  175. package/src/notifyAppReadyInsights.boundary.spec.ts +8 -0
  176. package/src/notifyAppReadyInsights.spec.ts +60 -0
  177. package/src/notifyAppReadyInsights.transport.spec.ts +66 -0
  178. package/src/notifyAppReadyInsights.ts +13 -1
  179. package/src/sdkVersion.ts +1 -1
  180. package/src/specs/NativeHotUpdater.ts +8 -36
  181. package/src/store.notification.spec.ts +13 -3
  182. package/src/store.spec.ts +0 -157
  183. package/src/store.ts +2 -23
  184. package/src/uuidv7.spec.ts +25 -0
  185. package/src/uuidv7.ts +38 -0
  186. package/src/wrap.spec.ts +8 -0
  187. package/src/wrap.tsx +0 -2
  188. package/src/wrap.types.ts +9 -4
  189. package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
  190. package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
  191. package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
  192. package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
  193. package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
  194. package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
  195. package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
  196. package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
  197. package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
  198. package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
  199. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
  200. package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
  201. package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
  202. 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
- * Optional signed manifest URL for manifest-driven installation.
9
+ * Signed manifest URL for installation.
36
10
  */
37
- manifestUrl?: string | null;
11
+ manifestUrl: string;
38
12
  /**
39
13
  * File hash/signature for the manifest file itself.
40
14
  */
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;
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 fileUrl provided
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: "archive",
65
- downloadedBytes: nextProgress * 1_000_000,
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: "archive" | "diff" | null;
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?: boolean;
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
- }