@hot-updater/react-native 1.0.0-rc.3 → 1.0.0-rc.31

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 (300) hide show
  1. package/android/proguard-rules.pro +17 -2
  2. package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
  3. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +954 -837
  4. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +15 -11
  5. package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
  6. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +287 -22
  7. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +43 -31
  8. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +37 -54
  9. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
  10. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
  11. package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
  12. package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
  13. package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
  14. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +127 -50
  15. package/android/src/main/java/com/hotupdater/PreviousProcessExit.kt +89 -0
  16. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
  17. package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
  18. package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
  19. package/android/src/newarch/HotUpdater.kt +7 -9
  20. package/android/src/newarch/HotUpdaterModule.kt +109 -84
  21. package/android/src/oldarch/HotUpdater.kt +7 -9
  22. package/android/src/oldarch/HotUpdaterModule.kt +98 -74
  23. package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
  24. package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
  25. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
  26. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +950 -1112
  27. package/ios/HotUpdater/Internal/BundleMetadata.swift +8 -53
  28. package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
  29. package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
  30. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
  31. package/ios/HotUpdater/Internal/HotUpdater.mm +13 -45
  32. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +190 -73
  33. package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
  34. package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
  35. package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
  36. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +119 -71
  37. package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
  38. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
  39. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
  40. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
  41. package/lib/commonjs/appReady.js +111 -0
  42. package/lib/commonjs/appReady.js.map +1 -0
  43. package/lib/commonjs/checkForUpdate.js +124 -69
  44. package/lib/commonjs/checkForUpdate.js.map +1 -1
  45. package/lib/commonjs/clientPlugin.js +13 -0
  46. package/lib/commonjs/clientPlugin.js.map +1 -0
  47. package/lib/commonjs/error.js +4 -13
  48. package/lib/commonjs/error.js.map +1 -1
  49. package/lib/commonjs/fetchJSON.js +16 -27
  50. package/lib/commonjs/fetchJSON.js.map +1 -1
  51. package/lib/commonjs/fetchUpdateResponse.js +54 -0
  52. package/lib/commonjs/fetchUpdateResponse.js.map +1 -0
  53. package/lib/commonjs/httpClient.js +63 -94
  54. package/lib/commonjs/httpClient.js.map +1 -1
  55. package/lib/commonjs/index.js +71 -18
  56. package/lib/commonjs/index.js.map +1 -1
  57. package/lib/commonjs/native.js +78 -60
  58. package/lib/commonjs/native.js.map +1 -1
  59. package/lib/commonjs/pluginHost.js +44 -0
  60. package/lib/commonjs/pluginHost.js.map +1 -0
  61. package/lib/commonjs/releaseCatalogCache.js +36 -87
  62. package/lib/commonjs/releaseCatalogCache.js.map +1 -1
  63. package/lib/commonjs/sdkVersion.js +1 -1
  64. package/lib/commonjs/sdkVersion.js.map +1 -1
  65. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  66. package/lib/commonjs/store.js +3 -10
  67. package/lib/commonjs/store.js.map +1 -1
  68. package/lib/commonjs/updateError.js +132 -0
  69. package/lib/commonjs/updateError.js.map +1 -0
  70. package/lib/commonjs/wrap.js +43 -26
  71. package/lib/commonjs/wrap.js.map +1 -1
  72. package/lib/module/appReady.js +103 -0
  73. package/lib/module/appReady.js.map +1 -0
  74. package/lib/module/checkForUpdate.js +117 -64
  75. package/lib/module/checkForUpdate.js.map +1 -1
  76. package/lib/module/clientPlugin.js +4 -0
  77. package/lib/module/clientPlugin.js.map +1 -0
  78. package/lib/module/error.js +4 -13
  79. package/lib/module/error.js.map +1 -1
  80. package/lib/module/fetchJSON.js +14 -26
  81. package/lib/module/fetchJSON.js.map +1 -1
  82. package/lib/module/fetchUpdateResponse.js +49 -0
  83. package/lib/module/fetchUpdateResponse.js.map +1 -0
  84. package/lib/module/httpClient.js +62 -93
  85. package/lib/module/httpClient.js.map +1 -1
  86. package/lib/module/index.js +58 -20
  87. package/lib/module/index.js.map +1 -1
  88. package/lib/module/native.js +71 -53
  89. package/lib/module/native.js.map +1 -1
  90. package/lib/module/pluginHost.js +35 -0
  91. package/lib/module/pluginHost.js.map +1 -0
  92. package/lib/module/releaseCatalogCache.js +32 -82
  93. package/lib/module/releaseCatalogCache.js.map +1 -1
  94. package/lib/module/sdkVersion.js +1 -1
  95. package/lib/module/sdkVersion.js.map +1 -1
  96. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  97. package/lib/module/store.js +3 -10
  98. package/lib/module/store.js.map +1 -1
  99. package/lib/module/updateError.js +126 -0
  100. package/lib/module/updateError.js.map +1 -0
  101. package/lib/module/wrap.js +43 -26
  102. package/lib/module/wrap.js.map +1 -1
  103. package/lib/typescript/commonjs/appReady.d.ts +23 -0
  104. package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/checkForUpdate.d.ts +16 -4
  106. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/clientPlugin.d.ts +2 -0
  108. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/error.d.ts +4 -13
  110. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/fetchJSON.d.ts +5 -5
  112. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts +14 -0
  114. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/httpClient.d.ts +4 -33
  116. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/index.d.ts +4 -8
  118. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/native.d.ts +31 -22
  120. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/pluginHost.d.ts +4 -0
  122. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +8 -14
  124. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  126. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +42 -47
  128. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/store.d.ts +1 -3
  130. package/lib/typescript/commonjs/store.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/types.d.ts +1 -1
  132. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/updateError.d.ts +26 -0
  134. package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/wrap.types.d.ts +6 -7
  137. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  138. package/lib/typescript/module/appReady.d.ts +23 -0
  139. package/lib/typescript/module/appReady.d.ts.map +1 -0
  140. package/lib/typescript/module/checkForUpdate.d.ts +16 -4
  141. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  142. package/lib/typescript/module/clientPlugin.d.ts +2 -0
  143. package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
  144. package/lib/typescript/module/error.d.ts +4 -13
  145. package/lib/typescript/module/error.d.ts.map +1 -1
  146. package/lib/typescript/module/fetchJSON.d.ts +5 -5
  147. package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
  148. package/lib/typescript/module/fetchUpdateResponse.d.ts +14 -0
  149. package/lib/typescript/module/fetchUpdateResponse.d.ts.map +1 -0
  150. package/lib/typescript/module/httpClient.d.ts +4 -33
  151. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  152. package/lib/typescript/module/index.d.ts +4 -8
  153. package/lib/typescript/module/index.d.ts.map +1 -1
  154. package/lib/typescript/module/native.d.ts +31 -22
  155. package/lib/typescript/module/native.d.ts.map +1 -1
  156. package/lib/typescript/module/pluginHost.d.ts +4 -0
  157. package/lib/typescript/module/pluginHost.d.ts.map +1 -0
  158. package/lib/typescript/module/releaseCatalogCache.d.ts +8 -14
  159. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
  160. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  161. package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
  162. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +42 -47
  163. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  164. package/lib/typescript/module/store.d.ts +1 -3
  165. package/lib/typescript/module/store.d.ts.map +1 -1
  166. package/lib/typescript/module/types.d.ts +1 -1
  167. package/lib/typescript/module/types.d.ts.map +1 -1
  168. package/lib/typescript/module/updateError.d.ts +26 -0
  169. package/lib/typescript/module/updateError.d.ts.map +1 -0
  170. package/lib/typescript/module/wrap.d.ts.map +1 -1
  171. package/lib/typescript/module/wrap.types.d.ts +6 -7
  172. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  173. package/package.json +7 -4
  174. package/src/appReady.ts +149 -0
  175. package/src/checkForUpdate.ts +151 -88
  176. package/src/clientPlugin.ts +18 -0
  177. package/src/error.ts +4 -14
  178. package/src/fetchJSON.ts +14 -30
  179. package/src/fetchUpdateResponse.ts +54 -0
  180. package/src/httpClient.ts +88 -159
  181. package/src/index.ts +90 -24
  182. package/src/native.ts +106 -102
  183. package/src/pluginHost.ts +45 -0
  184. package/src/releaseCatalogCache.ts +43 -172
  185. package/src/sdkVersion.ts +1 -1
  186. package/src/specs/NativeHotUpdater.ts +41 -48
  187. package/src/store.ts +2 -23
  188. package/src/types.ts +1 -1
  189. package/src/updateError.ts +153 -0
  190. package/src/wrap.tsx +57 -26
  191. package/src/wrap.types.ts +6 -7
  192. package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
  193. package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
  194. package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
  195. package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
  196. package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
  197. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +0 -46
  198. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -799
  199. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -273
  200. package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
  201. package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -111
  202. package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
  203. package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
  204. package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
  205. package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
  206. package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
  207. package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
  208. package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
  209. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
  210. package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
  211. package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
  212. package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
  213. package/lib/commonjs/catalogCacheNative.spec.js +0 -31
  214. package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
  215. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -615
  216. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  217. package/lib/commonjs/httpClient.spec.js +0 -102
  218. package/lib/commonjs/httpClient.spec.js.map +0 -1
  219. package/lib/commonjs/index.spec.js +0 -302
  220. package/lib/commonjs/index.spec.js.map +0 -1
  221. package/lib/commonjs/iosInterop.spec.js +0 -18
  222. package/lib/commonjs/iosInterop.spec.js.map +0 -1
  223. package/lib/commonjs/native.spec.js +0 -883
  224. package/lib/commonjs/native.spec.js.map +0 -1
  225. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -218
  226. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  227. package/lib/commonjs/notifyAppReadyInsights.js +0 -133
  228. package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
  229. package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -246
  230. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
  231. package/lib/commonjs/notifyAppReadyInsights.test-utils.js +0 -24
  232. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
  233. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -117
  234. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
  235. package/lib/commonjs/releaseCatalogCache.spec.js +0 -195
  236. package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
  237. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
  238. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  239. package/lib/commonjs/store.notification.spec.js +0 -76
  240. package/lib/commonjs/store.notification.spec.js.map +0 -1
  241. package/lib/commonjs/store.spec.js +0 -316
  242. package/lib/commonjs/store.spec.js.map +0 -1
  243. package/lib/commonjs/wrap.spec.js +0 -329
  244. package/lib/commonjs/wrap.spec.js.map +0 -1
  245. package/lib/module/catalogCacheNative.spec.js +0 -31
  246. package/lib/module/catalogCacheNative.spec.js.map +0 -1
  247. package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -614
  248. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  249. package/lib/module/httpClient.spec.js +0 -102
  250. package/lib/module/httpClient.spec.js.map +0 -1
  251. package/lib/module/index.spec.js +0 -301
  252. package/lib/module/index.spec.js.map +0 -1
  253. package/lib/module/iosInterop.spec.js +0 -18
  254. package/lib/module/iosInterop.spec.js.map +0 -1
  255. package/lib/module/native.spec.js +0 -882
  256. package/lib/module/native.spec.js.map +0 -1
  257. package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -217
  258. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  259. package/lib/module/notifyAppReadyInsights.js +0 -128
  260. package/lib/module/notifyAppReadyInsights.js.map +0 -1
  261. package/lib/module/notifyAppReadyInsights.spec.js +0 -245
  262. package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
  263. package/lib/module/notifyAppReadyInsights.test-utils.js +0 -18
  264. package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
  265. package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -116
  266. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
  267. package/lib/module/releaseCatalogCache.spec.js +0 -195
  268. package/lib/module/releaseCatalogCache.spec.js.map +0 -1
  269. package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
  270. package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  271. package/lib/module/store.notification.spec.js +0 -77
  272. package/lib/module/store.notification.spec.js.map +0 -1
  273. package/lib/module/store.spec.js +0 -315
  274. package/lib/module/store.spec.js.map +0 -1
  275. package/lib/module/wrap.spec.js +0 -330
  276. package/lib/module/wrap.spec.js.map +0 -1
  277. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -13
  278. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
  279. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
  280. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  281. package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -13
  282. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
  283. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
  284. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  285. package/src/catalogCacheNative.spec.ts +0 -63
  286. package/src/checkForUpdate.releaseCatalog.spec.ts +0 -611
  287. package/src/httpClient.spec.ts +0 -132
  288. package/src/index.spec.ts +0 -337
  289. package/src/iosInterop.spec.ts +0 -30
  290. package/src/native.spec.ts +0 -945
  291. package/src/notifyAppReadyInsights.boundary.spec.ts +0 -234
  292. package/src/notifyAppReadyInsights.spec.ts +0 -243
  293. package/src/notifyAppReadyInsights.test-utils.ts +0 -31
  294. package/src/notifyAppReadyInsights.transport.spec.ts +0 -130
  295. package/src/notifyAppReadyInsights.ts +0 -194
  296. package/src/releaseCatalogCache.spec.ts +0 -282
  297. package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
  298. package/src/store.notification.spec.ts +0 -92
  299. package/src/store.spec.ts +0 -372
  300. package/src/wrap.spec.ts +0 -406
