@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,47 +1,275 @@
1
1
  package com.hotupdater
2
2
 
3
+ import android.content.Context
4
+ import android.util.Log
5
+
6
+ private const val CONFIGURE_DEPRECATION =
7
+ "Pass channel, fingerprintHash, and publicKey to HotUpdaterOptions. " +
8
+ "Fingerprint downloads now survive native releases that ship the same artifact."
9
+
3
10
  /**
4
- * Thread-safe programmatic configuration holder for HotUpdater.
5
- *
6
- * When values are set here, they take priority over manifest metadata. This
7
- * enables brownfield/AAR setups where the RN module cannot rely on the host
8
- * app's manifest for configuration.
11
+ * Main's configuration: what [HotUpdaterOptions] of the `main` instance and the
12
+ * deprecated `HotUpdater.configure` set. Set values take priority over
13
+ * manifest metadata. Named instances never read it.
9
14
  */
10
15
  object HotUpdaterConfig {
16
+ // The values themselves. Main's options record theirs here directly;
17
+ // every legacy write goes through the setters below. Each write takes
18
+ // MainInstanceConfiguration.shared's lock, which main's impl reads its
19
+ // values under.
20
+ @Volatile
21
+ internal var recordedFingerprintHash: String? = null
22
+
11
23
  @Volatile
12
- var fingerprintHash: String? = null
24
+ internal var recordedPublicKey: String? = null
13
25
 
14
26
  @Volatile
15
- var publicKey: String? = null
27
+ internal var recordedChannel: String? = null
16
28
 
17
29
  @Volatile
18
- var channel: String? = null
30
+ internal var recordedIsolationKey: String? = null
19
31
 
20
32
  /**
21
- * When set, this value is used verbatim as the storage isolation key,
22
- * bypassing the default `HotUpdaterPrefs_{fingerprint}_{appVersion}_{channel}`
23
- * composition. Use this to keep the OTA cache stable across host app version
24
- * bumps (e.g. key only by fingerprint + channel).
33
+ * The fingerprint this build embedded, written by the
34
+ * generateHotUpdaterFingerprintHash build task, or null for an app
35
+ * without one.
25
36
  */
26
37
  @Volatile
27
- var isolationKey: String? = null
38
+ internal var buildFingerprintHash: String? = HotUpdaterBuildFingerprintHash.get().takeIf { it.isNotEmpty() }
39
+
40
+ /** Warns when it replaces a value main's [HotUpdaterOptions] set, as [configure] does. */
41
+ var fingerprintHash: String?
42
+ get() = recordedFingerprintHash
43
+
44
+ @Deprecated(CONFIGURE_DEPRECATION)
45
+ set(value) {
46
+ legacyWrite("HotUpdaterConfig.fingerprintHash", mapOf("fingerprintHash" to value)) { recordedFingerprintHash = value }
47
+ }
48
+
49
+ /** Warns when it replaces a value main's [HotUpdaterOptions] set, as [configure] does. */
50
+ var publicKey: String?
51
+ get() = recordedPublicKey
52
+
53
+ @Deprecated(CONFIGURE_DEPRECATION)
54
+ set(value) {
55
+ legacyWrite("HotUpdaterConfig.publicKey", mapOf("publicKey" to value)) { recordedPublicKey = value }
56
+ }
57
+
58
+ /** Warns when it replaces a value main's [HotUpdaterOptions] set, as [configure] does. */
59
+ var channel: String?
60
+ get() = recordedChannel
61
+
62
+ @Deprecated(CONFIGURE_DEPRECATION)
63
+ set(value) {
64
+ legacyWrite("HotUpdaterConfig.channel", mapOf("channel" to value)) { recordedChannel = value }
65
+ }
66
+
67
+ /**
68
+ * When set, main uses this value verbatim as its storage isolation key,
69
+ * bypassing the default `HotUpdaterPrefs_{fingerprint}_{minBundleId}_{channel}`
70
+ * (fingerprint strategy) or `HotUpdaterPrefs_{appVersion}_{channel}`
71
+ * (appVersion strategy) composition.
72
+ */
73
+ var isolationKey: String?
74
+ get() = recordedIsolationKey
28
75
 
76
+ @Deprecated(CONFIGURE_DEPRECATION)
77
+ set(value) {
78
+ synchronized(MainInstanceConfiguration.shared) { recordedIsolationKey = value }
79
+ }
80
+
81
+ /**
82
+ * Replaces all four values; a null argument means no override. It still
83
+ * replaces values main's [HotUpdaterOptions] set, and warns when it does.
84
+ * Main's impl reads the values once, when it is created; a later call
85
+ * changes only main's static entry points.
86
+ */
87
+ @Deprecated(CONFIGURE_DEPRECATION)
29
88
  fun configure(
30
89
  fingerprintHash: String? = null,
31
90
  publicKey: String? = null,
32
91
  channel: String? = null,
33
92
  isolationKey: String? = null,
34
93
  ) {
35
- this.fingerprintHash = fingerprintHash
36
- this.publicKey = publicKey
37
- this.channel = channel
38
- this.isolationKey = isolationKey
94
+ // One write, so main's impl never reads half of it.
95
+ legacyWrite(
96
+ "HotUpdater.configure",
97
+ mapOf("channel" to channel, "fingerprintHash" to fingerprintHash, "publicKey" to publicKey),
98
+ ) {
99
+ recordedFingerprintHash = fingerprintHash
100
+ recordedPublicKey = publicKey
101
+ recordedChannel = channel
102
+ recordedIsolationKey = isolationKey
103
+ }
39
104
  }
40
105
 
106
+ @Deprecated(CONFIGURE_DEPRECATION)
107
+ @Suppress("DEPRECATION")
41
108
  fun clear() {
42
- fingerprintHash = null
43
- publicKey = null
44
- channel = null
45
- isolationKey = null
109
+ configure()
110
+ }
111
+
112
+ // A write of the deprecated API, of the [written] values by field, under
113
+ // the lock main's impl reads its values under, warning when it replaces
114
+ // a value main's options set.
115
+ private fun legacyWrite(
116
+ source: String,
117
+ written: Map<String, String?>,
118
+ write: () -> Unit,
119
+ ) {
120
+ val configuration = MainInstanceConfiguration.shared
121
+ synchronized(configuration) {
122
+ configuration.warnIfLegacyWriteOverrides(source, written)
123
+ write()
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Records the channel, fingerprint hash, and public key of the `main`
130
+ * instance's [HotUpdaterOptions] into [HotUpdaterConfig] before main's impl
131
+ * exists, and refuses values that would contradict what main already uses.
132
+ */
133
+ internal class MainInstanceConfiguration {
134
+ companion object {
135
+ private const val TAG = "HotUpdaterConfig"
136
+
137
+ /** Main's: the one HotUpdater.init, HotUpdater.getInstance, and HotUpdaterConfig use. */
138
+ val shared = MainInstanceConfiguration()
139
+ }
140
+
141
+ /** Where the warning about `HotUpdater.configure` overriding main's options goes. */
142
+ @Volatile
143
+ internal var logWarning: (String) -> Unit = { message -> Log.w(TAG, message) }
144
+
145
+ // The values main's options set, by field, until HotUpdater.configure
146
+ // replaces them.
147
+ private var claimed = emptyMap<String, String>()
148
+
149
+ // The values main's impl was created with, once it was: its storage key
150
+ // and stored bundles are fixed to them, whatever HotUpdater.configure
151
+ // sets later.
152
+ @get:Synchronized
153
+ var createdWith: MainImplValues? = null
154
+ private set
155
+
156
+ /**
157
+ * Creates main's impl from one snapshot of its values, which it reads
158
+ * from then on, and records the snapshot. Writes to HotUpdaterConfig wait
159
+ * for the snapshot and never reach the impl.
160
+ */
161
+ fun createImpl(
162
+ context: Context,
163
+ create: (Context, HotUpdaterInstanceEnvironment) -> HotUpdaterImpl = { appContext, environment ->
164
+ HotUpdaterImpl(appContext, environment)
165
+ },
166
+ ): HotUpdaterImpl {
167
+ val values =
168
+ synchronized(this) {
169
+ MainImplValues.read(context).also { createdWith = it }
170
+ }
171
+ return create(context, HotUpdaterInstanceEnvironment.main(values))
172
+ }
173
+
174
+ /**
175
+ * Before main's impl exists, records every value [options] set and
176
+ * main's config doesn't hold yet. Values left null keep what main holds.
177
+ * @throws IllegalArgumentException when [options] set a channel,
178
+ * fingerprint hash, or public key other than the one main's config
179
+ * already holds, from earlier options or `HotUpdater.configure`, or, once
180
+ * main's impl exists, other than the one it uses
181
+ */
182
+ @Synchronized
183
+ fun claim(options: HotUpdaterOptions) = claim(options, createdWith)
184
+
185
+ /** [claim], with the values main's impl was created with, or null before it exists. */
186
+ @Synchronized
187
+ fun claim(
188
+ options: HotUpdaterOptions,
189
+ createdWith: MainImplValues?,
190
+ ) {
191
+ val implCreated = createdWith != null
192
+ val fields =
193
+ listOf(
194
+ Field("channel", options.channel.nonEmpty(), HotUpdaterConfig.channel.nonEmpty(), createdWith?.channel),
195
+ Field(
196
+ "fingerprintHash",
197
+ options.fingerprintHash.nonEmpty(),
198
+ HotUpdaterConfig.fingerprintHash.nonEmpty(),
199
+ createdWith?.fingerprintHash,
200
+ ),
201
+ Field("publicKey", options.publicKey.nonEmpty(), HotUpdaterConfig.publicKey.nonEmpty(), createdWith?.publicKey),
202
+ )
203
+ fields.forEach { field ->
204
+ val requested = field.requested ?: return@forEach
205
+ // Once the impl exists, a value it doesn't use, even where it uses
206
+ // none, would be ignored.
207
+ val conflicts = if (implCreated) field.inUse != requested else field.held != null && field.held != requested
208
+ require(!conflicts) {
209
+ "HotUpdater instance \"main\" was already created with another ${field.name}"
210
+ }
211
+ }
212
+ claimed = claimed + fields.mapNotNull { field -> field.requested?.let { field.name to it } }
213
+ if (implCreated) return
214
+ options.channel.nonEmpty()?.let { HotUpdaterConfig.recordedChannel = it }
215
+ options.fingerprintHash.nonEmpty()?.let { HotUpdaterConfig.recordedFingerprintHash = it }
216
+ options.publicKey.nonEmpty()?.let { HotUpdaterConfig.recordedPublicKey = it }
217
+ }
218
+
219
+ /**
220
+ * Warns, once for each value main's options set, when [source], the
221
+ * deprecated `HotUpdater.configure` or a `HotUpdaterConfig` setter,
222
+ * replaces it with another of [written], by field. The write still
223
+ * replaces it, as it always has; the field is the legacy API's from then on.
224
+ */
225
+ @Synchronized
226
+ fun warnIfLegacyWriteOverrides(
227
+ source: String,
228
+ written: Map<String, String?>,
229
+ ) {
230
+ val replaced =
231
+ written.mapNotNull { (name, value) ->
232
+ val set = claimed[name] ?: return@mapNotNull null
233
+ if (set == value.nonEmpty()) return@mapNotNull null
234
+ // A PEM key is long; its name says enough.
235
+ if (name == "publicKey") "publicKey" else "$name \"$set\" with ${value.nonEmpty()?.let { "\"$it\"" } ?: "none"}"
236
+ }
237
+ claimed = claimed - written.keys
238
+ if (replaced.isEmpty()) return
239
+ logWarning(
240
+ "$source replaced the ${replaced.joinToString()} that HotUpdaterOptions of instance \"main\" set. " +
241
+ "It is deprecated: pass channel, fingerprintHash, and publicKey only through HotUpdaterOptions.",
242
+ )
243
+ }
244
+
245
+ // An empty string sets nothing, as in HotUpdater.configure.
246
+ private fun String?.nonEmpty(): String? = this?.takeIf { it.isNotEmpty() }
247
+
248
+ private class Field(
249
+ val name: String,
250
+ val requested: String?,
251
+ val held: String?,
252
+ val inUse: String?,
253
+ )
254
+ }
255
+
256
+ /** The channel, fingerprint hash, public key, and isolation key override main's impl reads. */
257
+ internal class MainImplValues(
258
+ val channel: String,
259
+ val fingerprintHash: String?,
260
+ val publicKey: String?,
261
+ val isolationKey: String?,
262
+ ) {
263
+ companion object {
264
+ /** HotUpdaterConfig's values, then the manifest's, as main's static entry points read them. */
265
+ fun read(context: Context): MainImplValues {
266
+ val main = HotUpdaterInstanceEnvironment.main
267
+ return MainImplValues(
268
+ main.defaultChannel(context),
269
+ main.fingerprintHash(context),
270
+ main.publicKey(context),
271
+ HotUpdaterConfig.isolationKey,
272
+ )
273
+ }
46
274
  }
47
275
  }
@@ -8,13 +8,15 @@ import java.net.UnknownHostException
8
8
  import javax.net.ssl.SSLException
9
9
 
10
10
  /**
11
- * Where an update failed: [DOWNLOAD] covers transferring and verifying the
12
- * update's files, [INSTALL] producing the bundle on disk.
11
+ * Where an update failed: [CHECK] covers the Release catalog request,
12
+ * [DOWNLOAD] transferring and verifying the update's files, [INSTALL]
13
+ * producing the bundle on disk.
13
14
  * Values match `UpdateErrorStage` in packages/react-native/src/clientPlugin.ts.
14
15
  */
15
16
  enum class UpdateFailureStage(
16
17
  val value: String,
17
18
  ) {
19
+ CHECK("check"),
18
20
  DOWNLOAD("download"),
19
21
  INSTALL("install"),
20
22
  }
@@ -41,6 +43,8 @@ enum class UpdateFailureReason(
41
43
  enum class UpdateFailureResource(
42
44
  val value: String,
43
45
  ) {
46
+ CATALOG("catalog"),
47
+ ARTIFACT("artifact"),
44
48
  MANIFEST("manifest"),
45
49
  FILE("file"),
46
50
  PATCH("patch"),
@@ -120,7 +124,7 @@ data class UpdateFailure(
120
124
  install(UpdateFailureReason.STORAGE)
121
125
  }
122
126
 
123
- is MalformedURLException -> {
127
+ is MalformedURLException, is InvalidUpdateResponseException -> {
124
128
  download(UpdateFailureReason.INVALID_RESPONSE)
125
129
  }
126
130
 
@@ -152,7 +156,7 @@ data class UpdateFailure(
152
156
  * [SocketException] a refused, unreachable, or reset connection, and
153
157
  * OkHttp reports a canceled call as an IOException "Canceled".
154
158
  */
155
- private fun transportOf(
159
+ internal fun transportOf(
156
160
  error: IOException,
157
161
  isOffline: () -> Boolean,
158
162
  ): UpdateFailureTransport? =
@@ -0,0 +1,92 @@
1
+ package com.hotupdater
2
+
3
+ /**
4
+ * First content no instance could be told for yet. A named runtime may render
5
+ * before its HotUpdater module exists, in an activity nothing followed, or
6
+ * before its root view is attached to an activity, so its view can't be found
7
+ * until the module hands over its activity or the view is attached. Crediting
8
+ * that content to main would leave the named launch unfinished, and its next
9
+ * process would roll a working bundle back.
10
+ *
11
+ * Held content is resolved again whenever a named runtime registers and
12
+ * whenever a launch gate looks at an activity's views. After
13
+ * [HOLD_MS] still-unresolved content verifies main, as unattributed content
14
+ * always has, once; it stays held, so a named runtime that registers later
15
+ * still verifies its own launch with it.
16
+ *
17
+ * Each hold remembers every instance's launch generation then, so content
18
+ * never verifies a launch that started after it appeared.
19
+ */
20
+ internal class HotUpdaterHeldContent(
21
+ private val mainThread: HotUpdaterMainThread,
22
+ private val clock: () -> Long,
23
+ /** The instance of the view with this root tag, with its runtime's launch, or null while it can't be found. Main thread only. */
24
+ private val resolve: (Int) -> HotUpdaterRootOwner?,
25
+ /** Verifies the launch of [generation] of the instance. */
26
+ private val credit: (instanceName: String, generation: Long) -> Unit,
27
+ ) {
28
+ private class Held(
29
+ val rootTag: Int,
30
+ val heldAt: Long,
31
+ val generations: Map<String, Long>,
32
+ var creditedToMain: Boolean = false,
33
+ )
34
+
35
+ private val held = ArrayList<Held>()
36
+
37
+ /** Holds the content of [rootTag], seen while the instances had these launch [generations]. */
38
+ fun hold(
39
+ rootTag: Int,
40
+ generations: Map<String, Long>,
41
+ ) {
42
+ val evicted =
43
+ synchronized(this) {
44
+ // A root view's first content counts once.
45
+ if (held.any { it.rootTag == rootTag }) return
46
+ held += Held(rootTag, clock(), generations)
47
+ // Root tags are small and each is a root view that rendered,
48
+ // so every one is kept while a named launch may own it. Only
49
+ // past this bound does the oldest leave, and only after it
50
+ // verified main, as content no instance claims does.
51
+ if (held.size > MAX_HELD) held.removeAt(0).takeUnless { it.creditedToMain } else null
52
+ }
53
+ evicted?.generations?.get(HotUpdaterInstanceEnvironment.MAIN_NAME)?.let { credit(HotUpdaterInstanceEnvironment.MAIN_NAME, it) }
54
+ mainThread.postDelayed({ creditExpiredToMain() }, HOLD_MS)
55
+ }
56
+
57
+ /** A named runtime registered: its views may be found now. */
58
+ fun runtimeRegistered() {
59
+ if (mainThread.isCurrent()) resolveHeld() else mainThread.post { resolveHeld() }
60
+ }
61
+
62
+ /** Main thread: verifies the launches whose views can be told now. */
63
+ fun resolveHeld() {
64
+ val candidates = synchronized(this) { held.toList() }
65
+ if (candidates.isEmpty()) return
66
+ candidates.forEach { content ->
67
+ val owner = resolve(content.rootTag) ?: return@forEach
68
+ val instanceName = owner.instanceName
69
+ val generation = owner.boundTo(content.generations)[instanceName]
70
+ val alreadyCredited = instanceName == HotUpdaterInstanceEnvironment.MAIN_NAME && content.creditedToMain
71
+ synchronized(this) { held.remove(content) }
72
+ if (generation != null && !alreadyCredited) credit(instanceName, generation)
73
+ }
74
+ }
75
+
76
+ /** Content held [HOLD_MS] without an instance verifies main, once. */
77
+ fun creditExpiredToMain() {
78
+ val now = clock()
79
+ val expired =
80
+ synchronized(this) {
81
+ held.filter { !it.creditedToMain && now - it.heldAt >= HOLD_MS }.onEach { it.creditedToMain = true }
82
+ }
83
+ expired.forEach { content ->
84
+ content.generations[HotUpdaterInstanceEnvironment.MAIN_NAME]?.let { credit(HotUpdaterInstanceEnvironment.MAIN_NAME, it) }
85
+ }
86
+ }
87
+
88
+ companion object {
89
+ const val HOLD_MS = 10_000L
90
+ private const val MAX_HELD = 1024
91
+ }
92
+ }