@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
@@ -7,7 +7,9 @@ import {
7
7
  } from "@hot-updater/core";
8
8
  import { beforeEach, describe, expect, it, vi } from "vitest";
9
9
 
10
+ import type { HotUpdaterClientHooks } from "./clientPlugin";
10
11
  import type { HotUpdaterHttpClient } from "./httpClient";
12
+ import { InvalidUpdateResponseError, UpdateHttpError } from "./updateError";
11
13
 
12
14
  const MINIMUM_RELEASE_ID = "00000000-0000-7000-8000-000000000001";
13
15
  const RELEASE_ID = "00000000-0000-7000-8000-000000000002";
@@ -23,31 +25,38 @@ const SCOPE_KEY = createReleaseCatalogScopeKey({
23
25
  strategy: "APP_VERSION",
24
26
  });
25
27
 
26
- const mocks = vi.hoisted(() => ({
27
- addListener: vi.fn(() => () => {}),
28
- acceptReleaseCatalog: vi.fn(() => true),
29
- commitReleaseSelection: vi.fn(async () => true),
30
- getActiveUpdateState: vi.fn(() => ({
31
- activeSelection: null as PersistedSelectionReceipt | null,
32
- highestSeenCatalogs: {},
33
- stableSelection: null as PersistedSelectionReceipt | null,
34
- verificationPending: false,
35
- })),
36
- getAppVersion: vi.fn(() => "1.2"),
37
- getBundleId: vi.fn(() => CURRENT_BUNDLE_ID),
38
- getChannel: vi.fn(() => CHANNEL),
39
- getCohort: vi.fn(() => "123"),
40
- getCrashHistory: vi.fn(() => []),
41
- getDefaultChannel: vi.fn(() => CHANNEL),
42
- getFingerprintHash: vi.fn(() => null),
43
- getInstallId: vi.fn(() => "install-id"),
44
- getMinBundleId: vi.fn(() => MINIMUM_RELEASE_ID),
45
- getPersistedUserIdentity: vi.fn(() => ({})),
46
- isChannelSwitched: vi.fn(() => false),
47
- isReleaseSelectionCurrent: vi.fn(() => true),
48
- resetChannel: vi.fn(),
49
- updateBundle: vi.fn(async () => true),
50
- }));
28
+ const mocks = vi.hoisted(() => {
29
+ Reflect.set(globalThis, "HotUpdater", { SDK_VERSION: "test-sdk-version" });
30
+ return {
31
+ addListener: vi.fn(() => () => {}),
32
+ acceptReleaseCatalog: vi.fn(() => true),
33
+ commitReleaseSelection: vi.fn(async () => true),
34
+ getActiveUpdateState: vi.fn(() => ({
35
+ activeSelection: null as PersistedSelectionReceipt | null,
36
+ highestSeenCatalogs: {},
37
+ stableSelection: null as PersistedSelectionReceipt | null,
38
+ verificationPending: false,
39
+ })),
40
+ getAppVersion: vi.fn(() => "1.2"),
41
+ getBundleId: vi.fn(() => CURRENT_BUNDLE_ID),
42
+ getChannel: vi.fn(() => CHANNEL),
43
+ getCohort: vi.fn(() => "123"),
44
+ getCrashHistory: vi.fn(() => []),
45
+ getDefaultChannel: vi.fn(() => CHANNEL),
46
+ getFingerprintHash: vi.fn(() => null),
47
+ getInstallId: vi.fn(() => "install-id"),
48
+ getMinBundleId: vi.fn(() => MINIMUM_RELEASE_ID),
49
+ isChannelSwitched: vi.fn(() => false),
50
+ isReleaseSelectionCurrent: vi.fn(() => true),
51
+ resetChannel: vi.fn(),
52
+ stageBundle: vi.fn(
53
+ async (): Promise<{
54
+ delivery: "patch" | "manifest" | "archive";
55
+ patchFallback: boolean;
56
+ } | null> => ({ delivery: "manifest", patchFallback: false }),
57
+ ),
58
+ };
59
+ });
51
60
 
52
61
  vi.mock("react-native", () => ({ Platform: { OS: "ios" } }));
53
62
 
@@ -86,16 +95,47 @@ const createClient = (catalog = createCatalog()) => {
86
95
  archiveUrl: "https://updates.example.com/bundle.tar.br",
87
96
  };
88
97
  const resolveArtifact = vi.fn(async () => artifact);
