@hot-updater/react-native 1.0.8 → 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 (145) 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/ContentAppearedLog.kt +0 -31
  143. package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
  144. package/android/src/newarch/ReactIntegrationManager.kt +0 -212
  145. package/android/src/oldarch/ReactIntegrationManager.kt +0 -120
@@ -36,16 +36,39 @@ class HotUpdaterModule internal constructor(
36
36
  return value.toLong().takeIf { it in 0..9_007_199_254_740_991L }
37
37
  }
38
38
 
39
+ override fun initialize() {
40
+ super.initialize()
41
+ // Each runtime has its own module, so this reaches a named instance's
42
+ // runtime, whose host the app doesn't expose, and main's, whose root
43
+ // views are looked for too; main's errors are left alone.
44
+ registeredInstanceName = HotUpdaterRecoveryManager.registerRuntime(mReactApplicationContext)
45
+ }
46
+
47
+ // The named instance whose runtime this module registered; the runtime,
48
+ // whichever instance's, is unregistered when it is torn down.
49
+ private var registeredInstanceName: String? = null
50
+
51
+ private fun unregisterRuntime() {
52
+ HotUpdaterRecoveryManager.unregisterRuntime(mReactApplicationContext, registeredInstanceName)
53
+ registeredInstanceName = null
54
+ }
55
+
39
56
  override fun onCatalystInstanceDestroy() {
40
57
  super.onCatalystInstanceDestroy()
58
+ unregisterRuntime()
41
59
  // Cancel all ongoing coroutines when module is destroyed
42
60
  moduleScope.cancel()
43
61
  }
44
62
 
63
+ // The instance whose bundle this module's runtime runs, read from the
64
+ // URL React Native loaded it from.
65
+ private val runtimeInstance = HotUpdaterRuntimeInstance(mReactApplicationContext, { mReactApplicationContext.sourceURL })
66
+
45
67
  /**
46
- * Gets the singleton HotUpdaterImpl instance
68
+ * The impl of this runtime's instance: main's, or a named instance's
69
+ * when the runtime runs its bundle
47
70
  */
48
- private fun getInstance(): HotUpdaterImpl = HotUpdater.getInstance(mReactApplicationContext)
71
+ private fun getInstance(): HotUpdaterImpl = runtimeInstance.impl
49
72
 
50
73
  /**
51
74
  * Every changed file with its hash and URL or Hermes patch; with
@@ -211,8 +234,15 @@ class HotUpdaterModule internal constructor(
211
234
  )
212
235
  }
213
236
 
237
+ // Legacy: released JavaScript calls reload() on Android when the app chose
238
+ // setReloadBehavior("reload"). The in-place React Native reload is gone, so
239
+ // reload() restarts the process, and reloadProcess() runs the same code.
214
240
  @ReactMethod
215
241
  override fun reload(promise: Promise) {
242
+ runtimeInstance.reloadUnsupportedReason()?.let { reason ->
243
+ promise.reject(RELOAD_UNSUPPORTED_CODE, reason)
244
+ return
245
+ }
216
246
  moduleScope.launch {
217
247
  try {
218
248
  val impl = getInstance()
@@ -225,19 +255,10 @@ class HotUpdaterModule internal constructor(
225
255
  }
226
256
  }
227
257
 
258
+ // Deprecated: JavaScript calls reloadProcess() on Android, both released
259
+ // JavaScript and this SDK's, so it stays until the next major release.
228
260
  @ReactMethod
229
- override fun reloadProcess(promise: Promise) {
230
- moduleScope.launch {
231
- try {
232
- val impl = getInstance()
233
- impl.reloadProcess(mReactApplicationContext)
234
- promise.resolve(null)
235
- } catch (e: Exception) {
236
- Log.d("HotUpdater", "Failed to restart process", e)
237
- promise.reject("reloadProcess", e)
238
- }
239
- }
240
- }
261
+ override fun reloadProcess(promise: Promise) = reload(promise)
241
262
 
242
263
  @ReactMethod
243
264
  override fun updateBundle(
@@ -485,7 +506,7 @@ class HotUpdaterModule internal constructor(
485
506
  constants["APP_VERSION"] = HotUpdater.getAppVersion(mReactApplicationContext)
486
507
  constants["CHANNEL"] = getInstance().getChannel()
487
508
  constants["DEFAULT_CHANNEL"] = getInstance().getDefaultChannel()
488
- constants["FINGERPRINT_HASH"] = HotUpdater.getFingerprintHash(mReactApplicationContext)
509
+ constants["FINGERPRINT_HASH"] = getInstance().getFingerprintHash()
489
510
  return constants
490
511
  }
491
512
 
@@ -547,12 +568,17 @@ class HotUpdaterModule internal constructor(
547
568
  getInstance().setStorageItem(key, value)
548
569
  }
549
570
 
571
+ @ReactMethod(isBlockingSynchronousMethod = true)
572
+ override fun takeNativeUpdateEvents(): String? = getInstance().takeNativeUpdateEvents()
573
+
550
574
  @ReactMethod
551
575
  override fun resetChannel(promise: Promise) {
552
576
  moduleScope.launch {
553
577
  try {
554
- val impl = getInstance()
555
- val success = impl.resetChannel()
578
+ // Waits for a native check or download, which would save its
579
+ // channel again after the reset.
580
+ val name = runtimeInstance.name
581
+ val success = NativeUpdateCheck.resetChannel(mReactApplicationContext, name) { runtimeInstance.impl(name) }
556
582
  promise.resolve(success)
557
583
  } catch (e: HotUpdaterException) {
558
584
  promise.reject(e.code, e.message)
@@ -74,5 +74,7 @@ abstract class HotUpdaterSpec internal constructor(
74
74
  value: String?,
75
75
  )
76
76
 
77
+ abstract fun takeNativeUpdateEvents(): String?
78
+
77
79
  abstract fun resetChannel(promise: Promise)
78
80
  }
@@ -1,5 +1,6 @@
1
1
  package com.hotupdater
2
2
 
3
+ import android.content.Context
3
4
  import com.facebook.react.ReactApplication
4
5
  import com.facebook.react.bridge.JSExceptionHandler
5
6
  import com.facebook.react.bridge.ReactContext
@@ -7,20 +8,22 @@ import com.facebook.react.bridge.ReactContext
7
8
  /**
8
9
  * Routes JavaScript errors of the running React instance to crash recovery.
9
10
  * The old architecture reaches the instance through ReactNativeHost, which only
10
- * an old architecture build compiles.
11
+ * an old architecture build compiles, or through the context of a runtime
12
+ * whose HotUpdater module was created. An error belongs to the instance whose
13
+ * bundle the runtime runs.
11
14
  */
