@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
package/src/index.spec.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import type { HotUpdaterInitOptions, HotUpdaterOptions } from "./wrap";
4
4
 
@@ -31,6 +31,7 @@ const mocks = vi.hoisted(() => {
31
31
  getInstallId: vi.fn(() => "install-id"),
32
32
  getManifest: vi.fn(() => null),
33
33
  getMinBundleId: vi.fn(() => "min-bundle-id"),
34
+ getPersistedUserIdentity: vi.fn(() => ({})),
34
35
  init: vi.fn(),
35
36
  isChannelSwitched: vi.fn(() => false),
36
37
  notifyAppReady: vi.fn(() => ({ status: "UNCHANGED" as const })),
@@ -71,6 +72,7 @@ vi.mock("./native", () => ({
71
72
  getInstallId: mocks.getInstallId,
72
73
  getManifest: mocks.getManifest,
73
74
  getMinBundleId: mocks.getMinBundleId,
75
+ getPersistedUserIdentity: mocks.getPersistedUserIdentity,
74
76
  isChannelSwitched: mocks.isChannelSwitched,
75
77
  notifyAppReady: mocks.notifyAppReady,
76
78
  reload: mocks.reload,
@@ -99,6 +101,11 @@ describe("HotUpdater client initialization", () => {
99
101
  mocks.wrap.mockImplementation((Component: unknown) => Component);
100
102
  });
101
103
 
104
+ afterEach(() => {
105
+ vi.unstubAllGlobals();
106
+ vi.restoreAllMocks();
107
+ });
108
+
102
109
  it("exposes the console ID through the existing getter only", async () => {
103
110
  const HotUpdater = await importHotUpdater();
104
111
 
@@ -178,6 +185,68 @@ describe("HotUpdater client initialization", () => {
178
185
  );
179
186
  });
180
187
 
188
+ it.each([
189
+ { dev: true, insights: undefined, expected: false },
190
+ { dev: true, insights: true, expected: false },
191
+ { dev: true, insights: false, expected: false },
192
+ { dev: true, insights: { debug: true }, expected: true },
193
+ { dev: false, insights: { debug: true }, expected: true },
194
+ { dev: false, insights: { debug: false }, expected: true },
195
+ { dev: false, insights: false, expected: false },
196
+ ] as const)(
197
+ "resolves insights $insights to $expected when __DEV__ is $dev",
198
+ async ({ dev, insights, expected }) => {
199
+ vi.stubGlobal("__DEV__", dev);
200
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
201
+ const HotUpdater = await importHotUpdater();
202
+
203
+ HotUpdater.init({ baseURL: "https://updates.example.com", insights });
204
+ HotUpdater.wrap({
205
+ baseURL: "https://updates.example.com",
206
+ insights,
207
+ updateStrategy: "appVersion",
208
+ });
209
+
210
+ expect(mocks.init).toHaveBeenCalledWith(
211
+ expect.objectContaining({ insights: expected }),
212
+ );
213
+ expect(mocks.wrap).toHaveBeenCalledWith(
214
+ expect.objectContaining({ insights: expected }),
215
+ );
216
+ },
217
+ );
218
+
219
+ it.each([
220
+ { dev: true, reports: false },
221
+ { dev: false, reports: true },
222
+ ])(
223
+ "gates manual checks and downloads on the build type (__DEV__: $dev)",
224
+ async ({ dev, reports }) => {
225
+ vi.stubGlobal("__DEV__", dev);
226
+ const sendInsightsEvent = vi.fn(async () => undefined);
227
+ mocks.createHttpClient.mockReturnValue({
228
+ createSession: vi.fn(async () => ({ sendInsightsEvent })),
229
+ } as never);
230
+ mocks.updateBundle.mockResolvedValueOnce(true);
231
+ const HotUpdater = await importHotUpdater();
232
+ HotUpdater.init({ baseURL: "https://updates.example.com" });
233
+
234
+ await HotUpdater.checkForUpdate({ updateStrategy: "appVersion" });
235
+ await HotUpdater.updateBundle({
236
+ assets: {},
237
+ bundleId: "next-bundle-id",
238
+ manifestFileHash: "manifest-hash",
239
+ manifestUrl: "https://updates.example.com/manifest.json",
240
+ status: "UPDATE",
241
+ });
242
+
243
+ expect(mocks.checkForUpdate).toHaveBeenCalledWith(
244
+ expect.objectContaining({ insights: reports }),
245
+ );
246
+ expect(sendInsightsEvent).toHaveBeenCalledTimes(reports ? 1 : 0);
247
+ },
248
+ );
249
+
181
250
  it("requires baseURL and rejects the removed resolver shape", async () => {
182
251
  const HotUpdater = await importHotUpdater();
183
252
 
package/src/index.ts CHANGED
@@ -93,6 +93,21 @@ type HotUpdaterWrap = {
93
93
  (options: HotUpdaterOptions): ReturnType<typeof wrap>;
94
94
  };
95
95
 
96
+ /**
97
+ * Debug builds report only with `insights: { debug: true }`, so development
98
+ * sessions do not mix into production Insights. `__DEV__` is defined only in
99
+ * a React Native bundle; anywhere else, such as unit tests, counts as release.
100
+ */
101
+ const resolveInsights = (
102
+ insights: HotUpdaterInitOptions["insights"],
103
+ ): boolean => {
104
+ if (insights === false) return false;
105
+ const isDebugBuild = typeof __DEV__ !== "undefined" && __DEV__;
106
+ return (
107
+ !isDebugBuild || (typeof insights === "object" && insights?.debug === true)
108
+ );
109
+ };
110
+
96
111
  /**
97
112
  * Creates a HotUpdater client instance with all update management methods.
98
113
  * This function is called once on module initialization to create a singleton instance.
@@ -161,7 +176,7 @@ function createHotUpdaterClient() {
161
176
  const { baseURL, ...rest } = autoOptions;
162
177
  return {
163
178
  ...rest,
164
- insights: rest.insights ?? true,
179
+ insights: resolveInsights(rest.insights),
165
180
  client: createHttpClient(baseURL),
166
181
  };
167
182
  }
@@ -181,7 +196,7 @@ function createHotUpdaterClient() {
181
196
  const { baseURL, ...baseURLRest } = rest;
182
197
  return {
183
198
  ...baseURLRest,
184
- insights: baseURLRest.insights ?? true,
199
+ insights: resolveInsights(baseURLRest.insights),
185
200
  client: createHttpClient(baseURL),
186
201
  };
187
202
  }
@@ -469,8 +484,6 @@ function createHotUpdaterClient() {
469
484
  *
470
485
  * @param {UpdateBundleParams} params - Parameters object required for bundle update
471
486
  * @param {string} params.bundleId - The bundle ID of the app
472
- * @param {string|null} params.fileUrl - The URL of the zip file
473
- *
474
487
  * @returns {Promise<boolean>} Whether the update was successful
475
488
  *
476
489
  * @example
@@ -500,7 +513,7 @@ function createHotUpdaterClient() {
500
513
  const state = getActiveUpdateState();
501
514
  const active = state.activeSelection;
502
515
  const downloaded = await updateBundle(params);
503
- if (downloaded && params.fileUrl !== null) {
516
+ if (downloaded) {
504
517
  const selection = getActiveUpdateState().activeSelection;
505
518
  await reportBundleDownloaded(
506
519
  { ...globalConfig, client },
@@ -535,10 +548,8 @@ function createHotUpdaterClient() {
535
548
  hotUpdaterStore.setState({
536
549
  artifactType: null,
537
550
  details: null,
538
- downloadedBytes: undefined,
539
551
  isUpdateDownloaded: false,
540
552
  progress: 0,
541
- totalBytes: undefined,
542
553
  });
543
554
  }
544
555
  return ok;
@@ -538,13 +538,20 @@ describe("notifyAppReady", () => {
538
538
  expect(getBaseURL()).toBe("file:///bundle-123");
539
539
 
540
540
  await updateBundle({
541
+ assets: {},
541
542
  bundleId: "bundle-456",
542
- fileHash: null,
543
- fileUrl: "https://example.com/bundle.zip",
543
+ archiveUrl: "https://example.com/bundle.tar.br",
544
+ manifestFileHash: "manifest-hash",
545
+ manifestUrl: "https://example.com/manifest.json",
544
546
  status: "UPDATE",
545
547
  });
546
548
 
547
549
  expect(getBundleId()).toBe("bundle-123");
550
+ expect(nativeModuleMock.updateBundle).toHaveBeenCalledWith(
551
+ expect.objectContaining({
552
+ archiveUrl: "https://example.com/bundle.tar.br",
553
+ }),
554
+ );
548
555
  expect(getManifest()).toEqual({
549
556
  assets: {},
550
557
  bundleId: "bundle-123",
@@ -571,9 +578,10 @@ describe("notifyAppReady", () => {
571
578
  const { getBundleId, updateBundle } = await import("./native");
572
579
 
573
580
  await updateBundle({
581
+ assets: {},
574
582
  bundleId: "00000000-0000-0000-0000-000000000003",
575
- fileHash: null,
576
- fileUrl: "https://example.com/bundle.zip",
583
+ manifestFileHash: "manifest-hash",
584
+ manifestUrl: "https://example.com/manifest.json",
577
585
  status: "UPDATE",
578
586
  });
579
587
 
@@ -586,9 +594,10 @@ describe("notifyAppReady", () => {
586
594
 
587
595
  await expect(
588
596
  updateBundle({
597
+ assets: {},
589
598
  bundleId: "00000000-0000-0000-0000-000000000003",
590
- fileHash: null,
591
- fileUrl: "https://example.com/bundle.zip",
599
+ manifestFileHash: "manifest-hash",
600
+ manifestUrl: "https://example.com/manifest.json",
592
601
  status: "UPDATE",
593
602
  }),
594
603
  ).resolves.toBe(true);
@@ -612,9 +621,10 @@ describe("notifyAppReady", () => {
612
621
  const { updateBundle } = await import("./native");
613
622
 
614
623
  await updateBundle({
624
+ assets: {},
615
625
  bundleId: "00000000-0000-0000-0000-000000000003",
616
- fileHash: null,
617
- fileUrl: "https://example.com/bundle.zip",
626
+ manifestFileHash: "manifest-hash",
627
+ manifestUrl: "https://example.com/manifest.json",
618
628
  status: "UPDATE",
619
629
  });
620
630
 
@@ -624,9 +634,10 @@ describe("notifyAppReady", () => {
624
634
 
625
635
  await expect(
626
636
  updateBundle({
637
+ assets: {},
627
638
  bundleId: "00000000-0000-0000-0000-000000000003",
628
- fileHash: null,
629
- fileUrl: "https://example.com/bundle.zip",
639
+ manifestFileHash: "manifest-hash",
640
+ manifestUrl: "https://example.com/manifest.json",
630
641
  status: "UPDATE",
631
642
  }),
632
643
  ).resolves.toBe(true);
@@ -642,7 +653,7 @@ describe("notifyAppReady", () => {
642
653
 
643
654
  await updateBundle({
644
655
  bundleId: "bundle-789",
645
- changedAssets: {
656
+ assets: {
646
657
  "index.ios.bundle": {
647
658
  file: {
648
659
  compression: "br",
@@ -658,8 +669,6 @@ describe("notifyAppReady", () => {
658
669
  },
659
670
  },
660
671
  },
661
- fileHash: "sig:archive",
662
- fileUrl: "https://example.com/bundle.zip",
663
672
  manifestFileHash: "sig:manifest",
664
673
  manifestUrl: "https://example.com/manifest.json",
665
674
  status: "UPDATE",
@@ -667,7 +676,7 @@ describe("notifyAppReady", () => {
667
676
 
668
677
  expect(nativeModuleMock.updateBundle).toHaveBeenCalledWith({
669
678
  bundleId: "bundle-789",
670
- changedAssets: {
679
+ assets: {
671
680
  "index.ios.bundle": {
672
681
  file: {
673
682
  compression: "br",
@@ -684,8 +693,6 @@ describe("notifyAppReady", () => {
684
693
  },
685
694
  },
686
695
  channel: undefined,
687
- fileHash: "sig:archive",
688
- fileUrl: "https://example.com/bundle.zip",
689
696
  manifestFileHash: "sig:manifest",
690
697
  manifestUrl: "https://example.com/manifest.json",
691
698
  });
package/src/native.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import {
2
- type ChangedAsset,
2
+ type ArtifactAsset,
3
3
  type CatalogHighWater,
4
4
  INVALID_COHORT_ERROR_MESSAGE,
5
5
  isValidCohort,
@@ -234,7 +234,7 @@ const getReloadProcess = (): (() => Promise<void>) | null => {
234
234
  : null;
235
235
  };
236
236
 
237
- export type HotUpdaterProgressArtifactType = "archive" | "diff";
237
+ export type HotUpdaterProgressArtifactType = "diff";
238
238
 
239
239
  export type HotUpdaterDiffFileStatus =
240
240
  | "pending"
@@ -286,18 +286,11 @@ export interface HotUpdaterDiffProgressDetails {
286
286
  files: HotUpdaterDiffFileSnapshot[];
287
287
  }
288
288
 
289
- export type HotUpdaterProgressEvent =
290
- | {
291
- progress: number;
292
- artifactType: "archive";
293
- downloadedBytes?: number;
294
- totalBytes?: number;
295
- }
296
- | {
297
- progress: number;
298
- artifactType: "diff";
299
- details: HotUpdaterDiffProgressDetails;
300
- };
289
+ export type HotUpdaterProgressEvent = {
290
+ progress: number;
291
+ artifactType: "diff";
292
+ details: HotUpdaterDiffProgressDetails;
293
+ };
301
294
 
302
295
  export type HotUpdaterEvent = {
303
296
  onProgress: HotUpdaterProgressEvent;
@@ -434,8 +427,6 @@ export const commitReleaseSelection = async (input: {
434
427
  export async function updateBundle(params: UpdateParams): Promise<boolean> {
435
428
  const updateBundleId = params.bundleId;
436
429
  const status = params.status;
437
- const targetFileUrl = params.fileUrl;
438
-
439
430
  const currentBundleId = status === "UPDATE" ? getFreshBundleId() : undefined;
440
431
 
441
432
  // If native is still on the same bundle we installed in this session,
@@ -467,11 +458,10 @@ export async function updateBundle(params: UpdateParams): Promise<boolean> {
467
458
  const existing = sessionState.getInflightUpdate(updateBundleId);
468
459
  if (existing) return existing;
469
460
 
470
- const targetFileHash = params.fileHash;
471
461
  const targetChannel = params.channel;
472
462
  const targetManifestUrl = params.manifestUrl;
473
463
  const targetManifestFileHash = params.manifestFileHash;
474
- const targetChangedAssets = params.changedAssets;
464
+ const targetAssets = params.assets;
475
465
 
476
466
  const promise = (async () => {
477
467
  try {
@@ -479,12 +469,10 @@ export async function updateBundle(params: UpdateParams): Promise<boolean> {
479
469
  const ok = await HotUpdaterNative.updateBundle({
480
470
  bundleId: updateBundleId,
481
471
  channel: targetChannel,
482
- changedAssets:
483
- (targetChangedAssets as Record<string, ChangedAsset> | null) ?? null,
484
- fileUrl: targetFileUrl,
485
- fileHash: targetFileHash ?? null,
486
- manifestFileHash: targetManifestFileHash ?? null,
487
- manifestUrl: targetManifestUrl ?? null,
472
+ assets: targetAssets as Record<string, ArtifactAsset>,
473
+ manifestFileHash: targetManifestFileHash,
474
+ manifestUrl: targetManifestUrl,
475
+ ...(params.archiveUrl ? { archiveUrl: params.archiveUrl } : {}),
488
476
  ...(selection === undefined ? {} : { selection }),
489
477
  });
490
478
  if (ok) {
@@ -1,6 +1,7 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import type {
4
+ ActiveUpdateState,
4
5
  NotifyAppReadyInsightsEvent,
5
6
  NotifyAppReadyResult,
6
7
  } from "./native";
@@ -42,6 +43,7 @@ const mocks = vi.hoisted(() => ({
42
43
  getAppVersion: vi.fn<() => string | undefined>(() => "1.0.0"),
43
44
  getBundleId: vi.fn<() => string | undefined>(() => "bundle-id"),
44
45
  getChannel: vi.fn(() => "production"),
46
+ getActiveUpdateState: vi.fn<() => ActiveUpdateState>(),
45
47
  getCohort: vi.fn(() => "123"),
46
48
  getFingerprintHash: vi.fn(() => "fingerprint-hash"),
47
49
  getInstallId: vi.fn<() => string | undefined>(() => "install-id"),
@@ -65,6 +67,7 @@ vi.mock("./native", () => ({
65
67
  getAppVersion: mocks.getAppVersion,
66
68
  getBundleId: mocks.getBundleId,
67
69
  getChannel: mocks.getChannel,
70
+ getActiveUpdateState: mocks.getActiveUpdateState,
68
71
  getCohort: mocks.getCohort,
69
72
  getFingerprintHash: mocks.getFingerprintHash,
70
73
  getInstallId: mocks.getInstallId,
@@ -106,6 +109,11 @@ describe("automatic notifyAppReady insights boundaries", () => {
106
109
  mocks.getAppVersion.mockReturnValue("1.0.0");
107
110
  mocks.getBundleId.mockReturnValue("bundle-id");
108
111
  mocks.getChannel.mockReturnValue("production");
112
+ mocks.getActiveUpdateState.mockReturnValue({
113
+ activeSelection: null,
114
+ stableSelection: null,
115
+ verificationPending: false,
116
+ });
109
117
  mocks.getCohort.mockReturnValue("123");
110
118
  mocks.getFingerprintHash.mockReturnValue("fingerprint-hash");
111
119
  mocks.getInstallId.mockReturnValue("install-id");
@@ -1,6 +1,7 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import type {
4
+ ActiveUpdateState,
4
5
  NotifyAppReadyInsightsEvent,
5
6
  NotifyAppReadyResult,
6
7
  } from "./native";
@@ -19,6 +20,7 @@ const mocks = vi.hoisted(() => ({
19
20
  getAppVersion: vi.fn(() => "1.0.0"),
20
21
  getBundleId: vi.fn(() => "bundle-id"),
21
22
  getChannel: vi.fn(() => "production"),
23
+ getActiveUpdateState: vi.fn<() => ActiveUpdateState>(),
22
24
  getCohort: vi.fn(() => "123"),
23
25
  getFingerprintHash: vi.fn(() => "fingerprint-hash"),
24
26
  getInstallId: vi.fn(() => "install-id"),
@@ -42,6 +44,7 @@ vi.mock("./native", () => ({
42
44
  getAppVersion: mocks.getAppVersion,
43
45
  getBundleId: mocks.getBundleId,
44
46
  getChannel: mocks.getChannel,
47
+ getActiveUpdateState: mocks.getActiveUpdateState,
45
48
  getCohort: mocks.getCohort,
46
49
  getFingerprintHash: mocks.getFingerprintHash,
47
50
  getInstallId: mocks.getInstallId,
@@ -78,6 +81,11 @@ describe("automatic notifyAppReady insights", () => {
78
81
  mocks.addListener.mockReturnValue(() => {});
79
82
  mocks.getBundleId.mockReturnValue("bundle-id");
80
83
  mocks.getChannel.mockReturnValue("production");
84
+ mocks.getActiveUpdateState.mockReturnValue({
85
+ activeSelection: null,
86
+ stableSelection: null,
87
+ verificationPending: false,
88
+ });
81
89
  mocks.getCohort.mockReturnValue("123");
82
90
  mocks.getFingerprintHash.mockReturnValue("fingerprint-hash");
83
91
  mocks.getInstallId.mockReturnValue("install-id");
@@ -85,6 +93,58 @@ describe("automatic notifyAppReady insights", () => {
85
93
  mocks.readNotifyAppReady.mockReturnValue(createNotifyReadResult());
86
94
  });
87
95
 
96
+ it.each([
97
+ {
98
+ label: "running selection",
99
+ pending: false,
100
+ channel: "production",
101
+ expected: "release-running",
102
+ },
103
+ {
104
+ label: "staged selection",
105
+ pending: true,
106
+ channel: "production",
107
+ expected: "release-running",
108
+ },
109
+ {
110
+ label: "another channel",
111
+ pending: false,
112
+ channel: "preview",
113
+ expected: null,
114
+ },
115
+ ])(
116
+ "attributes an unchanged launch with $label",
117
+ async ({ pending, channel, expected }) => {
118
+ const running = {
119
+ kind: "BUNDLE" as const,
120
+ releaseId: "release-running",
121
+ bundleId: "bundle-id",
122
+ channel,
123
+ };
124
+ mocks.getActiveUpdateState.mockReturnValue({
125
+ activeSelection: pending
126
+ ? {
127
+ ...running,
128
+ releaseId: "release-pending",
129
+ bundleId: "bundle-pending",
130
+ }
131
+ : running,
132
+ stableSelection: pending ? running : null,
133
+ verificationPending: pending,
134
+ });
135
+ const { reportNoChange } = await import("./notifyAppReadyInsights");
136
+ const { client, sendInsightsEvent } = createClient();
137
+ await reportNoChange({ client, insights: true });
138
+ expect(sendInsightsEvent).toHaveBeenCalledWith(
139
+ expect.objectContaining({
140
+ type: "UNCHANGED",
141
+ toBundleId: "bundle-id",
142
+ toReleaseId: expected,
143
+ }),
144
+ );
145
+ },
146
+ );
147
+
88
148
  it("orders download after in-flight startup reporting and suppresses a later no-change report", async () => {
89
149
  const { handleNotifyAppReady, reportBundleDownloaded, reportNoChange } =
90
150
  await import("./notifyAppReadyInsights");
@@ -1,6 +1,7 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import type {
4
+ ActiveUpdateState,
4
5
  NotifyAppReadyInsightsEvent,
5
6
  NotifyAppReadyResult,
6
7
  } from "./native";
@@ -16,6 +17,9 @@ vi.mock("react-native", () => ({
16
17
  const failureCases = [
17
18
  { label: "400", responseStatus: 400 },
18
19
  { label: "404", responseStatus: 404 },
20
+ ] as const;
21
+
22
+ const retryableFailureCases = [
19
23
  { label: "500", responseStatus: 500 },
20
24
  {
21
25
  error: Object.assign(new Error("request aborted"), { name: "AbortError" }),
@@ -30,6 +34,7 @@ const mocks = vi.hoisted(() => ({
30
34
  getAppVersion: vi.fn(() => "1.0.0"),
31
35
  getBundleId: vi.fn(() => "bundle-id"),
32
36
  getChannel: vi.fn(() => "production"),
37
+ getActiveUpdateState: vi.fn<() => ActiveUpdateState>(),
33
38
  getCohort: vi.fn(() => "123"),
34
39
  getFingerprintHash: vi.fn(() => "fingerprint-hash"),
35
40
  getInstallId: vi.fn(() => "install-id"),
@@ -53,6 +58,7 @@ vi.mock("./native", () => ({
53
58
  getAppVersion: mocks.getAppVersion,
54
59
  getBundleId: mocks.getBundleId,
55
60
  getChannel: mocks.getChannel,
61
+ getActiveUpdateState: mocks.getActiveUpdateState,
56
62
  getCohort: mocks.getCohort,
57
63
  getFingerprintHash: mocks.getFingerprintHash,
58
64
  getInstallId: mocks.getInstallId,
@@ -75,6 +81,11 @@ describe("automatic notifyAppReady transport failures", () => {
75
81
  mocks.getAppVersion.mockReturnValue("1.0.0");
76
82
  mocks.getBundleId.mockReturnValue("bundle-id");
77
83
  mocks.getChannel.mockReturnValue("production");
84
+ mocks.getActiveUpdateState.mockReturnValue({
85
+ activeSelection: null,
86
+ stableSelection: null,
87
+ verificationPending: false,
88
+ });
78
89
  mocks.getCohort.mockReturnValue("123");
79
90
  mocks.getFingerprintHash.mockReturnValue("fingerprint-hash");
80
91
  mocks.getInstallId.mockReturnValue("install-id");
@@ -127,4 +138,59 @@ describe("automatic notifyAppReady transport failures", () => {
127
138
  }
128
139
  },
129
140
  );
141
+
142
+ it.each(retryableFailureCases)(
143
+ "retries a $label failure in the background without delaying UNCHANGED readiness",
144
+ async (failureCase) => {
145
+ stubNotifyFrame();
146
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
147
+ const onError = vi.fn();
148
+ const onNotifyAppReady = vi.fn();
149
+ const fetchMock = vi.fn<typeof fetch>(async () => {
150
+ if ("error" in failureCase) {
151
+ throw failureCase.error;
152
+ }
153
+
154
+ return new Response(null, { status: failureCase.responseStatus });
155
+ });
156
+ vi.stubGlobal("fetch", fetchMock);
157
+ Reflect.set(globalThis, "HotUpdater", {
158
+ SDK_VERSION: "test-sdk-version",
159
+ });
160
+
161
+ try {
162
+ const [{ createHttpClient }, { init }] = await Promise.all([
163
+ import("./httpClient"),
164
+ import("./wrap"),
165
+ ]);
166
+ const client = createHttpClient(
167
+ "https://updates.example.test/hot-updater",
168
+ );
169
+
170
+ init({ insights: true, client, onError, onNotifyAppReady });
171
+ await vi.runOnlyPendingTimersAsync();
172
+
173
+ expect(fetchMock).toHaveBeenCalledTimes(1);
174
+ expect(onNotifyAppReady).toHaveBeenCalledWith({
175
+ status: "UNCHANGED",
176
+ });
177
+ expect(warn).not.toHaveBeenCalled();
178
+
179
+ await vi.runAllTimersAsync();
180
+
181
+ const eventIds = fetchMock.mock.calls.map(
182
+ ([, request]) => JSON.parse(String(request?.body)).eventId,
183
+ );
184
+ expect(eventIds).toHaveLength(3);
185
+ expect(new Set(eventIds).size).toBe(1);
186
+ expect(onError).not.toHaveBeenCalled();
187
+ expect(warn).toHaveBeenCalledExactlyOnceWith(
188
+ "[HotUpdater] Insights UNCHANGED event was not delivered:",
189
+ expect.any(Error),
190
+ );
191
+ } finally {
192
+ warn.mockRestore();
193
+ }
194
+ },
195
+ );
130
196
  });
@@ -3,6 +3,7 @@ import { Platform } from "react-native";
3
3
  import type { HotUpdaterError } from "./error";
4
4
  import type { InsightsEventParams, HotUpdaterHttpClient } from "./httpClient";
5
5
  import {
6
+ getActiveUpdateState,
6
7
  getAppVersion,
7
8
  getBundleId,
8
9
  getChannel,
@@ -101,10 +102,21 @@ const buildNotifyAppReadyInsightsParams = (
101
102
  );
102
103
  }
103
104
 
105
+ const state = getActiveUpdateState();
106
+ const runningSelection = [
107
+ state.activeSelection,
108
+ state.stableSelection,
109
+ ].find(
110
+ (selection) =>
111
+ selection?.bundleId === bundleId &&
112
+ selection.channel === commonParams.channel,
113
+ );
114
+
104
115
  return {
105
116
  ...commonParams,
106
117
  fromBundleId: null,
107
118
  toBundleId: bundleId,
119
+ toReleaseId: runningSelection?.releaseId ?? null,
108
120
  type: "UNCHANGED",
109
121
  updateStrategy: null,
110
122
  };
@@ -237,7 +249,7 @@ export const reportNoChange = async (
237
249
 
238
250
  let lastDownloadedSelection: string | undefined;
239
251
 
240
- /** Report only after the archive/diff has been verified and staged successfully. */
252
+ /** Report only after the manifest update has been verified and staged successfully. */
241
253
  export const reportBundleDownloaded = async (
242
254
  options: Pick<
243
255
  NotifyAppReadyOptions,
package/src/sdkVersion.ts CHANGED
@@ -1 +1 @@
1
- export const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.14";
1
+ export const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.16";