@hot-updater/react-native 0.36.7 → 1.0.0-rc.0

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 (239) hide show
  1. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +357 -6
  2. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +268 -34
  3. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +75 -9
  4. package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +2 -16
  5. package/android/src/main/java/com/hotupdater/ReleaseCatalogCacheService.kt +166 -0
  6. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +1 -2
  7. package/android/src/newarch/HotUpdater.kt +1 -1
  8. package/android/src/newarch/HotUpdaterModule.kt +116 -0
  9. package/android/src/oldarch/HotUpdater.kt +1 -1
  10. package/android/src/oldarch/HotUpdaterModule.kt +128 -0
  11. package/android/src/oldarch/HotUpdaterSpec.kt +38 -0
  12. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +280 -2
  13. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +57 -2
  14. package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +118 -0
  15. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +491 -30
  16. package/ios/HotUpdater/Internal/BundleMetadata.swift +229 -6
  17. package/ios/HotUpdater/Internal/HotUpdater.mm +122 -0
  18. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +176 -7
  19. package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +213 -0
  20. package/lib/commonjs/catalogCacheNative.js +44 -0
  21. package/lib/commonjs/catalogCacheNative.js.map +1 -0
  22. package/lib/commonjs/catalogCacheNative.spec.js +31 -0
  23. package/lib/commonjs/catalogCacheNative.spec.js.map +1 -0
  24. package/lib/commonjs/checkForUpdate.js +231 -60
  25. package/lib/commonjs/checkForUpdate.js.map +1 -1
  26. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +604 -0
  27. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -0
  28. package/lib/commonjs/error.js +8 -1
  29. package/lib/commonjs/error.js.map +1 -1
  30. package/lib/commonjs/{fetchUpdateInfo.js → fetchJSON.js} +11 -14
  31. package/lib/commonjs/fetchJSON.js.map +1 -0
  32. package/lib/commonjs/httpClient.js +153 -0
  33. package/lib/commonjs/httpClient.js.map +1 -0
  34. package/lib/commonjs/httpClient.spec.js +102 -0
  35. package/lib/commonjs/httpClient.spec.js.map +1 -0
  36. package/lib/commonjs/index.js +48 -71
  37. package/lib/commonjs/index.js.map +1 -1
  38. package/lib/commonjs/index.spec.js +117 -161
  39. package/lib/commonjs/index.spec.js.map +1 -1
  40. package/lib/commonjs/iosInterop.spec.js +18 -0
  41. package/lib/commonjs/iosInterop.spec.js.map +1 -0
  42. package/lib/commonjs/native.js +222 -55
  43. package/lib/commonjs/native.js.map +1 -1
  44. package/lib/commonjs/native.spec.js +226 -12
  45. package/lib/commonjs/native.spec.js.map +1 -1
  46. package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js +218 -0
  47. package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js.map +1 -0
  48. package/lib/commonjs/notifyAppReadyAnalytics.js +133 -0
  49. package/lib/commonjs/notifyAppReadyAnalytics.js.map +1 -0
  50. package/lib/commonjs/notifyAppReadyAnalytics.spec.js +246 -0
  51. package/lib/commonjs/notifyAppReadyAnalytics.spec.js.map +1 -0
  52. package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js +24 -0
  53. package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js.map +1 -0
  54. package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js +117 -0
  55. package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js.map +1 -0
  56. package/lib/commonjs/releaseCatalogCache.js +179 -0
  57. package/lib/commonjs/releaseCatalogCache.js.map +1 -0
  58. package/lib/commonjs/releaseCatalogCache.spec.js +195 -0
  59. package/lib/commonjs/releaseCatalogCache.spec.js.map +1 -0
  60. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +64 -0
  61. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +1 -0
  62. package/lib/commonjs/sdkVersion.js +1 -1
  63. package/lib/commonjs/sdkVersion.js.map +1 -1
  64. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  65. package/lib/commonjs/types.js +0 -12
  66. package/lib/commonjs/types.js.map +1 -1
  67. package/lib/commonjs/wrap.js +7 -82
  68. package/lib/commonjs/wrap.js.map +1 -1
  69. package/lib/commonjs/wrap.spec.js +203 -56
  70. package/lib/commonjs/wrap.spec.js.map +1 -1
  71. package/lib/commonjs/wrap.types.js +6 -0
  72. package/lib/commonjs/wrap.types.js.map +1 -0
  73. package/lib/module/catalogCacheNative.js +36 -0
  74. package/lib/module/catalogCacheNative.js.map +1 -0
  75. package/lib/module/catalogCacheNative.spec.js +31 -0
  76. package/lib/module/catalogCacheNative.spec.js.map +1 -0
  77. package/lib/module/checkForUpdate.js +233 -62
  78. package/lib/module/checkForUpdate.js.map +1 -1
  79. package/lib/module/checkForUpdate.releaseCatalog.spec.js +603 -0
  80. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -0
  81. package/lib/module/error.js +6 -0
  82. package/lib/module/error.js.map +1 -1
  83. package/lib/module/{fetchUpdateInfo.js → fetchJSON.js} +9 -12
  84. package/lib/module/fetchJSON.js.map +1 -0
  85. package/lib/module/httpClient.js +148 -0
  86. package/lib/module/httpClient.js.map +1 -0
  87. package/lib/module/httpClient.spec.js +102 -0
  88. package/lib/module/httpClient.spec.js.map +1 -0
  89. package/lib/module/index.js +49 -66
  90. package/lib/module/index.js.map +1 -1
  91. package/lib/module/index.spec.js +117 -161
  92. package/lib/module/index.spec.js.map +1 -1
  93. package/lib/module/iosInterop.spec.js +18 -0
  94. package/lib/module/iosInterop.spec.js.map +1 -0
  95. package/lib/module/native.js +206 -51
  96. package/lib/module/native.js.map +1 -1
  97. package/lib/module/native.spec.js +226 -12
  98. package/lib/module/native.spec.js.map +1 -1
  99. package/lib/module/notifyAppReadyAnalytics.boundary.spec.js +217 -0
  100. package/lib/module/notifyAppReadyAnalytics.boundary.spec.js.map +1 -0
  101. package/lib/module/notifyAppReadyAnalytics.js +128 -0
  102. package/lib/module/notifyAppReadyAnalytics.js.map +1 -0
  103. package/lib/module/notifyAppReadyAnalytics.spec.js +245 -0
  104. package/lib/module/notifyAppReadyAnalytics.spec.js.map +1 -0
  105. package/lib/module/notifyAppReadyAnalytics.test-utils.js +18 -0
  106. package/lib/module/notifyAppReadyAnalytics.test-utils.js.map +1 -0
  107. package/lib/module/notifyAppReadyAnalytics.transport.spec.js +116 -0
  108. package/lib/module/notifyAppReadyAnalytics.transport.spec.js.map +1 -0
  109. package/lib/module/releaseCatalogCache.js +171 -0
  110. package/lib/module/releaseCatalogCache.js.map +1 -0
  111. package/lib/module/releaseCatalogCache.spec.js +195 -0
  112. package/lib/module/releaseCatalogCache.spec.js.map +1 -0
  113. package/lib/module/releaseCatalogCacheNativeContract.spec.js +64 -0
  114. package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +1 -0
  115. package/lib/module/sdkVersion.js +1 -1
  116. package/lib/module/sdkVersion.js.map +1 -1
  117. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  118. package/lib/module/types.js +0 -12
  119. package/lib/module/types.js.map +1 -1
  120. package/lib/module/wrap.js +6 -81
  121. package/lib/module/wrap.js.map +1 -1
  122. package/lib/module/wrap.spec.js +203 -56
  123. package/lib/module/wrap.spec.js.map +1 -1
  124. package/lib/module/wrap.types.js +4 -0
  125. package/lib/module/wrap.types.js.map +1 -0
  126. package/lib/typescript/commonjs/catalogCacheNative.d.ts +4 -0
  127. package/lib/typescript/commonjs/catalogCacheNative.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/checkForUpdate.d.ts +14 -4
  129. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/error.d.ts +3 -0
  131. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/fetchJSON.d.ts +6 -0
  133. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/httpClient.d.ts +56 -0
  135. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -0
  136. package/lib/typescript/commonjs/index.d.ts +32 -41
  137. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/native.d.ts +83 -18
  139. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  140. package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts +13 -0
  141. package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts.map +1 -0
  142. package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts +8 -0
  143. package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts.map +1 -0
  144. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +25 -0
  145. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -0
  146. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  147. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
  148. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +40 -5
  149. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/types.d.ts +0 -118
  151. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/wrap.d.ts +2 -191
  153. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/wrap.types.d.ts +65 -0
  155. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -0
  156. package/lib/typescript/module/catalogCacheNative.d.ts +4 -0
  157. package/lib/typescript/module/catalogCacheNative.d.ts.map +1 -0
  158. package/lib/typescript/module/checkForUpdate.d.ts +14 -4
  159. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  160. package/lib/typescript/module/error.d.ts +3 -0
  161. package/lib/typescript/module/error.d.ts.map +1 -1
  162. package/lib/typescript/module/fetchJSON.d.ts +6 -0
  163. package/lib/typescript/module/fetchJSON.d.ts.map +1 -0
  164. package/lib/typescript/module/httpClient.d.ts +56 -0
  165. package/lib/typescript/module/httpClient.d.ts.map +1 -0
  166. package/lib/typescript/module/index.d.ts +32 -41
  167. package/lib/typescript/module/index.d.ts.map +1 -1
  168. package/lib/typescript/module/native.d.ts +83 -18
  169. package/lib/typescript/module/native.d.ts.map +1 -1
  170. package/lib/typescript/module/notifyAppReadyAnalytics.d.ts +13 -0
  171. package/lib/typescript/module/notifyAppReadyAnalytics.d.ts.map +1 -0
  172. package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts +8 -0
  173. package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts.map +1 -0
  174. package/lib/typescript/module/releaseCatalogCache.d.ts +25 -0
  175. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -0
  176. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  177. package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
  178. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +40 -5
  179. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  180. package/lib/typescript/module/types.d.ts +0 -118
  181. package/lib/typescript/module/types.d.ts.map +1 -1
  182. package/lib/typescript/module/wrap.d.ts +2 -191
  183. package/lib/typescript/module/wrap.d.ts.map +1 -1
  184. package/lib/typescript/module/wrap.types.d.ts +65 -0
  185. package/lib/typescript/module/wrap.types.d.ts.map +1 -0
  186. package/package.json +6 -24
  187. package/src/catalogCacheNative.spec.ts +63 -0
  188. package/src/catalogCacheNative.ts +74 -0
  189. package/src/checkForUpdate.releaseCatalog.spec.ts +596 -0
  190. package/src/checkForUpdate.ts +368 -97
  191. package/src/error.ts +7 -0
  192. package/src/{fetchUpdateInfo.ts → fetchJSON.ts} +8 -15
  193. package/src/httpClient.spec.ts +132 -0
  194. package/src/httpClient.ts +261 -0
  195. package/src/index.spec.ts +110 -203
  196. package/src/index.ts +70 -93
  197. package/src/iosInterop.spec.ts +30 -0
  198. package/src/native.spec.ts +226 -10
  199. package/src/native.ts +385 -83
  200. package/src/notifyAppReadyAnalytics.boundary.spec.ts +234 -0
  201. package/src/notifyAppReadyAnalytics.spec.ts +243 -0
  202. package/src/notifyAppReadyAnalytics.test-utils.ts +31 -0
  203. package/src/notifyAppReadyAnalytics.transport.spec.ts +130 -0
  204. package/src/notifyAppReadyAnalytics.ts +194 -0
  205. package/src/releaseCatalogCache.spec.ts +282 -0
  206. package/src/releaseCatalogCache.ts +341 -0
  207. package/src/releaseCatalogCacheNativeContract.spec.ts +80 -0
  208. package/src/sdkVersion.ts +1 -1
  209. package/src/specs/NativeHotUpdater.ts +51 -5
  210. package/src/types.ts +0 -140
  211. package/src/wrap.spec.ts +251 -56
  212. package/src/wrap.tsx +23 -318
  213. package/src/wrap.types.ts +78 -0
  214. package/android/src/main/java/com/hotupdater/StringResourceUtils.kt +0 -45
  215. package/app.plugin.js +0 -1
  216. package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.h +0 -7
  217. package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.mm +0 -4
  218. package/lib/commonjs/DefaultResolver.js +0 -50
  219. package/lib/commonjs/DefaultResolver.js.map +0 -1
  220. package/lib/commonjs/DefaultResolver.spec.js +0 -144
  221. package/lib/commonjs/DefaultResolver.spec.js.map +0 -1
  222. package/lib/commonjs/fetchUpdateInfo.js.map +0 -1
  223. package/lib/module/DefaultResolver.js +0 -46
  224. package/lib/module/DefaultResolver.js.map +0 -1
  225. package/lib/module/DefaultResolver.spec.js +0 -144
  226. package/lib/module/DefaultResolver.spec.js.map +0 -1
  227. package/lib/module/fetchUpdateInfo.js.map +0 -1
  228. package/lib/typescript/commonjs/DefaultResolver.d.ts +0 -11
  229. package/lib/typescript/commonjs/DefaultResolver.d.ts.map +0 -1
  230. package/lib/typescript/commonjs/fetchUpdateInfo.d.ts +0 -7
  231. package/lib/typescript/commonjs/fetchUpdateInfo.d.ts.map +0 -1
  232. package/lib/typescript/module/DefaultResolver.d.ts +0 -11
  233. package/lib/typescript/module/DefaultResolver.d.ts.map +0 -1
  234. package/lib/typescript/module/fetchUpdateInfo.d.ts +0 -7
  235. package/lib/typescript/module/fetchUpdateInfo.d.ts.map +0 -1
  236. package/plugin/build/transformers.js +0 -314
  237. package/plugin/build/withHotUpdater.js +0 -228
  238. package/src/DefaultResolver.spec.ts +0 -193
  239. package/src/DefaultResolver.ts +0 -63
