@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
@@ -164,6 +164,34 @@ public struct UpdateProgressPayload {
164
164
  }
165
165
  }
166
166
 
167
+ /// How a staged bundle arrived. `updateBundle` resolves with `dictionary`.
168
+ public struct UpdateDelivery: Equatable {
169
+ public enum Method: String {
170
+ /// A bsdiff patch produced at least one file.
171
+ case patch
172
+ /// Only changed files were downloaded, possibly none.
173
+ case manifest
174
+ /// The full tar.br archive was downloaded.
175
+ case archive
176
+ }
177
+
178
+ public let method: Method
179
+ /// A file's patch was tried, but the full file was downloaded instead.
180
+ public let patchFallback: Bool
181
+
182
+ public init(method: Method, patchFallback: Bool) {
183
+ self.method = method
184
+ self.patchFallback = patchFallback
185
+ }
186
+
187
+ public var dictionary: [String: Any] {
188
+ [
189
+ "delivery": method.rawValue,
190
+ "patchFallback": patchFallback,
191
+ ]
192
+ }
193
+ }
194
+
167
195
  public enum BundleStorageError: Error, CustomNSError {
168
196
  case directoryCreationFailed
169
197
  case downloadFailed(Error)
@@ -257,7 +285,7 @@ public protocol BundleStorageService {
257
285
  func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection
258
286
 
259
287
  // Bundle update
260
- func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL?, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<Bool, Error>) -> Void)
288
+ func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL?, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void)
261
289
  func stageReleaseSelection(_ selection: PersistedSelection) -> Bool
262
290
  func acceptReleaseCatalog(catalogId: String, scopeKey: String, generation: Int64, catalogHash: String, channel: String, selectionContextHash: String) -> Bool
263
291
  func getActiveUpdateState() -> [String: Any]
