@hot-updater/react-native 1.0.0-rc.4 → 1.0.0-rc.41

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 (317) hide show
  1. package/android/build.gradle +13 -2
  2. package/android/proguard-rules.pro +8 -2
  3. package/android/src/bridge/BridgeReactInstance.kt +38 -0
  4. package/android/src/bridgeless/BridgeReactInstance.kt +24 -0
  5. package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +94 -0
  6. package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
  7. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +1064 -844
  8. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +50 -11
  9. package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
  10. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +287 -22
  11. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +46 -30
  12. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +75 -192
  13. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
  14. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
  15. package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
  16. package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
  17. package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
  18. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +127 -50
  19. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
  20. package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
  21. package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
  22. package/android/src/newarch/HotUpdater.kt +7 -9
  23. package/android/src/newarch/HotUpdaterModule.kt +109 -84
  24. package/android/src/newarch/JavaScriptExceptionHooks.kt +48 -0
  25. package/android/src/newarch/ReactIntegrationManager.kt +6 -32
  26. package/android/src/oldarch/HotUpdater.kt +7 -9
  27. package/android/src/oldarch/HotUpdaterModule.kt +98 -74
  28. package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
  29. package/android/src/oldarch/JavaScriptExceptionHooks.kt +71 -0
  30. package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
  31. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
  32. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +1032 -1116
  33. package/ios/HotUpdater/Internal/BundleMetadata.swift +61 -54
  34. package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
  35. package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
  36. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
  37. package/ios/HotUpdater/Internal/HotUpdater.mm +32 -67
  38. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +229 -73
  39. package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
  40. package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
  41. package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
  42. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +119 -71
  43. package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
  44. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
  45. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
  46. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
  47. package/lib/commonjs/appReady.js +96 -0
  48. package/lib/commonjs/appReady.js.map +1 -0
  49. package/lib/commonjs/checkForUpdate.js +121 -69
  50. package/lib/commonjs/checkForUpdate.js.map +1 -1
  51. package/lib/commonjs/clientPlugin.js +13 -0
  52. package/lib/commonjs/clientPlugin.js.map +1 -0
  53. package/lib/commonjs/error.js +5 -14
  54. package/lib/commonjs/error.js.map +1 -1
  55. package/lib/commonjs/fetchJSON.js +16 -27
  56. package/lib/commonjs/fetchJSON.js.map +1 -1
  57. package/lib/commonjs/fetchUpdateResponse.js +60 -0
  58. package/lib/commonjs/fetchUpdateResponse.js.map +1 -0
  59. package/lib/commonjs/httpClient.js +64 -95
  60. package/lib/commonjs/httpClient.js.map +1 -1
  61. package/lib/commonjs/index.js +355 -324
  62. package/lib/commonjs/index.js.map +1 -1
  63. package/lib/commonjs/init.types.js +6 -0
  64. package/lib/commonjs/init.types.js.map +1 -0
  65. package/lib/commonjs/native.js +77 -63
  66. package/lib/commonjs/native.js.map +1 -1
  67. package/lib/commonjs/pluginHost.js +41 -0
  68. package/lib/commonjs/pluginHost.js.map +1 -0
  69. package/lib/commonjs/releaseCatalogCache.js +36 -87
  70. package/lib/commonjs/releaseCatalogCache.js.map +1 -1
  71. package/lib/commonjs/sdkVersion.js +1 -1
  72. package/lib/commonjs/sdkVersion.js.map +1 -1
  73. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  74. package/lib/commonjs/store.js +3 -10
  75. package/lib/commonjs/store.js.map +1 -1
  76. package/lib/commonjs/updateError.js +145 -0
  77. package/lib/commonjs/updateError.js.map +1 -0
  78. package/lib/commonjs/wrap.js +46 -38
  79. package/lib/commonjs/wrap.js.map +1 -1
  80. package/lib/module/appReady.js +90 -0
  81. package/lib/module/appReady.js.map +1 -0
  82. package/lib/module/checkForUpdate.js +114 -64
  83. package/lib/module/checkForUpdate.js.map +1 -1
  84. package/lib/module/clientPlugin.js +4 -0
  85. package/lib/module/clientPlugin.js.map +1 -0
  86. package/lib/module/error.js +5 -14
  87. package/lib/module/error.js.map +1 -1
  88. package/lib/module/fetchJSON.js +14 -26
  89. package/lib/module/fetchJSON.js.map +1 -1
  90. package/lib/module/fetchUpdateResponse.js +55 -0
  91. package/lib/module/fetchUpdateResponse.js.map +1 -0
  92. package/lib/module/httpClient.js +63 -94
  93. package/lib/module/httpClient.js.map +1 -1
  94. package/lib/module/index.js +335 -327
  95. package/lib/module/index.js.map +1 -1
  96. package/lib/module/init.types.js +4 -0
  97. package/lib/module/init.types.js.map +1 -0
  98. package/lib/module/native.js +70 -56
  99. package/lib/module/native.js.map +1 -1
  100. package/lib/module/pluginHost.js +36 -0
  101. package/lib/module/pluginHost.js.map +1 -0
  102. package/lib/module/releaseCatalogCache.js +32 -82
  103. package/lib/module/releaseCatalogCache.js.map +1 -1
  104. package/lib/module/sdkVersion.js +1 -1
  105. package/lib/module/sdkVersion.js.map +1 -1
  106. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  107. package/lib/module/store.js +3 -10
  108. package/lib/module/store.js.map +1 -1
  109. package/lib/module/updateError.js +139 -0
  110. package/lib/module/updateError.js.map +1 -0
  111. package/lib/module/wrap.js +47 -38
  112. package/lib/module/wrap.js.map +1 -1
  113. package/lib/typescript/commonjs/appReady.d.ts +30 -0
  114. package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/checkForUpdate.d.ts +20 -6
  116. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/clientPlugin.d.ts +2 -0
  118. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
  119. package/lib/typescript/commonjs/error.d.ts +5 -14
  120. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/fetchJSON.d.ts +5 -5
  122. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts +14 -0
  124. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/httpClient.d.ts +5 -34
  126. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/index.d.ts +124 -116
  128. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/init.types.d.ts +21 -0
  130. package/lib/typescript/commonjs/init.types.d.ts.map +1 -0
  131. package/lib/typescript/commonjs/native.d.ts +32 -25
  132. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/pluginHost.d.ts +8 -0
  134. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +8 -14
  136. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  138. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
  139. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +35 -47
  140. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/store.d.ts +1 -3
  142. package/lib/typescript/commonjs/store.d.ts.map +1 -1
  143. package/lib/typescript/commonjs/types.d.ts +1 -1
  144. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/updateError.d.ts +26 -0
  146. package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
  147. package/lib/typescript/commonjs/wrap.d.ts +2 -3
  148. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/wrap.types.d.ts +13 -41
  150. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  151. package/lib/typescript/module/appReady.d.ts +30 -0
  152. package/lib/typescript/module/appReady.d.ts.map +1 -0
  153. package/lib/typescript/module/checkForUpdate.d.ts +20 -6
  154. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  155. package/lib/typescript/module/clientPlugin.d.ts +2 -0
  156. package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
  157. package/lib/typescript/module/error.d.ts +5 -14
  158. package/lib/typescript/module/error.d.ts.map +1 -1
  159. package/lib/typescript/module/fetchJSON.d.ts +5 -5
  160. package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
  161. package/lib/typescript/module/fetchUpdateResponse.d.ts +14 -0
  162. package/lib/typescript/module/fetchUpdateResponse.d.ts.map +1 -0
  163. package/lib/typescript/module/httpClient.d.ts +5 -34
  164. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  165. package/lib/typescript/module/index.d.ts +124 -116
  166. package/lib/typescript/module/index.d.ts.map +1 -1
  167. package/lib/typescript/module/init.types.d.ts +21 -0
  168. package/lib/typescript/module/init.types.d.ts.map +1 -0
  169. package/lib/typescript/module/native.d.ts +32 -25
  170. package/lib/typescript/module/native.d.ts.map +1 -1
  171. package/lib/typescript/module/pluginHost.d.ts +8 -0
  172. package/lib/typescript/module/pluginHost.d.ts.map +1 -0
  173. package/lib/typescript/module/releaseCatalogCache.d.ts +8 -14
  174. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
  175. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  176. package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
  177. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +35 -47
  178. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  179. package/lib/typescript/module/store.d.ts +1 -3
  180. package/lib/typescript/module/store.d.ts.map +1 -1
  181. package/lib/typescript/module/types.d.ts +1 -1
  182. package/lib/typescript/module/types.d.ts.map +1 -1
  183. package/lib/typescript/module/updateError.d.ts +26 -0
  184. package/lib/typescript/module/updateError.d.ts.map +1 -0
  185. package/lib/typescript/module/wrap.d.ts +2 -3
  186. package/lib/typescript/module/wrap.d.ts.map +1 -1
  187. package/lib/typescript/module/wrap.types.d.ts +13 -41
  188. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  189. package/package.json +8 -4
  190. package/src/appReady.ts +147 -0
  191. package/src/checkForUpdate.ts +156 -89
  192. package/src/clientPlugin.ts +21 -0
  193. package/src/error.ts +5 -15
  194. package/src/fetchJSON.ts +14 -30
  195. package/src/fetchUpdateResponse.ts +60 -0
  196. package/src/httpClient.ts +89 -160
  197. package/src/index.ts +413 -425
  198. package/src/init.types.ts +24 -0
  199. package/src/native.ts +101 -105
  200. package/src/pluginHost.ts +52 -0
  201. package/src/releaseCatalogCache.ts +43 -172
  202. package/src/sdkVersion.ts +1 -1
  203. package/src/specs/NativeHotUpdater.ts +34 -48
  204. package/src/store.ts +2 -23
  205. package/src/types.ts +1 -1
  206. package/src/updateError.ts +167 -0
  207. package/src/wrap.tsx +60 -43
  208. package/src/wrap.types.ts +18 -46
  209. package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
  210. package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
  211. package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
  212. package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
  213. package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
  214. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +0 -46
  215. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -799
  216. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -273
  217. package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
  218. package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -111
  219. package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
  220. package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
  221. package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
  222. package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
  223. package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
  224. package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
  225. package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
  226. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
  227. package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
  228. package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
  229. package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
  230. package/lib/commonjs/catalogCacheNative.spec.js +0 -31
  231. package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
  232. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -616
  233. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  234. package/lib/commonjs/httpClient.spec.js +0 -102
  235. package/lib/commonjs/httpClient.spec.js.map +0 -1
  236. package/lib/commonjs/index.spec.js +0 -302
  237. package/lib/commonjs/index.spec.js.map +0 -1
  238. package/lib/commonjs/iosInterop.spec.js +0 -18
  239. package/lib/commonjs/iosInterop.spec.js.map +0 -1
  240. package/lib/commonjs/native.spec.js +0 -883
  241. package/lib/commonjs/native.spec.js.map +0 -1
  242. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -218
  243. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  244. package/lib/commonjs/notifyAppReadyInsights.js +0 -133
  245. package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
  246. package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -246
  247. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
  248. package/lib/commonjs/notifyAppReadyInsights.test-utils.js +0 -24
  249. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
  250. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -117
  251. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
  252. package/lib/commonjs/releaseCatalogCache.spec.js +0 -195
  253. package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
  254. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
  255. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  256. package/lib/commonjs/store.notification.spec.js +0 -76
  257. package/lib/commonjs/store.notification.spec.js.map +0 -1
  258. package/lib/commonjs/store.spec.js +0 -316
  259. package/lib/commonjs/store.spec.js.map +0 -1
  260. package/lib/commonjs/wrap.spec.js +0 -329
  261. package/lib/commonjs/wrap.spec.js.map +0 -1
  262. package/lib/module/catalogCacheNative.spec.js +0 -31
  263. package/lib/module/catalogCacheNative.spec.js.map +0 -1
  264. package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -615
  265. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  266. package/lib/module/httpClient.spec.js +0 -102
  267. package/lib/module/httpClient.spec.js.map +0 -1
  268. package/lib/module/index.spec.js +0 -301
  269. package/lib/module/index.spec.js.map +0 -1
  270. package/lib/module/iosInterop.spec.js +0 -18
  271. package/lib/module/iosInterop.spec.js.map +0 -1
  272. package/lib/module/native.spec.js +0 -882
  273. package/lib/module/native.spec.js.map +0 -1
  274. package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -217
  275. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  276. package/lib/module/notifyAppReadyInsights.js +0 -128
  277. package/lib/module/notifyAppReadyInsights.js.map +0 -1
  278. package/lib/module/notifyAppReadyInsights.spec.js +0 -245
  279. package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
  280. package/lib/module/notifyAppReadyInsights.test-utils.js +0 -18
  281. package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
  282. package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -116
  283. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
  284. package/lib/module/releaseCatalogCache.spec.js +0 -195
  285. package/lib/module/releaseCatalogCache.spec.js.map +0 -1
  286. package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
  287. package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  288. package/lib/module/store.notification.spec.js +0 -77
  289. package/lib/module/store.notification.spec.js.map +0 -1
  290. package/lib/module/store.spec.js +0 -315
  291. package/lib/module/store.spec.js.map +0 -1
  292. package/lib/module/wrap.spec.js +0 -330
  293. package/lib/module/wrap.spec.js.map +0 -1
  294. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -13
  295. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
  296. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
  297. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  298. package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -13
  299. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
  300. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
  301. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  302. package/src/catalogCacheNative.spec.ts +0 -63
  303. package/src/checkForUpdate.releaseCatalog.spec.ts +0 -612
  304. package/src/httpClient.spec.ts +0 -132
  305. package/src/index.spec.ts +0 -337
  306. package/src/iosInterop.spec.ts +0 -30
  307. package/src/native.spec.ts +0 -945
  308. package/src/notifyAppReadyInsights.boundary.spec.ts +0 -234
  309. package/src/notifyAppReadyInsights.spec.ts +0 -243
  310. package/src/notifyAppReadyInsights.test-utils.ts +0 -31
  311. package/src/notifyAppReadyInsights.transport.spec.ts +0 -130
  312. package/src/notifyAppReadyInsights.ts +0 -194
  313. package/src/releaseCatalogCache.spec.ts +0 -282
  314. package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
  315. package/src/store.notification.spec.ts +0 -92
  316. package/src/store.spec.ts +0 -372
  317. package/src/wrap.spec.ts +0 -406