12
15
  internal object JavaScriptExceptionHooks {
13
- private val patchedInstanceManagerIds = mutableSetOf<Int>()
14
- private val patchedCatalystInstanceIds = mutableSetOf<Int>()
16
+ private val patchedInstanceManagers = WeakIdentitySet<Any>()
17
+ private val patchedCatalystInstances = WeakIdentitySet<Any>()
15
18
 
16
- fun install(application: ReactApplication) {
19
+ fun install(appContext: Context) {
20
+ val application = appContext as? ReactApplication ?: return
17
21
  val instanceManager = application.reactNativeHost.reactInstanceManager
18
- val managerIdentity = System.identityHashCode(instanceManager)
19
- if (patchedInstanceManagerIds.add(managerIdentity)) {
22
+ if (patchedInstanceManagers.add(instanceManager)) {
20
23
  val exceptionHandlerField =
21
24
  HotUpdaterRecoveryManager.findField(instanceManager.javaClass, "mJSExceptionHandler")
22
25
  if (exceptionHandlerField == null) {
23
- patchedInstanceManagerIds.remove(managerIdentity)
26
+ patchedInstanceManagers.remove(instanceManager)
24
27
  return
25
28
  }
26
29
 
@@ -28,14 +31,21 @@ internal object JavaScriptExceptionHooks {
28
31
  val previousHandler = exceptionHandlerField.get(instanceManager) as? JSExceptionHandler
29
32
  exceptionHandlerField.set(
30
33
  instanceManager,
31
- HotUpdaterRecoveryManager.RecoveryJSExceptionHandler(previousHandler),
34
+ HotUpdaterRecoveryManager.RecoveryJSExceptionHandler(
35
+ previousHandler,
36
+ { instanceManager.currentReactContext?.sourceURL },
37
+ { instanceManager.currentReactContext },
38
+ ),
32
39
  )
33
40
  }
34
41
 
35
- instanceManager.currentReactContext?.let { reactContext ->
36
- HotUpdaterRecoveryManager.patchReactContextExceptionHandler(reactContext)
37
- patchCatalystInstanceExceptionHandler(reactContext)
38
- }
42
+ instanceManager.currentReactContext?.let(::installForRuntime)
43
+ }
44
+
45
+ /** Hooks the catalyst instance of [reactContext]'s runtime, which a named instance's runtime is reached through. */
46
+ fun installForRuntime(reactContext: ReactContext) {
47
+ HotUpdaterRecoveryManager.patchReactContextExceptionHandler(reactContext)
48
+ patchCatalystInstanceExceptionHandler(reactContext)
39
49
  }
40
50
 
41
51
  private fun patchCatalystInstanceExceptionHandler(reactContext: ReactContext) {
@@ -46,15 +56,14 @@ internal object JavaScriptExceptionHooks {
46
56
  null
47
57
  } ?: return
48
58
 
49
- val catalystIdentity = System.identityHashCode(catalystInstance)
50
- if (!patchedCatalystInstanceIds.add(catalystIdentity)) {
59
+ if (!patchedCatalystInstances.add(catalystInstance)) {
51
60
  return
52
61
  }
53
62
 
54
63
  val exceptionHandlerField =
55
64
  HotUpdaterRecoveryManager.findField(catalystInstance.javaClass, "mJSExceptionHandler")
56
65
  if (exceptionHandlerField == null) {
57
- patchedCatalystInstanceIds.remove(catalystIdentity)
66
+ patchedCatalystInstances.remove(catalystInstance)
58
67
  return
59
68
  }
60
69
 
@@ -65,7 +74,7 @@ internal object JavaScriptExceptionHooks {
65
74
  }
66
75
  exceptionHandlerField.set(
67
76
  catalystInstance,
68
- HotUpdaterRecoveryManager.RecoveryJSExceptionHandler(previousHandler),
77
+ HotUpdaterRecoveryManager.RecoveryJSExceptionHandler(previousHandler, { catalystInstance.sourceURL }, { reactContext }),
69
78
  )
70
79
  }
71
80
  }
@@ -54,6 +54,7 @@ NS_ASSUME_NONNULL_BEGIN
54
54
  @end
55
55
 
56
56
  @protocol HotUpdaterConfigInterface <NSObject>
57
+ @property (nonatomic, strong) NSBundle *bundle;
57
58
  - (void)configureWithFingerprintHash:(nullable NSString *)fingerprintHash
58
59
  publicKey:(nullable NSString *)publicKey
59
60
  channel:(nullable NSString *)channel
@@ -61,9 +62,21 @@ NS_ASSUME_NONNULL_BEGIN
61
62
  NS_SWIFT_NAME(configureWithFingerprintHash(_:publicKey:channel:isolationKey:));
62
63
  @end
63
64
 
65
+ // The TurboModule's channel reset and event replay, which go through the
66
+ // Swift native update check, and the impl of the runtime that runs a bundle.
67
+ @protocol HotUpdaterNativeUpdatesInterface <NSObject>
68
+ - (void)resetChannelWithImpl:(id<HotUpdaterImplInterface>)impl
69
+ resolver:(RCTPromiseResolveBlock)resolve
70
+ rejecter:(RCTPromiseRejectBlock)reject;
71
+ - (nullable NSString *)takeEventsWithImpl:(id<HotUpdaterImplInterface>)impl;
72
+ - (id<HotUpdaterImplInterface>)implForBundleURL:(nullable NSURL *)bundleURL
73
+ NS_SWIFT_NAME(impl(forBundleURL:));
74
+ @end
75
+
64
76
  // Implemented in HotUpdaterInterface.swift.
65
77
  FOUNDATION_EXTERN id<HotUpdaterImplInterface> HotUpdaterMakeImpl(void);
66
78
  FOUNDATION_EXTERN NSString *_Nullable HotUpdaterAppVersion(void);
67
79
  FOUNDATION_EXTERN id<HotUpdaterConfigInterface> HotUpdaterSharedConfig(void);
80
+ FOUNDATION_EXTERN id<HotUpdaterNativeUpdatesInterface> HotUpdaterNativeUpdates(void);
68
81
 
69
82
  NS_ASSUME_NONNULL_END
@@ -26,7 +26,8 @@ enum ArtifactFileResolverError: Error {
26
26
  }
27
27
 
28
28
  /// POSTs `{ paths }` to the files URL the SDK composed, with the update
29
- /// check's headers and request timeout.
29
+ /// check's headers and request timeout. `onResponse` receives each
30
+ /// response's status and body, or a nil body when it could not be read.
30
31
  final class HTTPArtifactFileResolver: ArtifactFileResolver {
31
32
  /// The SDK's request timeout when the app sets none.
32
33
  static let defaultTimeout: TimeInterval = 5
@@ -34,13 +35,23 @@ final class HTTPArtifactFileResolver: ArtifactFileResolver {
34
35
  private let filesUrl: URL
35
36
  private let headers: [String: String]
36
37
  private let timeout: TimeInterval
37
- private let session: URLSession
38
+ private let protocolClasses: [AnyClass]?
39
+ private let onResponse: ((Int, String?) -> Void)?
38
40
 
39
- init(filesUrl: URL, headers: [String: String], timeout: TimeInterval = defaultTimeout, session: URLSession = .shared) {
41
+ /// `protocolClasses` replaces the session's URL protocols; tests stub
42
+ /// the network with it.
43
+ init(
44
+ filesUrl: URL,
45
+ headers: [String: String],
46
+ timeout: TimeInterval = defaultTimeout,
47
+ protocolClasses: [AnyClass]? = nil,
48
+ onResponse: ((Int, String?) -> Void)? = nil
49
+ ) {
40
50
  self.filesUrl = filesUrl
41
51
  self.headers = headers
42
52
  self.timeout = timeout
43
- self.session = session
53
+ self.protocolClasses = protocolClasses
54
+ self.onResponse = onResponse
44
55
  }
45
56
 
46
57
  func resolve(paths: [String]) throws -> [String: ResolvedArtifactFile] {
@@ -53,15 +64,33 @@ final class HTTPArtifactFileResolver: ArtifactFileResolver {
53
64
  request.setValue("application/json", forHTTPHeaderField: "Content-Type")
54
65
  request.httpBody = try JSONSerialization.data(withJSONObject: ["paths": paths])
55
66
 
67
+ // The resource timeout bounds the whole request, as Android's call
68
+ // timeout does; the request's own interval only bounds idle time.
69
+ let configuration = URLSessionConfiguration.ephemeral
70
+ configuration.urlCache = nil
71
+ configuration.timeoutIntervalForRequest = timeout
72
+ configuration.timeoutIntervalForResource = timeout
73
+ if let protocolClasses {
74
+ configuration.protocolClasses = protocolClasses
75
+ }
76
+ let session = URLSession(configuration: configuration)
77
+ defer { session.finishTasksAndInvalidate() }
78
+
56
79
  let finished = DispatchSemaphore(value: 0)
57
80
  var result: Result<Data, Error> = .failure(ArtifactFileResolverError.invalidResponse)
81
+ let onResponse = self.onResponse
58
82
  let task = session.dataTask(with: request) { data, response, error in
59
83
  defer { finished.signal() }
60
84
  if let error {
85
+ // The status arrived but the body broke: still reported.
86
+ if let httpResponse = response as? HTTPURLResponse {
87
+ onResponse?(httpResponse.statusCode, nil)
88
+ }
61
89
  result = .failure(error)
62
90
  return
63
91
  }
64
92
  guard let httpResponse = response as? HTTPURLResponse else { return }
93
+ onResponse?(httpResponse.statusCode, data.flatMap { String(data: $0, encoding: .utf8) } ?? "")
65
94
  guard (200..<300).contains(httpResponse.statusCode) else {
66
95
  result = .failure(DownloadError.httpStatus(httpResponse.statusCode))
67
96
  return
@@ -71,12 +100,14 @@ final class HTTPArtifactFileResolver: ArtifactFileResolver {
71
100
  task.resume()
72
101
  finished.wait()
73
102
 
74
- guard let json = try JSONSerialization.jsonObject(with: result.get()) as? [String: Any],
75
- let files = json["files"] as? [String: [String: Any]] else {
103
+ // As JSON.parse reads it, which JSONSerialization does not.
104
+ let body = String(decoding: try result.get(), as: UTF8.self)
105
+ guard let json = StrictJson.parse(body) as? [String: Any],
106
+ let files = json["files"] as? [String: Any] else {
76
107
  throw ArtifactFileResolverError.invalidResponse
77
108
  }
78
109
  return try paths.reduce(into: [:]) { resolved, path in
79
- guard let file = files[path],
110
+ guard let file = files[StrictJson.storedKey(path)] as? [String: Any],
80
111
  let urlString = file["url"] as? String,
81
112
  let url = URL(string: urlString) else {
82
113
  throw ArtifactFileResolverError.invalidResponse
@@ -14,15 +14,21 @@ final class IOSBuiltInAssetResolver: BuiltInAssetResolver {
14
14
 
15
15
  private let cacheURL: URL
16
16
  private let lock = NSLock()
17
- private var bundle: Bundle = .main
18
-
19
- init(cacheURL: URL) {
17
+ private var launchBundle: Bundle?
18
+ private let defaultBundle: () -> Bundle
19
+ /// The launch's bundle, or before a launch, the one the app registered
20
+ /// (`HotUpdaterConfig.bundle`), so an update staged before React Native
21
+ /// starts still finds the embedded files.
22
+ private var bundle: Bundle { launchBundle ?? defaultBundle() }
23
+
24
+ init(cacheURL: URL, defaultBundle: @escaping () -> Bundle = { HotUpdaterConfig.shared.bundle }) {
20
25
  self.cacheURL = cacheURL
26
+ self.defaultBundle = defaultBundle
21
27
  }
22
28
 
23
29
  func use(bundle: Bundle) {
24
30
  lock.lock()
25
- self.bundle = bundle
31
+ self.launchBundle = bundle
26
32
  lock.unlock()
27
33
  }
28
34