@@ -0,0 +1,149 @@
1
+ import type { AppReadyResult } from "./clientPlugin";
2
+ import type { HotUpdaterError } from "./error";
3
+ import {
4
+ getActiveUpdateState,
5
+ getBundleId,
6
+ getChannel,
7
+ type LaunchTransition,
8
+ type NotifyAppReadyResult,
9
+ readNotifyAppReady,
10
+ } from "./native";
11
+ import {
12
+ buildPluginEvent,
13
+ dispatchPluginHook,
14
+ emitPluginHook,
15
+ } from "./pluginHost";
16
+
17
+ export type NotifyAppReadyOptions = {
18
+ onNotifyAppReady?: (result: NotifyAppReadyResult) => void;
19
+ onError?: (error: HotUpdaterError | Error | unknown) => void;
20
+ };
21
+
22
+ type RequestAnimationFrame = (callback: (timestamp: number) => void) => number;
23
+
24
+ let didEmitAppReady = false;
25
+ /** Settles once this runtime's launch was read, whether it was reported or failed. */
26
+ let appReadyRead: Promise<unknown> | null = null;
27
+ /** Why the previous main process exited, read with this runtime's launch. */
28
+ let previousProcessExit: string | null = null;
29
+
30
+ /** Android 11+: why the app's previous main process exited, once the launch was read. */
31
+ export const getPreviousProcessExit = (): string | null => previousProcessExit;
32
+
33
+ const waitForNextFrame = () =>
34
+ new Promise<void>((resolve) => {
35
+ const requestAnimationFrame = (
36
+ globalThis as typeof globalThis & {
37
+ requestAnimationFrame?: RequestAnimationFrame;
38
+ }
39
+ )?.requestAnimationFrame;
40
+
41
+ if (requestAnimationFrame) {
42
+ requestAnimationFrame(() => resolve());
43
+ return;
44
+ }
45
+
46
+ void Promise.resolve().then(resolve);
47
+ });
48
+
49
+ /**
50
+ * The Release the running bundle was selected as: the active or stable
51
+ * selection that names it on the current channel.
52
+ */
53
+ export const getRunningReleaseId = (
54
+ bundleId: string,
55
+ channel: string,
56
+ ): string | null => {
57
+ const state = getActiveUpdateState();
58
+ return (
59
+ [state.activeSelection, state.stableSelection].find(
60
+ (selection) =>
61
+ selection?.bundleId === bundleId && selection.channel === channel,
62
+ )?.releaseId ?? null
63
+ );
64
+ };
65
+
66
+ const toAppReadyResult = (
67
+ result: NotifyAppReadyResult,
68
+ transition: LaunchTransition | null,
69
+ ): AppReadyResult | null => {
70
+ const channel = getChannel();
71
+ if (result.status === "UNCHANGED") {
72
+ const bundleId = getBundleId();
73
+ return {
74
+ status: "UNCHANGED",
75
+ channel,
76
+ bundleId,
77
+ releaseId: getRunningReleaseId(bundleId, channel),
78
+ previousProcessExit,
79
+ };
80
+ }
81
+ // Native persists the transition with every applied or recovered launch;
82
+ // a report without it cannot say what moved.
83
+ if (transition === null) return null;
84
+ return {
85
+ status: transition.type,
86
+ channel,
87
+ fromBundleId: transition.fromBundleId,
88
+ fromReleaseId: transition.fromReleaseId,
89
+ toBundleId: transition.toBundleId,
90
+ toReleaseId: transition.toReleaseId,
91
+ updateStrategy: transition.updateStrategy,
92
+ previousProcessExit,
93
+ };
94
+ };
95
+
96
+ const notifyAppReady = async (
97
+ options: NotifyAppReadyOptions,
98
+ ): Promise<NotifyAppReadyResult | undefined> => {
99
+ try {
100
+ let nativeReadResult: ReturnType<typeof readNotifyAppReady>;
101
+ do {
102
+ await waitForNextFrame();
103
+ nativeReadResult = readNotifyAppReady();
104
+ } while (nativeReadResult.pending);
105
+
106
+ const { result, transition } = nativeReadResult;
107
+ previousProcessExit = nativeReadResult.previousProcessExit;
108
+ if (!didEmitAppReady) {
109
+ didEmitAppReady = true;
110
+ emitPluginHook("onAppReady", () => toAppReadyResult(result, transition));
111
+ }
112
+
113
+ options.onNotifyAppReady?.(result);
114
+ return result;
115
+ } catch (error) {
116
+ const normalizedError =
117
+ error instanceof Error ? error : new Error(String(error));
118
+ options.onError?.(error);
119
+ console.warn("[HotUpdater] Failed to notify app ready:", normalizedError);
120
+ return undefined;
121
+ }
122
+ };
123
+
124
+ /** Reads this runtime's launch once native finalizes it and reports it to plugins. */
125
+ export const handleNotifyAppReady = (
126
+ options: NotifyAppReadyOptions,
127
+ ): Promise<NotifyAppReadyResult | undefined> => {
128
+ const readiness = notifyAppReady(options);
129
+ appReadyRead ??= readiness;
130
+ return readiness;
131
+ };
132
+
133
+ /**
134
+ * Calls a plugin hook after this runtime's `onAppReady`, so plugins see the
135
+ * launch before anything that followed it. The event is built now, from the
136
+ * state it describes, and only when a plugin listens.
137
+ */
138
+ export const emitAfterAppReady: typeof emitPluginHook = (
139
+ name,
140
+ createPayload,
141
+ ) => {
142
+ const payload = buildPluginEvent(name, createPayload);
143
+ if (payload === null) return;
144
+ if (appReadyRead === null) {
145
+ dispatchPluginHook(name, payload);
146
+ return;
147
+ }
148
+ void appReadyRead.then(() => dispatchPluginHook(name, payload));
149
+ };
@@ -1,17 +1,28 @@
1
1
  import {
2
2
  authorizeReleaseTransition,
3
+ canonicalizeAppVersion,
3
4
  createReleaseSelectionContextHash,
4
5
  encodeChannelKey,
6
+ type ExpectedReleaseCatalogScope,
7
+ hasExpectedReleaseCatalogScope,
5
8
  selectDesiredRelease,
6
- type ArtifactInfo,
7
9
  type PersistedSelectionReceipt,
8
10
  type ReleaseCatalog,
9
- } from "@hot-updater/core";
10
- import { canonicalizeAppVersion } from "@hot-updater/plugin-core";
11
+ } from "@hot-updater/protocol";
11
12
  import { Platform } from "react-native";