89
- const sendInsightsEvent = vi.fn(async () => undefined);
90
98
  const session = {
91
99
  fetchReleaseCatalog,
92
100
  resolveArtifact,
93
- sendInsightsEvent,
94
101
  };
95
102
  const client: HotUpdaterHttpClient = {
96
103
  createSession: vi.fn(async () => session),
97
104
  };
98
- return { client, fetchReleaseCatalog, resolveArtifact, sendInsightsEvent };
105
+ return { client, fetchReleaseCatalog, resolveArtifact };
106
+ };
107
+
108
+ type PluginEvent = {
109
+ [K in keyof HotUpdaterClientHooks]-?: [
110
+ K,
111
+ Parameters<NonNullable<HotUpdaterClientHooks[K]>>[0],
112
+ ];
113
+ }[keyof HotUpdaterClientHooks];
114
+
115
+ /** Records what checks report to plugins, in order. */
116
+ const recordPluginEvents = async () => {
117
+ const { configurePlugins } = await import("./pluginHost");
118
+ const events: PluginEvent[] = [];
119
+ configurePlugins(
120
+ [
121
+ {
122
+ id: "recorder",
123
+ setup: () => ({
124
+ onUpdateCheck: (result) => {
125
+ events.push(["onUpdateCheck", result]);
126
+ },
127
+ onBundleDownloaded: (info) => {
128
+ events.push(["onBundleDownloaded", info]);
129
+ },
130
+ onUpdateError: (error) => {
131
+ events.push(["onUpdateError", error]);
132
+ },
133
+ }),
134
+ },
135
+ ],
136
+ { baseURL: "https://updates.example.com" },
137
+ );
138
+ return events;
99
139
  };
100
140
 
101
141
  describe("checkForUpdate Release catalog protocol", () => {
@@ -112,59 +152,84 @@ describe("checkForUpdate Release catalog protocol", () => {
112
152
  });
113
153
  mocks.isReleaseSelectionCurrent.mockReturnValue(true);
114
154
  mocks.commitReleaseSelection.mockResolvedValue(true);
115
- mocks.updateBundle.mockResolvedValue(true);
155
+ mocks.stageBundle.mockResolvedValue({
156
+ delivery: "manifest",
157
+ patchFallback: false,
158
+ });
116
159
  });
117
160
 