@@ -270,10 +298,7 @@ public protocol BundleStorageService {
270
298
  func getCrashHistory() -> CrashedHistory
271
299
  func clearCrashHistory() -> Bool
272
300
  func getInstallId() -> String
273
- func getUserId() -> String?
274
- func getUsername() -> String?
275
- func setUser(userId: String?, username: String?)
276
-
301
+
277
302
  /**
278
303
  * Gets the base URL for the current active bundle directory
279
304
  * @return Base URL string (e.g., "file:///data/.../bundle-store/abc123") or empty string
@@ -338,6 +363,8 @@ class BundleFileStorageService: BundleStorageService {
338
363
 
339
364
  private let fileSystem: FileSystemService
340
365
  private let downloadService: DownloadService
366
+ /// The wait before a download's second attempt; it doubles for each later one.
367
+ private let downloadRetryDelay: TimeInterval
341
368
  private let preferences: PreferencesService
342
369
  private let isolationKey: String
343
370
 
@@ -354,26 +381,16 @@ class BundleFileStorageService: BundleStorageService {
354
381
  private let releaseStateLock = NSRecursiveLock()
355
382
  private var pendingInstallSelections: [String: PersistedSelection] = [:]
356
383
 
384
+ private let installIdentityLock = NSLock()
357
385
  private var currentInstallIdentity: InstallIdentity?
358
- private var currentUserIdentity: UserIdentity?
359
386
 
360
387
  private let protectedBundleStoreEntries: Set<String> = [
361
388
  BundleMetadata.metadataFilename,
362
389
  CrashedHistory.crashedHistoryFilename,
363
390
  LaunchReport.launchReportFilename,
364
- InstallIdentity.installIdentityFilename,
365
- UserIdentity.userIdentityFilename,
366
391
  "builtin-index-v1.json",
367
392
  ]
368
393
 
369
- private func normalizeIdentityValue(_ value: String?) -> String? {
370
- guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines),
371
- !trimmed.isEmpty else {
372
- return nil
373
- }
374
- return trimmed
375
- }
376
-
377
394
  private func createDiffProgressFiles(
378
395
  changedAssets: [String: ChangedAssetDescriptor]
379
396
  ) -> [UpdateProgressPayload.DiffProgressFileSnapshot] {
@@ -723,9 +740,11 @@ class BundleFileStorageService: BundleStorageService {
723
740
  let fingerprintHash = HotUpdaterConfig.shared.fingerprintHash
724
741
  ?? Bundle.main.object(forInfoDictionaryKey: "HOT_UPDATER_FINGERPRINT_HASH") as? String
725
742
  return (fingerprintHash?.isEmpty == false) ? .fingerprint : .appVersion
726
- }) {
743
+ },
744
+ downloadRetryDelay: TimeInterval = 1) {
727
745
 
728
746
  self.fileSystem = fileSystem
747
+ self.downloadRetryDelay = downloadRetryDelay
729
748
  self.downloadService = downloadService
730
749
  self.preferences = preferences
731
750
  self.isolationKey = isolationKey
@@ -773,18 +792,9 @@ class BundleFileStorageService: BundleStorageService {
773
792
  return URL(fileURLWithPath: storeDir).appendingPathComponent(LaunchReport.launchReportFilename)
774
793
  }
775
794
 
776
- private func installIdentityFileURL() -> URL? {
777
- guard case .success(let storeDir) = bundleStoreDir() else {
778
- return nil
779
- }
780
- return URL(fileURLWithPath: storeDir).appendingPathComponent(InstallIdentity.installIdentityFilename)
781
- }
782
-
783
- private func userIdentityFileURL() -> URL? {
784
- guard case .success(let storeDir) = bundleStoreDir() else {
785
- return nil
786
- }
787
- return URL(fileURLWithPath: storeDir).appendingPathComponent(UserIdentity.userIdentityFilename)
795
+ private func installIdentityFileURL() -> URL {
796
+ NoBackupStorage.directory(in: fileSystem)
797
+ .appendingPathComponent(InstallIdentity.installIdentityFilename)
788
798
  }
789
799
 
790
800
  // MARK: - Metadata Operations
@@ -833,54 +843,28 @@ class BundleFileStorageService: BundleStorageService {
833
843
  _ = report.save(to: file)
834
844
  }
835
845
 
836
- private func loadInstallIdentity() -> InstallIdentity? {
846
+ /// Loads the install identity, creating it once when its file is missing.
847
+ @discardableResult
848
+ private func loadOrCreateInstallIdentity() -> InstallIdentity {
849
+ installIdentityLock.lock()
850
+ defer { installIdentityLock.unlock() }
851
+
837
852
  if let currentInstallIdentity {
838
853
  return currentInstallIdentity
839
854
  }
840
- guard let file = installIdentityFileURL(),
841
- let identity = InstallIdentity.load(from: file) else {
842
- return nil
855
+ let file = installIdentityFileURL()
856
+ if let identity = InstallIdentity.load(from: file) {
857
+ currentInstallIdentity = identity
858
+ return identity
843
859
  }
844
- currentInstallIdentity = identity
845
- return identity
846
- }
847
860
 
848
- private func saveInstallIdentity(_ identity: InstallIdentity) -> Bool {
861
+ let identity = InstallIdentity()
862
+ // Keep the new id for this process even when it cannot be saved.
849
863
  currentInstallIdentity = identity
850
- guard let file = installIdentityFileURL() else {
851
- return false
852
- }
853
- return identity.save(to: file)
854
- }
855
-
856
- private func loadUserIdentity() -> UserIdentity? {
857
- if let currentUserIdentity {
858
- return currentUserIdentity
859
- }
860
- guard let file = userIdentityFileURL(),
861
- let identity = UserIdentity.load(from: file) else {
862
- return nil
863
- }
864
- currentUserIdentity = identity
864
+ _ = identity.save(to: file)
865
865
  return identity
866
866
  }
867
867
 
868
- private func saveUserIdentity(_ identity: UserIdentity?) -> Bool {
869
- currentUserIdentity = identity
870
- guard let file = userIdentityFileURL() else {
871
- return false
872
- }
873
-
874
- guard let identity else {
875
- if FileManager.default.fileExists(atPath: file.path) {
876
- try? FileManager.default.removeItem(at: file)
877
- }
878
- return true
879
- }
880
-
881
- return identity.save(to: file)
882
- }
883
-
884
868
  private func resolveBuiltInBundleId() -> String? {
885
869
  let bundleId = builtInBundleIdProvider().trimmingCharacters(in: .whitespacesAndNewlines)
886
870
  return bundleId.isEmpty ? nil : bundleId
@@ -1636,7 +1620,42 @@ class BundleFileStorageService: BundleStorageService {
1636
1620
  DispatchQueue.global(qos: .background).async(execute: workItem)
1637
1621
  }
1638
1622
 
1623
+ /**
1624
+ * Downloads `url`, trying again after a retryable failure (see
1625
+ * `DownloadError.isRetryable`) up to `DownloadError.maximumAttempts`
1626
+ * attempts in all, 1 and then 2 seconds apart, unless `retrying` is
1627
+ * false. The last attempt's result is returned, so a failure keeps what
1628
+ * classifies it. Blocks the calling thread.
1629
+ */
1639
1630
  private func downloadFileSynchronously(
1631
+ from url: URL,
1632
+ to destination: String,
1633
+ retrying: Bool = true,
1634
+ progressHandler: @escaping (DownloadProgress) -> Void
1635
+ ) -> Result<URL, Error> {
1636
+ var attempt = 1
1637
+ while true {
1638
+ let result = downloadFileOnce(
1639
+ from: url,
1640
+ to: destination,
1641
+ progressHandler: progressHandler
1642
+ )
1643
+ guard case .failure(let error) = result,
1644
+ retrying,
1645
+ attempt < DownloadError.maximumAttempts,
1646
+ DownloadError.isRetryable(error) else {
1647
+ return result
1648
+ }
1649
+ let delay = downloadRetryDelay * pow(2, Double(attempt - 1))
1650
+ NSLog("[BundleStorage] Download failed (attempt \(attempt)/\(DownloadError.maximumAttempts)): \(error.localizedDescription). Retrying in \(delay)s")
1651
+ if delay > 0 {
1652
+ Thread.sleep(forTimeInterval: delay)
1653
+ }
1654
+ attempt += 1
1655
+ }
1656
+ }
1657
+
1658
+ private func downloadFileOnce(
1640
1659
  from url: URL,
1641
1660
  to destination: String,
1642
1661
  progressHandler: @escaping (DownloadProgress) -> Void
@@ -1960,10 +1979,7 @@ class BundleFileStorageService: BundleStorageService {
1960
1979
  func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection {
1961
1980
  builtInAssetResolver.use(bundle: bundle)
1962
1981
  saveLaunchReport(nil)
1963
- if loadInstallIdentity() == nil {
1964
- let identity = InstallIdentity()
1965
- _ = saveInstallIdentity(identity)
1966
- }
1982
+ loadOrCreateInstallIdentity()
1967
1983
  applyPendingRecoveryIfNeeded(pendingRecovery)
1968
1984
  // Only a new storage instance consumes an unfinished launch. Repeated bundle
1969
1985
  // lookups or bridge reloads in this process must not reject its own launch.
@@ -1993,7 +2009,7 @@ class BundleFileStorageService: BundleStorageService {
1993
2009
  * @param progressHandler Callback for download and extraction progress (0.0 to 1.0)
1994
2010
  * @param completion Callback with result of the operation
1995
2011
  */
1996
- func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL? = nil, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<Bool, Error>) -> Void) {
2012
+ func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL? = nil, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void) {
1997
2013
  // Check if bundle is in crashed history
1998
2014
  let crashedHistory = loadCrashedHistory()
1999
2015
  if crashedHistory.contains(bundleId) {
@@ -2005,7 +2021,10 @@ class BundleFileStorageService: BundleStorageService {
2005
2021
  fileOperationQueue.async(flags: .barrier) {
2006
2022
  let storeDirResult = self.bundleStoreDir()
2007
2023
  guard case .success(let storeDir) = storeDirResult else {
2008
- completion(.failure(storeDirResult.failureError ?? BundleStorageError.unknown(nil)))
2024
+ completion(.failure(UpdateFailureError.classifying(
2025
+ storeDirResult.failureError ?? BundleStorageError.unknown(nil),
2026
+ during: .storage
2027
+ )))
2009
2028
  return
2010
2029
  }
2011
2030
  self.updateBundleFromManifest(
@@ -2021,6 +2040,14 @@ class BundleFileStorageService: BundleStorageService {
2021
2040
  }
2022
2041
  }
2023
2042
 
2043
+ /// How the per-file install produced one file.
2044
+ private enum AssetInstallOutcome {
2045
+ case patched
2046
+ /// `patchFallback` is true when a patch for the running bundle was
2047
+ /// offered but the original file was downloaded instead.
2048
+ case downloaded(patchFallback: Bool)
2049
+ }
2050
+
2024
2051
  private func downloadManifestAsset(
2025
2052
  assetPath: String,
2026
2053
  expectedAsset: ParsedManifestAsset,
@@ -2031,7 +2058,7 @@ class BundleFileStorageService: BundleStorageService {
2031
2058
  tempDirectory: String,
2032
2059
  progressFile: UpdateProgressPayload.DiffProgressFileSnapshot,
2033
2060
  progressHandler: @escaping (UpdateProgressPayload) -> Void
2034
- ) throws {
2061
+ ) throws -> AssetInstallOutcome {
2035
2062
  let expectedHash = expectedAsset.fileHash
2036
2063
  var diffFiles = [progressFile]
2037
2064
  let patched = applyPatchAssetIfPossible(
@@ -2046,7 +2073,10 @@ class BundleFileStorageService: BundleStorageService {
2046
2073
  progressHandler: progressHandler
2047
2074
  )
2048
2075
  if patched {
2049
- try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
2076
+ // The patch produced these bytes, so a mismatch is a patch failure.
2077
+ try UpdateFailureError.during(.patch, resource: .patch) {
2078
+ try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
2079
+ }
2050
2080
  updateDiffProgressFile(
2051
2081
  files: &diffFiles,
2052
2082
  assetPath: assetPath,
@@ -2060,7 +2090,7 @@ class BundleFileStorageService: BundleStorageService {
2060
2090
  : "downloading",
2061
2091
  files: diffFiles
2062
2092
  )
2063
- return
2093
+ return .patched
2064
2094
  }
2065
2095
 
2066
2096
  guard let changedAssetFileUrl = changedAsset.fileUrl else {
@@ -2075,10 +2105,14 @@ class BundleFileStorageService: BundleStorageService {
2075
2105
  phase: "downloading",
2076
2106
  files: diffFiles
2077
2107
  )
2078
- throw BundleStorageError.downloadFailed(
2079
- NSError(domain: "HotUpdater", code: 0, userInfo: [
2080
- NSLocalizedDescriptionKey: "Changed asset fileUrl missing and patch could not be applied: \(assetPath)"
2081
- ])
2108
+ // With no original file to fall back to, the patch decides the install.
2109
+ throw UpdateFailureError(
2110
+ underlying: BundleStorageError.downloadFailed(
2111
+ NSError(domain: "HotUpdater", code: 0, userInfo: [
2112
+ NSLocalizedDescriptionKey: "Changed asset fileUrl missing and patch could not be applied: \(assetPath)"
2113
+ ])
2114
+ ),
2115
+ failure: UpdateFailure.patch.with(resource: .patch)
2082
2116
  )
2083
2117
  }
2084
2118
 
@@ -2119,15 +2153,23 @@ class BundleFileStorageService: BundleStorageService {
2119
2153
  ) {
2120
2154
  case .success(let downloadedFileURL):
2121
2155
  do {
2122
- try materializeDownloadedAsset(
2123
- downloadedPath: downloadedFileURL.path,
2124
- destinationPath: destinationPath,
2125
- changedAsset: changedAsset
2126
- )
2127
- try verifyManifestAssetFile(
2128
- atPath: destinationPath,
2129
- asset: expectedAsset
2130
- )
2156
+ try UpdateFailureError.during(.extraction, resource: .file) {
2157
+ try materializeDownloadedAsset(
2158
+ downloadedPath: downloadedFileURL.path,
2159
+ destinationPath: destinationPath,
2160
+ changedAsset: changedAsset
2161
+ )
2162
+ }
2163
+ // Compressed files are checked after decoding, so their
2164
+ // mismatch is an extraction failure.
2165
+ let verificationPhase: UpdatePhase =
2166
+ changedAsset.fileCompression == "br" ? .extraction : .verification
2167
+ try UpdateFailureError.during(verificationPhase, resource: .file) {
2168
+ try verifyManifestAssetFile(
2169
+ atPath: destinationPath,
2170
+ asset: expectedAsset
2171
+ )
2172
+ }
2131
2173
  } catch {
2132
2174
  updateDiffProgressFile(
2133
2175
  files: &diffFiles,
@@ -2155,6 +2197,12 @@ class BundleFileStorageService: BundleStorageService {
2155
2197
  : "downloading",
2156
2198
  files: diffFiles
2157
2199
  )
2200
+ // A patch for the running bundle was offered, but the original
2201
+ // file came instead; a patch for another base was never tried.
2202
+ let patchWasTried = changedAsset.patch.map {
2203
+ $0.algorithm == "bsdiff" && $0.baseBundleId == currentBundleId
2204
+ } ?? false
2205
+ return .downloaded(patchFallback: patchWasTried)
2158
2206
  case .failure(let error):
2159
2207
  let lastKnownProgress = diffFiles.first(where: { $0.path == assetPath })?.progress ?? 0
2160
2208
  updateDiffProgressFile(
@@ -2168,11 +2216,14 @@ class BundleFileStorageService: BundleStorageService {
2168
2216
  phase: "downloading",
2169
2217
  files: diffFiles
2170
2218
  )
2219
+ let transferError: Error
2171
2220
  if let downloadError = error as? DownloadError,
2172
2221
  case .incompleteDownload(let expected, let actual) = downloadError {
2173
- throw BundleStorageError.incompleteDownload(expected: expected, actual: actual)
2222
+ transferError = BundleStorageError.incompleteDownload(expected: expected, actual: actual)
2223
+ } else {
2224
+ transferError = BundleStorageError.downloadFailed(error)
2174
2225
  }
2175
- throw BundleStorageError.downloadFailed(error)
2226
+ throw UpdateFailureError.classifying(transferError, during: .transfer, resource: .file)
2176
2227
  }
2177
2228
  }
2178
2229
 
@@ -2221,9 +2272,11 @@ class BundleFileStorageService: BundleStorageService {
2221
2272
  files: progressFiles
2222
2273
  )
2223
2274
 
2275
+ // One attempt: a failed archive falls back to per-file downloads, which retry.
2224
2276
  switch downloadFileSynchronously(
2225
2277
  from: archiveUrl,
2226
2278
  to: compressedPath,
2279
+ retrying: false,
2227
2280
  progressHandler: { progress in
2228
2281
  self.updateDiffProgressFile(
2229
2282
  files: &progressFiles,
@@ -2254,14 +2307,19 @@ class BundleFileStorageService: BundleStorageService {
2254
2307
  fileURL: URL(fileURLWithPath: compressedPath),
2255
2308
  expectedHash: archive.downloadFileHash
2256
2309
  ) else {
2257
- throw BundleStorageError.invalidBundle
2310
+ throw UpdateFailureError(
2311
+ underlying: BundleStorageError.invalidBundle,
2312
+ failure: .hashMismatch
2313
+ )
2258
2314
  }
2259
2315
 
2260
- try BrotliFileDecompressor.decompress(
2261
- from: compressedPath,
2262
- to: tarPath,
2263
- expectedOutputByteSize: archive.tarByteSize
2264
- )
2316
+ try UpdateFailureError.during(.extraction) {
2317
+ try BrotliFileDecompressor.decompress(
2318
+ from: compressedPath,
2319
+ to: tarPath,
2320
+ expectedOutputByteSize: archive.tarByteSize
2321
+ )
2322
+ }
2265
2323
 
2266
2324
  let expectedFiles = try targetManifest.assets.reduce(
2267
2325
  into: [String: Int64](),
@@ -2272,17 +2330,19 @@ class BundleFileStorageService: BundleStorageService {
2272
2330
  result[entry.key] = byteSize
2273
2331
  }
2274
2332
  )
2275
- try TarArchiveExtractor.extract(
2276
- from: tarPath,
2277
- to: extractedDirectory,
2278
- expectedFiles: expectedFiles
2279
- )
2280
- for (path, asset) in targetManifest.assets {
2281
- let extractedPath = try FileUtilities.fileURL(
2282
- for: path,
2283
- destinationRoot: extractedDirectory
2284
- ).path
2285
- try verifyManifestAssetFile(atPath: extractedPath, asset: asset)
2333
+ try UpdateFailureError.during(.extraction) {
2334
+ try TarArchiveExtractor.extract(
2335
+ from: tarPath,
2336
+ to: extractedDirectory,
2337
+ expectedFiles: expectedFiles
2338
+ )
2339
+ for (path, asset) in targetManifest.assets {
2340
+ let extractedPath = try FileUtilities.fileURL(
2341
+ for: path,
2342
+ destinationRoot: extractedDirectory
2343
+ ).path
2344
+ try verifyManifestAssetFile(atPath: extractedPath, asset: asset)
2345
+ }
2286
2346
  }
2287
2347
 
2288
2348
  try promoteVerifiedArchiveDirectory(
@@ -2380,8 +2440,7 @@ class BundleFileStorageService: BundleStorageService {
2380
2440
  defer { releaseStateLock.unlock() }
2381
2441
  let currentMetadata = loadMetadataOrNull() ?? createInitialMetadata()
2382
2442
  guard let updatedMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId) else {
2383
- throw BundleStorageError.unknown(NSError(domain: "HotUpdater", code: 0,
2384
- userInfo: [NSLocalizedDescriptionKey: "Release catalog selection is stale"]))
2443
+ throw BundleStorageError.unknown(StaleReleaseSelectionError())
2385
2444
  }
2386
2445
  let finalBundlePath = try resolveBundlePathAfterMove(
2387
2446
  bundlePathInStaging, from: stagingDirectory, to: finalDirectory
@@ -2400,8 +2459,11 @@ class BundleFileStorageService: BundleStorageService {
2400
2459
  // Metadata is the durable activation point. Launch never uses an
2401
2460
  // untracked preference if this write fails or the process terminates.
2402
2461
  guard saveMetadata(updatedMetadata) else {
2403
- throw BundleStorageError.unknown(NSError(domain: "HotUpdater", code: 0,
2404
- userInfo: [NSLocalizedDescriptionKey: "Failed to persist bundle activation metadata"]))
2462
+ throw UpdateFailureError(
2463
+ underlying: BundleStorageError.unknown(NSError(domain: "HotUpdater", code: 0,
2464
+ userInfo: [NSLocalizedDescriptionKey: "Failed to persist bundle activation metadata"])),
2465
+ failure: .storage
2466
+ )
2405
2467
  }
2406
2468
  } catch {
2407
2469
  let installError = error
@@ -2425,17 +2487,23 @@ class BundleFileStorageService: BundleStorageService {
2425
2487
  changedAssets: [String: ChangedAssetDescriptor],
2426
2488
  storeDir: String,
2427
2489
  progressHandler: @escaping (UpdateProgressPayload) -> Void,
2428
- completion: @escaping (Result<Bool, Error>) -> Void
2490
+ completion: @escaping (Result<UpdateDelivery, Error>) -> Void
2429
2491
  ) {
2430
2492
  let tempDirResult = tempDir()
2431
2493
  guard case .success(let tempDirectory) = tempDirResult else {
2432
- completion(.failure(tempDirResult.failureError ?? BundleStorageError.unknown(nil)))
2494
+ completion(.failure(UpdateFailureError.classifying(
2495
+ tempDirResult.failureError ?? BundleStorageError.unknown(nil),
2496
+ during: .storage
2497
+ )))
2433
2498
  return
2434
2499
  }
2435
2500
 
2436
2501
  try? self.fileSystem.removeItem(atPath: tempDirectory)
2437
2502
  guard self.fileSystem.createDirectory(atPath: tempDirectory) else {
2438
- completion(.failure(BundleStorageError.directoryCreationFailed))
2503
+ completion(.failure(UpdateFailureError.classifying(
2504
+ BundleStorageError.directoryCreationFailed,
2505
+ during: .storage
2506
+ )))
2439
2507
  return
2440
2508
  }
2441
2509
 
@@ -2447,9 +2515,14 @@ class BundleFileStorageService: BundleStorageService {
2447
2515
  let tmpDir = (storeDir as NSString).appendingPathComponent("\(bundleId).tmp")
2448
2516
  let realDir = (storeDir as NSString).appendingPathComponent(bundleId)
2449
2517
  var diffFiles: [UpdateProgressPayload.DiffProgressFileSnapshot] = []
2518
+ // The running step classifies the errors whose type does not.
2519
+ var failurePhase = UpdatePhase.storage
2520
+ var failureResource: UpdateFailureResource?
2450
2521
 
2451
2522
  do {
2452
2523
  try recoverInterruptedPromotion(finalDirectory: realDir)
2524
+ failurePhase = .transfer
2525
+ failureResource = .manifest
2453
2526
  self.emitDiffProgress(
2454
2527
  progressHandler: progressHandler,
2455
2528
  phase: "manifest",
@@ -2478,6 +2551,7 @@ class BundleFileStorageService: BundleStorageService {
2478
2551
  throw BundleStorageError.downloadFailed(error)
2479
2552
  }
2480
2553
 
2554
+ failurePhase = .verification
2481
2555
  let manifestVerificationResult = SignatureVerifier.verifyBundle(
2482
2556
  fileURL: URL(fileURLWithPath: tempManifestPath),
2483
2557
  fileHash: manifestFileHash
@@ -2489,6 +2563,7 @@ class BundleFileStorageService: BundleStorageService {
2489
2563
  throw BundleStorageError.unknown(nil)
2490
2564
  }
2491
2565
 
2566
+ failurePhase = .manifest
2492
2567
  guard let manifestData = try? Data(contentsOf: URL(fileURLWithPath: tempManifestPath)),
2493
2568
  let manifestJson = try JSONSerialization.jsonObject(with: manifestData) as? [String: Any],
2494
2569
  let targetManifest = parseBundleManifest(from: manifestJson),
@@ -2511,6 +2586,9 @@ class BundleFileStorageService: BundleStorageService {
2511
2586
  _ = try FileUtilities.fileURL(for: path, destinationRoot: tmpDir)
2512
2587
  }
2513
2588
 
2589
+ // Downloads, patches, and extraction below classify their own errors.
2590
+ failurePhase = .storage
2591
+ failureResource = nil
2514
2592
  guard self.fileSystem.createDirectory(atPath: tmpDir) else {
2515
2593
  throw BundleStorageError.directoryCreationFailed
2516
2594
  }
@@ -2554,13 +2632,19 @@ class BundleFileStorageService: BundleStorageService {
2554
2632
  }
2555
2633
  if reused { continue }
2556
2634
  if builtInAssetResolver.copyIfMatches(assetPath: assetPath, expectedHash: expectedAsset.fileHash, destination: destinationPath) {
2557
- try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
2635
+ try UpdateFailureError.during(.verification, resource: .file) {
2636
+ try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
2637
+ }
2558
2638
  continue
2559
2639
  }
2560
2640
  downloads[assetPath] = changedAssets[assetPath]
2561
2641
  }
2562
2642
 
2563
2643
  var archiveInstalled = false
2644
+ var patchedFiles = false
2645
+ // The archive is chosen before any patch is tried, so only the
2646
+ // per-file install can fall back from a patch.
2647
+ var patchFallbacks = false
2564
2648
  if shouldUseArchive(
2565
2649
  archive: targetManifest.archive,
2566
2650
  archiveUrl: archiveUrl,
@@ -2568,14 +2652,16 @@ class BundleFileStorageService: BundleStorageService {
2568
2652
  assets: targetManifest.assets
2569
2653
  ), let archive = targetManifest.archive, let archiveUrl {
2570
2654
  do {
2571
- diffFiles = try installArchive(
2572
- archive: archive,
2573
- archiveUrl: archiveUrl,
2574
- targetManifest: targetManifest,
2575
- preparedDirectory: tmpDir,
2576
- tempDirectory: tempDirectory,
2577
- progressHandler: progressHandler
2578
- )
2655
+ diffFiles = try UpdateFailureError.during(.storage, resource: .archive) {
2656
+ try installArchive(
2657
+ archive: archive,
2658
+ archiveUrl: archiveUrl,
2659
+ targetManifest: targetManifest,
2660
+ preparedDirectory: tmpDir,
2661
+ tempDirectory: tempDirectory,
2662
+ progressHandler: progressHandler
2663
+ )
2664
+ }
2579
2665
  archiveInstalled = true
2580
2666
  } catch {
2581
2667
  NSLog("[BundleStorage] tar.br optimization failed; falling back to individual files: \(error.localizedDescription)")
@@ -2592,6 +2678,7 @@ class BundleFileStorageService: BundleStorageService {
2592
2678
  let workers = OperationQueue()
2593
2679
  workers.maxConcurrentOperationCount = 4
2594
2680
  var installError: Error?
2681
+ var installOutcomes: [AssetInstallOutcome] = []
2595
2682
  for progressFile in diffFiles {
2596
2683
  let assetPath = progressFile.path
2597
2684
  let expectedAsset = targetManifest.assets[assetPath]!
@@ -2606,7 +2693,7 @@ class BundleFileStorageService: BundleStorageService {
2606
2693
  if shouldSkip { return }
2607
2694
  do {
2608
2695
  guard self.fileSystem.createDirectory(atPath: workerTemp) else { throw BundleStorageError.directoryCreationFailed }
2609
- try self.downloadManifestAsset(
2696
+ let outcome = try self.downloadManifestAsset(
2610
2697
  assetPath: assetPath, expectedAsset: expectedAsset, changedAsset: descriptor,
2611
2698
  currentBundleId: currentBundleId, currentBundleDir: currentBundleDir,
2612
2699
  destinationPath: destinationPath, tempDirectory: workerTemp, progressFile: progressFile,
@@ -2620,6 +2707,9 @@ class BundleFileStorageService: BundleStorageService {
2620
2707
  self.emitDiffProgress(progressHandler: progressHandler, phase: "downloading", files: diffFiles)
2621
2708
  }
2622
2709
  )
2710
+ progressLock.lock()
2711
+ installOutcomes.append(outcome)
2712
+ progressLock.unlock()
2623
2713
  } catch {
2624
2714
  progressLock.lock()
2625
2715
  if installError == nil { installError = error }
@@ -2630,6 +2720,14 @@ class BundleFileStorageService: BundleStorageService {
2630
2720
  // Join all workers before cleaning scratch files, even after a failure.
2631
2721
  workers.waitUntilAllOperationsAreFinished()
2632
2722
  if let installError { throw installError }
2723
+ for outcome in installOutcomes {
2724
+ switch outcome {
2725
+ case .patched:
2726
+ patchedFiles = true
2727
+ case .downloaded(let patchFallback):
2728
+ patchFallbacks = patchFallbacks || patchFallback
2729
+ }
2730
+ }
2633
2731
  }
2634
2732
 
2635
2733
  self.emitDiffProgress(
@@ -2644,7 +2742,11 @@ class BundleFileStorageService: BundleStorageService {
2644
2742
  switch self.findBundleFile(in: tmpDir, expectedBundleId: bundleId) {
2645
2743
  case .success(let maybeBundlePath):
2646
2744
  guard let bundlePathInTmp = maybeBundlePath else {
2647
- throw BundleStorageError.invalidBundle
2745
+ // The manifest does not name exactly one platform bundle file.
2746
+ throw UpdateFailureError(
2747
+ underlying: BundleStorageError.invalidBundle,
2748
+ failure: UpdateFailure.invalidResponse.with(resource: .manifest)
2749
+ )
2648
2750
  }
2649
2751
 
2650
2752
  let updatedMetadata = try promoteStagedBundle(
@@ -2666,7 +2768,9 @@ class BundleFileStorageService: BundleStorageService {
2666
2768
  files: diffFiles
2667
2769
  )
2668
2770
  ))
2669
- completion(.success(true))
2771
+ let delivery: UpdateDelivery.Method =
2772
+ archiveInstalled ? .archive : (patchedFiles ? .patch : .manifest)
2773
+ completion(.success(UpdateDelivery(method: delivery, patchFallback: patchFallbacks)))
2670
2774
  case .failure(let error):
2671
2775
  throw error
2672
2776
  }
@@ -2674,7 +2778,11 @@ class BundleFileStorageService: BundleStorageService {
2674
2778
  NSLog("[BundleStorage] Manifest-driven install failed: \(error.localizedDescription).")
2675
2779
  // Preserve staging for a hash-checked retry. Never trust its presence alone.
2676
2780
  self.cleanupTemporaryFiles([tempDirectory])
2677
- completion(.failure(error))
2781
+ completion(.failure(UpdateFailureError.classifying(
2782
+ error,
2783
+ during: failurePhase,
2784
+ resource: failureResource
2785
+ )))
2678
2786
  }
2679
2787
  }
2680
2788
 
@@ -2756,34 +2864,7 @@ class BundleFileStorageService: BundleStorageService {
2756
2864
  }
2757
2865
 
2758
2866
  func getInstallId() -> String {
2759
- if let installId = loadInstallIdentity()?.installId {
2760
- return installId
2761
- }
2762
-
2763
- let identity = InstallIdentity()
2764
- guard saveInstallIdentity(identity) else {
2765
- return identity.installId
2766
- }
2767
- return identity.installId
2768
- }
2769
-
2770
- func getUserId() -> String? {
2771
- loadUserIdentity()?.userId
2772
- }
2773
-
2774
- func getUsername() -> String? {
2775
- loadUserIdentity()?.username
2776
- }
2777
-
2778
- func setUser(userId: String?, username: String?) {
2779
- let normalizedUserId = normalizeIdentityValue(userId)
2780
- let normalizedUsername = normalizeIdentityValue(username)
2781
- let identity = UserIdentity(userId: normalizedUserId, username: normalizedUsername)
2782
- if identity.isEmpty {
2783
- _ = saveUserIdentity(nil)
2784
- return
2785
- }
2786
- _ = saveUserIdentity(identity)
2867
+ loadOrCreateInstallIdentity().installId
2787
2868
  }
2788
2869
 
2789
2870
  /**