@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
@@ -0,0 +1,492 @@
1
+ import Foundation
2
+
3
+ /// Where one native instance keeps its state, and which config it reads.
4
+ ///
5
+ /// Instances are told apart by `HotUpdaterOptions.name`. `main` is the one
6
+ /// the TurboModule and every static API use: it keeps the paths and
7
+ /// `HotUpdaterConfig.shared` of an app with a single instance, where main's
8
+ /// options are recorded. Any other name keeps its state beside main's, never
9
+ /// inside `bundle-store`, whose sweeps delete the entries they don't know,
10
+ /// and reads only its options and its own bundle. The install identity and
11
+ /// the cohort stay device-level, shared by every name.
12
+ struct HotUpdaterInstanceEnvironment {
13
+ static let mainName = "main"
14
+ static let main = HotUpdaterInstanceEnvironment.main(config: .shared)
15
+
16
+ static let maxNameLength = 32
17
+ /// `Documents/hot-updater-instances/<name>` holds a named instance's
18
+ /// `bundle-store` and `bundle-temp`.
19
+ static let instancesDirectoryName = "hot-updater-instances"
20
+ /// `Application Support/HotUpdater/instances/<name>` holds a named
21
+ /// instance's key-value store and Release catalog cache.
22
+ static let noBackupInstancesDirectoryName = "instances"
23
+ static let bundleStoreDirectoryName = "bundle-store"
24
+ static let tempDirectoryName = "bundle-temp"
25
+ static let releaseCatalogCacheDirectoryName = "ReleaseCatalogCache"
26
+ static let backgroundSessionIdentifierBase = "com.hotupdater.background.download"
27
+ private static let defaultChannel = "production"
28
+
29
+ let name: String
30
+ /// nil for main, which reads its bundle from `config` at each use.
31
+ private let namedBundle: Bundle?
32
+ /// Main's recorded options, programmatic config, and bundle; a named
33
+ /// instance never reads it.
34
+ private let config: HotUpdaterConfig
35
+ /// A named instance's `HotUpdaterOptions` values, over its Info.plist.
36
+ /// Main's options are recorded in `config` instead.
37
+ private let options: HotUpdaterConfig.Overrides
38
+ /// Main's values as its impl was created with them; nil reads `config`
39
+ /// at each use.
40
+ private let mainSnapshot: MainValues?
41
+
42
+ /// Main's channel, fingerprint hash, public key, and isolation key
43
+ /// override, resolved from one read of `config` so they always agree.
44
+ struct MainValues: Equatable {
45
+ let defaultChannel: String
46
+ let fingerprintHash: String?
47
+ /// nil for the appVersion strategy.
48
+ let isolationFingerprintHash: String?
49
+ let publicKey: String?
50
+ let isolationKeyOverride: String?
51
+ }
52
+
53
+ private init(
54
+ name: String,
55
+ namedBundle: Bundle?,
56
+ config: HotUpdaterConfig,
57
+ options: HotUpdaterConfig.Overrides = HotUpdaterConfig.Overrides(),
58
+ mainSnapshot: MainValues? = nil
59
+ ) {
60
+ self.name = name
61
+ self.namedBundle = namedBundle
62
+ self.config = config
63
+ self.options = options
64
+ self.mainSnapshot = mainSnapshot
65
+ }
66
+
67
+ /// Main, reading `config` at each use.
68
+ static func main(config: HotUpdaterConfig) -> HotUpdaterInstanceEnvironment {
69
+ HotUpdaterInstanceEnvironment(name: mainName, namedBundle: nil, config: config)
70
+ }
71
+
72
+ /// Main with `values` fixed, which main's impl is built from: a
73
+ /// `configure` call during or after its creation can't leave its key,
74
+ /// channel, and public key apart.
75
+ func snapshot(_ values: MainValues) -> HotUpdaterInstanceEnvironment {
76
+ guard isMain else {
77
+ return self
78
+ }
79
+ return HotUpdaterInstanceEnvironment(
80
+ name: name,
81
+ namedBundle: nil,
82
+ config: config,
83
+ mainSnapshot: values
84
+ )
85
+ }
86
+
87
+ /// Main's values: the snapshot, or resolved now from one read of
88
+ /// `config`'s values and bundle.
89
+ func mainValues() -> MainValues {
90
+ if let mainSnapshot {
91
+ return mainSnapshot
92
+ }
93
+ return Self.resolveMain(
94
+ overrides: config.overrides,
95
+ bundle: config.bundle,
96
+ buildFingerprintHash: config.buildFingerprintHash
97
+ )
98
+ }
99
+
100
+ /// How main resolves its values: `overrides`, then `bundle`'s Info.plist,
101
+ /// then the app's, then the default channel. The fingerprint comes from
102
+ /// `overrides`, then the build.
103
+ static func resolveMain(
104
+ overrides: HotUpdaterConfig.Overrides,
105
+ bundle: Bundle,
106
+ buildFingerprintHash: String?
107
+ ) -> MainValues {
108
+ MainValues(
109
+ defaultChannel: optionValue(overrides.channel)
110
+ ?? HotUpdaterConfig.infoValue(forKey: "HOT_UPDATER_CHANNEL", bundle: bundle)
111
+ ?? defaultChannel,
112
+ fingerprintHash: optionValue(overrides.fingerprintHash) ?? buildFingerprintHash,
113
+ // Main's isolation key and update strategy have always taken an
114
+ // empty programmatic fingerprint as "none" instead of reading
115
+ // the build's.
116
+ isolationFingerprintHash: optionValue(overrides.fingerprintHash ?? buildFingerprintHash),
117
+ publicKey: optionValue(overrides.publicKey)
118
+ ?? HotUpdaterConfig.infoValue(forKey: "HOT_UPDATER_PUBLIC_KEY", bundle: bundle),
119
+ isolationKeyOverride: optionValue(overrides.isolationKey)
120
+ )
121
+ }
122
+
123
+ /// The environment of `name`. A named instance reads `channel`,
124
+ /// `fingerprintHash`, and `publicKey`, then its `HOT_UPDATER_*` Info.plist
125
+ /// keys from `bundle`; main reads `config`, where
126
+ /// `HotUpdaterMainBundle.claim` records main's options. Throws when `name`
127
+ /// is not 1 to 32 of `a-z`, `0-9`, and `-`, since it becomes a directory
128
+ /// and a key prefix.
129
+ init(
130
+ name: String,
131
+ bundle: Bundle,
132
+ config: HotUpdaterConfig = .shared,
133
+ channel: String? = nil,
134
+ fingerprintHash: String? = nil,
135
+ publicKey: String? = nil
136
+ ) throws {
137
+ guard Self.isValidName(name) else {
138
+ throw NativeUpdateCheckError.hotUpdater(
139
+ "Invalid name: \(name). Use 1 to \(Self.maxNameLength) of a-z, 0-9, and -."
140
+ )
141
+ }
142
+ // Main's runtime is told apart by the bundle it runs, so a named
143
+ // instance embeds a bundle of its own.
144
+ if name != Self.mainName, bundle == Bundle.main {
145
+ throw NativeUpdateCheckError.hotUpdater(
146
+ "A named HotUpdater instance needs its own bundle, not Bundle.main: \(name)"
147
+ )
148
+ }
149
+ let isMain = name == Self.mainName
150
+ self.init(
151
+ name: name,
152
+ namedBundle: isMain ? nil : bundle,
153
+ config: config,
154
+ options: isMain ? HotUpdaterConfig.Overrides() : HotUpdaterConfig.Overrides(
155
+ fingerprintHash: Self.optionValue(fingerprintHash),
156
+ publicKey: Self.optionValue(publicKey),
157
+ channel: Self.optionValue(channel)
158
+ )
159
+ )
160
+ }
161
+
162
+ /// The environment of `options`.
163
+ init(options: HotUpdaterOptions, config: HotUpdaterConfig = .shared) throws {
164
+ try self.init(
165
+ name: options.name,
166
+ bundle: options.bundle,
167
+ config: config,
168
+ channel: options.channel,
169
+ fingerprintHash: options.fingerprintHash,
170
+ publicKey: options.publicKey
171
+ )
172
+ }
173
+
174
+ /// An option left nil or empty reads Info.plist.
175
+ static func optionValue(_ value: String?) -> String? {
176
+ guard let value, !value.isEmpty else {
177
+ return nil
178
+ }
179
+ return value
180
+ }
181
+
182
+ /// Whether `other` is the same named instance: one service, so a second
183
+ /// instance of a name must have the same bundle and resolve to the same
184
+ /// channel, fingerprint hash, and public key, from its options or its
185
+ /// bundle's Info.plist alike.
186
+ func hasSameConfiguration(_ other: HotUpdaterInstanceEnvironment) -> Bool {
187
+ name == other.name
188
+ && bundle == other.bundle
189
+ && defaultChannel() == other.defaultChannel()
190
+ && fingerprintHash() == other.fingerprintHash()
191
+ && publicKey() == other.publicKey()
192
+ }
193
+
194
+ static func isValidName(_ name: String) -> Bool {
195
+ let scalars = name.unicodeScalars
196
+ guard (1...maxNameLength).contains(scalars.count) else {
197
+ return false
198
+ }
199
+ return scalars.allSatisfy { scalar in
200
+ ("a"..."z").contains(scalar) || ("0"..."9").contains(scalar) || scalar == "-"
201
+ }
202
+ }
203
+
204
+ var isMain: Bool { namedBundle == nil }
205
+
206
+ /// The bundle with the embedded `main.jsbundle` and, for a named instance,
207
+ /// the `HOT_UPDATER_*` keys.
208
+ var bundle: Bundle { namedBundle ?? config.bundle }
209
+
210
+ // MARK: - Paths
211
+
212
+ /// The directory holding `bundle-store` and `bundle-temp`: Documents for
213
+ /// main, `Documents/hot-updater-instances/<name>` otherwise.
214
+ func storageRootPath(documentsPath: String) -> String {
215
+ Self.storageRootPath(name: name, documentsPath: documentsPath)
216
+ }
217
+
218
+ /// The storage root of `name`, which the router needs without the
219
+ /// instance's bundle.
220
+ static func storageRootPath(name: String, documentsPath: String) -> String {
221
+ guard name != mainName else {
222
+ return documentsPath
223
+ }
224
+ return ((documentsPath as NSString)
225
+ .appendingPathComponent(instancesDirectoryName) as NSString)
226
+ .appendingPathComponent(name)
227
+ }
228
+
229
+ func bundleStorePath(documentsPath: String) -> String {
230
+ (storageRootPath(documentsPath: documentsPath) as NSString)
231
+ .appendingPathComponent(Self.bundleStoreDirectoryName)
232
+ }
233
+
234
+ /// Wiped before each update, so each instance needs its own.
235
+ func tempPath(documentsPath: String) -> String {
236
+ (storageRootPath(documentsPath: documentsPath) as NSString)
237
+ .appendingPathComponent(Self.tempDirectoryName)
238
+ }
239
+
240
+ /// The directory of the key-value store, which also holds the native
241
+ /// update event queue.
242
+ func keyValueStorageDirectory(in fileSystem: FileSystemService) -> URL {
243
+ let base = NoBackupStorage.directory(in: fileSystem)
244
+ guard !isMain else {
245
+ return base
246
+ }
247
+ return base
248
+ .appendingPathComponent(Self.noBackupInstancesDirectoryName, isDirectory: true)
249
+ .appendingPathComponent(name, isDirectory: true)
250
+ }
251
+
252
+ /// Per name, so one instance's entry limit never evicts another's.
253
+ func releaseCatalogCacheDirectory(in fileSystem: FileSystemService) -> URL {
254
+ guard !isMain else {
255
+ return ReleaseCatalogCacheService.defaultRootDirectory()
256
+ }
257
+ return keyValueStorageDirectory(in: fileSystem)
258
+ .appendingPathComponent(Self.releaseCatalogCacheDirectoryName, isDirectory: true)
259
+ }
260
+
261
+ /// A background URLSession identifier must be unique in the process.
262
+ var backgroundSessionIdentifier: String {
263
+ isMain ? Self.backgroundSessionIdentifierBase : "\(Self.backgroundSessionIdentifierBase).\(name)"
264
+ }
265
+
266
+ // MARK: - Config
267
+
268
+ /// App-wide: every instance ships in the same app version.
269
+ static var appVersion: String? {
270
+ Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
271
+ }
272
+
273
+ func fingerprintHash() -> String? {
274
+ guard isMain else {
275
+ // Every instance's bundle comes from the project the build
276
+ // fingerprinted.
277
+ return options.fingerprintHash ?? config.buildFingerprintHash
278
+ }
279
+ return mainValues().fingerprintHash
280
+ }
281
+
282
+ func defaultChannel() -> String {
283
+ guard isMain else {
284
+ return options.channel ?? namedInfoValue(forKey: "HOT_UPDATER_CHANNEL") ?? Self.defaultChannel
285
+ }
286
+ return mainValues().defaultChannel
287
+ }
288
+
289
+ /// The key bundle signatures are verified with; nil disables signing.
290
+ func publicKey() -> String? {
291
+ guard isMain else {
292
+ return options.publicKey ?? namedInfoValue(forKey: "HOT_UPDATER_PUBLIC_KEY")
293
+ }
294
+ return mainValues().publicKey
295
+ }
296
+
297
+ func updateStrategy() -> UpdateStrategy {
298
+ isolationFingerprintHash() != nil ? .fingerprint : .appVersion
299
+ }
300
+
301
+ /// The fingerprint hash the isolation key and the update strategy use,
302
+ /// or nil for the appVersion strategy.
303
+ func isolationFingerprintHash() -> String? {
304
+ isMain ? mainValues().isolationFingerprintHash : fingerprintHash()
305
+ }
306
+
307
+ /// The prefix of this instance's preferences, also stamped into its
308
+ /// metadata: a change wipes its bundles. Main keeps
309
+ /// `hotupdater_{fingerprint}_{minBundleId}_{channel}_` under the
310
+ /// fingerprint strategy and `hotupdater_{appVersion}_{channel}_` under the
311
+ /// appVersion strategy, or the deprecated `HotUpdaterConfig.isolationKey`
312
+ /// verbatim; a named instance prefixes the same fields with
313
+ /// `hotupdater_instance_<name>_`.
314
+ ///
315
+ /// A fingerprint build's state is keyed by the artifact, not the host
316
+ /// app's version: a host release that ships the same artifact keeps its
317
+ /// downloads, and a new build, which has a new `minBundleId`, starts from
318
+ /// its newer embedded bundle. A key written before this format differs,
319
+ /// so the stored bundles are discarded once.
320
+ func isolationKey(appVersion: String?, minBundleId: String) -> String {
321
+ // One read of main's values, so the key's fields agree.
322
+ let main = isMain ? mainValues() : nil
323
+ if let override = main?.isolationKeyOverride {
324
+ return override
325
+ }
326
+
327
+ let baseKey: String
328
+ if let hash = main?.isolationFingerprintHash ?? (isMain ? nil : fingerprintHash()) {
329
+ let minBundleId = minBundleId.trimmingCharacters(in: .whitespacesAndNewlines)
330
+ baseKey = "\(hash)_\(minBundleId.isEmpty ? "unknown" : minBundleId)"
331
+ } else {
332
+ baseKey = appVersion ?? "unknown"
333
+ }
334
+
335
+ let prefix = isMain ? "hotupdater_" : "hotupdater_instance_\(name)_"
336
+ return "\(prefix)\(baseKey)_\(main?.defaultChannel ?? defaultChannel())_"
337
+ }
338
+
339
+ /// Only the instance's own bundle: falling back to the host app's
340
+ /// Info.plist would hand a framework the app's fingerprint, channel, or key.
341
+ private func namedInfoValue(forKey key: String) -> String? {
342
+ guard let value = namedBundle?.object(forInfoDictionaryKey: key) as? String,
343
+ !value.isEmpty else {
344
+ return nil
345
+ }
346
+ return value
347
+ }
348
+ }
349
+
350
+ /// The bundle and options main works with. Main is one service like any
351
+ /// name: its storage and isolation key come from the first bundle and values
352
+ /// it is given, so a main `HotUpdaterInstance` with another bundle, channel,
353
+ /// fingerprint hash, or public key would read them over state keyed by the
354
+ /// first.
355
+ final class HotUpdaterMainBundle {
356
+ static let shared = HotUpdaterMainBundle()
357
+
358
+ /// Lock order: this lock, then `HotUpdaterConfig`'s, which `claim` and
359
+ /// `implEnvironment` take while holding it.
360
+ private let lock = NSLock()
361
+ private var bundle: Bundle?
362
+ /// What main's impl was created from, which fixed its isolation key:
363
+ /// the config's values, the bundle, and the values they resolved to.
364
+ /// Nil until the impl exists.
365
+ private var created: (
366
+ overrides: HotUpdaterConfig.Overrides,
367
+ bundle: Bundle,
368
+ values: HotUpdaterInstanceEnvironment.MainValues
369
+ )?
370
+ private let config: HotUpdaterConfig
371
+
372
+ init(config: HotUpdaterConfig = .shared) {
373
+ self.config = config
374
+ }
375
+
376
+ /// Makes `options.bundle` main's and records `options`' channel,
377
+ /// fingerprint hash, and public key in `config` for a main
378
+ /// `HotUpdaterInstance`, before main's impl exists. A value left nil
379
+ /// keeps what main has. Throws when main already works with another
380
+ /// bundle, or already holds another of these values: from an earlier
381
+ /// main instance, from the deprecated `HotUpdater.configure`, or because
382
+ /// main's impl was created with another.
383
+ func claim(_ options: HotUpdaterOptions) throws {
384
+ try claim(
385
+ options.bundle,
386
+ channel: options.channel,
387
+ fingerprintHash: options.fingerprintHash,
388
+ publicKey: options.publicKey
389
+ )
390
+ }
391
+
392
+ func claim(
393
+ _ bundle: Bundle,
394
+ channel: String? = nil,
395
+ fingerprintHash: String? = nil,
396
+ publicKey: String? = nil
397
+ ) throws {
398
+ lock.lock()
399
+ defer { lock.unlock() }
400
+ if let claimed = self.bundle, claimed != bundle {
401
+ throw Self.conflict("bundle")
402
+ }
403
+ let channel = HotUpdaterInstanceEnvironment.optionValue(channel)
404
+ let fingerprintHash = HotUpdaterInstanceEnvironment.optionValue(fingerprintHash)
405
+ let publicKey = HotUpdaterInstanceEnvironment.optionValue(publicKey)
406
+ if let created {
407
+ // The impl's key is fixed: what this instance resolves to, its
408
+ // options, then the config the impl was created with, then the
409
+ // proposed bundle's Info.plist and the app's, must be it.
410
+ let proposed = HotUpdaterInstanceEnvironment.resolveMain(
411
+ overrides: HotUpdaterConfig.Overrides(
412
+ fingerprintHash: fingerprintHash ?? created.overrides.fingerprintHash,
413
+ publicKey: publicKey ?? created.overrides.publicKey,
414
+ channel: channel ?? created.overrides.channel,
415
+ isolationKey: created.overrides.isolationKey
416
+ ),
417
+ bundle: bundle,
418
+ buildFingerprintHash: config.buildFingerprintHash
419
+ )
420
+ try Self.check(proposed.defaultChannel, against: created.values.defaultChannel, field: "channel")
421
+ try Self.check(
422
+ proposed.isolationFingerprintHash,
423
+ against: created.values.isolationFingerprintHash,
424
+ field: "fingerprintHash"
425
+ )
426
+ try Self.check(proposed.publicKey, against: created.values.publicKey, field: "publicKey")
427
+ } else {
428
+ // Checked and recorded in one step under the config's lock.
429
+ try config.claimOptions(
430
+ channel: channel,
431
+ fingerprintHash: fingerprintHash,
432
+ publicKey: publicKey,
433
+ conflict: Self.conflict
434
+ )
435
+ }
436
+ self.bundle = bundle
437
+ config.bundle = bundle
438
+ }
439
+
440
+ /// Records the bundle `HotUpdater.bundleURL(bundle:)` launches main with,
441
+ /// so a later instance with another bundle throws. That static API keeps
442
+ /// recording each bundle it is given, as it always has.
443
+ func note(_ bundle: Bundle) {
444
+ lock.lock()
445
+ defer { lock.unlock() }
446
+ if self.bundle == nil {
447
+ self.bundle = bundle
448
+ }
449
+ config.bundle = bundle
450
+ }
451
+
452
+ /// The environment main's impl is built from: main's values fixed from
453
+ /// one read of `config`, which from then on a main instance must match.
454
+ /// The impl reads only this snapshot, so a `configure` call racing its
455
+ /// creation can't leave it apart from what is recorded here.
456
+ /// The impl reads the bundle it was created with as main's bundle, so
457
+ /// that bundle becomes main's: a later main instance with another bundle
458
+ /// throws, as after a claim.
459
+ func implEnvironment() -> HotUpdaterInstanceEnvironment {
460
+ lock.lock()
461
+ defer { lock.unlock() }
462
+ let overrides = config.overrides
463
+ let bundle = config.bundle
464
+ let values = HotUpdaterInstanceEnvironment.resolveMain(
465
+ overrides: overrides,
466
+ bundle: bundle,
467
+ buildFingerprintHash: config.buildFingerprintHash
468
+ )
469
+ if created == nil {
470
+ created = (overrides, bundle, values)
471
+ if self.bundle == nil {
472
+ self.bundle = bundle
473
+ }
474
+ }
475
+ return HotUpdaterInstanceEnvironment.main(config: config).snapshot(values)
476
+ }
477
+
478
+ /// A value the instance resolves to conflicts with the one main's impl
479
+ /// uses whenever they differ, even where one of them is none.
480
+ private static func check(_ value: String?, against inUse: String?, field: String) throws {
481
+ guard value != inUse else {
482
+ return
483
+ }
484
+ throw conflict(field)
485
+ }
486
+
487
+ private static func conflict(_ field: String) -> Error {
488
+ NativeUpdateCheckError.hotUpdater(
489
+ "HotUpdater instance \"\(HotUpdaterInstanceEnvironment.mainName)\" was already created with another \(field)"
490
+ )
491
+ }
492
+ }
@@ -0,0 +1,83 @@
1
+ import Foundation
2
+
3
+ /// Finds the instance name a React Native runtime belongs to from the URL of
4
+ /// the bundle it runs. Each runtime has its own TurboModule, but JavaScript
5
+ /// never names its instance, so the module asks this with its runtime's
6
+ /// bundle URL.
7
+ ///
8
+ /// 1. A URL a named instance handed out for its launch, including the
9
+ /// `main.jsbundle` embedded in its bundle, is that name's.
10
+ /// 2. A file inside a named instance's storage root
11
+ /// (`Documents/hot-updater-instances/<name>/`) is that name's, which
12
+ /// covers a bundle loaded after a reload.
13
+ /// 3. Anything else, such as main's bundles or a Metro URL, is main's.
14
+ ///
15
+ /// Only names with an impl are candidates, so a URL recorded for a name
16
+ /// nothing created in this process never routes there. A URL two names
17
+ /// handed out belongs to neither.
18
+ final class HotUpdaterInstanceRouter {
19
+ private let lock = NSLock()
20
+ // Every URL each name handed out in this process, not only the latest:
21
+ // a runtime that has not resolved yet may still run an earlier one.
22
+ private var launchURLs: [String: Set<String>] = [:]
23
+
24
+ /// Records a URL a named instance handed out for its launch. Main's are
25
+ /// not recorded: whatever no named instance claims is main's.
26
+ func record(launchURL: URL, name: String) {
27
+ guard name != HotUpdaterInstanceEnvironment.mainName else {
28
+ return
29
+ }
30
+ let key = Self.comparableKey(launchURL)
31
+ lock.lock()
32
+ launchURLs[name, default: []].insert(key)
33
+ lock.unlock()
34
+ }
35
+
36
+ /// The name whose bundle `bundleURL` is, among `candidates`, or main.
37
+ func resolve(bundleURL: URL?, candidates: [String], documentsPath: String) -> String {
38
+ let mainName = HotUpdaterInstanceEnvironment.mainName
39
+ guard let bundleURL else {
40
+ return mainName
41
+ }
42
+ let names = candidates.filter { $0 != mainName }.sorted()
43
+ let key = Self.comparableKey(bundleURL)
44
+
45
+ lock.lock()
46
+ let handedOut = names.filter { launchURLs[$0]?.contains(key) == true }
47
+ lock.unlock()
48
+ // Two names that hand out the same URL, such as one bundle shared by
49
+ // both, can't be told apart by it.
50
+ if handedOut.count == 1 {
51
+ return handedOut[0]
52
+ }
53
+ if handedOut.count > 1 {
54
+ return mainName
55
+ }
56
+
57
+ guard bundleURL.isFileURL else {
58
+ return mainName
59
+ }
60
+ let path = Self.comparablePath(bundleURL)
61
+ return names.first { name in
62
+ let root = Self.comparablePath(URL(
63
+ fileURLWithPath: HotUpdaterInstanceEnvironment.storageRootPath(
64
+ name: name,
65
+ documentsPath: documentsPath
66
+ ),
67
+ isDirectory: true
68
+ ))
69
+ // With the trailing slash, `app` never claims `app-2`'s files.
70
+ return path.hasPrefix(root + "/")
71
+ } ?? mainName
72
+ }
73
+
74
+ /// File URLs compare by their resolved path, since React Native may hold
75
+ /// `/private/var/...` where Hot Updater handed out `/var/...`.
76
+ static func comparableKey(_ url: URL) -> String {
77
+ url.isFileURL ? "file://" + comparablePath(url) : url.absoluteString
78
+ }
79
+
80
+ private static func comparablePath(_ url: URL) -> String {
81
+ url.standardizedFileURL.resolvingSymlinksInPath().path
82
+ }
83
+ }
@@ -11,7 +11,7 @@ extension HotUpdaterConfig: HotUpdaterConfigInterface {}
11
11
 