118
- it("reports an optional download only after staging, without treating the target as running", async () => {
161
+ it("reports the check, then the download only after staging", async () => {
119
162
  const { checkForUpdate } = await import("./checkForUpdate");
120
- const { client, sendInsightsEvent } = createClient();
121
- let complete!: (success: boolean) => void;
122
- mocks.updateBundle.mockImplementationOnce(
163
+ const events = await recordPluginEvents();
164
+ const { client } = createClient();
165
+ let complete!: (delivery: {
166
+ delivery: "patch" | "manifest" | "archive";
167
+ patchFallback: boolean;
168
+ }) => void;
169
+ mocks.stageBundle.mockImplementationOnce(
123
170
  () =>
124
- new Promise<boolean>((resolve) => {
171
+ new Promise((resolve) => {
125
172
  complete = resolve;
126
173
  }),
127
174
  );
128
175
  const update = await checkForUpdate({
129
176
  client,
130
- insights: true,
131
177
  updateStrategy: "appVersion",
132
178
  });
133
179
  expect(update?.shouldForceUpdate).toBe(false);
180
+ expect(events).toEqual([
181
+ [
182
+ "onUpdateCheck",
183
+ {
184
+ status: "UPDATE_AVAILABLE",
185
+ channel: CHANNEL,
186
+ fromBundleId: CURRENT_BUNDLE_ID,
187
+ fromReleaseId: null,
188
+ toBundleId: TARGET_BUNDLE_ID,
189
+ toReleaseId: RELEASE_ID,
190
+ transitionKind: "INSTALL",
191
+ updateStatus: "UPDATE",
192
+ shouldForceUpdate: false,
193
+ updateStrategy: "appVersion",
194
+ },
195
+ ],
196
+ ]);
134
197
  const pending = update!.updateBundle();
135
- await vi.waitFor(() => expect(mocks.updateBundle).toHaveBeenCalledOnce());
136
- expect(sendInsightsEvent).not.toHaveBeenCalled();
137
- complete(true);
198
+ await vi.waitFor(() => expect(mocks.stageBundle).toHaveBeenCalledOnce());
199
+ expect(events).toHaveLength(1);
200
+ complete({ delivery: "patch", patchFallback: true });
138
201
  await expect(pending).resolves.toBe(true);
139
- expect(sendInsightsEvent).toHaveBeenCalledExactlyOnceWith(
140
- expect.objectContaining({
141
- type: "UPDATE_DOWNLOADED",
202
+ expect(events[1]).toEqual([
203
+ "onBundleDownloaded",
204
+ {
205
+ channel: CHANNEL,
142
206
  fromBundleId: CURRENT_BUNDLE_ID,
143
- toBundleId: TARGET_BUNDLE_ID,
144
207
  fromReleaseId: null,
208
+ toBundleId: TARGET_BUNDLE_ID,
145
209
  toReleaseId: RELEASE_ID,
146
210
  updateStrategy: "appVersion",
147
- }),
148
- );
149
- await update!.updateBundle();
150
- expect(sendInsightsEvent).toHaveBeenCalledOnce();
211
+ delivery: "patch",
212
+ patchFallback: true,
213
+ },
214
+ ]);
151
215
  });
152
216
 
153
- it.each([false, "failure"])(
154
- "does not report a download when staging returns %s",
217
+ it.each(["nothing new", "failure"])(
218
+ "does not report a download when staging brings %s",
155
219
  async (result) => {
156
220
  const { checkForUpdate } = await import("./checkForUpdate");
157
- const { client, sendInsightsEvent } = createClient();
158
- if (result === false) mocks.updateBundle.mockResolvedValueOnce(false);
221
+ const events = await recordPluginEvents();
222
+ const { client } = createClient();
223
+ if (result === "nothing new")
224
+ mocks.stageBundle.mockResolvedValueOnce(null);
159
225
  else
160
- mocks.updateBundle.mockRejectedValueOnce(new Error("download failed"));
226
+ mocks.stageBundle.mockRejectedValueOnce(new Error("download failed"));
161
227
  const update = await checkForUpdate({
162
228
  client,
163
- insights: true,
164
229
  updateStrategy: "appVersion",
165
230
  });
166
231
  await update!.updateBundle().catch(() => false);
167
- expect(sendInsightsEvent).not.toHaveBeenCalled();
232
+ expect(events.map(([name]) => name)).not.toContain("onBundleDownloaded");
168
233
  },
169
234
  );
170
235
 
@@ -201,7 +266,7 @@ describe("checkForUpdate Release catalog protocol", () => {
201
266
  requestTimeout: undefined,
202
267
  targetBundleId: TARGET_BUNDLE_ID,
203
268
  });
204
- expect(mocks.updateBundle).toHaveBeenCalledWith(
269
+ expect(mocks.stageBundle).toHaveBeenCalledWith(
205
270
  expect.objectContaining({
206
271
  bundleId: TARGET_BUNDLE_ID,
207
272
  archiveUrl: "https://updates.example.com/bundle.tar.br",
@@ -242,8 +307,8 @@ describe("checkForUpdate Release catalog protocol", () => {
242
307
  },
243
308
  ],
244
309
  });
245
- const { client, resolveArtifact, sendInsightsEvent } =
246
- createClient(forceCatalog);
310
+ const events = await recordPluginEvents();
311
+ const { client, resolveArtifact } = createClient(forceCatalog);
247
312
 
248
313
  const result = await checkForUpdate({
249
314
  client,
@@ -257,8 +322,8 @@ describe("checkForUpdate Release catalog protocol", () => {
257
322
  });
258
323
  await expect(result?.updateBundle()).resolves.toBe(true);
259
324
  expect(resolveArtifact).not.toHaveBeenCalled();
260
- expect(mocks.updateBundle).not.toHaveBeenCalled();
261
- expect(sendInsightsEvent).not.toHaveBeenCalled();
325
+ expect(mocks.stageBundle).not.toHaveBeenCalled();
326
+ expect(events.map(([name]) => name)).not.toContain("onBundleDownloaded");
262
327
  expect(mocks.commitReleaseSelection).toHaveBeenCalledWith({
263
328
  guard: expect.objectContaining({ generation: 2, scopeKey: SCOPE_KEY }),
264
329
  selection: expect.objectContaining({
@@ -268,7 +333,7 @@ describe("checkForUpdate Release catalog protocol", () => {
268
333
  });
269
334
  });
270
335
 
271
- it("reports same-file selection as UNCHANGED with the selected release identity", async () => {
336
+ it("reports a same-file adoption as UNCHANGED with the adopted Release", async () => {
272
337
  const active: PersistedSelectionReceipt = {
273
338
  catalogId: CATALOG_ID,
274
339
  bundleId: TARGET_BUNDLE_ID,
@@ -288,25 +353,25 @@ describe("checkForUpdate Release catalog protocol", () => {
288
353
  verificationPending: false,
289
354
  });
290
355
  const { checkForUpdate } = await import("./checkForUpdate");
291
- const { client, sendInsightsEvent } = createClient();
356
+ const events = await recordPluginEvents();
357
+ const { client } = createClient();
292
358
 
293
359
  const result = await checkForUpdate({
294
- insights: true,
295
360
  client,
296
361
  updateStrategy: "appVersion",
297
362
  });
298
363
  await result?.updateBundle();
299
364
 
300
- expect(sendInsightsEvent).toHaveBeenCalledWith(
301
- expect.objectContaining({
302
- fromBundleId: null,
303
- updateStrategy: null,
304
- fromReleaseId: MINIMUM_RELEASE_ID,
305
- toBundleId: TARGET_BUNDLE_ID,
306
- toReleaseId: RELEASE_ID,
307
- type: "UNCHANGED",
308
- }),
309
- );
365
+ expect(events.at(-1)).toEqual([
366
+ "onUpdateCheck",
367
+ {
368
+ status: "UNCHANGED",
369
+ channel: CHANNEL,
370
+ bundleId: TARGET_BUNDLE_ID,
371
+ releaseId: RELEASE_ID,
372
+ previousReleaseId: MINIMUM_RELEASE_ID,
373
+ },
374
+ ]);
310
375
  });
311
376
 
312
377
  it("selects a lower-id Release when explicitly switching scopes", async () => {
@@ -385,7 +450,7 @@ describe("checkForUpdate Release catalog protocol", () => {
385
450
  });
386
451
  await expect(result?.updateBundle()).resolves.toBe(true);
387
452
  expect(resolveArtifact).not.toHaveBeenCalled();
388
- expect(mocks.updateBundle).not.toHaveBeenCalled();
453
+ expect(mocks.stageBundle).not.toHaveBeenCalled();
389
454
  expect(mocks.commitReleaseSelection).toHaveBeenCalledWith({
390
455
  guard: expect.objectContaining({
391
456
  catalogHash: CATALOG_HASH,
@@ -416,7 +481,7 @@ describe("checkForUpdate Release catalog protocol", () => {
416
481
  await expect(result?.updateBundle()).rejects.toBeInstanceOf(
417
482
  StaleReleaseCatalogError,
418
483
  );
419
- expect(mocks.updateBundle).not.toHaveBeenCalled();
484
+ expect(mocks.stageBundle).not.toHaveBeenCalled();
420
485
  });
421
486
 
422
487
  it("installs an older enabled Release as a forced rollback", async () => {
@@ -466,7 +531,7 @@ describe("checkForUpdate Release catalog protocol", () => {
466
531
  transitionKind: "INSTALL",
467
532
  });
468
533
  await expect(result?.updateBundle()).resolves.toBe(true);
469
- expect(mocks.updateBundle).toHaveBeenCalledWith(
534
+ expect(mocks.stageBundle).toHaveBeenCalledWith(
470
535
  expect.objectContaining({
471
536
  bundleId: TARGET_BUNDLE_ID,
472
537
  status: "ROLLBACK",
@@ -573,7 +638,7 @@ describe("checkForUpdate Release catalog protocol", () => {
573
638
  }),
574
639
  );
575
640
  expect(resolveArtifact).not.toHaveBeenCalled();
576
- expect(mocks.updateBundle).not.toHaveBeenCalled();
641
+ expect(mocks.stageBundle).not.toHaveBeenCalled();
577
642
  },
578
643
  );
579
644
 
@@ -629,7 +694,7 @@ describe("checkForUpdate Release catalog protocol", () => {
629
694
  );
630
695
  expect(mocks.acceptReleaseCatalog).not.toHaveBeenCalled();
631
696
  expect(mocks.commitReleaseSelection).not.toHaveBeenCalled();
632
- expect(mocks.updateBundle).not.toHaveBeenCalled();
697
+ expect(mocks.stageBundle).not.toHaveBeenCalled();
633
698
  });
634
699
 
635
700
  it("rejects an unexpected Catalog identity before accepting catalog state", async () => {
@@ -669,4 +734,233 @@ describe("checkForUpdate Release catalog protocol", () => {
669
734
  );
670
735
  expect(mocks.acceptReleaseCatalog).not.toHaveBeenCalled();
671
736
  });
737
+ it("reports a check that found nothing to install as UNCHANGED", async () => {
738
+ mocks.getBundleId.mockReturnValueOnce(MINIMUM_RELEASE_ID);
739
+ const { checkForUpdate } = await import("./checkForUpdate");
740
+ const events = await recordPluginEvents();
741
+ const { client } = createClient(
742
+ createCatalog({ releases: [], rollbackReleases: [] }),
743
+ );
744
+
745
+ await expect(
746
+ checkForUpdate({ client, updateStrategy: "appVersion" }),
747
+ ).resolves.toBeNull();
748
+
749
+ expect(events).toEqual([
750
+ [
751
+ "onUpdateCheck",
752
+ {
753
+ status: "UNCHANGED",
754
+ channel: CHANNEL,
755
+ bundleId: MINIMUM_RELEASE_ID,
756
+ releaseId: null,
757
+ previousReleaseId: null,
758
+ },
759
+ ],
760
+ ]);
761
+ });
762
+
763
+ it("treats a scope without a catalog as no update, not a failure", async () => {
764
+ const onError = vi.fn();
765
+ const { checkForUpdate } = await import("./checkForUpdate");
766
+ const events = await recordPluginEvents();
767
+ const { client, fetchReleaseCatalog } = createClient();
768
+ fetchReleaseCatalog.mockResolvedValueOnce(null as never);
769
+
770
+ await expect(
771
+ checkForUpdate({ client, onError, updateStrategy: "appVersion" }),
772
+ ).resolves.toBeNull();
773
+
774
+ expect(onError).not.toHaveBeenCalled();
775
+ expect(mocks.acceptReleaseCatalog).not.toHaveBeenCalled();
776
+ expect(events).toEqual([
777
+ [
778
+ "onUpdateCheck",
779
+ expect.objectContaining({ status: "UNCHANGED", channel: CHANNEL }),
780
+ ],
781
+ ]);
782
+ });
783
+
784
+ it.each([
785
+ {
786
+ label: "an HTTP status",
787
+ error: () => new UpdateHttpError(503, "Service Unavailable"),
788
+ expected: { reason: "http", httpStatus: 503 },
789
+ },
790
+ {
791
+ label: "no response",
792
+ error: () => new TypeError("Network request failed"),
793
+ expected: { reason: "network" },
794
+ },
795
+ {
796
+ label: "a 404 without the no-catalog mark",
797
+ error: () => new UpdateHttpError(404, "Not Found"),
798
+ expected: { reason: "http", httpStatus: 404 },
799
+ },
800
+ {
801
+ label: "a timeout",
802
+ error: () => new Error("Request timed out"),
803
+ expected: { reason: "network", transport: "timeout" },
804
+ },
805
+ {
806
+ label: "an invalid catalog",
807
+ error: () =>
808
+ new InvalidUpdateResponseError("Received an invalid Release catalog"),
809
+ expected: { reason: "invalid_response" },
810
+ },
811
+ ])("reports a check failed by $label", async ({ error, expected }) => {
812
+ const { checkForUpdate } = await import("./checkForUpdate");
813
+ const events = await recordPluginEvents();
814
+ const { client, fetchReleaseCatalog } = createClient();
815
+ const cause = error();
816
+ fetchReleaseCatalog.mockRejectedValueOnce(cause);
817
+
818
+ await expect(
819
+ checkForUpdate({ client, updateStrategy: "appVersion" }),
820
+ ).resolves.toBeNull();
821
+
822
+ expect(events).toEqual([
823
+ [
824
+ "onUpdateError",
825
+ {
826
+ stage: "check",
827
+ resource: "catalog",
828
+ ...expected,
829
+ channel: CHANNEL,
830
+ bundleId: CURRENT_BUNDLE_ID,
831
+ releaseId: null,
832
+ updateStrategy: "appVersion",
833
+ cause,
834
+ },
835
+ ],
836
+ ]);
837
+ });
838
+
839
+ it.each([
840
+ {
841
+ label: "an artifact request answered 500",
842
+ fail: (resolveArtifact: ReturnType<typeof vi.fn>) =>
843
+ resolveArtifact.mockRejectedValueOnce(
844
+ new UpdateHttpError(500, "Internal Server Error"),
845
+ ),
846
+ expected: {
847
+ stage: "download",
848
+ reason: "http",
849
+ resource: "artifact",
850
+ httpStatus: 500,
851
+ },
852
+ },
853
+ {
854
+ label: "native extraction",
855
+ fail: () =>
856
+ mocks.stageBundle.mockRejectedValueOnce(
857
+ Object.assign(new Error("Failed to extract archive"), {
858
+ code: "UNKNOWN_ERROR",
859
+ userInfo: { reason: "extract", stage: "install" },
860
+ }),
861
+ ),
862
+ expected: { stage: "install", reason: "extract" },
863
+ },
864
+ {
865
+ label: "a native HTTP status",
866
+ fail: () =>
867
+ mocks.stageBundle.mockRejectedValueOnce(
868
+ Object.assign(new Error("Failed to download bundle"), {
869
+ code: "DOWNLOAD_FAILED",
870
+ userInfo: {
871
+ httpStatus: 403,
872
+ originCode: "ExpiredToken",
873
+ reason: "http",
874
+ resource: "archive",
875
+ stage: "download",
876
+ },
877
+ }),
878
+ ),
879
+ expected: {
880
+ stage: "download",
881
+ reason: "http",
882
+ resource: "archive",
883
+ httpStatus: 403,
884
+ originCode: "ExpiredToken",
885
+ },
886
+ },
887
+ {
888
+ label: "a native connection loss",
889
+ fail: () =>
890
+ mocks.stageBundle.mockRejectedValueOnce(
891
+ Object.assign(new Error("The network connection was lost."), {
892
+ code: "DOWNLOAD_FAILED",
893
+ userInfo: {
894
+ reason: "network",
895
+ resource: "file",
896
+ stage: "download",
897
+ transport: "connection",
898
+ },
899
+ }),
900
+ ),
901
+ expected: {
902
+ stage: "download",
903
+ reason: "network",
904
+ resource: "file",
905
+ transport: "connection",
906
+ },
907
+ },
908
+ ])(
909
+ "reports an install failed by $label and rethrows it",
910
+ async ({ fail, expected }) => {
911
+ const { checkForUpdate } = await import("./checkForUpdate");
912
+ const events = await recordPluginEvents();
913
+ const { client, resolveArtifact } = createClient();
914
+ fail(resolveArtifact);
915
+ const result = await checkForUpdate({
916
+ client,
917
+ updateStrategy: "appVersion",
918
+ });
919
+
920
+ await expect(result!.updateBundle()).rejects.toThrow();
921
+
922
+ expect(events.filter(([name]) => name === "onUpdateError")).toEqual([
923
+ [
924
+ "onUpdateError",
925
+ expect.objectContaining({
926
+ ...expected,
927
+ channel: CHANNEL,
928
+ bundleId: CURRENT_BUNDLE_ID,
929
+ targetBundleId: TARGET_BUNDLE_ID,
930
+ targetReleaseId: RELEASE_ID,
931
+ updateStrategy: "appVersion",
932
+ }),
933
+ ],
934
+ ]);
935
+ },
936
+ );
937
+
938
+ it.each([
939
+ {
940
+ label: "a native rejection without a failure class",
941
+ fail: () =>
942
+ mocks.stageBundle.mockRejectedValueOnce(
943
+ Object.assign(new Error("Release catalog selection is stale"), {
944
+ code: "UNKNOWN_ERROR",
945
+ }),
946
+ ),
947
+ },
948
+ {
949
+ label: "a selection that became stale",
950
+ fail: () => mocks.isReleaseSelectionCurrent.mockReturnValueOnce(false),
951
+ },
952
+ ])("does not report $label as an update failure", async ({ fail }) => {
953
+ const { checkForUpdate } = await import("./checkForUpdate");
954
+ const events = await recordPluginEvents();
955
+ const { client } = createClient();
956
+ const result = await checkForUpdate({
957
+ client,
958
+ updateStrategy: "appVersion",
959
+ });
960
+ fail();
961
+
962
+ await expect(result!.updateBundle()).rejects.toThrow();
963
+
964
+ expect(events.map(([name]) => name)).toEqual(["onUpdateCheck"]);
965
+ });
672
966
  });