@hot-updater/react-native 1.0.0-rc.15 → 1.0.0-rc.17

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 (302) hide show
  1. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +491 -388
  2. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +5 -11
  3. package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
  4. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +290 -12
  5. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +35 -19
  6. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
  7. package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
  8. package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
  9. package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
  10. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +110 -52
  11. package/android/src/main/java/com/hotupdater/PreviousProcessExit.kt +89 -0
  12. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
  13. package/android/src/newarch/HotUpdaterModule.kt +74 -69
  14. package/android/src/oldarch/HotUpdaterModule.kt +63 -59
  15. package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
  16. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +1 -1
  17. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +446 -61
  18. package/android/src/test/java/com/hotupdater/HotUpdaterExceptionTest.kt +220 -0
  19. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +19 -27
  20. package/android/src/test/java/com/hotupdater/InstallIdentityServiceTest.kt +71 -0
  21. package/android/src/test/java/com/hotupdater/KeyValueStorageServiceTest.kt +106 -0
  22. package/android/src/test/java/com/hotupdater/NoBackupContext.kt +11 -0
  23. package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +309 -23
  24. package/android/src/test/java/com/hotupdater/PreviousProcessExitTest.kt +57 -0
  25. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +238 -157
  26. package/ios/HotUpdater/Internal/BundleMetadata.swift +3 -53
  27. package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
  28. package/ios/HotUpdater/Internal/HotUpdater.mm +8 -18
  29. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +48 -29
  30. package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
  31. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +108 -1
  32. package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
  33. package/lib/commonjs/appReady.js +111 -0
  34. package/lib/commonjs/appReady.js.map +1 -0
  35. package/lib/commonjs/appReady.spec.js +360 -0
  36. package/lib/commonjs/appReady.spec.js.map +1 -0
  37. package/lib/commonjs/{notifyAppReadyInsights.test-utils.js → appReady.test-utils.js} +4 -3
  38. package/lib/commonjs/appReady.test-utils.js.map +1 -0
  39. package/lib/commonjs/baseURL.js +16 -0
  40. package/lib/commonjs/baseURL.js.map +1 -0
  41. package/lib/commonjs/checkForUpdate.js +104 -56
  42. package/lib/commonjs/checkForUpdate.js.map +1 -1
  43. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +337 -74
  44. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  45. package/lib/commonjs/clientPlugin.js +49 -0
  46. package/lib/commonjs/clientPlugin.js.map +1 -0
  47. package/lib/commonjs/entryImportGraph.spec.js +50 -0
  48. package/lib/commonjs/entryImportGraph.spec.js.map +1 -0
  49. package/lib/commonjs/fetchJSON.js +2 -6
  50. package/lib/commonjs/fetchJSON.js.map +1 -1
  51. package/lib/commonjs/httpClient.js +15 -67
  52. package/lib/commonjs/httpClient.js.map +1 -1
  53. package/lib/commonjs/httpClient.spec.js +26 -9
  54. package/lib/commonjs/httpClient.spec.js.map +1 -1
  55. package/lib/commonjs/index.js +49 -25
  56. package/lib/commonjs/index.js.map +1 -1
  57. package/lib/commonjs/index.spec.js +126 -40
  58. package/lib/commonjs/index.spec.js.map +1 -1
  59. package/lib/commonjs/native.js +67 -48
  60. package/lib/commonjs/native.js.map +1 -1
  61. package/lib/commonjs/native.spec.js +80 -58
  62. package/lib/commonjs/native.spec.js.map +1 -1
  63. package/lib/commonjs/pluginHost.js +208 -0
  64. package/lib/commonjs/pluginHost.js.map +1 -0
  65. package/lib/commonjs/pluginHost.spec.js +292 -0
  66. package/lib/commonjs/pluginHost.spec.js.map +1 -0
  67. package/lib/commonjs/plugins/insights/eventId.js +129 -0
  68. package/lib/commonjs/plugins/insights/eventId.js.map +1 -0
  69. package/lib/commonjs/plugins/insights/eventId.spec.js +47 -0
  70. package/lib/commonjs/plugins/insights/eventId.spec.js.map +1 -0
  71. package/lib/commonjs/plugins/insights/index.js +272 -0
  72. package/lib/commonjs/plugins/insights/index.js.map +1 -0
  73. package/lib/commonjs/plugins/insights/insights.spec.js +640 -0
  74. package/lib/commonjs/plugins/insights/insights.spec.js.map +1 -0
  75. package/lib/commonjs/plugins/insights/sender.js +128 -0
  76. package/lib/commonjs/plugins/insights/sender.js.map +1 -0
  77. package/lib/commonjs/plugins/insights/sender.spec.js +265 -0
  78. package/lib/commonjs/plugins/insights/sender.spec.js.map +1 -0
  79. package/lib/commonjs/plugins/insights/state.js +145 -0
  80. package/lib/commonjs/plugins/insights/state.js.map +1 -0
  81. package/lib/commonjs/releaseCatalogCache.js +13 -2
  82. package/lib/commonjs/releaseCatalogCache.js.map +1 -1
  83. package/lib/commonjs/releaseCatalogCache.spec.js +18 -0
  84. package/lib/commonjs/releaseCatalogCache.spec.js.map +1 -1
  85. package/lib/commonjs/sdkVersion.js +1 -1
  86. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  87. package/lib/commonjs/updateError.js +132 -0
  88. package/lib/commonjs/updateError.js.map +1 -0
  89. package/lib/commonjs/updateError.spec.js +149 -0
  90. package/lib/commonjs/updateError.spec.js.map +1 -0
  91. package/lib/commonjs/wrap.js +43 -25
  92. package/lib/commonjs/wrap.js.map +1 -1
  93. package/lib/commonjs/wrap.progress.spec.js +195 -0
  94. package/lib/commonjs/wrap.progress.spec.js.map +1 -0
  95. package/lib/commonjs/wrap.spec.js +94 -230
  96. package/lib/commonjs/wrap.spec.js.map +1 -1
  97. package/lib/module/appReady.js +103 -0
  98. package/lib/module/appReady.js.map +1 -0
  99. package/lib/module/appReady.spec.js +359 -0
  100. package/lib/module/appReady.spec.js.map +1 -0
  101. package/lib/module/{notifyAppReadyInsights.test-utils.js → appReady.test-utils.js} +4 -3
  102. package/lib/module/appReady.test-utils.js.map +1 -0
  103. package/lib/module/baseURL.js +11 -0
  104. package/lib/module/baseURL.js.map +1 -0
  105. package/lib/module/checkForUpdate.js +103 -57
  106. package/lib/module/checkForUpdate.js.map +1 -1
  107. package/lib/module/checkForUpdate.releaseCatalog.spec.js +337 -74
  108. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  109. package/lib/module/clientPlugin.js +44 -0
  110. package/lib/module/clientPlugin.js.map +1 -0
  111. package/lib/module/entryImportGraph.spec.js +50 -0
  112. package/lib/module/entryImportGraph.spec.js.map +1 -0
  113. package/lib/module/fetchJSON.js +2 -6
  114. package/lib/module/fetchJSON.js.map +1 -1
  115. package/lib/module/httpClient.js +14 -66
  116. package/lib/module/httpClient.js.map +1 -1
  117. package/lib/module/httpClient.spec.js +26 -9
  118. package/lib/module/httpClient.spec.js.map +1 -1
  119. package/lib/module/index.js +44 -27
  120. package/lib/module/index.js.map +1 -1
  121. package/lib/module/index.spec.js +127 -41
  122. package/lib/module/index.spec.js.map +1 -1
  123. package/lib/module/native.js +63 -44
  124. package/lib/module/native.js.map +1 -1
  125. package/lib/module/native.spec.js +80 -58
  126. package/lib/module/native.spec.js.map +1 -1
  127. package/lib/module/pluginHost.js +200 -0
  128. package/lib/module/pluginHost.js.map +1 -0
  129. package/lib/module/pluginHost.spec.js +291 -0
  130. package/lib/module/pluginHost.spec.js.map +1 -0
  131. package/lib/module/plugins/insights/eventId.js +122 -0
  132. package/lib/module/plugins/insights/eventId.js.map +1 -0
  133. package/lib/module/plugins/insights/eventId.spec.js +47 -0
  134. package/lib/module/plugins/insights/eventId.spec.js.map +1 -0
  135. package/lib/module/plugins/insights/index.js +268 -0
  136. package/lib/module/plugins/insights/index.js.map +1 -0
  137. package/lib/module/plugins/insights/insights.spec.js +639 -0
  138. package/lib/module/plugins/insights/insights.spec.js.map +1 -0
  139. package/lib/module/plugins/insights/sender.js +123 -0
  140. package/lib/module/plugins/insights/sender.js.map +1 -0
  141. package/lib/module/plugins/insights/sender.spec.js +265 -0
  142. package/lib/module/plugins/insights/sender.spec.js.map +1 -0
  143. package/lib/module/plugins/insights/state.js +139 -0
  144. package/lib/module/plugins/insights/state.js.map +1 -0
  145. package/lib/module/releaseCatalogCache.js +13 -2
  146. package/lib/module/releaseCatalogCache.js.map +1 -1
  147. package/lib/module/releaseCatalogCache.spec.js +18 -0
  148. package/lib/module/releaseCatalogCache.spec.js.map +1 -1
  149. package/lib/module/sdkVersion.js +1 -1
  150. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  151. package/lib/module/updateError.js +126 -0
  152. package/lib/module/updateError.js.map +1 -0
  153. package/lib/module/updateError.spec.js +149 -0
  154. package/lib/module/updateError.spec.js.map +1 -0
  155. package/lib/module/wrap.js +43 -25
  156. package/lib/module/wrap.js.map +1 -1
  157. package/lib/module/wrap.progress.spec.js +196 -0
  158. package/lib/module/wrap.progress.spec.js.map +1 -0
  159. package/lib/module/wrap.spec.js +93 -229
  160. package/lib/module/wrap.spec.js.map +1 -1
  161. package/lib/typescript/commonjs/appReady.d.ts +23 -0
  162. package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
  163. package/lib/typescript/commonjs/appReady.test-utils.d.ts +9 -0
  164. package/lib/typescript/commonjs/appReady.test-utils.d.ts.map +1 -0
  165. package/lib/typescript/commonjs/baseURL.d.ts +4 -0
  166. package/lib/typescript/commonjs/baseURL.d.ts.map +1 -0
  167. package/lib/typescript/commonjs/checkForUpdate.d.ts +15 -2
  168. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/clientPlugin.d.ts +185 -0
  170. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
  171. package/lib/typescript/commonjs/fetchJSON.d.ts +2 -3
  172. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/httpClient.d.ts +2 -30
  174. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  175. package/lib/typescript/commonjs/index.d.ts +3 -6
  176. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  177. package/lib/typescript/commonjs/native.d.ts +29 -15
  178. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  179. package/lib/typescript/commonjs/pluginHost.d.ts +29 -0
  180. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
  181. package/lib/typescript/commonjs/plugins/insights/eventId.d.ts +18 -0
  182. package/lib/typescript/commonjs/plugins/insights/eventId.d.ts.map +1 -0
  183. package/lib/typescript/commonjs/plugins/insights/index.d.ts +40 -0
  184. package/lib/typescript/commonjs/plugins/insights/index.d.ts.map +1 -0
  185. package/lib/typescript/commonjs/plugins/insights/sender.d.ts +74 -0
  186. package/lib/typescript/commonjs/plugins/insights/sender.d.ts.map +1 -0
  187. package/lib/typescript/commonjs/plugins/insights/state.d.ts +46 -0
  188. package/lib/typescript/commonjs/plugins/insights/state.d.ts.map +1 -0
  189. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +5 -1
  190. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
  191. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  192. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +34 -12
  193. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  194. package/lib/typescript/commonjs/updateError.d.ts +26 -0
  195. package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
  196. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  197. package/lib/typescript/commonjs/wrap.types.d.ts +6 -5
  198. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  199. package/lib/typescript/module/appReady.d.ts +23 -0
  200. package/lib/typescript/module/appReady.d.ts.map +1 -0
  201. package/lib/typescript/module/appReady.test-utils.d.ts +9 -0
  202. package/lib/typescript/module/appReady.test-utils.d.ts.map +1 -0
  203. package/lib/typescript/module/baseURL.d.ts +4 -0
  204. package/lib/typescript/module/baseURL.d.ts.map +1 -0
  205. package/lib/typescript/module/checkForUpdate.d.ts +15 -2
  206. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  207. package/lib/typescript/module/clientPlugin.d.ts +185 -0
  208. package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
  209. package/lib/typescript/module/fetchJSON.d.ts +2 -3
  210. package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
  211. package/lib/typescript/module/httpClient.d.ts +2 -30
  212. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  213. package/lib/typescript/module/index.d.ts +3 -6
  214. package/lib/typescript/module/index.d.ts.map +1 -1
  215. package/lib/typescript/module/native.d.ts +29 -15
  216. package/lib/typescript/module/native.d.ts.map +1 -1
  217. package/lib/typescript/module/pluginHost.d.ts +29 -0
  218. package/lib/typescript/module/pluginHost.d.ts.map +1 -0
  219. package/lib/typescript/module/plugins/insights/eventId.d.ts +18 -0
  220. package/lib/typescript/module/plugins/insights/eventId.d.ts.map +1 -0
  221. package/lib/typescript/module/plugins/insights/index.d.ts +40 -0
  222. package/lib/typescript/module/plugins/insights/index.d.ts.map +1 -0
  223. package/lib/typescript/module/plugins/insights/sender.d.ts +74 -0
  224. package/lib/typescript/module/plugins/insights/sender.d.ts.map +1 -0
  225. package/lib/typescript/module/plugins/insights/state.d.ts +46 -0
  226. package/lib/typescript/module/plugins/insights/state.d.ts.map +1 -0
  227. package/lib/typescript/module/releaseCatalogCache.d.ts +5 -1
  228. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
  229. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  230. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +34 -12
  231. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  232. package/lib/typescript/module/updateError.d.ts +26 -0
  233. package/lib/typescript/module/updateError.d.ts.map +1 -0
  234. package/lib/typescript/module/wrap.d.ts.map +1 -1
  235. package/lib/typescript/module/wrap.types.d.ts +6 -5
  236. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  237. package/package.json +14 -2
  238. package/plugins/insights/package.json +8 -0
  239. package/src/appReady.spec.ts +393 -0
  240. package/src/{notifyAppReadyInsights.test-utils.ts → appReady.test-utils.ts} +7 -7
  241. package/src/appReady.ts +149 -0
  242. package/src/baseURL.ts +15 -0
  243. package/src/checkForUpdate.releaseCatalog.spec.ts +370 -76
  244. package/src/checkForUpdate.ts +135 -80
  245. package/src/clientPlugin.ts +231 -0
  246. package/src/entryImportGraph.spec.ts +65 -0
  247. package/src/fetchJSON.ts +3 -10
  248. package/src/httpClient.spec.ts +28 -9
  249. package/src/httpClient.ts +16 -109
  250. package/src/index.spec.ts +129 -36
  251. package/src/index.ts +67 -37
  252. package/src/native.spec.ts +98 -72
  253. package/src/native.ts +93 -77
  254. package/src/pluginHost.spec.ts +297 -0
  255. package/src/pluginHost.ts +262 -0
  256. package/src/plugins/insights/eventId.spec.ts +60 -0
  257. package/src/plugins/insights/eventId.ts +171 -0
  258. package/src/plugins/insights/index.ts +367 -0
  259. package/src/plugins/insights/insights.spec.ts +732 -0
  260. package/src/plugins/insights/sender.spec.ts +287 -0
  261. package/src/plugins/insights/sender.ts +221 -0
  262. package/src/plugins/insights/state.ts +184 -0
  263. package/src/releaseCatalogCache.spec.ts +22 -0
  264. package/src/releaseCatalogCache.ts +19 -4
  265. package/src/sdkVersion.ts +1 -1
  266. package/src/specs/NativeHotUpdater.ts +34 -13
  267. package/src/updateError.spec.ts +144 -0
  268. package/src/updateError.ts +153 -0
  269. package/src/wrap.progress.spec.ts +232 -0
  270. package/src/wrap.spec.ts +100 -283
  271. package/src/wrap.tsx +57 -26
  272. package/src/wrap.types.ts +6 -5
  273. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -225
  274. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  275. package/lib/commonjs/notifyAppReadyInsights.js +0 -188
  276. package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
  277. package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -378
  278. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
  279. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
  280. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -124
  281. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
  282. package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -224
  283. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  284. package/lib/module/notifyAppReadyInsights.js +0 -181
  285. package/lib/module/notifyAppReadyInsights.js.map +0 -1
  286. package/lib/module/notifyAppReadyInsights.spec.js +0 -377
  287. package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
  288. package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
  289. package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -123
  290. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
  291. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -25
  292. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
  293. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
  294. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  295. package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -25
  296. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
  297. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
  298. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  299. package/src/notifyAppReadyInsights.boundary.spec.ts +0 -242
  300. package/src/notifyAppReadyInsights.spec.ts +0 -362
  301. package/src/notifyAppReadyInsights.transport.spec.ts +0 -138
  302. package/src/notifyAppReadyInsights.ts +0 -293