12
12
  @_cdecl("HotUpdaterMakeImpl")
13
13
  public func hotUpdaterMakeImpl() -> HotUpdaterImplInterface {
14
- HotUpdaterImpl()
14
+ HotUpdaterNativeUpdateBridge.sharedImpl
15
15
  }
16
16
 
17
17
  @_cdecl("HotUpdaterAppVersion")
@@ -23,4 +23,31 @@ public func hotUpdaterAppVersion() -> NSString? {
23
23
  public func hotUpdaterSharedConfig() -> HotUpdaterConfigInterface {
24
24
  HotUpdaterConfig.shared
25
25
  }
26
+
27
+ /// The TurboModule's channel reset, event replay, and impl lookup, for the
28
+ /// Objective-C++ module.
29
+ final class HotUpdaterNativeUpdatesAdapter: NSObject, HotUpdaterNativeUpdatesInterface {
30
+ static let shared = HotUpdaterNativeUpdatesAdapter()
31
+
32
+ func resetChannel(
33
+ withImpl impl: HotUpdaterImplInterface,
34
+ resolver resolve: @escaping RCTPromiseResolveBlock,
35
+ rejecter reject: @escaping RCTPromiseRejectBlock
36
+ ) {
37
+ HotUpdaterNativeUpdateBridge.resetChannel(impl: impl as! HotUpdaterImpl, resolver: resolve, rejecter: reject)
38
+ }
39
+
40
+ func takeEvents(withImpl impl: HotUpdaterImplInterface) -> String? {
41
+ HotUpdaterNativeUpdateBridge.takeEvents(impl: impl as! HotUpdaterImpl)
42
+ }
43
+
44
+ func impl(forBundleURL bundleURL: URL?) -> HotUpdaterImplInterface {
45
+ HotUpdaterNativeUpdateBridge.impl(forBundleURL: bundleURL)
46
+ }
47
+ }
48
+
49
+ @_cdecl("HotUpdaterNativeUpdates")
50
+ public func hotUpdaterNativeUpdates() -> HotUpdaterNativeUpdatesInterface {
51
+ HotUpdaterNativeUpdatesAdapter.shared
52
+ }
26
53
  #endif