@@ -1,5 +1,6 @@
1
1
  import Foundation
2
2
  import React
3
+ import UIKit
3
4
 
4
5
  private func hotUpdaterUncaughtExceptionHandler(_ exception: NSException) {
5
6
  HotUpdaterRecoveryManager.shared.handleUncaughtException(exception)
@@ -17,6 +18,18 @@ private func hotUpdaterUpdateSignalLaunchStateSymbol(
17
18
  @_silgen_name("HotUpdaterPerformRecoveryReload")
18
19
  private func hotUpdaterPerformRecoveryReloadSymbol() -> ObjCBool
19
20
 
21
+ @_silgen_name("HotUpdaterMarkFailedRuntime")
22
+ private func hotUpdaterMarkFailedRuntime()
23
+
24
+ @_silgen_name("HotUpdaterIsFailedRuntime")
25
+ private func hotUpdaterIsFailedRuntime() -> ObjCBool
26
+
27
+ @_silgen_name("HotUpdaterInstallRecoveryHooks")
28
+ private func hotUpdaterInstallRecoveryHooks()
29
+
30
+ @_silgen_name("HotUpdaterAfterSurfaceStarts")
31
+ private func hotUpdaterAfterSurfaceStarts(_ action: @convention(block) @escaping () -> Void)
32
+
20
33
  @_silgen_name("HotUpdaterCopyMinBundleId")
21
34
  private func hotUpdaterCopyMinBundleIdSymbol() -> UnsafeMutableRawPointer
22
35
 
@@ -44,11 +57,28 @@ private func hotUpdaterGetMinBundleId() -> String {
44
57
  private let cohortService: CohortService
45
58
  private let recoveryManager: HotUpdaterRecoveryManager
46
59
  private let releaseCatalogCache: ReleaseCatalogCacheService
60
+ private let keyValueStorage: KeyValueStorageService
47
61
  private var currentLaunchSelection: LaunchSelection?
62
+ private var launchStartObservers: [NSObjectProtocol] = []
48
63
 
49
64
  private static let DEFAULT_CHANNEL = "production"
50
65
  private static let CHANNEL_STORAGE_KEY = "HotUpdaterChannel"
51
66
 
67
+ private static func parseSafeByteSize(_ value: Any?) -> Int64? {
68
+ guard let number = value as? NSNumber,
69
+ String(cString: number.objCType) != "c" else {
70
+ return nil
71
+ }
72
+ let doubleValue = number.doubleValue
73
+ guard doubleValue.isFinite,
74
+ doubleValue >= 0,
75
+ doubleValue <= 9_007_199_254_740_991,
76
+ doubleValue.rounded(.towardZero) == doubleValue else {
77
+ return nil
78
+ }
79
+ return Int64(doubleValue)
80
+ }
81
+
52
82
  // MARK: - Initialization
53
83
 
54
84
  /**
@@ -59,12 +89,9 @@ private func hotUpdaterGetMinBundleId() -> String {
59
89
  let isolationKey = HotUpdaterImpl.getIsolationKey()
60
90
  let preferences = VersionedPreferencesService()
61
91
  let downloadService = URLSessionDownloadService()
62
- let decompressService = DecompressService()
63
-
64
92
  let bundleStorage = BundleFileStorageService(
65
93
  fileSystem: fileSystem,
66
94
  downloadService: downloadService,
67
- decompressService: decompressService,
68
95
  preferences: preferences,
69
96
  isolationKey: isolationKey,
70
97
  builtInBundleIdProvider: { HotUpdaterImpl.minBundleId() }
@@ -75,7 +102,10 @@ private func hotUpdaterGetMinBundleId() -> String {
75
102
  bundleStorage: bundleStorage,
76
103
  preferences: preferences,
77
104
  recoveryManager: recoveryManager,
78
- releaseCatalogCache: ReleaseCatalogCacheService()
105
+ releaseCatalogCache: ReleaseCatalogCacheService(),
106
+ keyValueStorage: KeyValueStorageService(
107
+ directory: NoBackupStorage.directory(in: fileSystem)
108
+ )
79
109
  )
80
110
  }
81
111
 
@@ -83,18 +113,23 @@ private func hotUpdaterGetMinBundleId() -> String {
83
113
  * Primary initializer with dependency injection.
84
114
  * @param bundleStorage Service for bundle storage operations
85
115
  * @param preferences Service for preference storage
116
+ * @param keyValueStorage Persistent key-value store for client plugins
86
117
  */
87
118
  internal init(
88
119
  bundleStorage: BundleStorageService,
89
120
  preferences: PreferencesService,
90
121
  recoveryManager: HotUpdaterRecoveryManager,
91
- releaseCatalogCache: ReleaseCatalogCacheService = ReleaseCatalogCacheService()
122
+ releaseCatalogCache: ReleaseCatalogCacheService = ReleaseCatalogCacheService(),
123
+ keyValueStorage: KeyValueStorageService = KeyValueStorageService(
124
+ directory: NoBackupStorage.directory(in: FileManagerService())
125
+ )
92
126
  ) {
93
127
  self.bundleStorage = bundleStorage
94
128
  self.preferences = preferences
95
129
  self.cohortService = CohortService()
96
130
  self.recoveryManager = recoveryManager
97
131
  self.releaseCatalogCache = releaseCatalogCache
132
+ self.keyValueStorage = keyValueStorage
98
133
  super.init()
99
134
 
100
135
  // Configure preferences with isolation key
@@ -219,9 +254,11 @@ private func hotUpdaterGetMinBundleId() -> String {
219
254
  /**
220
255
  * Updates the bundle from JavaScript bridge.
221
256
  * This method acts as the primary error boundary for all bundle operations.
222
- * @param params Dictionary with bundleId and fileUrl parameters
223
- * @param resolve Promise resolve callback
224
- * @param reject Promise reject callback
257
+ * @param params Dictionary with bundleId, manifest, and asset parameters
258
+ * @param resolve Promise resolve callback, called with the staged bundle's
259
+ * `{ delivery, patchFallback }`
260
+ * @param reject Promise reject callback; download and install failures
261
+ * carry `stage`, `reason`, and their details in the error's userInfo
225
262
  */
226
263
  public func updateBundle(_ params: NSDictionary?,
227
264
  resolver resolve: @escaping RCTPromiseResolveBlock,
@@ -236,43 +273,49 @@ private func hotUpdaterGetMinBundleId() -> String {
236
273
  return
237
274
  }
238
275
 
276
+ // Invalid update parameters are an invalid update response.
239
277
  guard let bundleId = data["bundleId"] as? String, !bundleId.isEmpty else {
240
- let error = NSError(domain: "HotUpdater", code: 0,
241
- userInfo: [NSLocalizedDescriptionKey: "Missing or empty 'bundleId'"])
278
+ let error = UpdateFailure.invalidResponse.error(description: "Missing or empty 'bundleId'")
242
279
  reject("MISSING_BUNDLE_ID", error.localizedDescription, error)
243
280
  return
244
281
  }
245
282
 
246
- let fileUrlString = data["fileUrl"] as? String ?? ""
247
-
248
- var fileUrl: URL? = nil
249
- if !fileUrlString.isEmpty {
250
- guard let url = URL(string: fileUrlString) else {
251
- let error = NSError(domain: "HotUpdater", code: 0,
252
- userInfo: [NSLocalizedDescriptionKey: "Invalid 'fileUrl' provided: \(fileUrlString)"])
253
- reject("INVALID_FILE_URL", error.localizedDescription, error)
254
- return
255
- }
256
- fileUrl = url
283
+ guard let manifestFileHash = data["manifestFileHash"] as? String,
284
+ !manifestFileHash.isEmpty else {
285
+ let error = UpdateFailure.invalidResponse.with(resource: .manifest)
286
+ .error(description: "Missing manifest file hash")
287
+ reject("INVALID_MANIFEST", error.localizedDescription, error)
288
+ return
257
289
  }
258
-
259
- // Extract fileHash if provided
260
- let fileHash = data["fileHash"] as? String
261
- let manifestFileHash = data["manifestFileHash"] as? String
262
290
  let channel = data["channel"] as? String
263
291
  let manifestUrlString = data["manifestUrl"] as? String ?? ""
264
- var manifestUrl: URL? = nil
265
- if !manifestUrlString.isEmpty {
266
- guard let url = URL(string: manifestUrlString) else {
267
- let error = NSError(domain: "HotUpdater", code: 0,
268
- userInfo: [NSLocalizedDescriptionKey: "Invalid 'manifestUrl' provided: \(manifestUrlString)"])
292
+ guard let manifestUrl = URL(string: manifestUrlString) else {
293
+ let error = UpdateFailure.invalidResponse.with(resource: .manifest).error(
294
+ description: "Invalid 'manifestUrl' provided: \(manifestUrlString)"
295
+ )
296
+ reject("INVALID_FILE_URL", error.localizedDescription, error)
297
+ return
298
+ }
299
+ let archiveUrl: URL?
300
+ if let archiveUrlString = data["archiveUrl"] as? String {
301
+ guard let parsedArchiveUrl = URL(string: archiveUrlString) else {
302
+ let error = UpdateFailure.invalidResponse.with(resource: .archive).error(
303
+ description: "Invalid 'archiveUrl' provided: \(archiveUrlString)"
304
+ )
269
305
  reject("INVALID_FILE_URL", error.localizedDescription, error)
270
306
  return
271
307
  }
272
- manifestUrl = url
308
+ archiveUrl = parsedArchiveUrl
309
+ } else {
310
+ archiveUrl = nil
311
+ }
312
+ guard let assetsPayload = data["assets"] as? [String: [String: Any]] else {
313
+ let error = UpdateFailure.invalidResponse.with(resource: .manifest)
314
+ .error(description: "Missing manifest assets")
315
+ reject("INVALID_MANIFEST", error.localizedDescription, error)
316
+ return
273
317
  }
274
- let changedAssetsPayload = data["changedAssets"] as? [String: [String: Any]]
275
- let changedAssets = changedAssetsPayload?.reduce(into: [String: ChangedAssetDescriptor]()) { partialResult, entry in
318
+ let assets = assetsPayload.reduce(into: [String: ChangedAssetDescriptor]()) { partialResult, entry in
276
319
  guard let fileHash = entry.value["fileHash"] as? String,
277
320
  !fileHash.isEmpty
278
321
  else {
@@ -296,7 +339,8 @@ private func hotUpdaterGetMinBundleId() -> String {
296
339
  baseBundleId: baseBundleId,
297
340
  baseFileHash: baseFileHash,
298
341
  patchFileHash: patchFileHash,
299
- patchUrl: patchUrl
342
+ patchUrl: patchUrl,
343
+ byteSize: Self.parseSafeByteSize(payload["byteSize"])
300
344
  )
301
345
  }
302
346
  let filePayload = entry.value["file"] as? [String: Any]
@@ -315,11 +359,8 @@ private func hotUpdaterGetMinBundleId() -> String {
315
359
  }
316
360
  let selection = (data["selection"] as? [String: Any]).flatMap(Self.parseSelection)
317
361
  if let selection, !bundleStorage.stageReleaseSelection(selection) {
318
- let error = NSError(
319
- domain: "HotUpdater",
320
- code: 0,
321
- userInfo: [NSLocalizedDescriptionKey: "Release catalog selection is stale"]
322
- )
362
+ // Not an update failure, so the rejection carries no stage or reason.
363
+ let error = StaleReleaseSelectionError() as NSError
323
364
  reject("UNKNOWN_ERROR", error.localizedDescription, error)
324
365
  return
325
366
  }
@@ -327,10 +368,10 @@ private func hotUpdaterGetMinBundleId() -> String {
327
368
  // Extract progress callback if provided
328
369
  let progressCallback = data["progressCallback"] as? RCTResponseSenderBlock
329
370
 
330
- NSLog("[HotUpdaterImpl] updateBundle called with bundleId: \(bundleId), fileUrl: \(fileUrl?.absoluteString ?? "nil"), fileHash: \(fileHash ?? "nil")")
371
+ NSLog("[HotUpdaterImpl] updateBundle called with bundleId: \(bundleId), manifestUrl: \(manifestUrl.absoluteString)")
331
372
 
332
373
  // Heavy work is delegated to bundle storage service with safe error handling
333
- bundleStorage.updateBundle(bundleId: bundleId, fileUrl: fileUrl, fileHash: fileHash, manifestUrl: manifestUrl, manifestFileHash: manifestFileHash, changedAssets: changedAssets, progressHandler: { payload in
374
+ bundleStorage.updateBundle(bundleId: bundleId, manifestUrl: manifestUrl, manifestFileHash: manifestFileHash, archiveUrl: archiveUrl, assets: assets, progressHandler: { payload in
334
375
  DispatchQueue.main.async {
335
376
  NotificationCenter.default.post(
336
377
  name: .updateProgressDidChange,
@@ -354,7 +395,7 @@ private func hotUpdaterGetMinBundleId() -> String {
354
395
  // Return results on main thread for React Native bridge
355
396
  DispatchQueue.main.async {
356
397
  switch result {
357
- case .success:
398
+ case .success(let delivery):
358
399
  NSLog("[HotUpdaterImpl] Update successful for \(bundleId). Resolving promise.")
359
400
  if let channel, !channel.isEmpty {
360
401
  do {
@@ -367,12 +408,14 @@ private func hotUpdaterGetMinBundleId() -> String {
367
408
  NSLog("[HotUpdaterImpl] Failed to persist channel override: \(error)")
368
409
  }
369
410
  }
370
- resolve(true)
411
+ // { delivery: "patch" | "manifest" | "archive", patchFallback }
412
+ resolve(delivery.dictionary)
371
413
  case .failure(let error):
372
414
  NSLog("[HotUpdaterImpl] Update failed for \(bundleId) - Error: \(error)")
373
415
 
374
416
  let normalizedCode = HotUpdaterImpl.normalizeErrorCode(from: error)
375
- let nsError = error as NSError
417
+ // A classified failure adds stage, reason, and its details to userInfo.
418
+ let nsError = UpdateFailureError.rejectionError(for: error)
376
419
  reject(normalizedCode, nsError.localizedDescription, nsError)
377
420
  }
378
421
  }
@@ -507,6 +550,8 @@ private func hotUpdaterGetMinBundleId() -> String {
507
550
  * Rare or platform-specific codes are collapsed to UNKNOWN_ERROR to reduce surface area.
508
551
  */
509
552
  private static func normalizeErrorCode(from error: Error) -> String {
553
+ // Classification wraps an error without changing its code.
554
+ let error = UpdateFailureError.unwrap(error)
510
555
  let baseCode: String
511
556
 
512
557
  if let storageError = error as? BundleStorageError {
@@ -532,7 +577,6 @@ private func hotUpdaterGetMinBundleId() -> String {
532
577
  "DIRECTORY_CREATION_FAILED",
533
578
  "DOWNLOAD_FAILED",
534
579
  "INCOMPLETE_DOWNLOAD",
535
- "EXTRACTION_FORMAT_ERROR",
536
580
  "INVALID_BUNDLE",
537
581
  "INSUFFICIENT_DISK_SPACE",
538
582
  "SIGNATURE_VERIFICATION_FAILED",
@@ -545,6 +589,11 @@ private func hotUpdaterGetMinBundleId() -> String {
545
589
  // MARK: - Rollback Support
546
590
 
547
591
  public func notifyAppReady() -> [String: Any] {
592
+ // A failed runtime can finish queued native calls while recovery starts.
593
+ // Keep its readiness loop from consuming the new launch's report.
594
+ if hotUpdaterIsFailedRuntime().boolValue {
595
+ return ["status": "PENDING"]
596
+ }
548
597
  return bundleStorage.notifyAppReady()
549
598
  }
550
599
 
@@ -568,16 +617,21 @@ private func hotUpdaterGetMinBundleId() -> String {
568
617
  return bundleStorage.getInstallId()
569
618
  }
570
619
 
571
- public func getUserId() -> String? {
572
- return bundleStorage.getUserId()
573
- }
620
+ // MARK: - Client Plugin Storage
574
621
 
575
- public func getUsername() -> String? {
576
- return bundleStorage.getUsername()
622
+ /**
623
+ * Reads a value from the persistent key-value store for client plugins.
624
+ * @return The stored value or nil when the key has none
625
+ */
626
+ public func getStorageItem(_ key: String) -> String? {
627
+ return keyValueStorage.getItem(key)
577
628
  }
578
629
 
579
- public func setUser(_ userId: String?, username: String?) {
580
- bundleStorage.setUser(userId: userId, username: username)
630
+ /**
631
+ * Writes a value to the persistent key-value store; nil removes the key.
632
+ */
633
+ public func setStorageItem(_ key: String, value: String?) {
634
+ keyValueStorage.setItem(key, value: value)
581
635
  }
582
636
 
583
637
  /**
@@ -597,7 +651,7 @@ private func hotUpdaterGetMinBundleId() -> String {
597
651
 
598
652
  /**
599
653
  * Gets the current active bundle ID from bundle storage.
600
- * Reads manifest.json first and falls back to the legacy BUNDLE_ID file.
654
+ * Reads the required OTA manifest.json.
601
655
  * Built-in bundle fallback is handled in JS.
602
656
  */
603
657
  public func getBundleId() -> String? {
@@ -651,12 +705,49 @@ private func hotUpdaterGetMinBundleId() -> String {
651
705
 
652
706
  let pendingRecovery = recoveryManager.consumePendingCrashRecovery()
653
707
  let selection = bundleStorage.prepareLaunch(bundle: bundle, pendingRecovery: pendingRecovery)
708
+ markLaunchStartedInForeground(selection)
654
709
  recoveryManager.startMonitoring(bundleId: selection.launchedBundleId, shouldRollback: selection.shouldRollbackOnCrash) { [weak self] launchedBundleId in
655
710
  self?.bundleStorage.markLaunchCompleted(bundleId: launchedBundleId)
656
711
  }
657
712
  currentLaunchSelection = selection
658
713
  return selection
659
714
  }
715
+
716
+ // A background launch, such as a silent push, may never render. Only a
717
+ // launch in the foreground may leave an unfinished launch for the next
718
+ // process to roll back.
719
+ private func markLaunchStartedInForeground(_ selection: LaunchSelection) {
720
+ let bundleId = selection.shouldRollbackOnCrash ? selection.launchedBundleId : nil
721
+ let mark = { [weak self] in
722
+ guard let self else { return }
723
+ self.removeLaunchStartObservers()
724
+ guard let bundleId else { return }
725
+ guard UIApplication.shared.applicationState == .background else {
726
+ self.bundleStorage.markLaunchStarted(bundleId: bundleId)
727
+ return
728
+ }
729
+ self.launchStartObservers = [
730
+ UIApplication.willEnterForegroundNotification,
731
+ UIApplication.didBecomeActiveNotification,
732
+ ].map { name in
733
+ NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
734
+ self?.removeLaunchStartObservers()
735
+ self?.bundleStorage.markLaunchStarted(bundleId: bundleId)
736
+ }
737
+ }
738
+ }
739
+ // UIApplication is read on the main thread.
740
+ if Thread.isMainThread {
741
+ mark()
742
+ } else {
743
+ DispatchQueue.main.async(execute: mark)
744
+ }
745
+ }
746
+
747
+ private func removeLaunchStartObservers() {
748
+ launchStartObservers.forEach { NotificationCenter.default.removeObserver($0) }
749
+ launchStartObservers = []
750
+ }
660
751
  }
661
752
 
662
753
  @objcMembers
@@ -672,6 +763,7 @@ final class HotUpdaterRecoveryManager: NSObject {
672
763
  private var handlersInstalled = false
673
764
  private var isMonitoring = false
674
765
  private var recoveryRequested = false
766
+ private var monitoringGeneration = 0
675
767
  private var currentBundleId: String?
676
768
  private var shouldRollbackOnCrash = false
677
769
  private var contentAppearedCallback: ((String?) -> Void)?
@@ -709,6 +801,10 @@ final class HotUpdaterRecoveryManager: NSObject {
709
801
  shouldRollback: Bool,
710
802
  onContentAppeared: @escaping (String?) -> Void
711
803
  ) {
804
+ objc_sync_enter(self)
805
+ defer { objc_sync_exit(self) }
806
+
807
+ monitoringGeneration += 1
712
808
  currentBundleId = bundleId
713
809
  shouldRollbackOnCrash = shouldRollback
714
810
  recoveryRequested = false
@@ -719,6 +815,7 @@ final class HotUpdaterRecoveryManager: NSObject {
719
815
  stopMonitoringWorkItem = nil
720
816
 
721
817
  installHandlersIfNeeded()
818
+ hotUpdaterInstallRecoveryHooks()
722
819
  registerObservers()
723
820
  installSignalHandlersIfNeeded()
724
821
  hotUpdaterUpdateSignalLaunchState(bundleId, shouldRollback: shouldRollback)
@@ -726,9 +823,8 @@ final class HotUpdaterRecoveryManager: NSObject {
726
823
 
727
824
  func handleUncaughtException(_ exception: NSException) {
728
825
  writeCrashMarker()
729
- if requestRecoveryReloadIfNeeded() {
730
- return
731
- }
826
+ // An uncaught native exception cannot safely continue in this process.
827
+ // Preserve the marker so the next launch can select the safe bundle.
732
828
  previousUncaughtExceptionHandler?(exception)
733
829
  }
734
830
 
@@ -742,16 +838,22 @@ final class HotUpdaterRecoveryManager: NSObject {
742
838
  previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler()
743
839
 
744
840
  RCTSetFatalHandler { [weak self] error in
745
- self?.writeCrashMarker()
746
- if self?.requestRecoveryReloadIfNeeded() != true {
747
- self?.previousFatalHandler?(error)
841
+ guard !hotUpdaterIsFailedRuntime().boolValue else { return }
842
+ if self?.requestRecoveryReloadIfNeeded(onFailure: {
843
+ self?.forwardFatalError(error)
844
+ }) != true {
845
+ self?.writeCrashMarker()
846
+ self?.forwardFatalError(error)
748
847
  }
749
848
  }
750
849
 
751
850
  RCTSetFatalExceptionHandler { [weak self] exception in
752
- self?.writeCrashMarker()
753
- if self?.requestRecoveryReloadIfNeeded() != true {
754
- self?.previousFatalExceptionHandler?(exception)
851
+ guard !hotUpdaterIsFailedRuntime().boolValue else { return }
852
+ if self?.requestRecoveryReloadIfNeeded(onFailure: {
853
+ self?.forwardFatalException(exception)
854
+ }) != true {
855
+ self?.writeCrashMarker()
856
+ self?.forwardFatalException(exception)
755
857
  }
756
858
  }
757
859
 
@@ -759,6 +861,33 @@ final class HotUpdaterRecoveryManager: NSObject {
759
861
  handlersInstalled = true
760
862
  }
761
863
 
864
+ private func forwardFatalError(_ error: Error?) {
865
+ if let previousFatalHandler {
866
+ previousFatalHandler(error)
867
+ } else {
868
+ let handler = RCTGetFatalHandler()
869
+ RCTSetFatalHandler(nil)
870
+ RCTFatal(error)
871
+ // React Native's DEBUG default handler can return.
872
+ if RCTGetFatalHandler() == nil {
873
+ RCTSetFatalHandler(handler)
874
+ }
875
+ }
876
+ }
877
+
878
+ private func forwardFatalException(_ exception: NSException?) {
879
+ if let previousFatalExceptionHandler {
880
+ previousFatalExceptionHandler(exception)
881
+ } else {
882
+ let handler = RCTGetFatalExceptionHandler()
883
+ RCTSetFatalExceptionHandler(nil)
884
+ RCTFatalException(exception)
885
+ if RCTGetFatalExceptionHandler() == nil {
886
+ RCTSetFatalExceptionHandler(handler)
887
+ }
888
+ }
889
+ }
890
+
762
891
  private func installSignalHandlersIfNeeded() {
763
892
  guard !signalHandlersInstalled else {
764
893
  return
@@ -777,7 +906,7 @@ final class HotUpdaterRecoveryManager: NSObject {
777
906
  unregisterObservers()
778
907
  NotificationCenter.default.addObserver(
779
908
  self,
780
- selector: #selector(handleJavaScriptDidFailToLoad),
909
+ selector: #selector(handleJavaScriptDidFailToLoad(_:)),
781
910
  name: NSNotification.Name.RCTJavaScriptDidFailToLoad,
782
911
  object: nil
783
912
  )
@@ -802,15 +931,25 @@ final class HotUpdaterRecoveryManager: NSObject {
802
931
  )
803
932
  }
804
933
 
805
- @objc private func handleJavaScriptDidFailToLoad() {
806
- if requestRecoveryReloadIfNeeded() {
934
+ @objc private func handleJavaScriptDidFailToLoad(_ notification: Notification) {
935
+ let error = notification.userInfo?["error"] as? NSError ?? NSError(
936
+ domain: "HotUpdaterRecovery",
937
+ code: 1,
938
+ userInfo: [NSLocalizedDescriptionKey: "JavaScript bundle failed to load"]
939
+ )
940
+ if requestRecoveryReloadIfNeeded(onFailure: { [weak self] in
941
+ self?.forwardFatalError(error)
942
+ }) {
807
943
  return
808
944
  }
809
945
  unregisterObservers()
810
946
  }
811
947
 
812
948
  @objc private func handleContentDidAppear() {
813
- guard isMonitoring else {
949
+ objc_sync_enter(self)
950
+ defer { objc_sync_exit(self) }
951
+
952
+ guard isMonitoring, !recoveryRequested else {
814
953
  return
815
954
  }
816
955
 
@@ -820,14 +959,19 @@ final class HotUpdaterRecoveryManager: NSObject {
820
959
  hotUpdaterUpdateSignalLaunchState(currentBundleId, shouldRollback: false)
821
960
 
822
961
  stopMonitoringWorkItem?.cancel()
962
+ let generation = monitoringGeneration
823
963
  let workItem = DispatchWorkItem { [weak self] in
824
- self?.finishMonitoring()
964
+ self?.finishMonitoring(generation: generation)
825
965
  }
826
966
  stopMonitoringWorkItem = workItem
827
967
  DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(10), execute: workItem)
828
968
  }
829
969
 
830
- private func finishMonitoring() {
970
+ private func finishMonitoring(generation: Int) {
971
+ objc_sync_enter(self)
972
+ defer { objc_sync_exit(self) }
973
+ guard monitoringGeneration == generation else { return }
974
+
831
975
  isMonitoring = false
832
976
  recoveryRequested = false
833
977
  stopMonitoringWorkItem = nil
@@ -837,31 +981,40 @@ final class HotUpdaterRecoveryManager: NSObject {
837
981
  hotUpdaterUpdateSignalLaunchState(nil, shouldRollback: false)
838
982
  }
839
983
 
840
- private func requestRecoveryReloadIfNeeded() -> Bool {
984
+ private func requestRecoveryReloadIfNeeded(onFailure: @escaping () -> Void) -> Bool {
985
+ objc_sync_enter(self)
986
+ defer { objc_sync_exit(self) }
987
+
841
988
  guard isMonitoring, shouldRollbackOnCrash else {
842
989
  return false
843
990
  }
844
991
 
845
- objc_sync_enter(self)
992
+ hotUpdaterMarkFailedRuntime()
846
993
  if recoveryRequested {
847
- objc_sync_exit(self)
848
994
  return true
849
995
  }
850
996
  recoveryRequested = true
851
- objc_sync_exit(self)
997
+ writeCrashMarker()
852
998
 
853
999
  let bundleId = currentBundleId
854
- DispatchQueue.main.async { [weak self] in
1000
+ let generation = monitoringGeneration
1001
+ hotUpdaterAfterSurfaceStarts { [weak self] in
855
1002
  guard let self else {
856
1003
  return
857
1004
  }
858
1005
 
1006
+ objc_sync_enter(self)
1007
+ let isCurrentLaunch = self.monitoringGeneration == generation
1008
+ objc_sync_exit(self)
1009
+ guard isCurrentLaunch else { return }
1010
+
859
1011
  let started = hotUpdaterPerformRecoveryReload()
860
1012
  if !started {
861
1013
  objc_sync_enter(self)
862
1014
  self.recoveryRequested = false
863
1015
  objc_sync_exit(self)
864
1016
  NSLog("[HotUpdaterRecovery] Failed to trigger recovery reload")
1017
+ onFailure()
865
1018
  } else {
866
1019
  NSLog("[HotUpdaterRecovery] Triggered recovery reload for bundleId=\(bundleId ?? "nil")")
867
1020
  }
@@ -871,6 +1024,9 @@ final class HotUpdaterRecoveryManager: NSObject {
871
1024
  }
872
1025
 
873
1026
  private func writeCrashMarker() {
1027
+ objc_sync_enter(self)
1028
+ defer { objc_sync_exit(self) }
1029
+
874
1030
  guard isMonitoring else {
875
1031
  return
876
1032
  }
@@ -0,0 +1,71 @@
1
+ import Foundation
2
+
3
+ /// Persistent string key-value store for client plugins, kept in
4
+ /// `Application Support/HotUpdater/storage.json` outside device backups.
5
+ ///
6
+ /// Keys are opaque; JavaScript namespaces them per plugin. The file is read
7
+ /// into memory on first use, and every write replaces the whole file.
8
+ final class KeyValueStorageService {
9
+ static let storageFilename = "storage.json"
10
+
11
+ private let fileURL: URL
12
+ private let lock = NSLock()
13
+ private var items: [String: String]?
14
+
15
+ init(directory: URL) {
16
+ self.fileURL = directory.appendingPathComponent(Self.storageFilename)
17
+ }
18
+
19
+ func getItem(_ key: String) -> String? {
20
+ lock.lock()
21
+ defer { lock.unlock() }
22
+ return loadedItems()[key]
23
+ }
24
+
25
+ /// Stores `value` under `key`, or removes the key when `value` is nil.
26
+ /// Returns false when the file could not be written.
27
+ @discardableResult
28
+ func setItem(_ key: String, value: String?) -> Bool {
29
+ lock.lock()
30
+ defer { lock.unlock() }
31
+
32
+ var updatedItems = loadedItems()
33
+ updatedItems[key] = value
34
+ // Memory stays the source of truth for this process, so the next
35
+ // successful write also persists a value whose own write failed.
36
+ items = updatedItems
37
+
38
+ do {
39
+ let data = try JSONSerialization.data(
40
+ withJSONObject: updatedItems,
41
+ options: [.sortedKeys]
42
+ )
43
+ try NoBackupStorage.write(data, to: fileURL)
44
+ return true
45
+ } catch {
46
+ NSLog("[KeyValueStorage] Failed to write \(fileURL.path): \(error)")
47
+ return false
48
+ }
49
+ }
50
+
51
+ private func loadedItems() -> [String: String] {
52
+ if let items {
53
+ return items
54
+ }
55
+ let loadedItems = Self.readItems(from: fileURL)
56
+ items = loadedItems
57
+ return loadedItems
58
+ }
59
+
60
+ /// A missing, unreadable, or corrupt file reads as an empty store.
61
+ private static func readItems(from file: URL) -> [String: String] {
62
+ guard let data = try? Data(contentsOf: file) else {
63
+ return [:]
64
+ }
65
+ guard let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
66
+ NSLog("[KeyValueStorage] Ignoring unreadable store at \(file.path)")
67
+ return [:]
68
+ }
69
+ return object.compactMapValues { $0 as? String }
70
+ }
71
+ }
@@ -1,7 +1,5 @@
1
1
  import Foundation
2
2
 
3
3
  extension Notification.Name {
4
- static let downloadProgressUpdate = Notification.Name("HotUpdaterDownloadProgressUpdate")
5
- static let downloadDidFinish = Notification.Name("HotUpdaterDownloadDidFinish")
6
4
  static let updateProgressDidChange = Notification.Name("HotUpdaterUpdateProgressDidChange")
7
5
  }