12
13
 
14
+ import {
15
+ emitAfterAppReady,
16
+ getPreviousProcessExit,
17
+ getRunningReleaseId,
18
+ } from "./appReady";
19
+ import type {
20
+ ReleaseTransitionKind,
21
+ UpdateErrorResource,
22
+ UpdateErrorStage,
23
+ } from "./clientPlugin";
13
24
  import { HotUpdaterError, StaleReleaseCatalogError } from "./error";
14
- import type { HotUpdaterHttpClient, HotUpdaterHttpSession } from "./httpClient";
25
+ import type { HotUpdaterHttpClient } from "./httpClient";
15
26
  import {
16
27
  acceptReleaseCatalog,
17
28
  commitReleaseSelection,
@@ -22,19 +33,16 @@ import {
22
33
  getCohort,
23
34
  getDefaultChannel,
24
35
  getFingerprintHash,
25
- getInstallId,
26
36
  getMinBundleId,
27
37
  getCrashHistory,
28
- getPersistedUserIdentity,
29
38
  isReleaseSelectionCurrent,
30
39
  isChannelSwitched,
31
- updateBundle,
40
+ stageBundle,
32
41
  } from "./native";
33
- import {
34
- hasExpectedReleaseCatalogScope,
35
- type ExpectedReleaseCatalogScope,
36
- } from "./releaseCatalogCache";
37
42
  import { hotUpdaterStore } from "./store";
43
+ import { classifyUpdateError, InvalidUpdateResponseError } from "./updateError";
44
+
45
+ export type { ReleaseTransitionKind };
38
46
 
39
47
  export interface CheckForUpdateOptions {
40
48
  /**
@@ -60,7 +68,7 @@ export interface CheckForUpdateOptions {
60
68
  requestTimeout?: number;
61
69
  }
62
70
 
63
- export type CheckForUpdateResult = ArtifactInfo & {
71
+ export type CheckForUpdateResult = {
64
72
  readonly id: string;
65
73
  readonly message: string | null;
66
74
  readonly rolloutCohortCount: number;
@@ -76,14 +84,7 @@ export type CheckForUpdateResult = ArtifactInfo & {
76
84
  updateBundle: () => Promise<boolean>;
77
85
  };
78
86
 
79
- export type ReleaseTransitionKind =
80
- | "INSTALL"
81
- | "ADOPT_RELEASE"
82
- | "USE_EMBEDDED"
83
- | "USE_BUILTIN";
84
-
85
87
  export interface InternalCheckForUpdateOptions extends CheckForUpdateOptions {
86
- insights?: boolean;
87
88
  client: HotUpdaterHttpClient;
88
89
  }
89
90
 
@@ -113,7 +114,7 @@ const validateCatalog = (
113
114
  !/^sha256:[0-9a-f]{64}$/.test(catalog.catalogHash) ||
114
115
  !hasExpectedReleaseCatalogScope(catalog, expectedScope)
115
116
  ) {
116
- throw new HotUpdaterError("Received an invalid Release catalog");
117
+ throw new InvalidUpdateResponseError("Received an invalid Release catalog");
117
118
  }
118
119
  };
119
120
 
@@ -121,48 +122,50 @@ const resetProgress = () => {
121
122
  hotUpdaterStore.setState({
122
123
  artifactType: null,
123
124
  details: null,
124
- downloadedBytes: undefined,
125
125
  isUpdateDownloaded: false,
126
126
  progress: 0,
127
- totalBytes: undefined,
128
127
  });
129
128
  };
130
129
 
131
- const notifyReleaseAdoption = async (input: {
132
- readonly active: PersistedSelectionReceipt | null;
133
- readonly desired: PersistedSelectionReceipt;
134
- readonly appVersion: string;
135
- readonly cohort: string;
136
- readonly fingerprintHash: string | null;
137
- readonly platform: "ios" | "android";
138
- readonly session: HotUpdaterHttpSession;
139
- readonly requestHeaders?: Record<string, string>;
140
- readonly requestTimeout?: number;
141
- readonly updateStrategy: "appVersion" | "fingerprint";
142
- }): Promise<void> => {
143
- const { userId, username } = getPersistedUserIdentity();
144
- try {
145
- await input.session.sendInsightsEvent({
146
- appVersion: input.appVersion,
147
- channel: input.desired.channel,
148
- cohort: input.cohort,
149
- fingerprintHash: input.fingerprintHash,
150
- fromBundleId: input.desired.bundleId,
151
- fromReleaseId: input.active?.releaseId ?? null,
152
- installId: getInstallId(),
153
- platform: input.platform,
154
- requestHeaders: input.requestHeaders,
155
- requestTimeout: input.requestTimeout,
156
- toBundleId: input.desired.bundleId,
157
- toReleaseId: input.desired.releaseId,
158
- type: "RELEASE_ADOPTED",
159
- updateStrategy: input.updateStrategy,
160
- ...(userId === undefined ? {} : { userId }),
161
- ...(username === undefined ? {} : { username }),
162
- });
163
- } catch (error) {
164
- console.warn("[HotUpdater] Release adoption insights failed:", error);
165
- }
130
+ /** The running bundle, the channel checked, and the update's target. */
131
+ type UpdateErrorScope = {
132
+ readonly channel: string;
133
+ readonly bundleId: string;
134
+ readonly updateStrategy: "fingerprint" | "appVersion";
135
+ readonly targetBundleId?: string;
136
+ readonly targetReleaseId?: string | null;
137
+ };
138
+
139
+ /**
140
+ * Tells plugins why an update failed, when it failed as an update. An error
141
+ * the SDK's JavaScript raised happened in `stage`, fetching `resource`.
142
+ */
143
+ export const reportUpdateError = (
144
+ error: unknown,
145
+ stage: UpdateErrorStage,
146
+ resource: UpdateErrorResource | undefined,
147
+ scope: UpdateErrorScope,
148
+ ): void => {
149
+ emitAfterAppReady("onUpdateError", () => {
150
+ const classification = classifyUpdateError(error, stage, resource);
151
+ if (classification === null) return null;
152
+ const previousProcessExit = getPreviousProcessExit();
153
+ return {
154
+ ...classification,
155
+ ...(previousProcessExit === null ? {} : { previousProcessExit }),
156
+ channel: scope.channel,
157
+ bundleId: scope.bundleId,
158
+ releaseId: getRunningReleaseId(scope.bundleId, getChannel()),
159
+ updateStrategy: scope.updateStrategy,
160
+ ...(scope.targetBundleId === undefined
161
+ ? {}
162
+ : { targetBundleId: scope.targetBundleId }),
163
+ ...(scope.targetReleaseId == null
164
+ ? {}
165
+ : { targetReleaseId: scope.targetReleaseId }),
166
+ cause: error,
167
+ };
168
+ });
166
169
  };
167
170
 
168
171
  async function checkForReleaseCatalogUpdate(input: {
@@ -213,12 +216,15 @@ async function checkForReleaseCatalogUpdate(input: {
213
216
  requestTimeout: options.requestTimeout,
214
217
  updateStrategy: options.updateStrategy,
215
218
  });
219
+ // The server says this scope has no catalog yet: no update, not a failure.
220
+ if (catalog === null) return null;
216
221
  validateCatalog(catalog, expectedScope);
217
222
  const catalogId = catalog.catalogId;
218
223
  const scopeKey = catalog.scopeKey;
219
224
 
220
225
  const crashedBundleIds = getCrashHistory();
221
- const active = getActiveUpdateState().activeSelection;
226
+ const activeState = getActiveUpdateState();
227
+ const active = activeState.activeSelection;
222
228
  const explicitScopeSwitch =
223
229
  input.explicitChannel !== undefined &&
224
230
  input.explicitChannel !== input.currentChannel;
@@ -265,7 +271,7 @@ async function checkForReleaseCatalogUpdate(input: {
265
271
  scopeKey,
266
272
  })
267
273
  ) {
268
- throw new HotUpdaterError(
274
+ throw new InvalidUpdateResponseError(
269
275
  "Rejected a stale or inconsistent Release catalog",
270
276
  );
271
277
  }
@@ -329,7 +335,20 @@ async function checkForReleaseCatalogUpdate(input: {
329
335
  scopeKey,
330
336
  selectionContextHash,
331
337
  };
332
- const updateBundleForSelection = async (): Promise<boolean> => {
338
+ const fromReleaseId =
339
+ active?.bundleId === input.currentBundleId
340
+ ? active.releaseId
341
+ : activeState.stableSelection?.bundleId === input.currentBundleId
342
+ ? activeState.stableSelection.releaseId
343
+ : null;
344
+ const errorScope = {
345
+ bundleId: input.currentBundleId,
346
+ channel: input.targetChannel,
347
+ targetBundleId: desired.bundleId,
348
+ targetReleaseId: desired.releaseId,
349
+ updateStrategy: options.updateStrategy,
350
+ };
351
+ const installSelection = async (): Promise<boolean> => {
333
352
  resetProgress();
334
353
  if (!isReleaseSelectionCurrent(guard)) {
335
354
  throw new StaleReleaseCatalogError();
@@ -340,19 +359,14 @@ async function checkForReleaseCatalogUpdate(input: {
340
359
  guard,
341
360
  selection: receipt,
342
361
  });
343
- if (committed && transitionKind === "ADOPT_RELEASE" && options.insights) {
344
- await notifyReleaseAdoption({
345
- active,
346
- appVersion: input.currentAppVersion,
347
- cohort: input.cohort,
348
- desired: receipt,
349
- fingerprintHash: input.fingerprintHash,
350
- platform: input.platform,
351
- requestHeaders: options.requestHeaders,
352
- requestTimeout: options.requestTimeout,
353
- session,
354
- updateStrategy: options.updateStrategy,
355
- });
362
+ if (committed && transitionKind === "ADOPT_RELEASE") {
363
+ emitAfterAppReady("onUpdateCheck", () => ({
364
+ status: "UNCHANGED",
365
+ channel: receipt.channel,
366
+ bundleId: receipt.bundleId,
367
+ releaseId: receipt.releaseId,
368
+ previousReleaseId: active?.releaseId ?? null,
369
+ }));
356
370
  }
357
371
  return committed;
358
372
  }
@@ -366,33 +380,64 @@ async function checkForReleaseCatalogUpdate(input: {
366
380
  if (!isReleaseSelectionCurrent(guard)) {
367
381
  throw new StaleReleaseCatalogError();
368
382
  }
369
- return updateBundle({
383
+ const delivery = await stageBundle({
384
+ assets: artifact.assets,
370
385
  bundleId: desired.bundleId,
371
- changedAssets: artifact.changedAssets ?? null,
372
386
  channel: input.targetChannel,
373
- fileHash: artifact.fileHash,
374
- fileUrl: artifact.fileUrl,
375
- manifestFileHash: artifact.manifestFileHash ?? null,
376
- manifestUrl: artifact.manifestUrl ?? null,
387
+ manifestFileHash: artifact.manifestFileHash,
388
+ manifestUrl: artifact.manifestUrl,
389
+ ...(artifact.archiveUrl ? { archiveUrl: artifact.archiveUrl } : {}),
377
390
  selection: receipt,
378
391
  shouldSkipCurrentBundleIdCheck: true,
379
392
  status: desired.status,
380
393
  });
394
+ if (delivery !== null && desired.bundleId !== input.currentBundleId) {
395
+ emitAfterAppReady("onBundleDownloaded", () => ({
396
+ channel: input.targetChannel,
397
+ fromBundleId: input.currentBundleId,
398
+ fromReleaseId,
399
+ toBundleId: desired.bundleId,
400
+ toReleaseId: desired.releaseId,
401
+ updateStrategy: options.updateStrategy,
402
+ ...delivery,
403
+ }));
404
+ }
405
+ return true;
406
+ };
407
+ const updateBundleForSelection = async (): Promise<boolean> => {
408
+ try {
409
+ return await installSelection();
410
+ } catch (error) {
411
+ reportUpdateError(error, "download", "artifact", errorScope);
412
+ throw error;
413
+ }
381
414
  };
382
415
 
416
+ const shouldForceUpdate =
417
+ transitionKind === "ADOPT_RELEASE"
418
+ ? false
419
+ : desired.status === "ROLLBACK"
420
+ ? true
421
+ : (release?.shouldForceUpdate ?? false);
422
+ emitAfterAppReady("onUpdateCheck", () => ({
423
+ status: "UPDATE_AVAILABLE",
424
+ channel: input.targetChannel,
425
+ fromBundleId: input.currentBundleId,
426
+ fromReleaseId,
427
+ toBundleId: desired.bundleId,
428
+ toReleaseId: desired.releaseId,
429
+ transitionKind,
430
+ updateStatus: desired.status,
431
+ shouldForceUpdate,
432
+ updateStrategy: options.updateStrategy,
433
+ }));
434
+
383
435
  return {
384
- fileHash: null,
385
- fileUrl: null,
386
436
  id: desired.releaseId ?? desired.bundleId,
387
437
  message: release?.message ?? null,
388
438
  releaseId: desired.releaseId,
389
439
  rolloutCohortCount: release?.rolloutCohortCount ?? 1000,
390
- shouldForceUpdate:
391
- transitionKind === "ADOPT_RELEASE"
392
- ? false
393
- : desired.status === "ROLLBACK"
394
- ? true
395
- : (release?.shouldForceUpdate ?? false),
440
+ shouldForceUpdate,
396
441
  status: desired.status,
397
442
  targetCohorts: release?.targetCohorts ? [...release.targetCohorts] : [],
398
443
  transitionKind,
@@ -441,7 +486,7 @@ export async function checkForUpdate(
441
486
  const fingerprintHash = getFingerprintHash();
442
487
 
443
488
  try {
444
- return await checkForReleaseCatalogUpdate({
489
+ const result = await checkForReleaseCatalogUpdate({
445
490
  cohort,
446
491
  currentAppVersion,
447
492
  currentBundleId,
@@ -455,7 +500,25 @@ export async function checkForUpdate(
455
500
  platform,
456
501
  targetChannel,
457
502
  });
503
+ if (result === null) {
504
+ emitAfterAppReady("onUpdateCheck", () => {
505
+ const releaseId = getRunningReleaseId(currentBundleId, currentChannel);
506
+ return {
507
+ status: "UNCHANGED",
508
+ channel: currentChannel,
509
+ bundleId: currentBundleId,
510
+ releaseId,
511
+ previousReleaseId: releaseId,
512
+ };
513
+ });
514
+ }
515
+ return result;
458
516
  } catch (error) {
517
+ reportUpdateError(error, "check", "catalog", {
518
+ bundleId: currentBundleId,
519
+ channel: targetChannel,
520
+ updateStrategy: options.updateStrategy,
521
+ });
459
522
  options.onError?.(error as Error);
460
523
  return null;
461
524
  }
@@ -0,0 +1,18 @@
1
+ export {
2
+ defineClientPlugin,
3
+ type AppReadyResult,
4
+ type BundleDownloadedInfo,
5
+ type HotUpdaterClientContext,
6
+ type HotUpdaterClientHooks,
7
+ type HotUpdaterClientPlugin,
8
+ type HotUpdaterClientStorage,
9
+ type ReleaseTransitionKind,
10
+ type UpdateCheckResult,
11
+ type UpdateError,
12
+ type UpdateErrorReason,
13
+ type UpdateErrorResource,
14
+ type UpdateErrorStage,
15
+ type UpdateErrorTransport,
16
+ type UpdateHttpResponse,
17
+ type UpdateStrategy,
18
+ } from "@hot-updater/protocol";
package/src/error.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Error Classification:
9
9
  * - Parameter Validation: Invalid or missing function parameters
10
- * - Bundle Storage: Errors during download, extraction, and storage
10
+ * - Bundle Storage: Errors during download, verification, and storage
11
11
  * - Signature Verification: Cryptographic verification failures (collapsed to a single public code)
12
12
  * - Internal: Platform-specific or unexpected errors
13
13
  *
@@ -27,8 +27,7 @@ export enum HotUpdaterErrorCode {
27
27
  MISSING_BUNDLE_ID = "MISSING_BUNDLE_ID",
28
28
 
29
29
  /**
30
- * File URL is invalid or malformed.
31
- * Thrown when fileUrl parameter cannot be parsed as a valid URL.
30
+ * Manifest or asset URL is invalid or malformed.
32
31
  * @retryable false
33
32
  */
34
33
  INVALID_FILE_URL = "INVALID_FILE_URL",
@@ -58,25 +57,16 @@ export enum HotUpdaterErrorCode {
58
57
  */
59
58
  INCOMPLETE_DOWNLOAD = "INCOMPLETE_DOWNLOAD",
60
59
 
61
- /**
62
- * Bundle archive format is invalid or corrupted.
63
- * Thrown when ZIP file has wrong magic bytes, invalid structure, or unsupported format.
64
- * Also thrown for path traversal attempts during extraction.
65
- * @retryable false - Indicates corrupted or malicious bundle
66
- */
67
- EXTRACTION_FORMAT_ERROR = "EXTRACTION_FORMAT_ERROR",
68
-
69
60
  /**
70
61
  * Bundle missing required platform files.
71
- * Thrown when extracted bundle doesn't contain index.android.bundle (Android)
62
+ * Thrown when the staged bundle doesn't contain index.android.bundle (Android)
72
63
  * or main.jsbundle (iOS).
73
64
  * @retryable false - Indicates incorrectly built bundle
74
65
  */
75
66
  INVALID_BUNDLE = "INVALID_BUNDLE",
76
67
 
77
68
  /**
78
- * Insufficient disk space for bundle download and extraction.
79
- * Thrown when available disk space is less than required (file size * 2).
69
+ * Insufficient disk space for bundle staging.
80
70
  * Error message includes required and available bytes.
81
71
  * @retryable false - User must free up disk space
82
72
  */
package/src/fetchJSON.ts CHANGED
@@ -1,33 +1,17 @@
1
- export const fetchJSON = async <T>({
2
- url,
3
- requestHeaders,
4
- requestTimeout = 5000,
5
- }: {
6
- url: string;
7
- requestHeaders?: Record<string, string>;
8
- requestTimeout?: number;
9
- }): Promise<T> => {
10
- const controller = new AbortController();
11
- const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
1
+ import { fetchUpdateResponse, type UpdateRequest } from "./fetchUpdateResponse";
2
+ import { UpdateHttpError } from "./updateError";
12
3
 
13
- try {
14
- const response = await fetch(url, {
15
- headers: {
16
- Accept: "application/json",
17
- ...requestHeaders,
18
- },
19
- signal: controller.signal,
20
- });
21
- if (response.status !== 200) {
22
- throw new Error(response.statusText);
23
- }
24
- return (await response.json()) as T;
25
- } catch (error: unknown) {
26
- if (error instanceof Error && error.name === "AbortError") {
27
- throw new Error("Request timed out");
28
- }
29
- throw error;
30
- } finally {
31
- clearTimeout(timeoutId);
4
+ export class FetchJSONResponseError extends UpdateHttpError {}
5
+
6
+ export const fetchJSON = async <T>(
7
+ input: Omit<UpdateRequest, "resource">,
8
+ ): Promise<T> => {
9
+ const { response, body } = await fetchUpdateResponse({
10
+ ...input,
11
+ resource: "artifact",
12
+ });
13
+ if (response.status !== 200) {
14
+ throw new FetchJSONResponseError(response.status, response.statusText);
32
15
  }
16
+ return JSON.parse(body!) as T;
33
17
  };
@@ -0,0 +1,54 @@
1
+ import type { UpdateHttpResponse } from "@hot-updater/protocol";
2
+
3
+ export interface UpdateRequest {
4
+ readonly url: string;
5
+ readonly resource: UpdateHttpResponse["resource"];
6
+ readonly requestHeaders?: Record<string, string>;
7
+ readonly requestTimeout?: number;
8
+ readonly onResponse?: (response: UpdateHttpResponse) => void;
9
+ }
10
+
11
+ /** Read once for parsing and diagnostics, under the same request timeout. */
12
+ export const fetchUpdateResponse = async ({
13
+ url,
14
+ resource,
15
+ requestHeaders,
16
+ requestTimeout = 5000,
17
+ onResponse,
18
+ }: UpdateRequest): Promise<{ response: Response; body: string | null }> => {
19
+ const controller = new AbortController();
20
+ const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
21
+ try {
22
+ const headers = new Headers(requestHeaders);
23
+ if (!headers.has("Accept")) headers.set("Accept", "application/json");
24
+ const response = await fetch(url, {
25
+ headers,
26
+ signal: controller.signal,
27
+ });
28
+ let body: string | null = null;
29
+ try {
30
+ body = await response.text();
31
+ } catch (error) {
32
+ // A broken error body must not hide an HTTP error already received.
33
+ if (response.status === 200) throw error;
34
+ } finally {
35
+ onResponse?.({
36
+ resource,
37
+ // React Native URL implementations do not all expose pathname.
38
+ path:
39
+ url.replace(/^https?:\/\/[^/?#]*/i, "").split(/[?#]/, 1)[0] || "/",
40
+ status: response.status,
41
+ body,
42
+ bodyTruncated: false,
43
+ });
44
+ }
45
+ return { response, body };
46
+ } catch (error: unknown) {
47
+ if (error instanceof Error && error.name === "AbortError") {
48
+ throw new Error("Request timed out");
49
+ }
50
+ throw error;
51
+ } finally {
52
+ clearTimeout(timeoutId);
53
+ }
54
+ };