@hot-updater/react-native 1.0.7 → 1.1.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 (144) hide show
  1. package/HotUpdater.podspec +14 -2
  2. package/Package.swift +24 -3
  3. package/android/build.gradle +2 -6
  4. package/android/hot-updater-fingerprint.gradle +135 -0
  5. package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
  6. package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
  7. package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
  8. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
  9. package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
  10. package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
  11. package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
  12. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
  13. package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
  14. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
  15. package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
  16. package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
  17. package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
  18. package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
  19. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +492 -0
  20. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
  21. package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
  22. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
  23. package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
  24. package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
  25. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
  26. package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
  27. package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
  28. package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
  29. package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
  30. package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
  31. package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
  32. package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
  33. package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
  34. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
  35. package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
  36. package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
  37. package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
  38. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
  39. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
  40. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
  41. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
  42. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
  43. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
  44. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
  45. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
  46. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
  47. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
  48. package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
  49. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
  50. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
  51. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
  52. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
  53. package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
  54. package/android/src/newarch/HotUpdater.kt +53 -15
  55. package/android/src/newarch/HotUpdaterModule.kt +42 -17
  56. package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
  57. package/android/src/oldarch/HotUpdater.kt +51 -12
  58. package/android/src/oldarch/HotUpdaterModule.kt +43 -17
  59. package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
  60. package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
  61. package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
  62. package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
  63. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
  64. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
  65. package/ios/HotUpdater/Internal/CohortService.swift +8 -0
  66. package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
  67. package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
  68. package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
  69. package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
  70. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
  71. package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
  72. package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
  73. package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
  74. package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
  75. package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
  76. package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
  77. package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
  78. package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
  79. package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
  80. package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
  81. package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
  82. package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
  83. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
  84. package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
  85. package/ios/HotUpdater/Package.swift +11 -0
  86. package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
  87. package/ios/HotUpdater/Public/HotUpdater.h +21 -2
  88. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
  89. package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
  90. package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
  91. package/lib/commonjs/checkForUpdate.js +3 -1
  92. package/lib/commonjs/checkForUpdate.js.map +1 -1
  93. package/lib/commonjs/index.js +9 -12
  94. package/lib/commonjs/index.js.map +1 -1
  95. package/lib/commonjs/native.js +24 -79
  96. package/lib/commonjs/native.js.map +1 -1
  97. package/lib/commonjs/nativeUpdateEvents.js +66 -0
  98. package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
  99. package/lib/commonjs/sdkVersion.js +1 -1
  100. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  101. package/lib/module/checkForUpdate.js +3 -1
  102. package/lib/module/checkForUpdate.js.map +1 -1
  103. package/lib/module/index.js +10 -13
  104. package/lib/module/index.js.map +1 -1
  105. package/lib/module/native.js +19 -73
  106. package/lib/module/native.js.map +1 -1
  107. package/lib/module/nativeUpdateEvents.js +61 -0
  108. package/lib/module/nativeUpdateEvents.js.map +1 -0
  109. package/lib/module/sdkVersion.js +1 -1
  110. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  111. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/index.d.ts +4 -14
  113. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/native.d.ts +9 -46
  115. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
  117. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  119. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
  120. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  121. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  122. package/lib/typescript/module/index.d.ts +4 -14
  123. package/lib/typescript/module/index.d.ts.map +1 -1
  124. package/lib/typescript/module/native.d.ts +9 -46
  125. package/lib/typescript/module/native.d.ts.map +1 -1
  126. package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
  127. package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
  128. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  129. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
  130. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  131. package/package.json +4 -4
  132. package/src/checkForUpdate.ts +7 -1
  133. package/src/index.ts +9 -17
  134. package/src/native.ts +19 -91
  135. package/src/nativeUpdateEvents.ts +77 -0
  136. package/src/sdkVersion.ts +1 -1
  137. package/src/specs/NativeHotUpdater.ts +20 -3
  138. package/android/libs/README.md +0 -50
  139. package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
  140. package/android/src/bridge/BridgeReactInstance.kt +0 -38
  141. package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
  142. package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
  143. package/android/src/newarch/ReactIntegrationManager.kt +0 -185
  144. package/android/src/oldarch/ReactIntegrationManager.kt +0 -120