@@ -1,4 +1,10 @@
1
1
  import Foundation
2
+ import OSLog
3
+
4
+ private let bundleStorageLog = OSLog(
5
+ subsystem: "com.hotupdater",
6
+ category: "BundleStorage"
7
+ )
2
8
 
3
9
  @_silgen_name("HotUpdaterApplyBsdiffPatch")
4
10
  private func hotUpdaterApplyBsdiffPatchSymbol(
@@ -275,12 +281,21 @@ public protocol BundleStorageService {
275
281
 
276
282
  // Bundle update
277
283
  func updateBundle(bundleId: String, fileUrl: URL?, fileHash: String?, manifestUrl: URL?, manifestFileHash: String?, changedAssets: [String: ChangedAssetDescriptor]?, progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<Bool, Error>) -> Void)
284
+ func stageReleaseSelection(_ selection: PersistedSelection) -> Bool
285
+ func acceptReleaseCatalog(catalogId: String, scopeKey: String, generation: Int64, catalogHash: String, channel: String, selectionContextHash: String) -> Bool
286
+ func getActiveUpdateState() -> [String: Any]
287
+ func isReleaseSelectionCurrent(catalogId: String, scopeKey: String, generation: Int64, catalogHash: String, channel: String, selectionContextHash: String) -> Bool
288
+ func commitReleaseSelection(_ selection: PersistedSelection) -> Bool
278
289
 
279
290
  // Rollback support
280
291
  func markLaunchCompleted(bundleId: String?)
281
292
  func notifyAppReady() -> [String: Any]
282
293
  func getCrashHistory() -> CrashedHistory
283
294
  func clearCrashHistory() -> Bool
295
+ func getInstallId() -> String
296
+ func getUserId() -> String?
297
+ func getUsername() -> String?
298
+ func setUser(userId: String?, username: String?)
284
299
 
285
300
  /**
286
301
  * Gets the base URL for the current active bundle directory
@@ -308,6 +323,14 @@ public protocol BundleStorageService {
308
323
  func resetChannel() -> Result<Bool, Error>
309
324
  }
310
325
 
326
+ public extension BundleStorageService {
327
+ func stageReleaseSelection(_: PersistedSelection) -> Bool { false }
328
+ func acceptReleaseCatalog(catalogId _: String, scopeKey _: String, generation _: Int64, catalogHash _: String, channel _: String, selectionContextHash _: String) -> Bool { false }
329
+ func getActiveUpdateState() -> [String: Any] { [:] }
330
+ func isReleaseSelectionCurrent(catalogId _: String, scopeKey _: String, generation _: Int64, catalogHash _: String, channel _: String, selectionContextHash _: String) -> Bool { false }
331
+ func commitReleaseSelection(_: PersistedSelection) -> Bool { false }
332
+ }
333
+
311
334
  class BundleFileStorageService: BundleStorageService {
312
335
  private struct ActiveBundleMetadataSnapshot {
313
336
  let activeBundleId: String
@@ -352,6 +375,29 @@ class BundleFileStorageService: BundleStorageService {
352
375
  private var currentLaunchReport: LaunchReport?
353
376
  private let activeBundleMetadataLock = NSLock()
354
377
  private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot?
378
+ private let builtInBundleIdProvider: () -> String
379
+ private let updateStrategyProvider: () -> UpdateStrategy
380
+ private let releaseStateLock = NSRecursiveLock()
381
+ private var pendingInstallSelections: [String: PersistedSelection] = [:]
382
+
383
+ private var currentInstallIdentity: InstallIdentity?
384
+ private var currentUserIdentity: UserIdentity?
385
+
386
+ private let protectedBundleStoreEntries: Set<String> = [
387
+ BundleMetadata.metadataFilename,
388
+ CrashedHistory.crashedHistoryFilename,
389
+ LaunchReport.launchReportFilename,
390
+ InstallIdentity.installIdentityFilename,
391
+ UserIdentity.userIdentityFilename,
392
+ ]
393
+
394
+ private func normalizeIdentityValue(_ value: String?) -> String? {
395
+ guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines),
396
+ !trimmed.isEmpty else {
397
+ return nil
398
+ }
399
+ return trimmed
400
+ }
355
401
 
356
402
  private func emitArchiveProgress(
357
403
  progressHandler: @escaping (UpdateProgressPayload) -> Void,
@@ -654,7 +700,13 @@ class BundleFileStorageService: BundleStorageService {
654
700
  return false
655
701
  }
656
702
 
657
- NSLog("[BundleStorage] HotUpdaterBsdiffPatchApplied asset=\(assetPath) baseBundleId=\(patch.baseBundleId)")
703
+ os_log(
704
+ "HotUpdaterBsdiffPatchApplied asset=%{public}@ baseBundleId=%{public}@",
705
+ log: bundleStorageLog,
706
+ type: .default,
707
+ assetPath,
708
+ patch.baseBundleId
709
+ )
658
710
  return true
659
711
  case .failure:
660
712
  resetDiffProgressFile(
@@ -701,13 +753,21 @@ class BundleFileStorageService: BundleStorageService {
701
753
  downloadService: DownloadService,
702
754
  decompressService: DecompressService,
703
755
  preferences: PreferencesService,
704
- isolationKey: String) {
756
+ isolationKey: String,
757
+ builtInBundleIdProvider: @escaping () -> String = { "" },
758
+ updateStrategyProvider: @escaping () -> UpdateStrategy = {
759
+ let fingerprintHash = HotUpdaterConfig.shared.fingerprintHash
760
+ ?? Bundle.main.object(forInfoDictionaryKey: "HOT_UPDATER_FINGERPRINT_HASH") as? String
761
+ return (fingerprintHash?.isEmpty == false) ? .fingerprint : .appVersion
762
+ }) {
705
763
 
706
764
  self.fileSystem = fileSystem
707
765
  self.downloadService = downloadService
708
766
  self.decompressService = decompressService
709
767
  self.preferences = preferences
710
768
  self.isolationKey = isolationKey
769
+ self.builtInBundleIdProvider = builtInBundleIdProvider
770
+ self.updateStrategyProvider = updateStrategyProvider
711
771
 
712
772
  // Create queue for file operations
713
773
  self.fileOperationQueue = DispatchQueue(label: "com.hotupdater.fileoperations",
@@ -744,6 +804,20 @@ class BundleFileStorageService: BundleStorageService {
744
804
  return URL(fileURLWithPath: storeDir).appendingPathComponent(LaunchReport.launchReportFilename)
745
805
  }
746
806
 
807
+ private func installIdentityFileURL() -> URL? {
808
+ guard case .success(let storeDir) = bundleStoreDir() else {
809
+ return nil
810
+ }
811
+ return URL(fileURLWithPath: storeDir).appendingPathComponent(InstallIdentity.installIdentityFilename)
812
+ }
813
+
814
+ private func userIdentityFileURL() -> URL? {
815
+ guard case .success(let storeDir) = bundleStoreDir() else {
816
+ return nil
817
+ }
818
+ return URL(fileURLWithPath: storeDir).appendingPathComponent(UserIdentity.userIdentityFilename)
819
+ }
820
+
747
821
  // MARK: - Metadata Operations
748
822
 
749
823
  private func loadMetadataOrNull() -> BundleMetadata? {
@@ -790,13 +864,87 @@ class BundleFileStorageService: BundleStorageService {
790
864
  _ = report.save(to: file)
791
865
  }
792
866
 
867
+ private func loadInstallIdentity() -> InstallIdentity? {
868
+ if let currentInstallIdentity {
869
+ return currentInstallIdentity
870
+ }
871
+ guard let file = installIdentityFileURL(),
872
+ let identity = InstallIdentity.load(from: file) else {
873
+ return nil
874
+ }
875
+ currentInstallIdentity = identity
876
+ return identity
877
+ }
878
+
879
+ private func saveInstallIdentity(_ identity: InstallIdentity) -> Bool {
880
+ currentInstallIdentity = identity
881
+ guard let file = installIdentityFileURL() else {
882
+ return false
883
+ }
884
+ return identity.save(to: file)
885
+ }
886
+
887
+ private func loadUserIdentity() -> UserIdentity? {
888
+ if let currentUserIdentity {
889
+ return currentUserIdentity
890
+ }
891
+ guard let file = userIdentityFileURL(),
892
+ let identity = UserIdentity.load(from: file) else {
893
+ return nil
894
+ }
895
+ currentUserIdentity = identity
896
+ return identity
897
+ }
898
+
899
+ private func saveUserIdentity(_ identity: UserIdentity?) -> Bool {
900
+ currentUserIdentity = identity
901
+ guard let file = userIdentityFileURL() else {
902
+ return false
903
+ }
904
+
905
+ guard let identity else {
906
+ if FileManager.default.fileExists(atPath: file.path) {
907
+ try? FileManager.default.removeItem(at: file)
908
+ }
909
+ return true
910
+ }
911
+
912
+ return identity.save(to: file)
913
+ }
914
+
915
+ private func resolveBuiltInBundleId() -> String? {
916
+ let bundleId = builtInBundleIdProvider().trimmingCharacters(in: .whitespacesAndNewlines)
917
+ return bundleId.isEmpty ? nil : bundleId
918
+ }
919
+
920
+ private func createPendingTransition(
921
+ fromBundleId: String,
922
+ toBundleId: String
923
+ ) -> PendingBundleTransition {
924
+ PendingBundleTransition(
925
+ fromBundleId: fromBundleId,
926
+ toBundleId: toBundleId,
927
+ updateStrategy: updateStrategyProvider()
928
+ )
929
+ }
930
+
931
+ private func launchReport(for status: LaunchReportStatus, transition: PendingBundleTransition?) -> LaunchReport {
932
+ LaunchReport(
933
+ status: status,
934
+ fromBundleId: transition?.fromBundleId,
935
+ toBundleId: transition?.toBundleId,
936
+ updateStrategy: transition?.updateStrategy
937
+ )
938
+ }
939
+
793
940
  private func createInitialMetadata() -> BundleMetadata {
794
941
  let currentBundleId = getCachedBundleId()
795
942
  return BundleMetadata(
796
943
  isolationKey: isolationKey,
797
944
  stableBundleId: nil,
798
945
  stagingBundleId: currentBundleId,
799
- verificationPending: false
946
+ verificationPending: false,
947
+ pendingTransition: nil
800
948
  )
801
949
  }
802
950
 
@@ -804,7 +952,7 @@ class BundleFileStorageService: BundleStorageService {
804
952
  if let stagingBundleId = metadata.stagingBundleId, !metadata.verificationPending {
805
953
  return stagingBundleId
806
954
  }
807
- return metadata.stableBundleId
955
+ return metadata.stableBundleId ?? resolveBuiltInBundleId()
808
956
  }
809
957
 
810
958
  private func getActiveBundleId() -> String? {
@@ -1076,8 +1224,7 @@ class BundleFileStorageService: BundleStorageService {
1076
1224
  for item in contents {
1077
1225
  let fullPath = (storeDir as NSString).appendingPathComponent(item)
1078
1226
 
1079
- // Skip metadata files
1080
- if item == "metadata.json" || item == "crashed-history.json" {
1227
+ if protectedBundleStoreEntries.contains(item) {
1081
1228
  continue
1082
1229
  }
1083
1230
 
@@ -1116,13 +1263,47 @@ class BundleFileStorageService: BundleStorageService {
1116
1263
  return metadata.verificationPending && metadata.stagingBundleId != nil
1117
1264
  }
1118
1265
 
1119
- private func prepareMetadataForNewStagingBundle(_ metadata: BundleMetadata, bundleId: String) -> BundleMetadata {
1266
+ private func prepareMetadataForNewStagingBundle(_ metadata: BundleMetadata, bundleId: String) -> BundleMetadata? {
1120
1267
  let currentVerifiedBundleId = getCurrentVerifiedBundleId(metadata).flatMap { $0 == bundleId ? nil : $0 }
1268
+ releaseStateLock.lock()
1269
+ let incomingSelection = pendingInstallSelections.removeValue(forKey: bundleId)
1270
+ releaseStateLock.unlock()
1271
+ if let incomingSelection, !isSelectionCurrent(incomingSelection) {
1272
+ return nil
1273
+ }
1274
+ let currentVerifiedSelection: PersistedSelection? = {
1275
+ if metadata.stagingBundleId != nil, !metadata.verificationPending {
1276
+ return metadata.stagingSelection
1277
+ }
1278
+ return metadata.stableSelection
1279
+ }()?.bundleId == bundleId ? nil : ({
1280
+ if metadata.stagingBundleId != nil, !metadata.verificationPending {
1281
+ return metadata.stagingSelection
1282
+ }
1283
+ return metadata.stableSelection
1284
+ }())
1285
+ let stagingSelection = incomingSelection ?? PersistedSelection.legacyBundle(bundleId)
1286
+ let fromSelection = currentVerifiedSelection
1287
+ ?? PersistedSelection.legacyBundle(resolveBuiltInBundleId() ?? bundleId)
1288
+ let pendingTransition = currentVerifiedBundleId.map {
1289
+ createPendingTransition(fromBundleId: $0, toBundleId: bundleId)
1290
+ }
1121
1291
  return BundleMetadata(
1122
1292
  isolationKey: isolationKey,
1123
1293
  stableBundleId: currentVerifiedBundleId,
1124
1294
  stagingBundleId: bundleId,
1295
+ stableSelection: currentVerifiedSelection,
1296
+ stagingSelection: stagingSelection,
1125
1297
  verificationPending: true,
1298
+ pendingTransition: pendingTransition,
1299
+ pendingSelectionTransition: PendingSelectionTransition(
1300
+ fromReleaseId: fromSelection.releaseId,
1301
+ fromBundleId: fromSelection.bundleId,
1302
+ toReleaseId: stagingSelection.releaseId,
1303
+ toBundleId: stagingSelection.bundleId
1304
+ ),
1305
+ highestSeenCatalogs: metadata.highestSeenCatalogs,
1306
+ currentSelectionContexts: metadata.currentSelectionContexts,
1126
1307
  updatedAt: Date().timeIntervalSince1970 * 1000
1127
1308
  )
1128
1309
  }
@@ -1133,6 +1314,10 @@ class BundleFileStorageService: BundleStorageService {
1133
1314
  return false
1134
1315
  }
1135
1316
 
1317
+ let pendingTransition = metadata.pendingTransition
1318
+ let pendingSelectionTransition = metadata.pendingSelectionTransition
1319
+ let stableSelection = metadata.stableSelection
1320
+
1136
1321
  var crashedHistory = loadCrashedHistory()
1137
1322
  crashedHistory.addEntry(stagingId)
1138
1323
  let _ = saveCrashedHistory(crashedHistory)
@@ -1154,7 +1339,13 @@ class BundleFileStorageService: BundleStorageService {
1154
1339
  isolationKey: isolationKey,
1155
1340
  stableBundleId: nil,
1156
1341
  stagingBundleId: fallbackBundleId,
1342
+ stableSelection: nil,
1343
+ stagingSelection: stableSelection,
1157
1344
  verificationPending: false,
1345
+ pendingTransition: nil,
1346
+ pendingSelectionTransition: nil,
1347
+ highestSeenCatalogs: metadata.highestSeenCatalogs,
1348
+ currentSelectionContexts: metadata.currentSelectionContexts,
1158
1349
  updatedAt: Date().timeIntervalSince1970 * 1000
1159
1350
  )
1160
1351
 
@@ -1180,7 +1371,18 @@ class BundleFileStorageService: BundleStorageService {
1180
1371
  try? fileSystem.removeItem(atPath: stagingDir)
1181
1372
  }
1182
1373
 
1183
- saveLaunchReport(LaunchReport(status: "RECOVERED", crashedBundleId: stagingId))
1374
+ saveLaunchReport(
1375
+ LaunchReport(
1376
+ status: .recovered,
1377
+ fromReleaseId: pendingSelectionTransition?.toReleaseId,
1378
+ fromBundleId: stagingId,
1379
+ toReleaseId: stableSelection?.releaseId,
1380
+ toBundleId: fallbackBundleId
1381
+ ?? pendingTransition?.fromBundleId
1382
+ ?? resolveBuiltInBundleId(),
1383
+ updateStrategy: pendingTransition?.updateStrategy ?? updateStrategyProvider()
1384
+ )
1385
+ )
1184
1386
  return true
1185
1387
  }
1186
1388
 
@@ -1195,6 +1397,170 @@ class BundleFileStorageService: BundleStorageService {
1195
1397
 
1196
1398
  _ = rollbackPendingBundle(stagingBundleId)
1197
1399
  }
1400
+
1401
+ private func releaseScopeStateKey(catalogId: String, scopeKey: String) -> String {
1402
+ "\(catalogId)|\(scopeKey)"
1403
+ }
1404
+
1405
+ private func selectionContextValue(channel: String, selectionContextHash: String) -> String {
1406
+ "\(channel)\n\(selectionContextHash)"
1407
+ }
1408
+
1409
+ func acceptReleaseCatalog(
1410
+ catalogId: String,
1411
+ scopeKey: String,
1412
+ generation: Int64,
1413
+ catalogHash: String,
1414
+ channel: String,
1415
+ selectionContextHash: String
1416
+ ) -> Bool {
1417
+ releaseStateLock.lock()
1418
+ defer { releaseStateLock.unlock() }
1419
+ var metadata = loadMetadataOrNull() ?? createInitialMetadata()
1420
+ let stateKey = releaseScopeStateKey(catalogId: catalogId, scopeKey: scopeKey)
1421
+ if let highestSeen = metadata.highestSeenCatalogs[stateKey] {
1422
+ if generation < highestSeen.generation { return false }
1423
+ if generation == highestSeen.generation, catalogHash != highestSeen.catalogHash {
1424
+ return false
1425
+ }
1426
+ }
1427
+ metadata.highestSeenCatalogs[stateKey] = CatalogHighWater(
1428
+ generation: generation,
1429
+ catalogHash: catalogHash
1430
+ )
1431
+ metadata.currentSelectionContexts[stateKey] = selectionContextValue(
1432
+ channel: channel,
1433
+ selectionContextHash: selectionContextHash
1434
+ )
1435
+ metadata.updatedAt = Date().timeIntervalSince1970 * 1000
1436
+ return saveMetadata(metadata)
1437
+ }
1438
+
1439
+ private func activeSelection(_ metadata: BundleMetadata) -> PersistedSelection? {
1440
+ metadata.stagingSelection
1441
+ ?? metadata.stagingBundleId.map(PersistedSelection.legacyBundle)
1442
+ ?? metadata.stableSelection
1443
+ ?? metadata.stableBundleId.map(PersistedSelection.legacyBundle)
1444
+ }
1445
+
1446
+ func getActiveUpdateState() -> [String: Any] {
1447
+ releaseStateLock.lock()
1448
+ defer { releaseStateLock.unlock() }
1449
+ guard let metadata = loadMetadataOrNull() else {
1450
+ return [
1451
+ "activeSelection": NSNull(),
1452
+ "stableSelection": NSNull(),
1453
+ "verificationPending": false,
1454
+ "highestSeenCatalogs": [:]
1455
+ ]
1456
+ }
1457
+ let highWaters = metadata.highestSeenCatalogs.mapValues { highWater in
1458
+ [
1459
+ "generation": NSNumber(value: highWater.generation),
1460
+ "catalogHash": highWater.catalogHash
1461
+ ] as [String: Any]
1462
+ }
1463
+ return [
1464
+ "activeSelection": activeSelection(metadata)?.dictionary ?? NSNull(),
1465
+ "stableSelection": metadata.stableSelection?.dictionary ?? NSNull(),
1466
+ "verificationPending": metadata.verificationPending,
1467
+ "highestSeenCatalogs": highWaters
1468
+ ]
1469
+ }
1470
+
1471
+ func isReleaseSelectionCurrent(
1472
+ catalogId: String,
1473
+ scopeKey: String,
1474
+ generation: Int64,
1475
+ catalogHash: String,
1476
+ channel: String,
1477
+ selectionContextHash: String
1478
+ ) -> Bool {
1479
+ releaseStateLock.lock()
1480
+ defer { releaseStateLock.unlock() }
1481
+ guard let metadata = loadMetadataOrNull() else { return false }
1482
+ let stateKey = releaseScopeStateKey(catalogId: catalogId, scopeKey: scopeKey)
1483
+ return metadata.highestSeenCatalogs[stateKey] == CatalogHighWater(
1484
+ generation: generation,
1485
+ catalogHash: catalogHash
1486
+ ) && metadata.currentSelectionContexts[stateKey] == selectionContextValue(
1487
+ channel: channel,
1488
+ selectionContextHash: selectionContextHash
1489
+ )
1490
+ }
1491
+
1492
+ private func isSelectionCurrent(_ selection: PersistedSelection) -> Bool {
1493
+ guard let catalogId = selection.catalogId,
1494
+ let scopeKey = selection.scopeKey,
1495
+ let generation = selection.generation,
1496
+ let catalogHash = selection.catalogHash,
1497
+ let selectionContextHash = selection.selectionContextHash else {
1498
+ return false
1499
+ }
1500
+ return isReleaseSelectionCurrent(
1501
+ catalogId: catalogId,
1502
+ scopeKey: scopeKey,
1503
+ generation: generation,
1504
+ catalogHash: catalogHash,
1505
+ channel: selection.channel,
1506
+ selectionContextHash: selectionContextHash
1507
+ )
1508
+ }
1509
+
1510
+ func stageReleaseSelection(_ selection: PersistedSelection) -> Bool {
1511
+ releaseStateLock.lock()
1512
+ defer { releaseStateLock.unlock() }
1513
+ guard isSelectionCurrent(selection) else { return false }
1514
+ pendingInstallSelections[selection.bundleId] = selection
1515
+ return true
1516
+ }
1517
+
1518
+ func commitReleaseSelection(_ selection: PersistedSelection) -> Bool {
1519
+ releaseStateLock.lock()
1520
+ defer { releaseStateLock.unlock() }
1521
+ guard isSelectionCurrent(selection) else { return false }
1522
+ var metadata = loadMetadataOrNull() ?? createInitialMetadata()
1523
+ if selection.kind == "BUNDLE" {
1524
+ if metadata.stagingSelection?.bundleId == selection.bundleId
1525
+ || metadata.stagingBundleId == selection.bundleId {
1526
+ metadata.stagingBundleId = selection.bundleId
1527
+ metadata.stagingSelection = selection
1528
+ } else if metadata.stableSelection?.bundleId == selection.bundleId
1529
+ || metadata.stableBundleId == selection.bundleId {
1530
+ metadata.stableBundleId = selection.bundleId
1531
+ metadata.stableSelection = selection
1532
+ } else {
1533
+ return false
1534
+ }
1535
+ } else {
1536
+ guard case .success = setBundleURL(localPath: nil) else { return false }
1537
+ metadata.stableBundleId = nil
1538
+ metadata.stableSelection = nil
1539
+ metadata.stagingBundleId = nil
1540
+ metadata.stagingSelection = selection
1541
+ metadata.verificationPending = false
1542
+ metadata.pendingTransition = nil
1543
+ metadata.pendingSelectionTransition = nil
1544
+ }
1545
+ metadata.updatedAt = Date().timeIntervalSince1970 * 1000
1546
+ guard saveMetadata(metadata) else { return false }
1547
+ do {
1548
+ try preferences.setItem(
1549
+ selection.channel,
1550
+ forKey: "HotUpdaterChannel"
1551
+ )
1552
+ } catch {
1553
+ return false
1554
+ }
1555
+ if selection.kind != "BUNDLE", case .success(let storeDir) = bundleStoreDir() {
1556
+ for item in (try? fileSystem.contentsOfDirectory(atPath: storeDir)) ?? [] {
1557
+ guard !protectedBundleStoreEntries.contains(item) else { continue }
1558
+ let path = (storeDir as NSString).appendingPathComponent(item)
1559
+ try? fileSystem.removeItem(atPath: path)
1560
+ }
1561
+ }
1562
+ return true
1563
+ }
1198
1564
 
1199
1565
  // MARK: - Directory Management
1200
1566
 
@@ -1386,8 +1752,7 @@ class BundleFileStorageService: BundleStorageService {
1386
1752
  let bundles = contents.compactMap { item -> String? in
1387
1753
  let fullPath = (storeDir as NSString).appendingPathComponent(item)
1388
1754
 
1389
- // Skip metadata files - DO NOT delete
1390
- if item == "metadata.json" || item == "crashed-history.json" {
1755
+ if protectedBundleStoreEntries.contains(item) {
1391
1756
  return nil
1392
1757
  }
1393
1758
 
@@ -1608,6 +1973,10 @@ class BundleFileStorageService: BundleStorageService {
1608
1973
 
1609
1974
  func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection {
1610
1975
  saveLaunchReport(nil)
1976
+ if loadInstallIdentity() == nil {
1977
+ let identity = InstallIdentity()
1978
+ _ = saveInstallIdentity(identity)
1979
+ }
1611
1980
  applyPendingRecoveryIfNeeded(pendingRecovery)
1612
1981
  return selectLaunch(bundle: bundle)
1613
1982
  }
@@ -1642,7 +2011,11 @@ class BundleFileStorageService: BundleStorageService {
1642
2011
  let setResult = self.setBundleURL(localPath: nil)
1643
2012
  switch setResult {
1644
2013
  case .success:
1645
- let _ = self.saveMetadata(self.createInitialMetadata())
2014
+ let previousMetadata = self.loadMetadataOrNull()
2015
+ var resetMetadata = self.createInitialMetadata()
2016
+ resetMetadata.highestSeenCatalogs = previousMetadata?.highestSeenCatalogs ?? [:]
2017
+ resetMetadata.currentSelectionContexts = previousMetadata?.currentSelectionContexts ?? [:]
2018
+ let _ = self.saveMetadata(resetMetadata)
1646
2019
  self.saveLaunchReport(nil)
1647
2020
  let cleanupResult = self.cleanupOldBundles(currentBundleId: currentBundleId, bundleId: bundleId)
1648
2021
  switch cleanupResult {
@@ -1680,11 +2053,18 @@ class BundleFileStorageService: BundleStorageService {
1680
2053
  case .success(let existingBundlePath):
1681
2054
  if let bundlePath = existingBundlePath {
1682
2055
  NSLog("[BundleStorage] Using cached bundle at path: \(bundlePath)")
2056
+ let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
2057
+ guard let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId) else {
2058
+ completion(.failure(BundleStorageError.unknown(
2059
+ NSError(domain: "HotUpdater", code: 0, userInfo: [
2060
+ NSLocalizedDescriptionKey: "Release catalog selection is stale"
2061
+ ])
2062
+ )))
2063
+ return
2064
+ }
1683
2065
  let setResult = self.setBundleURL(localPath: bundlePath)
1684
2066
  switch setResult {
1685
2067
  case .success:
1686
- let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
1687
- let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId)
1688
2068
  let _ = self.saveMetadata(updatedMetadata)
1689
2069
  NSLog("[BundleStorage] Set staging bundle (cached): \(bundleId), verificationPending: true")
1690
2070
 
@@ -2113,10 +2493,18 @@ class BundleFileStorageService: BundleStorageService {
2113
2493
  from: tmpDir,
2114
2494
  to: realDir
2115
2495
  )
2496
+ let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
2497
+ guard let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId) else {
2498
+ self.cleanupTemporaryFiles([tempDirectory])
2499
+ completion(.failure(BundleStorageError.unknown(
2500
+ NSError(domain: "HotUpdater", code: 0, userInfo: [
2501
+ NSLocalizedDescriptionKey: "Release catalog selection is stale"
2502
+ ])
2503
+ )))
2504
+ return
2505
+ }
2116
2506
  switch self.setBundleURL(localPath: finalBundlePath) {
2117
2507
  case .success:
2118
- let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
2119
- let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId)
2120
2508
  let _ = self.saveMetadata(updatedMetadata)
2121
2509
  self.cleanupTemporaryFiles([tempDirectory])
2122
2510
  self.scheduleCleanupOldBundles(
@@ -2447,14 +2835,22 @@ class BundleFileStorageService: BundleStorageService {
2447
2835
  )
2448
2836
 
2449
2837
  // 12) Set the bundle URL in preferences (for backwards compatibility)
2838
+ let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
2839
+ guard let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId) else {
2840
+ self.cleanupTemporaryFiles([tempDirectory])
2841
+ completion(.failure(BundleStorageError.unknown(
2842
+ NSError(domain: "HotUpdater", code: 0, userInfo: [
2843
+ NSLocalizedDescriptionKey: "Release catalog selection is stale"
2844
+ ])
2845
+ )))
2846
+ return
2847
+ }
2450
2848
  let setResult = self.setBundleURL(localPath: finalBundlePath)
2451
2849
  switch setResult {
2452
2850
  case .success:
2453
2851
  NSLog("[BundleStorage] Successfully set bundle URL: \(finalBundlePath)")
2454
2852
 
2455
2853
  // 13) Set staging metadata for rollback support
2456
- let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
2457
- let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId)
2458
2854
  let _ = self.saveMetadata(updatedMetadata)
2459
2855
  NSLog("[BundleStorage] Set staging bundle: \(bundleId), verificationPending: true")
2460
2856
 
@@ -2534,19 +2930,52 @@ class BundleFileStorageService: BundleStorageService {
2534
2930
  return
2535
2931
  }
2536
2932
 
2933
+ let pendingTransition = metadata.pendingTransition
2934
+ let pendingSelectionTransition = metadata.pendingSelectionTransition
2935
+ metadata.stableBundleId = nil
2936
+ metadata.stableSelection = nil
2537
2937
  metadata.verificationPending = false
2938
+ metadata.pendingTransition = nil
2939
+ metadata.pendingSelectionTransition = nil
2538
2940
  metadata.updatedAt = Date().timeIntervalSince1970 * 1000
2539
2941
  let _ = saveMetadata(metadata)
2942
+ saveLaunchReport(
2943
+ LaunchReport(
2944
+ status: .updateApplied,
2945
+ fromReleaseId: pendingSelectionTransition?.fromReleaseId,
2946
+ fromBundleId: pendingSelectionTransition?.fromBundleId ?? pendingTransition?.fromBundleId,
2947
+ toReleaseId: pendingSelectionTransition?.toReleaseId,
2948
+ toBundleId: pendingSelectionTransition?.toBundleId ?? pendingTransition?.toBundleId,
2949
+ updateStrategy: pendingTransition?.updateStrategy
2950
+ )
2951
+ )
2540
2952
  }
2541
2953
 
2542
2954
  func notifyAppReady() -> [String: Any] {
2543
2955
  guard let report = loadLaunchReport() else {
2544
- return ["status": "STABLE"]
2956
+ if let metadata = loadMetadataOrNull(),
2957
+ metadata.verificationPending,
2958
+ metadata.stagingBundleId != nil {
2959
+ return ["status": "PENDING"]
2960
+ }
2961
+ return ["status": LaunchReportStatus.unchanged.rawValue]
2545
2962
  }
2546
2963
 
2547
- var result: [String: Any] = ["status": report.status]
2548
- if let crashedBundleId = report.crashedBundleId {
2549
- result["crashedBundleId"] = crashedBundleId
2964
+ var result: [String: Any] = ["status": report.status.rawValue]
2965
+ if let fromReleaseId = report.fromReleaseId {
2966
+ result["fromReleaseId"] = fromReleaseId
2967
+ }
2968
+ if let fromBundleId = report.fromBundleId {
2969
+ result["fromBundleId"] = fromBundleId
2970
+ }
2971
+ if let toBundleId = report.toBundleId {
2972
+ result["toBundleId"] = toBundleId
2973
+ }
2974
+ if let toReleaseId = report.toReleaseId {
2975
+ result["toReleaseId"] = toReleaseId
2976
+ }
2977
+ if let updateStrategy = report.updateStrategy {
2978
+ result["updateStrategy"] = updateStrategy.rawValue
2550
2979
  }
2551
2980
  return result
2552
2981
  }
@@ -2569,6 +2998,37 @@ class BundleFileStorageService: BundleStorageService {
2569
2998
  return saveCrashedHistory(history)
2570
2999
  }
2571
3000
 
3001
+ func getInstallId() -> String {
3002
+ if let installId = loadInstallIdentity()?.installId {
3003
+ return installId
3004
+ }
3005
+
3006
+ let identity = InstallIdentity()
3007
+ guard saveInstallIdentity(identity) else {
3008
+ return identity.installId
3009
+ }
3010
+ return identity.installId
3011
+ }
3012
+
3013
+ func getUserId() -> String? {
3014
+ loadUserIdentity()?.userId
3015
+ }
3016
+
3017
+ func getUsername() -> String? {
3018
+ loadUserIdentity()?.username
3019
+ }
3020
+
3021
+ func setUser(userId: String?, username: String?) {
3022
+ let normalizedUserId = normalizeIdentityValue(userId)
3023
+ let normalizedUsername = normalizeIdentityValue(username)
3024
+ let identity = UserIdentity(userId: normalizedUserId, username: normalizedUsername)
3025
+ if identity.isEmpty {
3026
+ _ = saveUserIdentity(nil)
3027
+ return
3028
+ }
3029
+ _ = saveUserIdentity(identity)
3030
+ }
3031
+
2572
3032
  /**
2573
3033
  * Gets the base URL for the current active bundle directory
2574
3034
  * Returns the file:// URL to the bundle directory without trailing slash
@@ -2618,12 +3078,15 @@ class BundleFileStorageService: BundleStorageService {
2618
3078
  return .failure(BundleStorageError.unknown(nil))
2619
3079
  }
2620
3080
 
2621
- let clearedMetadata = BundleMetadata(
2622
- isolationKey: isolationKey,
2623
- stableBundleId: nil,
2624
- stagingBundleId: nil,
2625
- verificationPending: false
2626
- )
3081
+ var clearedMetadata = loadMetadataOrNull() ?? BundleMetadata(isolationKey: isolationKey)
3082
+ clearedMetadata.stableBundleId = nil
3083
+ clearedMetadata.stagingBundleId = nil
3084
+ clearedMetadata.stableSelection = nil
3085
+ clearedMetadata.stagingSelection = nil
3086
+ clearedMetadata.verificationPending = false
3087
+ clearedMetadata.pendingTransition = nil
3088
+ clearedMetadata.pendingSelectionTransition = nil
3089
+ clearedMetadata.updatedAt = Date().timeIntervalSince1970 * 1000
2627
3090
 
2628
3091
  guard saveMetadata(clearedMetadata) else {
2629
3092
  return .failure(BundleStorageError.unknown(nil))
@@ -2637,9 +3100,7 @@ class BundleFileStorageService: BundleStorageService {
2637
3100
 
2638
3101
  do {
2639
3102
  for item in try fileSystem.contentsOfDirectory(atPath: storeDir) {
2640
- if item == BundleMetadata.metadataFilename ||
2641
- item == CrashedHistory.crashedHistoryFilename ||
2642
- item == LaunchReport.launchReportFilename {
3103
+ if protectedBundleStoreEntries.contains(item) {
2643
3104
  continue
2644
3105
  }
2645
3106