@hot-updater/react-native 1.0.0-rc.1 → 1.0.0-rc.15

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 (227) hide show
  1. package/android/proguard-rules.pro +17 -2
  2. package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
  3. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +651 -637
  4. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +10 -0
  5. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +0 -13
  6. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +10 -14
  7. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +37 -54
  8. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
  9. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +19 -0
  10. package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
  11. package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
  12. package/android/src/newarch/HotUpdater.kt +7 -9
  13. package/android/src/newarch/HotUpdaterModule.kt +40 -20
  14. package/android/src/oldarch/HotUpdater.kt +7 -9
  15. package/android/src/oldarch/HotUpdaterModule.kt +40 -20
  16. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +1126 -74
  17. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -5
  18. package/android/src/test/java/com/hotupdater/HotUpdaterRecoveryStateTest.kt +97 -0
  19. package/android/src/test/java/com/hotupdater/PendingCrashRecoveryTest.kt +44 -0
  20. package/android/src/test/java/com/hotupdater/TarBrArchiveExtractorTest.kt +206 -0
  21. package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
  22. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
  23. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +773 -1016
  24. package/ios/HotUpdater/Internal/BundleMetadata.swift +5 -0
  25. package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
  26. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
  27. package/ios/HotUpdater/Internal/HotUpdater.mm +5 -27
  28. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +149 -51
  29. package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
  30. package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
  31. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +13 -72
  32. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
  33. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
  34. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
  35. package/lib/commonjs/checkForUpdate.js +32 -23
  36. package/lib/commonjs/checkForUpdate.js.map +1 -1
  37. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +97 -28
  38. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  39. package/lib/commonjs/error.js +4 -13
  40. package/lib/commonjs/error.js.map +1 -1
  41. package/lib/commonjs/fetchJSON.js +9 -2
  42. package/lib/commonjs/fetchJSON.js.map +1 -1
  43. package/lib/commonjs/httpClient.js +46 -29
  44. package/lib/commonjs/httpClient.js.map +1 -1
  45. package/lib/commonjs/httpClient.spec.js +57 -5
  46. package/lib/commonjs/httpClient.spec.js.map +1 -1
  47. package/lib/commonjs/index.js +31 -14
  48. package/lib/commonjs/index.js.map +1 -1
  49. package/lib/commonjs/index.spec.js +56 -11
  50. package/lib/commonjs/index.spec.js.map +1 -1
  51. package/lib/commonjs/native.js +15 -14
  52. package/lib/commonjs/native.js.map +1 -1
  53. package/lib/commonjs/native.spec.js +128 -22
  54. package/lib/commonjs/native.spec.js.map +1 -1
  55. package/lib/commonjs/{notifyAppReadyAnalytics.boundary.spec.js → notifyAppReadyInsights.boundary.spec.js} +28 -21
  56. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +1 -0
  57. package/lib/commonjs/notifyAppReadyInsights.js +188 -0
  58. package/lib/commonjs/notifyAppReadyInsights.js.map +1 -0
  59. package/lib/commonjs/{notifyAppReadyAnalytics.spec.js → notifyAppReadyInsights.spec.js} +161 -29
  60. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +1 -0
  61. package/lib/commonjs/{notifyAppReadyAnalytics.test-utils.js → notifyAppReadyInsights.test-utils.js} +3 -3
  62. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +1 -0
  63. package/lib/commonjs/{notifyAppReadyAnalytics.transport.spec.js → notifyAppReadyInsights.transport.spec.js} +14 -7
  64. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +1 -0
  65. package/lib/commonjs/sdkVersion.js +1 -1
  66. package/lib/commonjs/sdkVersion.js.map +1 -1
  67. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  68. package/lib/commonjs/store.js +3 -10
  69. package/lib/commonjs/store.js.map +1 -1
  70. package/lib/commonjs/store.notification.spec.js +12 -4
  71. package/lib/commonjs/store.notification.spec.js.map +1 -1
  72. package/lib/commonjs/store.spec.js +2 -136
  73. package/lib/commonjs/store.spec.js.map +1 -1
  74. package/lib/commonjs/wrap.js +11 -12
  75. package/lib/commonjs/wrap.js.map +1 -1
  76. package/lib/commonjs/wrap.spec.js +97 -25
  77. package/lib/commonjs/wrap.spec.js.map +1 -1
  78. package/lib/module/checkForUpdate.js +32 -23
  79. package/lib/module/checkForUpdate.js.map +1 -1
  80. package/lib/module/checkForUpdate.releaseCatalog.spec.js +97 -28
  81. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  82. package/lib/module/error.js +4 -13
  83. package/lib/module/error.js.map +1 -1
  84. package/lib/module/fetchJSON.js +7 -1
  85. package/lib/module/fetchJSON.js.map +1 -1
  86. package/lib/module/httpClient.js +48 -31
  87. package/lib/module/httpClient.js.map +1 -1
  88. package/lib/module/httpClient.spec.js +57 -5
  89. package/lib/module/httpClient.spec.js.map +1 -1
  90. package/lib/module/index.js +32 -15
  91. package/lib/module/index.js.map +1 -1
  92. package/lib/module/index.spec.js +56 -11
  93. package/lib/module/index.spec.js.map +1 -1
  94. package/lib/module/native.js +13 -12
  95. package/lib/module/native.js.map +1 -1
  96. package/lib/module/native.spec.js +128 -22
  97. package/lib/module/native.spec.js.map +1 -1
  98. package/lib/module/{notifyAppReadyAnalytics.boundary.spec.js → notifyAppReadyInsights.boundary.spec.js} +23 -16
  99. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +1 -0
  100. package/lib/module/notifyAppReadyInsights.js +181 -0
  101. package/lib/module/notifyAppReadyInsights.js.map +1 -0
  102. package/lib/module/{notifyAppReadyAnalytics.spec.js → notifyAppReadyInsights.spec.js} +153 -21
  103. package/lib/module/notifyAppReadyInsights.spec.js.map +1 -0
  104. package/lib/module/{notifyAppReadyAnalytics.test-utils.js → notifyAppReadyInsights.test-utils.js} +3 -3
  105. package/lib/module/notifyAppReadyInsights.test-utils.js.map +1 -0
  106. package/lib/module/{notifyAppReadyAnalytics.transport.spec.js → notifyAppReadyInsights.transport.spec.js} +11 -4
  107. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +1 -0
  108. package/lib/module/sdkVersion.js +1 -1
  109. package/lib/module/sdkVersion.js.map +1 -1
  110. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  111. package/lib/module/store.js +3 -10
  112. package/lib/module/store.js.map +1 -1
  113. package/lib/module/store.notification.spec.js +12 -4
  114. package/lib/module/store.notification.spec.js.map +1 -1
  115. package/lib/module/store.spec.js +2 -136
  116. package/lib/module/store.spec.js.map +1 -1
  117. package/lib/module/wrap.js +11 -12
  118. package/lib/module/wrap.js.map +1 -1
  119. package/lib/module/wrap.spec.js +99 -25
  120. package/lib/module/wrap.spec.js.map +1 -1
  121. package/lib/typescript/commonjs/checkForUpdate.d.ts +2 -3
  122. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/error.d.ts +4 -13
  124. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/fetchJSON.d.ts +4 -0
  126. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/httpClient.d.ts +6 -6
  128. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/index.d.ts +4 -7
  130. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/native.d.ts +5 -9
  132. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +25 -0
  134. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +8 -0
  136. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +1 -0
  137. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  138. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
  139. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +8 -35
  140. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/store.d.ts +1 -3
  142. package/lib/typescript/commonjs/store.d.ts.map +1 -1
  143. package/lib/typescript/commonjs/wrap.types.d.ts +5 -8
  144. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  145. package/lib/typescript/module/checkForUpdate.d.ts +2 -3
  146. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  147. package/lib/typescript/module/error.d.ts +4 -13
  148. package/lib/typescript/module/error.d.ts.map +1 -1
  149. package/lib/typescript/module/fetchJSON.d.ts +4 -0
  150. package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
  151. package/lib/typescript/module/httpClient.d.ts +6 -6
  152. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  153. package/lib/typescript/module/index.d.ts +4 -7
  154. package/lib/typescript/module/index.d.ts.map +1 -1
  155. package/lib/typescript/module/native.d.ts +5 -9
  156. package/lib/typescript/module/native.d.ts.map +1 -1
  157. package/lib/typescript/module/notifyAppReadyInsights.d.ts +25 -0
  158. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +1 -0
  159. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +8 -0
  160. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +1 -0
  161. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  162. package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
  163. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +8 -35
  164. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  165. package/lib/typescript/module/store.d.ts +1 -3
  166. package/lib/typescript/module/store.d.ts.map +1 -1
  167. package/lib/typescript/module/wrap.types.d.ts +5 -8
  168. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  169. package/package.json +3 -3
  170. package/src/checkForUpdate.releaseCatalog.spec.ts +137 -61
  171. package/src/checkForUpdate.ts +35 -29
  172. package/src/error.ts +4 -14
  173. package/src/fetchJSON.ts +12 -1
  174. package/src/httpClient.spec.ts +70 -5
  175. package/src/httpClient.ts +78 -64
  176. package/src/index.spec.ts +59 -11
  177. package/src/index.ts +41 -17
  178. package/src/native.spec.ts +133 -22
  179. package/src/native.ts +21 -32
  180. package/src/{notifyAppReadyAnalytics.boundary.spec.ts → notifyAppReadyInsights.boundary.spec.ts} +25 -17
  181. package/src/{notifyAppReadyAnalytics.spec.ts → notifyAppReadyInsights.spec.ts} +141 -22
  182. package/src/{notifyAppReadyAnalytics.test-utils.ts → notifyAppReadyInsights.test-utils.ts} +4 -4
  183. package/src/{notifyAppReadyAnalytics.transport.spec.ts → notifyAppReadyInsights.transport.spec.ts} +13 -5
  184. package/src/notifyAppReadyInsights.ts +293 -0
  185. package/src/sdkVersion.ts +1 -1
  186. package/src/specs/NativeHotUpdater.ts +8 -36
  187. package/src/store.notification.spec.ts +13 -3
  188. package/src/store.spec.ts +0 -157
  189. package/src/store.ts +2 -23
  190. package/src/wrap.spec.ts +109 -31
  191. package/src/wrap.tsx +11 -11
  192. package/src/wrap.types.ts +5 -8
  193. package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
  194. package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
  195. package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
  196. package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
  197. package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
  198. package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
  199. package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
  200. package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
  201. package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
  202. package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
  203. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
  204. package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
  205. package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
  206. package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
  207. package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js.map +0 -1
  208. package/lib/commonjs/notifyAppReadyAnalytics.js +0 -133
  209. package/lib/commonjs/notifyAppReadyAnalytics.js.map +0 -1
  210. package/lib/commonjs/notifyAppReadyAnalytics.spec.js.map +0 -1
  211. package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js.map +0 -1
  212. package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js.map +0 -1
  213. package/lib/module/notifyAppReadyAnalytics.boundary.spec.js.map +0 -1
  214. package/lib/module/notifyAppReadyAnalytics.js +0 -128
  215. package/lib/module/notifyAppReadyAnalytics.js.map +0 -1
  216. package/lib/module/notifyAppReadyAnalytics.spec.js.map +0 -1
  217. package/lib/module/notifyAppReadyAnalytics.test-utils.js.map +0 -1
  218. package/lib/module/notifyAppReadyAnalytics.transport.spec.js.map +0 -1
  219. package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts +0 -13
  220. package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts.map +0 -1
  221. package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts +0 -8
  222. package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts.map +0 -1
  223. package/lib/typescript/module/notifyAppReadyAnalytics.d.ts +0 -13
  224. package/lib/typescript/module/notifyAppReadyAnalytics.d.ts.map +0 -1
  225. package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts +0 -8
  226. package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts.map +0 -1
  227. package/src/notifyAppReadyAnalytics.ts +0 -194