@@ -1,5 +1,6 @@
1
1
  package com.hotupdater
2
2
 
3
+ import android.app.Activity
3
4
  import android.app.ActivityOptions
4
5
  import android.app.Application
5
6
  import android.content.Context
@@ -7,23 +8,56 @@ import android.content.Intent
7
8
  import android.os.Process
8
9
  import android.util.Log
9
10
  import kotlinx.coroutines.Dispatchers
11
+ import kotlinx.coroutines.runBlocking
10
12
  import kotlinx.coroutines.withContext
11
- import java.io.File
13
+ import java.util.concurrent.atomic.AtomicLong
12
14
 
13
15
  /**
14
16
  * Core implementation class for HotUpdater functionality
15
17
  */
16
18
  class HotUpdaterImpl {
17
19
  private val context: Context
20
+ private val environment: HotUpdaterInstanceEnvironment
18
21
  private val bundleStorage: BundleStorageService
19
22
  private val preferences: PreferencesService
23
+
24
+ // Each instance watches its own launch: its crash marker, and the launch
25
+ // gate that waits for one of its own views.
20
26
  private val recoveryManager: HotUpdaterRecoveryManager
27
+ private val activityStartGate: ActivityStartGate
21
28
  private val releaseCatalogCache: ReleaseCatalogCacheService
22
29
  private val installIdentity: InstallIdentityService
23
30
  private val keyValueStorage: KeyValueStorageService
24
- private val activityStartGate: ActivityStartGate
31
+
32
+ // Read by native update checks on other threads while React Native sets it.
33
+ @Volatile
25
34
  private var currentLaunchSelection: LaunchSelection? = null
26
35
 
36
+ // The selection React Native runs in this process. A channel reset clears
37
+ // currentLaunchSelection, so React Native's next read picks the default
38
+ // channel's bundle, but the running bundle stays until a reload.
39
+ @Volatile
40
+ private var runningLaunchSelection: LaunchSelection? = null
41
+
42
+ // Counts the crash markers written for this instance's launches. A
43
+ // prepared launch keeps the count it started from: one written since,
44
+ // even while it was being prepared, makes the next start prepare again,
45
+ // which reads the marker. Crash recovery bumps it under its own lock, so
46
+ // it never waits for the launch lock.
47
+ private val launchInvalidations = AtomicLong()
48
+
49
+ @Volatile
50
+ private var currentLaunchInvalidations = 0L
51
+
52
+ // The selection a named instance's monitored launch runs, with that
53
+ // launch, under the launch lock.
54
+ private var monitoredLaunch: Pair<LaunchSelection, Long>? = null
55
+
56
+ // Serializes the launch's bundle selection with the update check's read
57
+ // of it, so a check never reads metadata the launch is still selecting from,
58
+ // and with a channel reset or built-in commit, which delete downloaded bundles.
59
+ private val launchPreparationLock = Any()
60
+
27
61
  /**
28
62
  * Primary constructor with dependency injection (for testing)
29
63
  */
@@ -31,27 +65,50 @@ class HotUpdaterImpl {
31
65
  context: Context,
32
66
  bundleStorage: BundleStorageService,
33
67
  preferences: PreferencesService,
68
+ ) : this(context, bundleStorage, preferences, HotUpdaterInstanceEnvironment.main)
69
+
70
+ /**
71
+ * @param activityStartGate the gate that arms this instance's launch;
72
+ * by default one that waits for a started activity showing one of its views
73
+ */
74
+ internal constructor(
75
+ context: Context,
76
+ bundleStorage: BundleStorageService,
77
+ preferences: PreferencesService,
78
+ environment: HotUpdaterInstanceEnvironment,
79
+ activityStartGate: ActivityStartGate? = null,
34
80
  ) {
35
81
  this.context = context.applicationContext
82
+ // An activity started before this process followed activities is
83
+ // known only from the context the app hands over.
84
+ (context as? Activity)?.let(HotUpdaterRecoveryAttribution.shared::noteActivity)
85
+ this.environment = environment
36
86
  this.bundleStorage = bundleStorage
37
87
  this.preferences = preferences
38
- this.activityStartGate = ActivityStartGate(this.context as? Application)
39
- this.recoveryManager = HotUpdaterRecoveryManager(this.context)
88
+ this.recoveryManager = HotUpdaterRecoveryManager(this.context, environment)
89
+ this.activityStartGate = activityStartGate ?: createActivityStartGate(this.context, environment)
40
90
  this.releaseCatalogCache =
41
- ReleaseCatalogCacheService(
42
- File(this.context.noBackupFilesDir, "hot-updater-release-catalog-cache"),
43
- )
91
+ ReleaseCatalogCacheService(environment.releaseCatalogCacheDirectory(this.context))
44
92
  this.installIdentity = InstallIdentityService.create(this.context)
45
- this.keyValueStorage = KeyValueStorageService.create(this.context)
93
+ this.keyValueStorage = KeyValueStorageService(environment.keyValueStorageFile(this.context))
46
94
  }
47
95
 
48
96
  /**
49
97
  * Convenience constructor for simple usage
50
98
  */
51
- constructor(context: Context) : this(
99
+ constructor(context: Context) : this(context, HotUpdaterInstanceEnvironment.main)
100
+
101
+ /** The impl of a named instance, or of main, with its own storage and configuration. */
102
+ internal constructor(
103
+ context: Context,
104
+ environment: HotUpdaterInstanceEnvironment,
105
+ activityStartGate: ActivityStartGate? = null,
106
+ ) : this(
52
107
  context = context,
53
- bundleStorage = createBundleStorage(context),
54
- preferences = createPreferences(context),
108
+ bundleStorage = createBundleStorage(context, environment),
109
+ preferences = createPreferences(context, environment),
110
+ environment = environment,
111
+ activityStartGate = activityStartGate,
55
112
  )
56
113
 
57
114
  /**
@@ -79,18 +136,25 @@ class HotUpdaterImpl {
79
136
 
80
137
  companion object {
81
138
  private const val TAG = "HotUpdaterImpl"
82
- private const val DEFAULT_CHANNEL = "production"
139
+ internal const val DEFAULT_CHANNEL = "production"
83
140
  private const val CHANNEL_STORAGE_KEY = "HotUpdaterChannel"
84
141
 
142
+ // How many times one start prepares its launch while crashes of the
143
+ // running launch keep writing markers.
144
+ private const val MAX_LAUNCH_PREPARATIONS = 3
145
+
85
146
  /**
86
147
  * Create BundleStorageService with all dependencies
87
148
  */
88
- private fun createBundleStorage(context: Context): BundleStorageService {
149
+ private fun createBundleStorage(
150
+ context: Context,
151
+ environment: HotUpdaterInstanceEnvironment,
152
+ ): BundleStorageService {
89
153
  val appContext = context.applicationContext
90
- val fileSystem = FileManagerService(appContext)
91
- val preferences = createPreferences(appContext)
154
+ val fileSystem = FileManagerService(appContext, environment.filesRoot(appContext))
155
+ val preferences = createPreferences(appContext, environment)
92
156
  val downloadService = OkHttpDownloadService()
93
- val isolationKey = getIsolationKey(appContext)
157
+ val isolationKey = environment.isolationKey(appContext)
94
158
 
95
159
  return BundleFileStorageService(
96
160
  appContext,
@@ -98,43 +162,54 @@ class HotUpdaterImpl {
98
162
  downloadService,
99
163
  preferences,
100
164
  isolationKey,
165
+ defaultChannelProvider = { environment.defaultChannel(appContext) },
166
+ fingerprintHashProvider = { environment.fingerprintHash(appContext) },
167
+ publicKeyProvider = { environment.publicKey(appContext) },
168
+ fallbackBundleURL = environment.fallbackBundleURL,
101
169
  )
102
170
  }
103
171
 
104
172
  /**
105
- * Create PreferencesService with isolation key
173
+ * A launch counts once a started activity shows one of the instance's
174
+ * views. Main takes every view no named instance claims, so an app
175
+ * with only main waits for any React Native view, as it always has,
176
+ * and counts an activity started before the gate at once. A named
177
+ * instance is often prepared while the app already shows other
178
+ * screens, so it checks the activities already started for its view.
106
179
  */
107
- private fun createPreferences(context: Context): PreferencesService {
108
- val appContext = context.applicationContext
109
- val isolationKey = getIsolationKey(appContext)
110
- return VersionedPreferencesService(appContext, isolationKey)
180
+ private fun createActivityStartGate(
181
+ context: Context,
182
+ environment: HotUpdaterInstanceEnvironment,
183
+ ): ActivityStartGate {
184
+ val attribution = HotUpdaterRecoveryAttribution.shared
185
+ return ActivityStartGate(
186
+ context as? Application,
187
+ showsReactView = { activity -> attribution.showsViewOf(activity, environment.name) },
188
+ activitiesStartedBefore = activitiesStartedBefore(environment, attribution),
189
+ beforeCheckingViews = HotUpdaterRecoveryManager::resolveHeldContent,
190
+ )
111
191
  }
112
192
 
113
193
  /**
114
- * Gets the complete isolation key for preferences storage
115
- * @param context Application context
116
- * @return The isolation key in format: HotUpdaterPrefs_{fingerprintOrVersion}_{channel}
194
+ * The activities already started that a gate created now checks for
195
+ * the instance's view: a named instance's, while main counts any
196
+ * started activity at once, as it always has (null).
117
197
  */
118
- private fun getIsolationKey(context: Context): String {
119
- // Programmatic override (brownfield/AAR). When set, it is used verbatim
120
- // so the OTA cache can stay stable across host app version bumps.
121
- HotUpdaterConfig.isolationKey?.takeIf { it.isNotEmpty() }?.let { return it }
198
+ internal fun activitiesStartedBefore(
199
+ environment: HotUpdaterInstanceEnvironment,
200
+ attribution: HotUpdaterRecoveryAttribution = HotUpdaterRecoveryAttribution.shared,
201
+ ): (() -> Collection<Activity>)? = if (environment.isMain) null else attribution::startedActivities
122
202
 
123
- val fingerprintHash = getFingerprintHash(context)
124
-
125
- // Get app version and channel
126
- val appVersion = getAppVersion(context) ?: "unknown"
127
- val appChannel = getChannel(context)
128
-
129
- // Include both fingerprint hash and app version for complete isolation
130
- val baseKey =
131
- if (!fingerprintHash.isNullOrEmpty()) {
132
- "${fingerprintHash}_$appVersion"
133
- } else {
134
- appVersion
135
- }
136
-
137
- return "HotUpdaterPrefs_${baseKey}_$appChannel"
203
+ /**
204
+ * Create PreferencesService with isolation key
205
+ */
206
+ private fun createPreferences(
207
+ context: Context,
208
+ environment: HotUpdaterInstanceEnvironment,
209
+ ): PreferencesService {
210
+ val appContext = context.applicationContext
211
+ val isolationKey = environment.isolationKey(appContext)
212
+ return VersionedPreferencesService(appContext, isolationKey, environment.preferencesPrefix)
138
213
  }
139
214
 
140
215
  fun getAppVersion(context: Context): String? =
@@ -177,22 +252,25 @@ class HotUpdaterImpl {
177
252
  * @param context Application context
178
253
  * @return The fingerprint hash or null if not set
179
254
  */
180
- fun getFingerprintHash(context: Context): String? =
181
- NativeConfigUtils.getString(
182
- context,
183
- NativeConfigUtils.FINGERPRINT_HASH_META_DATA_KEY,
184
- )
255
+ fun getFingerprintHash(
256
+ @Suppress("UNUSED_PARAMETER") context: Context,
257
+ ): String? = resolveFingerprintHash()
258
+
259
+ /**
260
+ * The programmatic override (brownfield/AAR setups, see HotUpdaterConfig),
261
+ * else the fingerprint the build embedded, written by the
262
+ * generateHotUpdaterFingerprintHash build task.
263
+ */
264
+ private fun resolveFingerprintHash(): String? =
265
+ HotUpdaterConfig.fingerprintHash?.takeIf { it.isNotEmpty() }
266
+ ?: HotUpdaterConfig.buildFingerprintHash
185
267
  }
186
268
 
187
269
  /**
188
270
  * Gets the current fingerprint hash
189
271
  * @return The fingerprint hash or null if not set
190
272
  */
191
- fun getFingerprintHash(): String? =
192
- NativeConfigUtils.getString(
193
- context,
194
- NativeConfigUtils.FINGERPRINT_HASH_META_DATA_KEY,
195
- )
273
+ fun getFingerprintHash(): String? = environment.fingerprintHash(context)
196
274
 
197
275
  /**
198
276
  * Gets the current update channel
@@ -209,19 +287,24 @@ class HotUpdaterImpl {
209
287
  return overriddenChannel
210
288
  }
211
289
 
212
- return NativeConfigUtils.getString(
213
- context,
214
- NativeConfigUtils.CHANNEL_META_DATA_KEY,
215
- ) ?: DEFAULT_CHANNEL
290
+ return environment.defaultChannel(context)
216
291
  }
217
292
 
218
- fun getDefaultChannel(): String = getChannel(context)
293
+ fun getDefaultChannel(): String = environment.defaultChannel(context)
219
294
 
220
295
  /**
221
296
  * Gets the path to the bundle file
222
297
  * @return The path to the bundle file
223
298
  */
224
- fun getJSBundleFile(): String = prepareLaunchIfNeeded().bundleUrl
299
+ fun getJSBundleFile(): String {
300
+ // Hot Updater's restart process only relaunches the app; preparing a
301
+ // launch there would consume the crash marker and launch state the
302
+ // relaunched process needs.
303
+ if (HotUpdaterProcess.isRestartProcess(context)) {
304
+ return bundleStorage.getFallbackBundleURL()
305
+ }
306
+ return prepareLaunchIfNeeded().bundleUrl
307
+ }
225
308
 
226
309
  /**
227
310
  * Updates the bundle from its manifest and complete asset descriptors.
@@ -244,18 +327,16 @@ class HotUpdaterImpl {
244
327
  fileResolver: ArtifactFileResolver? = null,
245
328
  progressCallback: (UpdateProgressPayload) -> Unit,
246
329
  ): UpdateBundleResult {
247
- if (selection != null && !bundleStorage.stageReleaseSelection(selection)) {
248
- throw StaleReleaseSelectionException()
249
- }
250
330
  val result =
251
331
  bundleStorage.updateBundle(
252
- bundleId,
253
- manifestUrl,
254
- manifestFileHash,
255
- assets,
256
- archiveUrl,
257
- fileResolver,
258
- progressCallback,
332
+ bundleId = bundleId,
333
+ manifestUrl = manifestUrl,
334
+ manifestFileHash = manifestFileHash,
335
+ assets = assets,
336
+ archiveUrl = archiveUrl,
337
+ fileResolver = fileResolver,
338
+ selection = selection,
339
+ progressCallback = progressCallback,
259
340
  )
260
341
 
261
342
  if (!channel.isNullOrEmpty()) {
@@ -269,43 +350,25 @@ class HotUpdaterImpl {
269
350
  }
270
351
 
271
352
  /**
272
- * Reloads the React Native application
353
+ * Restarts the app process so the next launch loads the current bundle.
273
354
  * @param reactContext The original context (preferably ReactApplicationContext to get current activity)
274
355
  */
275
356
  suspend fun reload(reactContext: Context) {
276
357
  try {
277
- currentLaunchSelection = null
278
- withContext(Dispatchers.Main) {
279
- performReactReload(reactContext)
280
- }
281
- } catch (e: Exception) {
282
- Log.e("HotUpdaterImpl", "Failed to reload application", e)
283
- }
284
- }
285
-
286
- suspend fun reloadProcess(reactContext: Context) {
287
- try {
288
- currentLaunchSelection = null
289
358
  withContext(Dispatchers.Main) {
290
359
  if (!restartApplication(reactContext)) {
291
- Log.w(TAG, "Falling back to in-process reload because process restart could not be started")
292
- performReactReload(reactContext)
360
+ throw IllegalStateException("Process restart could not be started")
293
361
  }
294
362
  }
363
+ currentLaunchSelection = null
364
+ runningLaunchSelection = null
295
365
  } catch (e: Exception) {
296
366
  Log.e("HotUpdaterImpl", "Failed to restart application process", e)
297
367
  throw e
298
368
  }
299
369
  }
300
370
 
301
- private suspend fun performReactReload(reactContext: Context) {
302
- val reactIntegrationManager = ReactIntegrationManager(reactContext)
303
- val bundleURL = getJSBundleFile()
304
- reactIntegrationManager.setJSBundle(bundleURL)
305
- reactIntegrationManager.reload()
306
- }
307
-
308
- // Use a cold restart in release builds so bundle application does not depend on RN reload timing.
371
+ // A cold restart, so applying a bundle does not depend on React Native reload timing.
309
372
  private fun restartApplication(reactContext: Context): Boolean {
310
373
  val applicationContext = reactContext.applicationContext
311
374
  val currentActivity =
@@ -381,7 +444,24 @@ class HotUpdaterImpl {
381
444
  selectionContextHash,
382
445
  )
383
446
 
384
- suspend fun commitReleaseSelection(selection: PersistedSelection): Boolean = bundleStorage.commitReleaseSelection(selection)
447
+ suspend fun commitReleaseSelection(selection: PersistedSelection): Boolean {
448
+ if (selection.kind == "BUNDLE") return bundleStorage.commitReleaseSelection(selection)
449
+
450
+ // Committing the built-in bundle deletes every downloaded bundle, so it
451
+ // runs under the launch lock as a channel reset does: React Native never
452
+ // selects a bundle the commit is about to delete, and its next read picks
453
+ // the built-in bundle. The storage commit runs on another IO thread, so
454
+ // blocking this one cannot deadlock.
455
+ return withContext(Dispatchers.IO) {
456
+ synchronized(launchPreparationLock) {
457
+ val committed = runBlocking { bundleStorage.commitReleaseSelection(selection) }
458
+ if (committed) {
459
+ currentLaunchSelection = null
460
+ }
461
+ committed
462
+ }
463
+ }
464
+ }
385
465
 
386
466
  fun getInstallId(): String = installIdentity.getInstallId()
387
467
 
@@ -438,6 +518,38 @@ class HotUpdaterImpl {
438
518
  return null
439
519
  }
440
520
 
521
+ /**
522
+ * The bundle this process runs. A channel reset clears [currentLaunchSelection]
523
+ * for the next launch, but the running bundle stays until React Native reloads.
524
+ */
525
+ fun getRunningBundleId(): String? = (currentLaunchSelection ?: runningLaunchSelection)?.launchedBundleId
526
+
527
+ /** Takes the queued native update events for JavaScript's `takeNativeUpdateEvents`. */
528
+ fun takeNativeUpdateEvents(): String? =
529
+ NativeUpdateEventQueue(::getStorageItem, ::setStorageItem, build = {
530
+ NativeUpdateEventQueue.buildOf(getAppVersion(), getMinBundleId(), CohortService(context).getCohort())
531
+ }).take()
532
+
533
+ /**
534
+ * The bundle the native update check counts as running: the launched one,
535
+ * or before React Native starts, the one its launch would pick. Before
536
+ * React Native starts, what the previous process left is settled first, so
537
+ * the check reads the selection, launch state, and crash history the
538
+ * launch will use. Preparing the launch here would arm crash monitoring and
539
+ * keep a forced update staged before React Native starts from applying in
540
+ * this process.
541
+ */
542
+ fun getBundleIdForUpdateCheck(): String? =
543
+ synchronized(launchPreparationLock) {
544
+ val launchSelection = currentLaunchSelection ?: runningLaunchSelection
545
+ if (launchSelection != null) {
546
+ return@synchronized launchSelection.launchedBundleId
547
+ }
548
+
549
+ bundleStorage.settlePreviousLaunch { recoveryManager.consumePendingCrashRecovery() }
550
+ bundleStorage.peekLaunchBundleId()
551
+ }
552
+
441
553
  /**
442
554
  * Gets the current manifest from bundle storage.
443
555
  */
@@ -450,40 +562,134 @@ class HotUpdaterImpl {
450
562
  return emptyMap()
451
563
  }
452
564
 
453
- suspend fun resetChannel(): Boolean {
454
- val success = bundleStorage.resetChannel()
455
- if (success) {
456
- preferences.setItem(CHANNEL_STORAGE_KEY, null)
457
- currentLaunchSelection = null
565
+ suspend fun resetChannel(): Boolean =
566
+ withContext(Dispatchers.IO) {
567
+ // Under the launch lock, so React Native never selects a bundle the
568
+ // reset is about to delete. The storage reset runs on another IO
569
+ // thread, so blocking this one cannot deadlock.
570
+ synchronized(launchPreparationLock) {
571
+ val success = runBlocking { bundleStorage.resetChannel() }
572
+ if (success) {
573
+ preferences.setItem(CHANNEL_STORAGE_KEY, null)
574
+ currentLaunchSelection = null
575
+ }
576
+ success
577
+ }
578
+ }
579
+
580
+ private fun prepareLaunchIfNeeded(): LaunchSelection =
581
+ synchronized(launchPreparationLock) {
582
+ prepareLaunchLocked()
583
+ }
584
+
585
+ private fun prepareLaunchLocked(): LaunchSelection {
586
+ var invalidations = launchInvalidations.get()
587
+ currentLaunchSelection?.takeIf { currentLaunchInvalidations == invalidations }?.let { return it }
588
+
589
+ // The running launch's crash may write its marker while this one is
590
+ // prepared, after the marker was read: the selection would launch
591
+ // the failing bundle again. Its monitoring is replaced only once the
592
+ // new launch's starts, so the count is checked after that, and the
593
+ // launch prepared again, reading the marker, while it changed. A
594
+ // crash can't recur faster than a launch is prepared; should it, the
595
+ // latest selection goes, and the count it started from has the next
596
+ // start prepare again.
597
+ repeat(MAX_LAUNCH_PREPARATIONS - 1) {
598
+ val prepared = selectAndMonitorLaunchLocked()
599
+ val now = launchInvalidations.get()
600
+ if (now == invalidations) return publishLaunchLocked(prepared, invalidations)
601
+ invalidations = now
458
602
  }
459
- return success
603
+ return publishLaunchLocked(selectAndMonitorLaunchLocked(), invalidations)
460
604
  }
461
605
 
462
- private fun prepareLaunchIfNeeded(): LaunchSelection {
463
- currentLaunchSelection?.let { return it }
606
+ private class PreparedLaunch(
607
+ val selection: LaunchSelection,
608
+ val launch: Long,
609
+ /** False when the selection continues the launch already monitored. */
610
+ val started: Boolean,
611
+ )
612
+
613
+ /**
614
+ * Whether [next] continues the named instance's launch of [current]
615
+ * rather than starting another: the same bundle, from the same URL,
616
+ * still unverified, or verified since. A launch's state is its bundle's
617
+ * persisted verification, so the same bundle selected again in this
618
+ * process, as after a reset that keeps it or a reload before the
619
+ * process restarts, is the same launch; another one would make its
620
+ * runtimes, which load the same URL, look like a replacement's.
621
+ */
622
+ private fun continuesLaunch(
623
+ current: LaunchSelection,
624
+ next: LaunchSelection,
625
+ ): Boolean =
626
+ current.launchedBundleId == next.launchedBundleId &&
627
+ current.bundleUrl == next.bundleUrl &&
628
+ (current.shouldRollbackOnCrash || !next.shouldRollbackOnCrash)
464
629
 
630
+ private fun selectAndMonitorLaunchLocked(): PreparedLaunch {
465
631
  val pendingRecovery = recoveryManager.consumePendingCrashRecovery()
466
632
  val selection = bundleStorage.prepareLaunch(pendingRecovery)
633
+ monitoredLaunch?.let { (current, launch) ->
634
+ if (!environment.isMain && continuesLaunch(current, selection) && recoveryManager.monitorsLaunch(launch)) {
635
+ return PreparedLaunch(selection, launch, started = false)
636
+ }
637
+ }
638
+ val launch =
639
+ recoveryManager.startMonitoring(
640
+ bundleId = selection.launchedBundleId,
641
+ shouldRollback = selection.shouldRollbackOnCrash,
642
+ onContentAppeared = { launchedBundleId ->
643
+ bundleStorage.markLaunchCompleted(launchedBundleId)
644
+ },
645
+ onCrashOfAnotherInstance = { launchedBundleId ->
646
+ bundleStorage.clearLaunchStarted(launchedBundleId)
647
+ },
648
+ // Restarting the app restarts main's host. A named instance's
649
+ // crash leaves its marker for its next launch to roll back.
650
+ onRecoveryRestartRequested =
651
+ if (environment.isMain) {
652
+ { restartApplication(context) }
653
+ } else {
654
+ null
655
+ },
656
+ onRuntimeActivity = activityStartGate::watchActivity,
657
+ launchURL = selection.bundleUrl,
658
+ onCharged = { launchInvalidations.incrementAndGet() },
659
+ )
660
+ return PreparedLaunch(selection, launch, started = true)
661
+ }
662
+
663
+ private fun publishLaunchLocked(
664
+ prepared: PreparedLaunch,
665
+ invalidations: Long,
666
+ ): LaunchSelection {
667
+ val selection = prepared.selection
668
+ val launch = prepared.launch
669
+ monitoredLaunch = selection to launch
670
+ // A launch continued keeps its gate, which may have armed it already.
671
+ if (!prepared.started) {
672
+ runningLaunchSelection = selection
673
+ currentLaunchInvalidations = invalidations
674
+ currentLaunchSelection = selection
675
+ return selection
676
+ }
467
677
  // A headless JS task, such as a background push, loads the bundle without
468
678
  // an activity and never renders. Only a launch with an activity may leave
469
- // an unfinished launch for the next process to roll back.
679
+ // an unfinished launch for the next process to roll back. A named
680
+ // runtime may also be pre-warmed while the app shows other screens, so
681
+ // its launch waits for one of its own views.
470
682
  activityStartGate.runWhenActivityStarted(
471
683
  if (selection.shouldRollbackOnCrash) {
472
- { bundleStorage.markLaunchStarted(selection.launchedBundleId) }
684
+ {
685
+ recoveryManager.armLaunch(launch) { bundleStorage.markLaunchStarted(selection.launchedBundleId) }
686
+ }
473
687
  } else {
474
688
  null
475
689
  },
476
690
  )
477
- recoveryManager.startMonitoring(
478
- bundleId = selection.launchedBundleId,
479
- shouldRollback = selection.shouldRollbackOnCrash,
480
- onContentAppeared = { launchedBundleId ->
481
- bundleStorage.markLaunchCompleted(launchedBundleId)
482
- },
483
- onRecoveryRestartRequested = {
484
- restartApplication(context)
485
- },
486
- )
691
+ runningLaunchSelection = selection
692
+ currentLaunchInvalidations = invalidations
487
693
  currentLaunchSelection = selection
488
694
  return selection
489
695
  }