@@ -199,6 +199,28 @@ describe("Release Catalog persistent cache", () => {
199
199
  },
200
200
  );
201
201
 
202
+ it("reads a 404 the server marks x-hot-updater-catalog: none as no catalog", async () => {
203
+ const fetchMock = vi.fn().mockResolvedValueOnce(
204
+ new Response(null, {
205
+ headers: { "x-hot-updater-catalog": "none" },
206
+ status: 404,
207
+ }),
208
+ );
209
+ vi.stubGlobal("fetch", fetchMock);
210
+
211
+ await expect(fetchReleaseCatalogWithCache(input())).resolves.toBeNull();
212
+ expect(nativeMocks.write).not.toHaveBeenCalled();
213
+ });
214
+
215
+ it("throws the HTTP status of any other 404", async () => {
216
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValueOnce(response(404)));
217
+
218
+ await expect(fetchReleaseCatalogWithCache(input())).rejects.toMatchObject({
219
+ name: "UpdateHttpError",
220
+ status: 404,
221
+ });
222
+ });
223
+
202
224
  it("does not use cached data as a fallback on network failure", async () => {
203
225
  const fetchMock = vi
204
226
  .fn()
@@ -8,6 +8,8 @@ import {
8
8
  type ReleaseCatalog,
9
9
  } from "@hot-updater/core";
10
10
 
11
+ import { InvalidUpdateResponseError, UpdateHttpError } from "./updateError";
12
+
11
13
  const CACHE_FORMAT_VERSION = "1";
12
14
  const MAX_ETAG_BYTES = 1024;
13
15
  export const MAX_RELEASE_CATALOG_WIRE_BYTES =
@@ -290,16 +292,25 @@ const consumeSuccessfulResponse = async (
290
292
  response: Response,
291
293
  input: FetchReleaseCatalogInput,
292
294
  partition: string,
293
- ): Promise<ReleaseCatalog> => {
295
+ ): Promise<ReleaseCatalog | null> => {
296
+ // The server marks the 404 of a scope that has no catalog yet: no update,
297
+ // not a failure. Any other 404 is a wrong baseURL or route.
298
+ if (
299
+ response.status === 404 &&
300
+ response.headers.get("x-hot-updater-catalog")?.trim().toLowerCase() ===
301
+ "none"
302
+ ) {
303
+ return null;
304
+ }
294
305
  if (response.status !== 200) {
295
- throw new Error(response.statusText);
306
+ throw new UpdateHttpError(response.status, response.statusText);
296
307
  }
297
308
 
298
309
  const body = await response.text();
299
310
  const catalog = parseValidatedCatalog(body, input.expectedScope);
300
311
  if (catalog === null) {
301
312
  await removeNativeReleaseCatalogCache(partition);
302
- throw new Error("Received an invalid Release catalog");
313
+ throw new InvalidUpdateResponseError("Received an invalid Release catalog");
303
314
  }
304
315
 
305
316
  const etag = response.headers.get("etag");
@@ -323,9 +334,13 @@ const consumeSuccessfulResponse = async (
323
334
  return catalog;
324
335
  };
325
336
 
337
+ /**
338
+ * Fetches a scope's catalog, or null when the server answers that the scope
339
+ * has none (a 404 with `x-hot-updater-catalog: none`).
340
+ */
326
341
  export const fetchReleaseCatalogWithCache = async (
327
342
  input: FetchReleaseCatalogInput,
328
- ): Promise<ReleaseCatalog> => {
343
+ ): Promise<ReleaseCatalog | null> => {
329
344
  const partition = createReleaseCatalogCachePartition(input);
330
345
  const cached = await readValidatedCache(partition, input.expectedScope);
331
346
  const response = await fetchCatalogResponse(input, cached?.etag);
package/src/sdkVersion.ts CHANGED
@@ -1 +1 @@
1
- export const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.15";
1
+ export const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.17";
@@ -34,7 +34,11 @@ export interface Spec extends TurboModule {
34
34
  * Downloads and applies a bundle update.
35
35
  *
36
36
  * @param params - Update bundle parameters
37
- * @returns Promise that resolves to true if successful
37
+ * @returns Promise that resolves, once the bundle is staged, to how it
38
+ * arrived: `{ delivery, patchFallback }`. `delivery` is "patch" when a
39
+ * bsdiff patch produced a file, "manifest" when only changed files were
40
+ * downloaded, or "archive" for the full archive; `patchFallback` is true
41
+ * when a patch was tried but the file or archive was downloaded instead.
38
42
  * @throws {HotUpdaterErrorCode} Rejects with one of the following error codes:
39
43
  *
40
44
  * Parameter validation:
@@ -59,8 +63,20 @@ export interface Spec extends TurboModule {
59
63
  *
60
64
  * Note: iOS normalizes rare signature/storage errors to SIGNATURE_VERIFICATION_FAILED
61
65
  * or UNKNOWN_ERROR to keep the JS error surface small.
62
- */
63
- updateBundle(params: UpdateBundleParams): Promise<boolean>;
66
+ *
67
+ * A rejection from the download or install also carries `userInfo`
68
+ * `{ stage, reason, resource?, httpStatus?, transport?, originCode? }`:
69
+ * `stage` is "download" (transfer and verification) or "install" (patch,
70
+ * extract, move into place); `reason` is one of "network", "http",
71
+ * "invalid_response", "hash_mismatch", "signature", "patch", "extract",
72
+ * "storage", or "unknown"; `resource` is what was being fetched or
73
+ * applied: "manifest", "file", "patch", or "archive". `httpStatus` is
74
+ * present when `reason` is "http", with `originCode`, the storage
75
+ * origin's XML error `<Code>` when the body names one. `transport` is
76
+ * present when no response arrived: "timeout", "dns", "tls",
77
+ * "connection", "offline", or "cancelled".
78
+ */
79
+ updateBundle(params: UpdateBundleParams): Promise<UnsafeObject>;
64
80
 
65
81
  /** Accepts and durably advances the catalog high-water for a scope. */
66
82
  acceptReleaseCatalog(params: UnsafeObject): boolean;
@@ -98,6 +114,13 @@ export interface Spec extends TurboModule {
98
114
  toBundleId?: string;
99
115
  updateStrategy?: "fingerprint" | "appVersion";
100
116
  crashedBundleId?: string;
117
+ /**
118
+ * Android 11+: why the app's previous main process exited, the
119
+ * `ApplicationExitInfo` reason without its `REASON_` prefix (for
120
+ * example "CRASH", "ANR", "LOW_MEMORY", "USER_REQUESTED"), read once
121
+ * when the process starts. Absent on iOS and older Android.
122
+ */
123
+ previousProcessExit?: string;
101
124
  };
102
125
 
103
126
  /**
@@ -164,24 +187,22 @@ export interface Spec extends TurboModule {
164
187
  getCohort: () => string;
165
188
 
166
189
  /**
167
- * Gets the persisted install id for this app installation.
190
+ * Gets the install id: a random id native creates once per app
191
+ * installation and keeps out of device backups.
168
192
  */
169
193
  getInstallId: () => string;
170
194
 
171
195
  /**
172
- * Gets the persisted nullable user id associated with this installation.
173
- */
174
- getUserId: () => string | null;
175
-
176
- /**
177
- * Gets the persisted nullable username associated with this installation.
196
+ * Reads a value from the SDK's persistent key-value store, which native
197
+ * keeps out of device backups. Returns null when the key has no value.
178
198
  */
179
- getUsername: () => string | null;
199
+ getStorageItem: (key: string) => string | null;
180
200
 
181
201
  /**
182
- * Persists nullable user identity fields associated with this installation.
202
+ * Writes a value to the SDK's persistent key-value store; null removes the
203
+ * key.
183
204
  */
184
- setUser: (userId: string | null, username: string | null) => void;
205
+ setStorageItem: (key: string, value: string | null) => void;
185
206
 
186
207
  // EventEmitter
187
208
  addListener(eventName: string): void;
@@ -0,0 +1,144 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { StaleReleaseCatalogError } from "./error";
4
+ import { FetchJSONResponseError } from "./fetchJSON";
5
+ import {
6
+ classifyUpdateError,
7
+ InvalidUpdateResponseError,
8
+ UpdateHttpError,
9
+ } from "./updateError";
10
+
11
+ const nativeRejection = (userInfo?: Record<string, unknown>) =>
12
+ Object.assign(new Error("native failure"), {
13
+ code: "DOWNLOAD_FAILED",
14
+ ...(userInfo === undefined ? {} : { userInfo }),
15
+ });
16
+
17
+ describe("classifyUpdateError", () => {
18
+ it.each([
19
+ {
20
+ label: "an HTTP status",
21
+ error: new UpdateHttpError(503, "Service Unavailable"),
22
+ expected: { reason: "http", httpStatus: 503 },
23
+ },
24
+ {
25
+ label: "a JSON request's HTTP status",
26
+ error: new FetchJSONResponseError(404, "Not Found"),
27
+ expected: { reason: "http", httpStatus: 404 },
28
+ },
29
+ {
30
+ label: "fetch without a response",
31
+ error: new TypeError("Network request failed"),
32
+ expected: { reason: "network" },
33
+ },
34
+ {
35
+ label: "a timeout",
36
+ error: new Error("Request timed out"),
37
+ expected: { reason: "network", transport: "timeout" },
38
+ },
39
+ {
40
+ label: "a cancelled request",
41
+ error: Object.assign(new Error("aborted"), { name: "AbortError" }),
42
+ expected: { reason: "network", transport: "cancelled" },
43
+ },
44
+ {
45
+ label: "an invalid response",
46
+ error: new InvalidUpdateResponseError(
47
+ "Received an invalid Release catalog",
48
+ ),
49
+ expected: { reason: "invalid_response" },
50
+ },
51
+ {
52
+ label: "unparsable JSON",
53
+ error: new SyntaxError("Unexpected token"),
54
+ expected: { reason: "invalid_response" },
55
+ },
56
+ {
57
+ label: "anything else",
58
+ error: new Error("Fingerprint hash is required"),
59
+ expected: { reason: "unknown" },
60
+ },
61
+ ])("classifies $label in the stage it happened", ({ error, expected }) => {
62
+ expect(classifyUpdateError(error, "check", "catalog")).toEqual({
63
+ stage: "check",
64
+ resource: "catalog",
65
+ ...expected,
66
+ });
67
+ });
68
+
69
+ it("leaves out the resource when the caller names none", () => {
70
+ expect(classifyUpdateError(new Error("unexpected"), "download")).toEqual({
71
+ stage: "download",
72
+ reason: "unknown",
73
+ });
74
+ });
75
+
76
+ it("takes a native rejection's own classification", () => {
77
+ expect(
78
+ classifyUpdateError(
79
+ nativeRejection({
80
+ reason: "patch",
81
+ resource: "patch",
82
+ stage: "install",
83
+ }),
84
+ "download",
85
+ "artifact",
86
+ ),
87
+ ).toEqual({ stage: "install", reason: "patch", resource: "patch" });
88
+ expect(
89
+ classifyUpdateError(
90
+ nativeRejection({
91
+ httpStatus: 403,
92
+ originCode: "AccessDenied",
93
+ reason: "http",
94
+ resource: "file",
95
+ stage: "download",
96
+ }),
97
+ "download",
98
+ ),
99
+ ).toEqual({
100
+ stage: "download",
101
+ reason: "http",
102
+ resource: "file",
103
+ httpStatus: 403,
104
+ originCode: "AccessDenied",
105
+ });
106
+ expect(
107
+ classifyUpdateError(
108
+ nativeRejection({
109
+ reason: "network",
110
+ stage: "download",
111
+ transport: "dns",
112
+ }),
113
+ "download",
114
+ ),
115
+ ).toEqual({ stage: "download", reason: "network", transport: "dns" });
116
+ });
117
+
118
+ it("drops native details that do not fit their field", () => {
119
+ expect(
120
+ classifyUpdateError(
121
+ nativeRejection({
122
+ httpStatus: 403,
123
+ originCode: "Access Denied <Key>secret</Key>",
124
+ reason: "http",
125
+ resource: "bucket",
126
+ stage: "download",
127
+ transport: "timeout",
128
+ }),
129
+ "download",
130
+ ),
131
+ ).toEqual({ stage: "download", reason: "http", httpStatus: 403 });
132
+ });
133
+
134
+ it.each([
135
+ { label: "a stale selection", error: new StaleReleaseCatalogError() },
136
+ { label: "an unclassified native rejection", error: nativeRejection() },
137
+ {
138
+ label: "a native rejection with an unknown class",
139
+ error: nativeRejection({ reason: "cosmic", stage: "download" }),
140
+ },
141
+ ])("reports $label as no update failure", ({ error }) => {
142
+ expect(classifyUpdateError(error, "download")).toBeNull();
143
+ });
144
+ });
@@ -0,0 +1,153 @@
1
+ import type {
2
+ UpdateError,
3
+ UpdateErrorReason,
4
+ UpdateErrorResource,
5
+ UpdateErrorStage,
6
+ UpdateErrorTransport,
7
+ } from "./clientPlugin";
8
+ import { HotUpdaterError } from "./error";
9
+
10
+ /**
11
+ * A response the SDK cannot use: a malformed or stale catalog, an artifact
12
+ * in another protocol, or an invalid URL.
13
+ */
14
+ export class InvalidUpdateResponseError extends HotUpdaterError {
15
+ constructor(message: string, options?: { cause?: unknown }) {
16
+ super(message);
17
+ this.name = "InvalidUpdateResponseError";
18
+ if (options?.cause !== undefined) this.cause = options.cause;
19
+ }
20
+ }
21
+
22
+ /** A request the server answered with a status the SDK does not accept. */
23
+ export class UpdateHttpError extends HotUpdaterError {
24
+ constructor(
25
+ readonly status: number,
26
+ statusText: string,
27
+ ) {
28
+ super(
29
+ `Request failed with HTTP ${status}${statusText ? ` ${statusText}` : ""}`,
30
+ );
31
+ this.name = "UpdateHttpError";
32
+ }
33
+ }
34
+
35
+ const STAGES: readonly UpdateErrorStage[] = ["check", "download", "install"];
36
+ const REASONS: readonly UpdateErrorReason[] = [
37
+ "network",
38
+ "http",
39
+ "invalid_response",
40
+ "hash_mismatch",
41
+ "signature",
42
+ "patch",
43
+ "extract",
44
+ "storage",
45
+ "unknown",
46
+ ];
47
+ const RESOURCES: readonly UpdateErrorResource[] = [
48
+ "catalog",
49
+ "artifact",
50
+ "manifest",
51
+ "file",
52
+ "patch",
53
+ "archive",
54
+ ];
55
+ const TRANSPORTS: readonly UpdateErrorTransport[] = [
56
+ "timeout",
57
+ "dns",
58
+ "tls",
59
+ "connection",
60
+ "offline",
61
+ "cancelled",
62
+ ];
63
+ /** An S3, R2, or GCS error code: letters, digits, and punctuation only. */
64
+ const ORIGIN_CODE = /^[A-Za-z0-9._-]{1,64}$/;
65
+
66
+ type Classification = Pick<
67
+ UpdateError,
68
+ "stage" | "reason" | "resource" | "httpStatus" | "originCode" | "transport"
69
+ >;
70
+
71
+ const oneOf = <T extends string>(
72
+ values: readonly T[],
73
+ value: unknown,
74
+ ): value is T => values.includes(value as T);
75
+
76
+ const readNativeClassification = (
77
+ error: object,
78
+ ): Classification | null | undefined => {
79
+ if (!("code" in error) || typeof error.code !== "string") return undefined;
80
+ const userInfo =
81
+ "userInfo" in error && typeof error.userInfo === "object"
82
+ ? (error.userInfo as Record<string, unknown> | null)
83
+ : null;
84
+ const { stage, reason, resource, transport, originCode } = userInfo ?? {};
85
+ // Native classifies every failure of its download and install; a rejection
86
+ // without it, such as a stale selection, is not an update failure.
87
+ if (!oneOf(STAGES, stage) || !oneOf(REASONS, reason)) return null;
88
+ const httpStatus = Number(userInfo?.httpStatus);
89
+ return {
90
+ stage,
91
+ reason,
92
+ ...(oneOf(RESOURCES, resource) ? { resource } : {}),
93
+ ...(reason === "http" && Number.isSafeInteger(httpStatus)
94
+ ? { httpStatus }
95
+ : {}),
96
+ ...(reason === "http" &&
97
+ typeof originCode === "string" &&
98
+ ORIGIN_CODE.test(originCode)
99
+ ? { originCode }
100
+ : {}),
101
+ ...(reason === "network" && oneOf(TRANSPORTS, transport)
102
+ ? { transport }
103
+ : {}),
104
+ };
105
+ };
106
+
107
+ /**
108
+ * Where and why an update failed, or null when the error is not an update
109
+ * failure. Native rejections carry their own classification; an error the
110
+ * SDK's JavaScript raised happened in `stage`, fetching `resource` when one
111
+ * is given.
112
+ */
113
+ export const classifyUpdateError = (
114
+ error: unknown,
115
+ stage: UpdateErrorStage,
116
+ resource?: UpdateErrorResource,
117
+ ): Classification | null => {
118
+ if (typeof error === "object" && error !== null) {
119
+ const native = readNativeClassification(error);
120
+ if (native !== undefined) return native;
121
+ }
122
+ const scope = {
123
+ stage,
124
+ ...(resource === undefined ? {} : { resource }),
125
+ };
126
+ // Names, not classes, so an error from another copy of a module still
127
+ // classifies, as it does across test module resets.
128
+ const name = error instanceof Error ? error.name : null;
129
+ // A selection that became stale is a race between checks, not a failure.
130
+ if (name === "StaleReleaseCatalogError") return null;
131
+ if (name === "UpdateHttpError") {
132
+ return {
133
+ ...scope,
134
+ reason: "http",
135
+ httpStatus: (error as UpdateHttpError).status,
136
+ };
137
+ }
138
+ if (name === "InvalidUpdateResponseError" || name === "SyntaxError") {
139
+ return { ...scope, reason: "invalid_response" };
140
+ }
141
+ if (error instanceof Error && error.message === "Request timed out") {
142
+ return { ...scope, reason: "network", transport: "timeout" };
143
+ }
144
+ if (name === "AbortError") {
145
+ return { ...scope, reason: "network", transport: "cancelled" };
146
+ }
147
+ if (name === "TypeError") {
148
+ // fetch rejects with a TypeError when no response arrives, without
149
+ // saying why.
150
+ return { ...scope, reason: "network" };
151
+ }
152
+ return { ...scope, reason: "unknown" };
153
+ };
@@ -0,0 +1,232 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, cleanup, render, screen } from "@testing-library/react";
4
+ import { createElement } from "react";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import type { HotUpdaterProgressEvent } from "./native";
8
+ import type {
9
+ HotUpdaterFallbackComponentProps,
10
+ InternalWrapOptions,
11
+ } from "./wrap";
12
+
13
+ const mocks = vi.hoisted(() => ({
14
+ listeners: new Map<string, (event: HotUpdaterProgressEvent) => void>(),
15
+ checkForUpdate: vi.fn(),
16
+ reload: vi.fn(),
17
+ }));
18
+
19
+ vi.mock("./checkForUpdate", () => ({
20
+ checkForUpdate: mocks.checkForUpdate,
21
+ }));
22
+
23
+ vi.mock("./appReady", () => ({
24
+ handleNotifyAppReady: vi.fn().mockResolvedValue({ status: "UNCHANGED" }),
25
+ }));
26
+
27
+ vi.mock("./native", () => ({
28
+ addListener: vi.fn(
29
+ (eventName: string, listener: (event: HotUpdaterProgressEvent) => void) => {
30
+ mocks.listeners.set(eventName, listener);
31
+ return () => {
32
+ mocks.listeners.delete(eventName);
33
+ };
34
+ },
35
+ ),
36
+ getUpdateId: vi.fn(() => "release-id"),
37
+ reload: mocks.reload,
38
+ }));
39
+
40
+ describe("HotUpdater wrap progress subscription", () => {
41
+ let frameCallbacks: ((timestamp: number) => void)[];
42
+
43
+ const flushFrames = async () => {
44
+ await act(async () => {
45
+ await new Promise<void>((resolve) => setImmediate(resolve));
46
+ while (frameCallbacks.length > 0) {
47
+ frameCallbacks.shift()?.(0);
48
+ }
49
+ });
50
+ };
51
+
52
+ const emitProgress = async (progress: number) => {
53
+ const event: HotUpdaterProgressEvent = {
54
+ artifactType: "diff",
55
+ progress,
56
+ details: {
57
+ totalFilesCount: 1,
58
+ completedFilesCount: progress === 1 ? 1 : 0,
59
+ files: [
60
+ {
61
+ order: 0,
62
+ path: "index.bundle",
63
+ downloadPath: "index.bundle",
64
+ downloadedBytes: progress * 1_000,
65
+ totalBytes: 1_000,
66
+ progress,
67
+ status: progress === 1 ? "downloaded" : "downloading",
68
+ },
69
+ ],
70
+ },
71
+ };
72
+ await act(async () => {
73
+ mocks.listeners.get("onProgress")?.(event);
74
+ });
75
+ await flushFrames();
76
+ return event;
77
+ };
78
+
79
+ const renderWrapped = async (
80
+ options: Pick<
81
+ InternalWrapOptions,
82
+ "fallbackComponent" | "onProgress" | "onUpdateProcessCompleted"
83
+ >,
84
+ onRender: () => void,
85
+ ) => {
86
+ const { wrap } = await import("./wrap");
87
+ const App = () => {
88
+ onRender();
89
+ return createElement("div", null, "app");
90
+ };
91
+ const Wrapped = wrap({
92
+ client: { createSession: vi.fn() },
93
+ updateStrategy: "appVersion",
94
+ ...options,
95
+ })(App);
96
+
97
+ render(createElement(Wrapped));
98
+ await flushFrames();
99
+ };
100
+
101
+ beforeEach(() => {
102
+ vi.resetModules();
103
+ mocks.listeners.clear();
104
+ mocks.checkForUpdate.mockReset();
105
+ mocks.checkForUpdate.mockResolvedValue(null);
106
+ mocks.reload.mockReset();
107
+ frameCallbacks = [];
108
+ vi.stubGlobal(
109
+ "requestAnimationFrame",
110
+ (callback: (timestamp: number) => void) => {
111
+ frameCallbacks.push(callback);
112
+ return frameCallbacks.length;
113
+ },
114
+ );
115
+ });
116
+
117
+ afterEach(() => {
118
+ cleanup();
119
+ vi.unstubAllGlobals();
120
+ });
121
+
122
+ it("does not re-render the wrapped app on progress updates without fallbackComponent or onProgress", async () => {
123
+ const onRender = vi.fn();
124
+ await renderWrapped({}, onRender);
125
+ const rendersBeforeDownload = onRender.mock.calls.length;
126
+
127
+ for (const progress of [0.1, 0.2, 0.3, 0.4, 0.5]) {
128
+ await emitProgress(progress);
129
+ }
130
+
131
+ expect(onRender).toHaveBeenCalledTimes(rendersBeforeDownload);
132
+ });
133
+
134
+ it("keeps reporting progress to onProgress without re-rendering the wrapped app", async () => {
135
+ const onRender = vi.fn();
136
+ const onProgress = vi.fn();
137
+ await renderWrapped({ onProgress }, onRender);
138
+ const rendersBeforeDownload = onRender.mock.calls.length;
139
+
140
+ await emitProgress(0.25);
141
+ await emitProgress(0.75);
142
+
143
+ expect(onProgress.mock.calls).toEqual([[0], [0.25], [0.75]]);
144
+ expect(onRender).toHaveBeenCalledTimes(rendersBeforeDownload);
145
+ });
146
+
147
+ it("keeps passing progress details to fallbackComponent during an update check", async () => {
148
+ mocks.checkForUpdate.mockReturnValue(new Promise(() => {}));
149
+ const fallbackProps = vi.fn();
150
+ const Fallback = (props: HotUpdaterFallbackComponentProps) => {
151
+ fallbackProps(props);
152
+ return createElement("div", null, `progress:${props.progress}`);
153
+ };
154
+ await renderWrapped({ fallbackComponent: Fallback }, vi.fn());
155
+
156
+ const event = await emitProgress(0.5);
157
+
158
+ expect(screen.getByText("progress:0.5")).toBeTruthy();
159
+ expect(fallbackProps).toHaveBeenLastCalledWith({
160
+ ...event,
161
+ status: "CHECK_FOR_UPDATE",
162
+ message: null,
163
+ });
164
+ });
165
+
166
+ it.each([false, true])(
167
+ "preserves progress reporting across the fallback transition with shouldForceUpdate=%s",
168
+ async (shouldForceUpdate) => {
169
+ let resolveCheck!: (update: unknown) => void;
170
+ mocks.checkForUpdate.mockReturnValue(
171
+ new Promise((resolve) => {
172
+ resolveCheck = resolve;
173
+ }),
174
+ );
175
+ let finishDownload!: (success: boolean) => void;
176
+ const download = new Promise<boolean>((resolve) => {
177
+ finishDownload = resolve;
178
+ });
179
+ const updateBundle = vi.fn(() => download);
180
+ const onProgress = vi.fn();
181
+ const onRender = vi.fn();
182
+ const onUpdateProcessCompleted = vi.fn();
183
+ const Fallback = ({
184
+ status,
185
+ progress,
186
+ }: HotUpdaterFallbackComponentProps) =>
187
+ createElement("div", null, `${status}:${progress}`);
188
+ await renderWrapped(
189
+ { fallbackComponent: Fallback, onProgress, onUpdateProcessCompleted },
190
+ onRender,
191
+ );
192
+ expect(screen.getByText("CHECK_FOR_UPDATE:0")).toBeTruthy();
193
+ expect(onRender).not.toHaveBeenCalled();
194
+
195
+ const update = {
196
+ id: "new-release",
197
+ status: "UPDATE",
198
+ shouldForceUpdate,
199
+ message: "New release",
200
+ };
201
+ await act(async () => {
202
+ resolveCheck({ ...update, updateBundle });
203
+ });
204
+ expect(updateBundle).toHaveBeenCalledOnce();
205
+ expect(onUpdateProcessCompleted).toHaveBeenCalledTimes(
206
+ shouldForceUpdate ? 0 : 1,
207
+ );
208
+ expect(
209
+ screen.getByText(shouldForceUpdate ? "UPDATING:0" : "app"),
210
+ ).toBeTruthy();
211
+ const rendersDuringDownload = onRender.mock.calls.length;
212
+
213
+ for (const progress of [0.25, 0.75, 1]) {
214
+ await emitProgress(progress);
215
+ }
216
+ expect(onRender).toHaveBeenCalledTimes(rendersDuringDownload);
217
+ if (shouldForceUpdate) {
218
+ expect(screen.getByText("UPDATING:1")).toBeTruthy();
219
+ }
220
+
221
+ await act(async () => {
222
+ finishDownload(true);
223
+ });
224
+ expect(screen.getByText("app")).toBeTruthy();
225
+ expect(onRender).toHaveBeenCalledOnce();
226
+ expect(onUpdateProcessCompleted).toHaveBeenCalledOnce();
227
+ expect(onUpdateProcessCompleted).toHaveBeenCalledWith(update);
228
+ expect(mocks.reload).toHaveBeenCalledTimes(shouldForceUpdate ? 1 : 0);
229
+ expect(onProgress.mock.calls).toEqual([[0], [0.25], [0.75], [1]]);
230
+ },
231
+ );
232
+ });