@@ -21,6 +21,7 @@ data class BundleMetadata(
21
21
  val pendingUpdateStrategy: String? = null,
22
22
  val pendingTransition: PendingSelectionTransition? = null,
23
23
  val verificationPending: Boolean = false,
24
+ val launchInProgress: Boolean = false,
24
25
  val highestSeenCatalogs: Map<String, CatalogHighWater> = emptyMap(),
25
26
  val currentSelectionContexts: Map<String, String> = emptyMap(),
26
27
  val updatedAt: Long = System.currentTimeMillis(),
@@ -71,6 +72,7 @@ data class BundleMetadata(
71
72
  pendingTransition =
72
73
  json.optJSONObject("pendingTransition")?.let(PendingSelectionTransition::fromJson),
73
74
  verificationPending = json.optBoolean("verificationPending", false),
75
+ launchInProgress = json.optBoolean("launchInProgress", false),
74
76
  highestSeenCatalogs = highestSeenCatalogs,
75
77
  currentSelectionContexts = currentSelectionContexts,
76
78
  updatedAt = json.optLong("updatedAt", System.currentTimeMillis()),
@@ -121,6 +123,7 @@ data class BundleMetadata(
121
123
  put("pendingUpdateStrategy", pendingUpdateStrategy ?: JSONObject.NULL)
122
124
  put("pendingTransition", pendingTransition?.toJson() ?: JSONObject.NULL)
123
125
  put("verificationPending", verificationPending)
126
+ put("launchInProgress", launchInProgress)
124
127
  put(
125
128
  "highestSeenCatalogs",
126
129
  JSONObject().apply {
@@ -394,6 +397,13 @@ data class PendingCrashRecovery(
394
397
  val shouldRollback: Boolean,
395
398
  ) {
396
399
  companion object {
400
+ internal fun loadFromFile(file: File): PendingCrashRecovery? =
401
+ try {
402
+ fromJson(JSONObject(file.readText()))
403
+ } catch (_: Exception) {
404
+ null
405
+ }
406
+
397
407
  fun fromJson(json: JSONObject): PendingCrashRecovery =
398
408
  PendingCrashRecovery(
399
409
  launchedBundleId =
@@ -17,12 +17,6 @@ class HotUpdaterException(
17
17
  "Missing or empty 'bundleId'",
18
18
  )
19
19
 
20
- fun invalidFileUrl() =
21
- HotUpdaterException(
22
- "INVALID_FILE_URL",
23
- "Invalid 'fileUrl' provided",
24
- )
25
-
26
20
  // Bundle storage errors
27
21
  fun directoryCreationFailed() =
28
22
  HotUpdaterException(
@@ -45,13 +39,6 @@ class HotUpdaterException(
45
39
  "Download incomplete: received $actualSize bytes, expected $expectedSize bytes",
46
40
  )
47
41
 
48
- fun extractionFormatError(cause: Throwable? = null) =
49
- HotUpdaterException(
50
- "EXTRACTION_FORMAT_ERROR",
51
- "The downloaded bundle file is not a valid compressed archive",
52
- cause,
53
- )
54
-
55
42
  fun invalidBundle() =
56
43
  HotUpdaterException(
57
44
  "INVALID_BUNDLE",
@@ -83,14 +83,12 @@ class HotUpdaterImpl {
83
83
  val fileSystem = FileManagerService(appContext)
84
84
  val preferences = createPreferences(appContext)
85
85
  val downloadService = OkHttpDownloadService()
86
- val decompressService = DecompressService()
87
86
  val isolationKey = getIsolationKey(appContext)
88
87
 
89
88
  return BundleFileStorageService(
90
89
  appContext,
91
90
  fileSystem,
92
91
  downloadService,
93
- decompressService,
94
92
  preferences,
95
93
  isolationKey,
96
94
  )
@@ -268,22 +266,22 @@ class HotUpdaterImpl {
268
266
  fun getJSBundleFile(): String = prepareLaunchIfNeeded().bundleUrl
269
267
 
270
268
  /**
271
- * Updates the bundle from the specified URL
269
+ * Updates the bundle from its manifest and complete asset descriptors.
272
270
  * @param bundleId ID of the bundle to update
273
- * @param fileUrl URL of the bundle file to download (or null to reset)
274
- * @param fileHash Combined hash string for verification (sig:<signature> or <hex_hash>)
271
+ * @param manifestUrl URL of the target manifest
272
+ * @param manifestFileHash Hash or signature used to verify the target manifest
273
+ * @param assets Complete target asset descriptor map
275
274
  * @param progressCallback Callback for download progress updates
276
275
  * @throws HotUpdaterException if the update fails
277
276
  */
278
277
  suspend fun updateBundle(
279
278
  bundleId: String,
280
- fileUrl: String?,
281
- fileHash: String?,
282
- manifestUrl: String?,
283
- manifestFileHash: String?,
284
- changedAssets: Map<String, ChangedAssetDescriptor>?,
279
+ manifestUrl: String,
280
+ manifestFileHash: String,
281
+ assets: Map<String, ChangedAssetDescriptor>,
285
282
  channel: String?,
286
283
  selection: PersistedSelection? = null,
284
+ archiveUrl: String? = null,
287
285
  progressCallback: (UpdateProgressPayload) -> Unit,
288
286
  ) {
289
287
  if (selection != null && !bundleStorage.stageReleaseSelection(selection)) {
@@ -291,11 +289,10 @@ class HotUpdaterImpl {
291
289
  }
292
290
  bundleStorage.updateBundle(
293
291
  bundleId,
294
- fileUrl,
295
- fileHash,
296
292
  manifestUrl,
297
293
  manifestFileHash,
298
- changedAssets,
294
+ assets,
295
+ archiveUrl,
299
296
  progressCallback,
300
297
  )
301
298
 
@@ -467,7 +464,6 @@ class HotUpdaterImpl {
467
464
 
468
465
  /**
469
466
  * Gets the current launched bundle ID.
470
- * Reads manifest.json first and falls back to the legacy BUNDLE_ID file.
471
467
  * Built-in bundle fallback is handled in JS.
472
468
  */
473
469
  fun getBundleId(): String? {
@@ -9,6 +9,7 @@ import android.os.Looper
9
9
  import android.os.Process
10
10
  import android.os.SystemClock
11
11
  import android.util.Log
12
+ import androidx.core.util.AtomicFile
12
13
  import com.facebook.react.ReactApplication
13
14
  import com.facebook.react.bridge.JSExceptionHandler
14
15
  import com.facebook.react.bridge.ReactContext
@@ -16,6 +17,7 @@ import com.facebook.react.bridge.ReactMarker
16
17
  import com.facebook.react.bridge.ReactMarkerConstants
17
18
  import org.json.JSONObject
18
19
  import java.io.File
20
+ import java.io.FileOutputStream
19
21
  import java.lang.reflect.Field
20
22
  import kotlin.system.exitProcess
21
23
 
@@ -28,10 +30,7 @@ internal class HotUpdaterRecoveryManager(
28
30
  private val crashMarkerFile = File(bundleStoreDir, CRASH_MARKER_FILENAME)
29
31
  private val watchdogStateFile = File(bundleStoreDir, WATCHDOG_STATE_FILENAME)
30
32
 
31
- private var currentBundleId: String? = null
32
- private var shouldRollbackOnCrash = false
33
- private var isMonitoring = false
34
- private var recoveryRequested = false
33
+ private val launchState = HotUpdaterRecoveryState()
35
34
  private var contentAppearedCallback: ((String?) -> Unit)? = null
36
35
  private var recoveryRestartCallback: (() -> Boolean)? = null
37
36
 
@@ -39,10 +38,7 @@ internal class HotUpdaterRecoveryManager(
39
38
  Runnable {
40
39
  cancelRecoveryWatchdog()
41
40
  Log.d(TAG, "Stopping crash monitoring for current launch")
42
- isMonitoring = false
43
- recoveryRequested = false
44
- shouldRollbackOnCrash = false
45
- currentBundleId = null
41
+ launchState.stop()
46
42
  contentAppearedCallback = null
47
43
  recoveryRestartCallback = null
48
44
  activeManager = null
@@ -52,7 +48,7 @@ internal class HotUpdaterRecoveryManager(
52
48
  private val installJsExceptionHooksRunnable =
53
49
  object : Runnable {
54
50
  override fun run() {
55
- if (!isMonitoring) {
51
+ if (launchState.snapshot() == null) {
56
52
  return
57
53
  }
58
54
 
@@ -74,7 +70,7 @@ internal class HotUpdaterRecoveryManager(
74
70
  }
75
71
 
76
72
  return try {
77
- val recovery = PendingCrashRecovery.fromJson(JSONObject(crashMarkerFile.readText()))
73
+ val recovery = PendingCrashRecovery.loadFromFile(crashMarkerFile) ?: return null
78
74
  Log.d(
79
75
  TAG,
80
76
  "Consumed pending crash marker bundleId=${recovery.launchedBundleId} shouldRollback=${recovery.shouldRollback}",
@@ -96,11 +92,8 @@ internal class HotUpdaterRecoveryManager(
96
92
  ) {
97
93
  crashMarkerFile.parentFile?.mkdirs()
98
94
  contentAppearedCallback = onContentAppeared
99
- currentBundleId = bundleId
100
- shouldRollbackOnCrash = shouldRollback
101
- isMonitoring = true
102
- recoveryRequested = false
103
95
  recoveryRestartCallback = onRecoveryRestartRequested
96
+ launchState.start(bundleId, shouldRollback)
104
97
  activeManager = this
105
98
 
106
99
  ensureExceptionHandlerInstalled()
@@ -122,17 +115,13 @@ internal class HotUpdaterRecoveryManager(
122
115
  }
123
116
 
124
117
  private fun handleContentAppeared() {
125
- if (!isMonitoring) {
126
- return
127
- }
128
-
129
- Log.d(TAG, "First content appeared for bundleId=$currentBundleId")
118
+ val launch = launchState.completeLaunch() ?: return
119
+ Log.d(TAG, "First content appeared for bundleId=${launch.bundleId}")
130
120
  ReactMarker.removeListener(contentAppearedListener)
131
121
  mainHandler.removeCallbacks(installJsExceptionHooksRunnable)
132
122
  mainHandler.removeCallbacks(stopMonitoringRunnable)
133
- contentAppearedCallback?.invoke(currentBundleId)
134
- shouldRollbackOnCrash = false
135
- updateNativeLaunchState(currentBundleId, false)
123
+ contentAppearedCallback?.invoke(launch.bundleId)
124
+ updateNativeLaunchState(launch.bundleId, false)
136
125
  cancelRecoveryWatchdog()
137
126
  mainHandler.postDelayed(stopMonitoringRunnable, MONITORING_GRACE_PERIOD_MS)
138
127
  }
@@ -145,11 +134,10 @@ internal class HotUpdaterRecoveryManager(
145
134
  previousExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
146
135
  Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
147
136
  val manager = activeManager
148
- manager?.writeCrashMarker()
149
-
150
137
  if (manager?.requestAutomaticRecovery() == true) {
151
- Process.killProcess(Process.myPid())
152
- exitProcess(10)
138
+ // The separate restart activity kills this process after it starts.
139
+ // A second fatal error must not kill it while that request is in flight.
140
+ return@setDefaultUncaughtExceptionHandler
153
141
  }
154
142
 
155
143
  previousExceptionHandler?.uncaughtException(thread, throwable)
@@ -161,20 +149,21 @@ internal class HotUpdaterRecoveryManager(
161
149
  exceptionHandlerInstalled = true
162
150
  }
163
151
 
164
- private fun writeCrashMarker() {
165
- if (!isMonitoring) {
166
- return
167
- }
168
-
152
+ private fun writeCrashMarker(launch: HotUpdaterRecoveryState.Launch) {
153
+ val atomicFile = AtomicFile(crashMarkerFile)
154
+ var output: FileOutputStream? = null
169
155
  try {
170
156
  val payload =
171
157
  JSONObject().apply {
172
- put("bundleId", currentBundleId ?: JSONObject.NULL)
173
- put("shouldRollback", shouldRollbackOnCrash)
158
+ put("bundleId", launch.bundleId ?: JSONObject.NULL)
159
+ put("shouldRollback", launch.shouldRollback)
174
160
  }
175
161
  crashMarkerFile.parentFile?.mkdirs()
176
- crashMarkerFile.writeText(payload.toString())
162
+ output = atomicFile.startWrite()
163
+ output.write(payload.toString().toByteArray(Charsets.UTF_8))
164
+ atomicFile.finishWrite(output)
177
165
  } catch (e: Exception) {
166
+ output?.let(atomicFile::failWrite)
178
167
  Log.e(TAG, "Failed to write crash marker", e)
179
168
  }
180
169
  }
@@ -194,35 +183,28 @@ internal class HotUpdaterRecoveryManager(
194
183
  cancelRecoveryWatchdogAlarm(appContext)
195
184
  }
196
185
 
197
- private fun requestAutomaticRecovery(): Boolean {
198
- if (!isMonitoring || !shouldRollbackOnCrash) {
199
- return false
186
+ private fun requestAutomaticRecovery(exception: Exception? = null): Boolean {
187
+ val request = launchState.requestRecovery()
188
+ if (exception != null) {
189
+ Log.e(TAG, "Caught React startup exception for bundleId=${launchState.snapshot()?.bundleId}", exception)
200
190
  }
201
-
202
- synchronized(this) {
203
- if (recoveryRequested) {
204
- return true
205
- }
206
- recoveryRequested = true
191
+ if (request == null) {
192
+ launchState.snapshot()?.let(::writeCrashMarker)
193
+ return false
207
194
  }
208
-
195
+ if (!request.shouldRestart) return true
196
+ writeCrashMarker(request.launch)
209
197
  val started = recoveryRestartCallback?.invoke() == true
198
+ launchState.finishRecoveryRequest(request, started)
210
199
  if (!started) {
211
- synchronized(this) {
212
- recoveryRequested = false
213
- }
214
200
  Log.w(TAG, "Failed to schedule automatic recovery restart")
215
201
  } else {
216
- Log.i(TAG, "Scheduled automatic recovery restart for bundleId=$currentBundleId")
202
+ Log.i(TAG, "Scheduled automatic recovery restart for bundleId=${request.launch.bundleId}")
217
203
  }
218
204
  return started
219
205
  }
220
206
 
221
- private fun handleJavaScriptException(exception: Exception): Boolean {
222
- Log.e(TAG, "Caught React startup exception for bundleId=$currentBundleId", exception)
223
- writeCrashMarker()
224
- return requestAutomaticRecovery()
225
- }
207
+ private fun handleJavaScriptException(exception: Exception): Boolean = requestAutomaticRecovery(exception)
226
208
 
227
209
  private fun ensureNativeSignalHandlerInstalled() {
228
210
  if (signalHandlerInstalled || !loadNativeLibrary()) {
@@ -467,7 +449,8 @@ internal class HotUpdaterRecoveryManager(
467
449
  }
468
450
 
469
451
  val crashMarkerFile = File(getBundleStoreDir(appContext), CRASH_MARKER_FILENAME)
470
- if (crashMarkerFile.exists()) {
452
+ val recovery = PendingCrashRecovery.loadFromFile(crashMarkerFile)
453
+ if (recovery?.shouldRollback == true && recovery.launchedBundleId != null) {
471
454
  Log.i(TAG, "Recovery watchdog detected crash marker, relaunching app")
472
455
  watchdogStateFile.delete()
473
456
  cancelRecoveryWatchdogAlarm(appContext)
@@ -0,0 +1,64 @@
1
+ package com.hotupdater
2
+
3
+ internal class HotUpdaterRecoveryState {
4
+ data class Launch(
5
+ val bundleId: String?,
6
+ val shouldRollback: Boolean,
7
+ )
8
+
9
+ data class RecoveryRequest(
10
+ val launch: Launch,
11
+ val shouldRestart: Boolean,
12
+ )
13
+
14
+ private enum class RestartState { NONE, REQUESTED, SCHEDULED, FAILED }
15
+
16
+ private var launch: Launch? = null
17
+ private var restartState = RestartState.NONE
18
+ private var contentAppeared = false
19
+
20
+ @Synchronized
21
+ fun start(
22
+ bundleId: String?,
23
+ shouldRollback: Boolean,
24
+ ) {
25
+ launch = Launch(bundleId, shouldRollback)
26
+ restartState = RestartState.NONE
27
+ contentAppeared = false
28
+ }
29
+
30
+ @Synchronized
31
+ fun snapshot(): Launch? = launch
32
+
33
+ @Synchronized
34
+ fun stop() {
35
+ launch = null
36
+ restartState = RestartState.NONE
37
+ }
38
+
39
+ @Synchronized
40
+ fun completeLaunch(): Launch? {
41
+ val current = launch ?: return null
42
+ if (contentAppeared || restartState != RestartState.NONE) return null
43
+ contentAppeared = true
44
+ launch = current.copy(shouldRollback = false)
45
+ return current
46
+ }
47
+
48
+ @Synchronized
49
+ fun requestRecovery(): RecoveryRequest? {
50
+ val current = launch?.takeIf { it.shouldRollback } ?: return null
51
+ val shouldRestart = restartState == RestartState.NONE || restartState == RestartState.FAILED
52
+ if (shouldRestart) restartState = RestartState.REQUESTED
53
+ return RecoveryRequest(current, shouldRestart)
54
+ }
55
+
56
+ @Synchronized
57
+ fun finishRecoveryRequest(
58
+ request: RecoveryRequest,
59
+ started: Boolean,
60
+ ) {
61
+ if (launch !== request.launch) return
62
+ restartState = if (started) RestartState.SCHEDULED else RestartState.FAILED
63
+ }
64
+ }
@@ -65,6 +65,13 @@ interface DownloadService {
65
65
  fileSizeCallback: ((Long) -> Unit)? = null,
66
66
  progressCallback: (DownloadProgress) -> Unit,
67
67
  ): DownloadResult
68
+
69
+ suspend fun downloadFileOnce(
70
+ fileUrl: URL,
71
+ destination: File,
72
+ fileSizeCallback: ((Long) -> Unit)? = null,
73
+ progressCallback: (DownloadProgress) -> Unit,
74
+ ): DownloadResult = downloadFile(fileUrl, destination, fileSizeCallback, progressCallback)
68
75
  }
69
76
 
70
77
  /**
@@ -185,6 +192,18 @@ class OkHttpDownloadService : DownloadService {
185
192
  DownloadResult.Error(lastException ?: Exception("Download failed after $MAX_RETRIES attempts"))
186
193
  }
187
194
 
195
+ override suspend fun downloadFileOnce(
196
+ fileUrl: URL,
197
+ destination: File,
198
+ fileSizeCallback: ((Long) -> Unit)?,
199
+ progressCallback: (DownloadProgress) -> Unit,
200
+ ): DownloadResult =
201
+ try {
202
+ attemptDownload(fileUrl, destination, fileSizeCallback, progressCallback)
203
+ } catch (error: Exception) {
204
+ DownloadResult.Error(error)
205
+ }
206
+
188
207
  private suspend fun attemptDownload(
189
208
  fileUrl: URL,
190
209
  destination: File,