@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
@@ -10,6 +10,7 @@ import java.io.File
10
10
  import java.io.FileInputStream
11
11
  import java.io.FileOutputStream
12
12
  import java.net.URL
13
+ import java.util.UUID
13
14
 
14
15
  data class ChangedAssetDescriptor(
15
16
  val fileUrl: String?,
@@ -98,6 +99,30 @@ interface BundleStorageService {
98
99
  progressCallback: (UpdateProgressPayload) -> Unit,
99
100
  )
100
101
 
102
+ fun stageReleaseSelection(selection: PersistedSelection): Boolean = false
103
+
104
+ fun acceptReleaseCatalog(
105
+ catalogId: String,
106
+ scopeKey: String,
107
+ generation: Long,
108
+ catalogHash: String,
109
+ channel: String,
110
+ selectionContextHash: String,
111
+ ): Boolean = false
112
+
113
+ fun getActiveUpdateState(): Map<String, Any?> = emptyMap()
114
+
115
+ fun isReleaseSelectionCurrent(
116
+ catalogId: String,
117
+ scopeKey: String,
118
+ generation: Long,
119
+ catalogHash: String,
120
+ channel: String,
121
+ selectionContextHash: String,
122
+ ): Boolean = false
123
+
124
+ suspend fun commitReleaseSelection(selection: PersistedSelection): Boolean = false
125
+
101
126
  /**
102
127
  * Marks the current launch as successful after the first content appeared.
103
128
  */
@@ -108,6 +133,30 @@ interface BundleStorageService {
108
133
  */
109
134
  fun notifyAppReady(): Map<String, Any?>
110
135
 
136
+ /**
137
+ * Returns the stable install ID for this app installation.
138
+ */
139
+ fun getInstallId(): String
140
+
141
+ /**
142
+ * Returns the persisted nullable user id for this app installation.
143
+ */
144
+ fun getUserId(): String?
145
+
146
+ /**
147
+ * Returns the persisted nullable username for this app installation.
148
+ */
149
+ fun getUsername(): String?
150
+
151
+ /**
152
+ * Persists the optional user envelope for analytics.
153
+ * Passing null clears the stored user identity.
154
+ */
155
+ fun setUser(
156
+ userId: String?,
157
+ username: String?,
158
+ )
159
+
111
160
  /**
112
161
  * Gets the crashed bundle history
113
162
  * @return CrashedHistory containing crashed bundles
@@ -167,11 +216,15 @@ class BundleFileStorageService(
167
216
  private val decompressService: DecompressService,
168
217
  private val preferences: PreferencesService,
169
218
  private val isolationKey: String,
219
+ private val defaultChannelProvider: () -> String = { HotUpdaterImpl.getChannel(context) },
170
220
  ) : BundleStorageService {
171
221
  companion object {
172
222
  private const val TAG = "BundleStorage"
173
223
  }
174
224
 
225
+ private val releaseStateLock = Any()
226
+ private val pendingInstallSelections = java.util.concurrent.ConcurrentHashMap<String, PersistedSelection>()
227
+
175
228
  private fun emitArchiveProgress(
176
229
  progressCallback: (UpdateProgressPayload) -> Unit,
177
230
  progress: Double,
@@ -487,6 +540,10 @@ class BundleFileStorageService(
487
540
 
488
541
  private var currentLaunchReport: LaunchReport? = null
489
542
 
543
+ @Volatile
544
+ private var currentInstallationIdentity: InstallationIdentity? = null
545
+ private val installationIdentityLock = Any()
546
+
490
547
  @Volatile
491
548
  private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot? = null
492
549
  private val activeBundleMetadataLock = Any()
@@ -504,6 +561,8 @@ class BundleFileStorageService(
504
561
 
505
562
  private fun getLaunchReportFile(): File = File(getBundleStoreDir(), LaunchReport.LAUNCH_REPORT_FILENAME)
506
563
 
564
+ private fun getInstallationIdentityFile(): File = File(getBundleStoreDir(), InstallationIdentity.IDENTITY_FILENAME)
565
+
507
566
  // MARK: - Metadata Operations
508
567
 
509
568
  private fun loadMetadataOrNull(): BundleMetadata? = BundleMetadata.loadFromFile(getMetadataFile(), isolationKey)
@@ -530,6 +589,39 @@ class BundleFileStorageService(
530
589
  report.saveToFile(file)
531
590
  }
532
591
 
592
+ private fun resolveUpdateStrategy(): String =
593
+ try {
594
+ if (HotUpdaterImpl.getFingerprintHash(context).isNullOrEmpty()) {
595
+ "appVersion"
596
+ } else {
597
+ "fingerprint"
598
+ }
599
+ } catch (_: Exception) {
600
+ "appVersion"
601
+ }
602
+
603
+ private fun loadInstallationIdentity(): InstallationIdentity? =
604
+ currentInstallationIdentity ?: InstallationIdentity.loadFromFile(getInstallationIdentityFile())?.also {
605
+ currentInstallationIdentity = it
606
+ }
607
+
608
+ private fun saveInstallationIdentity(identity: InstallationIdentity): Boolean {
609
+ currentInstallationIdentity = identity
610
+ return identity.saveToFile(getInstallationIdentityFile())
611
+ }
612
+
613
+ private fun getOrCreateInstallationIdentity(): InstallationIdentity =
614
+ synchronized(installationIdentityLock) {
615
+ loadInstallationIdentity()?.let { return@synchronized it }
616
+
617
+ InstallationIdentity(installId = UUID.randomUUID().toString()).also {
618
+ saveInstallationIdentity(it)
619
+ }
620
+ }
621
+
622
+ private fun getKnownLaunchBundleId(metadata: BundleMetadata): String =
623
+ getCurrentVerifiedBundleId(metadata) ?: HotUpdaterImpl.getMinBundleId()
624
+
533
625
  private fun createInitialMetadata(): BundleMetadata {
534
626
  val currentBundleId = extractBundleIdFromCurrentURL()
535
627
  Log.d(TAG, "Creating initial metadata with stagingBundleId: $currentBundleId")
@@ -925,10 +1017,34 @@ class BundleFileStorageService(
925
1017
  ): BundleMetadata {
926
1018
  val currentVerifiedBundleId =
927
1019
  getCurrentVerifiedBundleId(metadata)?.takeIf { it != bundleId }
1020
+ val incomingSelection = pendingInstallSelections.remove(bundleId)
1021
+ if (incomingSelection != null && !isSelectionCurrent(incomingSelection)) {
1022
+ throw IllegalStateException("Release catalog selection is stale")
1023
+ }
1024
+ val currentVerifiedSelection =
1025
+ when {
1026
+ metadata.stagingBundleId != null && !metadata.verificationPending -> metadata.stagingSelection
1027
+ metadata.stableBundleId != null -> metadata.stableSelection
1028
+ else -> null
1029
+ }?.takeIf { it.bundleId != bundleId }
1030
+ val stagingSelection = incomingSelection ?: PersistedSelection.legacyBundle(bundleId)
1031
+ val fromSelection =
1032
+ currentVerifiedSelection
1033
+ ?: PersistedSelection.legacyBundle(HotUpdaterImpl.getMinBundleId())
928
1034
 
929
1035
  return metadata.copy(
930
1036
  stableBundleId = currentVerifiedBundleId,
931
1037
  stagingBundleId = bundleId,
1038
+ stableSelection = currentVerifiedSelection,
1039
+ stagingSelection = stagingSelection,
1040
+ pendingUpdateStrategy = resolveUpdateStrategy(),
1041
+ pendingTransition =
1042
+ PendingSelectionTransition(
1043
+ fromReleaseId = fromSelection.releaseId,
1044
+ fromBundleId = fromSelection.bundleId,
1045
+ toReleaseId = stagingSelection.releaseId,
1046
+ toBundleId = stagingSelection.bundleId,
1047
+ ),
932
1048
  verificationPending = true,
933
1049
  updatedAt = System.currentTimeMillis(),
934
1050
  )
@@ -955,16 +1071,30 @@ class BundleFileStorageService(
955
1071
  metadata.copy(
956
1072
  stableBundleId = null,
957
1073
  stagingBundleId = fallbackBundleId,
1074
+ stableSelection = null,
1075
+ stagingSelection = metadata.stableSelection,
1076
+ pendingUpdateStrategy = null,
1077
+ pendingTransition = null,
958
1078
  verificationPending = false,
959
1079
  updatedAt = System.currentTimeMillis(),
960
1080
  )
1081
+
961
1082
  saveMetadata(updatedMetadata)
962
1083
 
963
1084
  val fallbackBundleUrl = fallbackBundleId?.let { getBundleUrlForId(it) }
964
1085
  setBundleURL(fallbackBundleUrl)
965
1086
 
966
1087
  File(getBundleStoreDir(), stagingBundleId).deleteRecursively()
967
- saveLaunchReport(LaunchReport(status = "RECOVERED", crashedBundleId = stagingBundleId))
1088
+ saveLaunchReport(
1089
+ LaunchReport(
1090
+ status = "RECOVERED",
1091
+ fromReleaseId = metadata.pendingTransition?.toReleaseId,
1092
+ fromBundleId = stagingBundleId,
1093
+ toReleaseId = metadata.stableSelection?.releaseId,
1094
+ toBundleId = fallbackBundleId ?: HotUpdaterImpl.getMinBundleId(),
1095
+ updateStrategy = metadata.pendingUpdateStrategy ?: resolveUpdateStrategy(),
1096
+ ),
1097
+ )
968
1098
  return true
969
1099
  }
970
1100
 
@@ -1025,6 +1155,172 @@ class BundleFileStorageService(
1025
1155
  )
1026
1156
  }
1027
1157
 
1158
+ private fun releaseScopeStateKey(
1159
+ catalogId: String,
1160
+ scopeKey: String,
1161
+ ): String = "$catalogId|$scopeKey"
1162
+
1163
+ private fun selectionContextValue(
1164
+ channel: String,
1165
+ selectionContextHash: String,
1166
+ ): String = "$channel\n$selectionContextHash"
1167
+
1168
+ override fun acceptReleaseCatalog(
1169
+ catalogId: String,
1170
+ scopeKey: String,
1171
+ generation: Long,
1172
+ catalogHash: String,
1173
+ channel: String,
1174
+ selectionContextHash: String,
1175
+ ): Boolean =
1176
+ synchronized(releaseStateLock) {
1177
+ val metadata = loadMetadataOrNull() ?: createInitialMetadata()
1178
+ val stateKey = releaseScopeStateKey(catalogId, scopeKey)
1179
+ val highestSeen = metadata.highestSeenCatalogs[stateKey]
1180
+ if (highestSeen != null && generation < highestSeen.generation) {
1181
+ return@synchronized false
1182
+ }
1183
+ if (
1184
+ highestSeen != null &&
1185
+ generation == highestSeen.generation &&
1186
+ catalogHash != highestSeen.catalogHash
1187
+ ) {
1188
+ return@synchronized false
1189
+ }
1190
+
1191
+ val highWaters = metadata.highestSeenCatalogs.toMutableMap()
1192
+ highWaters[stateKey] = CatalogHighWater(generation, catalogHash)
1193
+ val contexts = metadata.currentSelectionContexts.toMutableMap()
1194
+ contexts[stateKey] = selectionContextValue(channel, selectionContextHash)
1195
+ saveMetadata(
1196
+ metadata.copy(
1197
+ highestSeenCatalogs = highWaters,
1198
+ currentSelectionContexts = contexts,
1199
+ updatedAt = System.currentTimeMillis(),
1200
+ ),
1201
+ )
1202
+ }
1203
+
1204
+ private fun activeSelection(metadata: BundleMetadata): PersistedSelection? =
1205
+ metadata.stagingSelection
1206
+ ?: metadata.stagingBundleId?.let(PersistedSelection::legacyBundle)
1207
+ ?: metadata.stableSelection
1208
+ ?: metadata.stableBundleId?.let(PersistedSelection::legacyBundle)
1209
+
1210
+ override fun getActiveUpdateState(): Map<String, Any?> =
1211
+ synchronized(releaseStateLock) {
1212
+ val metadata = loadMetadataOrNull()
1213
+ mapOf(
1214
+ "activeSelection" to metadata?.let(::activeSelection)?.toMap(),
1215
+ "stableSelection" to metadata?.stableSelection?.toMap(),
1216
+ "verificationPending" to (metadata?.verificationPending ?: false),
1217
+ "highestSeenCatalogs" to
1218
+ (metadata?.highestSeenCatalogs ?: emptyMap()).mapValues { (_, value) ->
1219
+ mapOf(
1220
+ "generation" to value.generation.toDouble(),
1221
+ "catalogHash" to value.catalogHash,
1222
+ )
1223
+ },
1224
+ )
1225
+ }
1226
+
1227
+ override fun isReleaseSelectionCurrent(
1228
+ catalogId: String,
1229
+ scopeKey: String,
1230
+ generation: Long,
1231
+ catalogHash: String,
1232
+ channel: String,
1233
+ selectionContextHash: String,
1234
+ ): Boolean =
1235
+ synchronized(releaseStateLock) {
1236
+ val metadata = loadMetadataOrNull() ?: return@synchronized false
1237
+ val stateKey = releaseScopeStateKey(catalogId, scopeKey)
1238
+ metadata.highestSeenCatalogs[stateKey] == CatalogHighWater(generation, catalogHash) &&
1239
+ metadata.currentSelectionContexts[stateKey] ==
1240
+ selectionContextValue(channel, selectionContextHash)
1241
+ }
1242
+
1243
+ private fun isSelectionCurrent(selection: PersistedSelection): Boolean {
1244
+ val catalogId = selection.catalogId ?: return false
1245
+ val scopeKey = selection.scopeKey ?: return false
1246
+ val generation = selection.generation ?: return false
1247
+ val catalogHash = selection.catalogHash ?: return false
1248
+ val selectionContextHash = selection.selectionContextHash ?: return false
1249
+ return isReleaseSelectionCurrent(
1250
+ catalogId = catalogId,
1251
+ scopeKey = scopeKey,
1252
+ generation = generation,
1253
+ catalogHash = catalogHash,
1254
+ channel = selection.channel,
1255
+ selectionContextHash = selectionContextHash,
1256
+ )
1257
+ }
1258
+
1259
+ override fun stageReleaseSelection(selection: PersistedSelection): Boolean {
1260
+ if (!isSelectionCurrent(selection)) return false
1261
+ pendingInstallSelections[selection.bundleId] = selection
1262
+ return true
1263
+ }
1264
+
1265
+ override suspend fun commitReleaseSelection(selection: PersistedSelection): Boolean =
1266
+ withContext(Dispatchers.IO) {
1267
+ synchronized(releaseStateLock) {
1268
+ if (!isSelectionCurrent(selection)) return@synchronized false
1269
+ val metadata = loadMetadataOrNull() ?: createInitialMetadata()
1270
+ val nextMetadata =
1271
+ if (selection.kind == "BUNDLE") {
1272
+ when {
1273
+ metadata.stagingSelection?.bundleId == selection.bundleId ||
1274
+ metadata.stagingBundleId == selection.bundleId -> {
1275
+ metadata.copy(
1276
+ stagingBundleId = selection.bundleId,
1277
+ stagingSelection = selection,
1278
+ updatedAt = System.currentTimeMillis(),
1279
+ )
1280
+ }
1281
+
1282
+ metadata.stableSelection?.bundleId == selection.bundleId ||
1283
+ metadata.stableBundleId == selection.bundleId -> {
1284
+ metadata.copy(
1285
+ stableBundleId = selection.bundleId,
1286
+ stableSelection = selection,
1287
+ updatedAt = System.currentTimeMillis(),
1288
+ )
1289
+ }
1290
+
1291
+ else -> {
1292
+ return@synchronized false
1293
+ }
1294
+ }
1295
+ } else {
1296
+ setBundleURL(null)
1297
+ metadata.copy(
1298
+ stableBundleId = null,
1299
+ stableSelection = null,
1300
+ stagingBundleId = null,
1301
+ stagingSelection = selection,
1302
+ pendingUpdateStrategy = null,
1303
+ pendingTransition = null,
1304
+ verificationPending = false,
1305
+ updatedAt = System.currentTimeMillis(),
1306
+ )
1307
+ }
1308
+ if (!saveMetadata(nextMetadata)) return@synchronized false
1309
+
1310
+ if (selection.channel == defaultChannelProvider()) {
1311
+ preferences.setItem("HotUpdaterChannel", null)
1312
+ } else {
1313
+ preferences.setItem("HotUpdaterChannel", selection.channel)
1314
+ }
1315
+ if (selection.kind != "BUNDLE") {
1316
+ getBundleStoreDir().listFiles()?.forEach { file ->
1317
+ if (file.isDirectory) file.deleteRecursively()
1318
+ }
1319
+ }
1320
+ true
1321
+ }
1322
+ }
1323
+
1028
1324
  // MARK: - Crashed History
1029
1325
 
1030
1326
  private fun loadCrashedHistory(): CrashedHistory = CrashedHistory.loadFromFile(getCrashedHistoryFile())
@@ -1049,24 +1345,70 @@ class BundleFileStorageService(
1049
1345
  return
1050
1346
  }
1051
1347
 
1348
+ val transition = metadata.pendingTransition
1349
+ val fromBundleId = transition?.fromBundleId ?: getKnownLaunchBundleId(metadata)
1052
1350
  saveMetadata(
1053
1351
  metadata.copy(
1352
+ stableBundleId = null,
1353
+ stableSelection = null,
1354
+ pendingUpdateStrategy = null,
1355
+ pendingTransition = null,
1054
1356
  verificationPending = false,
1055
1357
  updatedAt = System.currentTimeMillis(),
1056
1358
  ),
1057
1359
  )
1360
+ saveLaunchReport(
1361
+ LaunchReport(
1362
+ status = "UPDATE_APPLIED",
1363
+ fromReleaseId = transition?.fromReleaseId,
1364
+ fromBundleId = fromBundleId,
1365
+ toReleaseId = transition?.toReleaseId,
1366
+ toBundleId = stagingBundleId,
1367
+ updateStrategy = metadata.pendingUpdateStrategy ?: resolveUpdateStrategy(),
1368
+ ),
1369
+ )
1058
1370
  }
1059
1371
 
1060
1372
  // MARK: - notifyAppReady
1061
1373
 
1062
1374
  override fun notifyAppReady(): Map<String, Any?> {
1063
- val report = loadLaunchReport() ?: return mapOf("status" to "STABLE")
1375
+ val report = loadLaunchReport()
1376
+ if (report == null) {
1377
+ val metadata = loadMetadataOrNull()
1378
+ if (metadata?.verificationPending == true && metadata.stagingBundleId != null) {
1379
+ return mapOf("status" to "PENDING")
1380
+ }
1381
+ return mapOf("status" to "UNCHANGED")
1382
+ }
1064
1383
  return buildMap {
1065
1384
  put("status", report.status)
1066
- report.crashedBundleId?.let { put("crashedBundleId", it) }
1385
+ report.fromReleaseId?.let { put("fromReleaseId", it) }
1386
+ report.fromBundleId?.let { put("fromBundleId", it) }
1387
+ report.toReleaseId?.let { put("toReleaseId", it) }
1388
+ report.toBundleId?.let { put("toBundleId", it) }
1389
+ report.updateStrategy?.let { put("updateStrategy", it) }
1067
1390
  }
1068
1391
  }
1069
1392
 
1393
+ override fun getInstallId(): String = getOrCreateInstallationIdentity().installId
1394
+
1395
+ override fun getUserId(): String? = getOrCreateInstallationIdentity().userId
1396
+
1397
+ override fun getUsername(): String? = getOrCreateInstallationIdentity().username
1398
+
1399
+ override fun setUser(
1400
+ userId: String?,
1401
+ username: String?,
1402
+ ) = synchronized(installationIdentityLock) {
1403
+ saveInstallationIdentity(
1404
+ getOrCreateInstallationIdentity().copy(
1405
+ userId = userId?.trim()?.takeIf { it.isNotEmpty() },
1406
+ username = username?.trim()?.takeIf { it.isNotEmpty() },
1407
+ ),
1408
+ )
1409
+ Unit
1410
+ }
1411
+
1070
1412
  // MARK: - Bundle URL Operations
1071
1413
 
1072
1414
  override fun setBundleURL(localPath: String?): Boolean {
@@ -1128,7 +1470,6 @@ class BundleFileStorageService(
1128
1470
  TAG,
1129
1471
  "updateBundle bundleId $bundleId fileUrl $fileUrl fileHash $fileHash manifestUrl $manifestUrl",
1130
1472
  )
1131
-
1132
1473
  // If no URL is provided, reset to fallback and clean up all bundles
1133
1474
  if (fileUrl.isNullOrEmpty()) {
1134
1475
  Log.d(TAG, "fileUrl is null or empty, resetting to fallback bundle")
@@ -1141,7 +1482,12 @@ class BundleFileStorageService(
1141
1482
  }
1142
1483
 
1143
1484
  // 2. Reset metadata to initial state (clear all bundle references)
1144
- val metadata = createInitialMetadata()
1485
+ val previousMetadata = loadMetadataOrNull()
1486
+ val metadata =
1487
+ createInitialMetadata().copy(
1488
+ highestSeenCatalogs = previousMetadata?.highestSeenCatalogs ?: emptyMap(),
1489
+ currentSelectionContexts = previousMetadata?.currentSelectionContexts ?: emptyMap(),
1490
+ )
1145
1491
  val saveResult = saveMetadata(metadata)
1146
1492
  if (!saveResult) {
1147
1493
  Log.w(TAG, "Failed to reset metadata")
@@ -1932,11 +2278,16 @@ class BundleFileStorageService(
1932
2278
  }
1933
2279
 
1934
2280
  val clearedMetadata =
1935
- BundleMetadata(
2281
+ (loadMetadataOrNull() ?: BundleMetadata()).copy(
1936
2282
  isolationKey = isolationKey,
1937
2283
  stableBundleId = null,
1938
2284
  stagingBundleId = null,
2285
+ stableSelection = null,
2286
+ stagingSelection = null,
2287
+ pendingUpdateStrategy = null,
2288
+ pendingTransition = null,
1939
2289
  verificationPending = false,
2290
+ updatedAt = System.currentTimeMillis(),
1940
2291
  )
1941
2292
 
1942
2293
  if (!saveMetadata(clearedMetadata)) {