@hot-updater/react-native 1.0.8 → 1.1.1

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 +493 -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
@@ -0,0 +1,353 @@
1
+ import Foundation
2
+ import React
3
+
4
+ /// Runs the native update check over the same `HotUpdaterImpl` the
5
+ /// TurboModule uses, so staging, verification, and patches are shared.
6
+ final class HotUpdaterImplUpdateHost: NativeUpdateHost {
7
+ private let impl: HotUpdaterImpl
8
+
9
+ init(impl: HotUpdaterImpl) {
10
+ self.impl = impl
11
+ }
12
+
13
+ var appVersion: String? { HotUpdaterImpl.appVersion }
14
+
15
+ var minBundleId: String { HotUpdaterImpl.minBundleId() }
16
+
17
+ func bundleId() -> String? { impl.bundleIdForUpdateCheck() }
18
+
19
+ func channel() -> String { impl.getChannel() }
20
+
21
+ func defaultChannel() -> String { impl.getDefaultChannel() }
22
+
23
+ func cohort() -> String { impl.getCohort() }
24
+
25
+ func fingerprintHash() -> String? { impl.getFingerprintHash() }
26
+
27
+ func crashHistory() -> [String] { impl.getCrashHistory() }
28
+
29
+ func activeUpdateState() -> [String: Any] { impl.getActiveUpdateState() }
30
+
31
+ func acceptReleaseCatalog(_ params: [String: Any]) -> Bool {
32
+ impl.acceptReleaseCatalog(params)
33
+ }
34
+
35
+ func isReleaseSelectionCurrent(_ params: [String: Any]) -> Bool {
36
+ impl.isReleaseSelectionCurrent(params)
37
+ }
38
+
39
+ func commitReleaseSelection(_ params: [String: Any]) async throws -> Bool {
40
+ let value = try await promise { resolve, reject in
41
+ self.impl.commitReleaseSelection(params, resolver: resolve, rejecter: reject)
42
+ }
43
+ return (value as? Bool) ?? false
44
+ }
45
+
46
+ func updateBundle(
47
+ _ params: [String: Any],
48
+ onFilesResponse: ((Int, String?) -> Void)?,
49
+ progress: @escaping (Double) -> Void
50
+ ) async throws -> Any? {
51
+ try await promise { resolve, reject in
52
+ // Artifact protocol v2 goes to installArtifact, as the update check
53
+ // in JavaScript sends it.
54
+ if params["filesUrl"] != nil {
55
+ self.impl.installArtifact(
56
+ params,
57
+ onProgress: progress,
58
+ onFilesResponse: onFilesResponse,
59
+ resolver: resolve,
60
+ rejecter: reject
61
+ )
62
+ } else {
63
+ self.impl.updateBundle(
64
+ params,
65
+ onProgress: progress,
66
+ onFilesResponse: onFilesResponse,
67
+ resolver: resolve,
68
+ rejecter: reject
69
+ )
70
+ }
71
+ }
72
+ }
73
+
74
+ func resetChannel() async -> Bool {
75
+ let value = try? await promise { resolve, reject in
76
+ self.impl.resetChannel(resolve, rejecter: reject)
77
+ }
78
+ return (value as? Bool) ?? false
79
+ }
80
+
81
+ func releaseCatalogCache(_ partition: String) -> String? {
82
+ impl.releaseCatalogCacheValue(partition)
83
+ }
84
+
85
+ func setReleaseCatalogCache(_ partition: String, value: String) -> Bool {
86
+ impl.setReleaseCatalogCacheValue(partition, value: value)
87
+ }
88
+
89
+ func removeReleaseCatalogCache(_ partition: String) {
90
+ impl.removeReleaseCatalogCacheValue(partition)
91
+ }
92
+
93
+ func storageItem(_ key: String) -> String? { impl.getStorageItem(key) }
94
+
95
+ func setStorageItem(_ key: String, value: String?) { impl.setStorageItem(key, value: value) }
96
+
97
+ /// Awaits a TurboModule method's resolve or reject.
98
+ private func promise(
99
+ _ call: @escaping (@escaping RCTPromiseResolveBlock, @escaping RCTPromiseRejectBlock) -> Void
100
+ ) async throws -> Any? {
101
+ try await withCheckedThrowingContinuation { continuation in
102
+ call(
103
+ { value in continuation.resume(returning: value) },
104
+ { code, message, error in
105
+ continuation.resume(throwing: NativeUpdateRejection(
106
+ code: code,
107
+ message: message,
108
+ userInfo: (error as NSError?)?.userInfo ?? [:]
109
+ ))
110
+ }
111
+ )
112
+ }
113
+ }
114
+ }
115
+
116
+ /// The impl and native update checker of each instance name, shared by the
117
+ /// `HotUpdaterInstance`s of that name and by the TurboModule of each React
118
+ /// Native runtime that runs its bundle.
119
+ @objc(HotUpdaterNativeUpdateBridge)
120
+ public final class HotUpdaterNativeUpdateBridge: NSObject {
121
+ private static let lock = NSLock()
122
+ // Keyed by impl identity: a checker holds its impl, so the impl and its
123
+ // identifier live as long as the process.
124
+ private static var checkers: [ObjectIdentifier: NativeUpdateChecker] = [:]
125
+ private static var namedImpls: [String: HotUpdaterImpl] = [:]
126
+ /// The launch URLs named impls handed out, which tell the TurboModule of
127
+ /// each React Native runtime whose bundle it runs.
128
+ static let router = HotUpdaterInstanceRouter()
129
+
130
+ /// Main's impl, which HotUpdater.mm's `sharedImpl` returns too.
131
+ /// Record the bundle and main's options in `HotUpdaterConfig` first:
132
+ /// creating it fixes main's isolation key from them and that bundle's
133
+ /// Info.plist.
134
+ @objc public static let sharedImpl = HotUpdaterImpl()
135
+
136
+ /// The impl of `environment`'s name: `sharedImpl` for main, otherwise
137
+ /// one per name, created on first use. A name is one service, so an
138
+ /// instance of an existing name with another bundle, channel,
139
+ /// fingerprint hash, or public key throws, as on Android.
140
+ static func impl(for environment: HotUpdaterInstanceEnvironment) throws -> HotUpdaterImpl {
141
+ guard !environment.isMain else {
142
+ return sharedImpl
143
+ }
144
+ lock.lock()
145
+ defer { lock.unlock() }
146
+ if let impl = namedImpls[environment.name] {
147
+ guard impl.environment.hasSameConfiguration(environment) else {
148
+ throw NativeUpdateCheckError.hotUpdater(
149
+ "HotUpdater instance \"\(environment.name)\" was already created with another bundle, channel, fingerprintHash, or publicKey"
150
+ )
151
+ }
152
+ return impl
153
+ }
154
+ let impl = HotUpdaterImpl(environment: environment)
155
+ namedImpls[environment.name] = impl
156
+ return impl
157
+ }
158
+
159
+ /// The impl of the React Native runtime that runs `bundleURL`: the named
160
+ /// impl that handed it out or keeps it in its storage, otherwise main's.
161
+ /// Each runtime's TurboModule asks with its own bundle URL, since
162
+ /// JavaScript never names its instance.
163
+ @objc(implForBundleURL:)
164
+ public static func impl(forBundleURL bundleURL: URL?) -> HotUpdaterImpl {
165
+ lock.lock()
166
+ let impls = namedImpls
167
+ lock.unlock()
168
+ let name = router.resolve(
169
+ bundleURL: bundleURL,
170
+ candidates: Array(impls.keys),
171
+ documentsPath: FileManagerService().documentsPath()
172
+ )
173
+ return impls[name] ?? sharedImpl
174
+ }
175
+
176
+ /// One checker per impl, so its gate serializes every native check of
177
+ /// that instance name.
178
+ static func checker(impl: HotUpdaterImpl) -> NativeUpdateChecker {
179
+ lock.lock()
180
+ defer { lock.unlock() }
181
+ if let checker = checkers[ObjectIdentifier(impl)] {
182
+ return checker
183
+ }
184
+ let checker = NativeUpdateChecker(host: HotUpdaterImplUpdateHost(impl: impl))
185
+ checkers[ObjectIdentifier(impl)] = checker
186
+ return checker
187
+ }
188
+
189
+ /// JavaScript's `resetChannel`, run once a native check or download
190
+ /// finishes, which would save its channel again after the reset.
191
+ @objc(resetChannelWithImpl:resolver:rejecter:)
192
+ public static func resetChannel(
193
+ impl: HotUpdaterImpl,
194
+ resolver resolve: @escaping RCTPromiseResolveBlock,
195
+ rejecter reject: @escaping RCTPromiseRejectBlock
196
+ ) {
197
+ let checker = checker(impl: impl)
198
+ Task {
199
+ await checker.whenIdle {
200
+ await withCheckedContinuation { (done: CheckedContinuation<Void, Never>) in
201
+ impl.resetChannel({ value in
202
+ // The reset deleted staged bundles; their queued downloads go too.
203
+ if (value as? Bool) == true {
204
+ NativeUpdateEvents.remove(name: "onBundleDownloaded", from: HotUpdaterImplUpdateHost(impl: impl))
205
+ }
206
+ resolve(value)
207
+ done.resume()
208
+ }, rejecter: { code, message, error in
209
+ reject(code, message, error)
210
+ done.resume()
211
+ })
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ /// The queued plugin events as a JSON array, cleared as they are taken.
218
+ @objc(takeEventsWithImpl:)
219
+ public static func takeEvents(impl: HotUpdaterImpl) -> String? {
220
+ let host = HotUpdaterImplUpdateHost(impl: impl)
221
+ return NativeUpdateEvents.take(
222
+ from: host,
223
+ build: NativeUpdateEvents.build(
224
+ appVersion: host.appVersion,
225
+ minBundleId: host.minBundleId,
226
+ cohort: host.cohort()
227
+ )
228
+ )
229
+ }
230
+ }
231
+
232
+ /// The native counterpart of the instance JavaScript's `HotUpdater.init`
233
+ /// returns, for apps that check for and stage updates before React Native
234
+ /// starts: `checkForUpdate`, the returned update's `updateBundle`,
235
+ /// `resetChannel`, and the getters, with this instance's server and request
236
+ /// defaults. `bundleURL()` is the bundle React Native starts with.
237
+ ///
238
+ /// An update staged before `bundleURL()` is read applies at this launch;
239
+ /// one staged after it applies at the next launch.
240
+ ///
241
+ /// `options.name` picks the state the instance works on; see
242
+ /// `HotUpdaterOptions.name`.
243
+ @objc public final class HotUpdaterInstance: NSObject, @unchecked Sendable {
244
+ @objc public let options: HotUpdaterOptions
245
+ private let impl: HotUpdaterImpl
246
+
247
+ /// Throws when `baseURL` is not an absolute HTTP(S) URL, as
248
+ /// `HotUpdater.init` does, when `name` is not 1 to 32 of `a-z`, `0-9`,
249
+ /// and `-`, or when an instance of the same `name`, `main` included, was
250
+ /// created or launched with another bundle, channel, fingerprint hash,
251
+ /// or public key. For main, that includes values from the deprecated
252
+ /// `HotUpdater.configure`, and the values main's impl was created with.
253
+ @objc public init(_ options: HotUpdaterOptions) throws {
254
+ _ = try NativeUpdateChecker.validatedBaseURL(options.baseURL)
255
+ _ = HotUpdaterInstanceEnvironment.registerBuildFingerprintHash
256
+ let environment = try HotUpdaterInstanceEnvironment(options: options)
257
+ self.options = options
258
+ if environment.isMain {
259
+ // Before the impl exists: creating it fixes main's isolation key
260
+ // from these values and this bundle's Info.plist. A named
261
+ // instance reads its own options and bundle and leaves main's
262
+ // alone.
263
+ try HotUpdaterMainBundle.shared.claim(options)
264
+ }
265
+ impl = try HotUpdaterNativeUpdateBridge.impl(for: environment)
266
+ super.init()
267
+ }
268
+
269
+ private var checker: NativeUpdateChecker {
270
+ HotUpdaterNativeUpdateBridge.checker(impl: impl)
271
+ }
272
+
273
+ /// The update the app should apply, or nil when there is none. Unlike
274
+ /// JavaScript, which returns null and calls `onError`, a failed check
275
+ /// throws; it is still reported to plugins as `onUpdateError`. Waits for
276
+ /// other native update work; cancel the caller to stop waiting.
277
+ public func checkForUpdate(_ options: HotUpdaterCheckOptions) async throws -> HotUpdaterUpdate? {
278
+ try await checker.checkForUpdate(NativeCheckRequest(self.options, options))
279
+ }
280
+
281
+ /// `checkForUpdate` for Objective-C; completes on the main queue.
282
+ /// `cancel` on the returned task cancels the check.
283
+ @discardableResult
284
+ @objc(checkForUpdate:completion:)
285
+ public func checkForUpdate(
286
+ _ options: HotUpdaterCheckOptions,
287
+ completion: @escaping (HotUpdaterUpdate?, Error?) -> Void
288
+ ) -> HotUpdaterNativeUpdateTask {
289
+ let request = NativeCheckRequest(self.options, options)
290
+ let checker = checker
291
+ return HotUpdaterNativeUpdateTask {
292
+ do {
293
+ let update = try await checker.checkForUpdate(request)
294
+ DispatchQueue.main.async { completion(update, nil) }
295
+ } catch {
296
+ DispatchQueue.main.async { completion(nil, error) }
297
+ }
298
+ }
299
+ }
300
+
301
+ /// The bundle React Native starts with: the staged update, or the
302
+ /// `main.jsbundle` embedded in `options.bundle`. Reading it fixes this
303
+ /// launch's bundle.
304
+ @objc public func bundleURL() -> URL? {
305
+ // Main's impl records the bundle in HotUpdaterConfig for reloads.
306
+ return impl.bundleURL(bundle: options.bundle)
307
+ }
308
+
309
+ /// Clears the runtime channel switch and returns to the default channel's
310
+ /// bundle at the next launch, once other native update work finishes.
311
+ public func resetChannel() async -> Bool {
312
+ await checker.resetChannel()
313
+ }
314
+
315
+ /// `resetChannel` for Objective-C; completes on the main queue.
316
+ @objc(resetChannelWithCompletion:)
317
+ public func resetChannel(completion: @escaping (Bool) -> Void) {
318
+ let checker = checker
319
+ Task {
320
+ let reset = await checker.resetChannel()
321
+ DispatchQueue.main.async { completion(reset) }
322
+ }
323
+ }
324
+
325
+ @objc public func getAppVersion() -> String? { HotUpdaterImpl.appVersion }
326
+
327
+ /// The bundle this launch runs, or nil for the built-in bundle or before
328
+ /// `bundleURL()` is read.
329
+ @objc public func getBundleId() -> String? { impl.runningBundleId() }
330
+
331
+ @objc public func getMinBundleId() -> String { HotUpdaterImpl.minBundleId() }
332
+
333
+ @objc public func getChannel() -> String { impl.getChannel() }
334
+
335
+ @objc public func getDefaultChannel() -> String { impl.getDefaultChannel() }
336
+
337
+ @objc public func getFingerprintHash() -> String? { impl.getFingerprintHash() }
338
+ }
339
+
340
+ /// A native update call started from Objective-C; cancelling it cancels the
341
+ /// check and its requests, as cancelling a Swift task does.
342
+ @objc(HotUpdaterNativeUpdateTask)
343
+ public final class HotUpdaterNativeUpdateTask: NSObject, @unchecked Sendable {
344
+ private let task: Task<Void, Never>
345
+
346
+ init(_ operation: @escaping @Sendable () async -> Void) {
347
+ task = Task { await operation() }
348
+ }
349
+
350
+ @objc public func cancel() {
351
+ task.cancel()
352
+ }
353
+ }
@@ -0,0 +1,54 @@
1
+ import Foundation
2
+
3
+ /// Serializes the launch's bundle selection with what reads or changes it: the
4
+ /// update check's read, so a check never reads metadata the launch is still
5
+ /// selecting from, and a channel reset or built-in commit, which delete the
6
+ /// downloaded bundles, so React Native never selects a bundle one of them is
7
+ /// about to delete.
8
+ final class LaunchPreparationLock {
9
+ private let lock = NSLock()
10
+
11
+ func withLock<T>(_ body: () throws -> T) rethrows -> T {
12
+ lock.lock()
13
+ defer { lock.unlock() }
14
+ return try body()
15
+ }
16
+
17
+ /// Resets the channel in `storage`. Once it resets, `clearNextLaunch` runs
18
+ /// under the lock, so React Native's next read picks the default channel's
19
+ /// bundle.
20
+ func resetChannel(
21
+ in storage: BundleStorageService,
22
+ clearNextLaunch: () -> Void
23
+ ) -> Result<Bool, Error> {
24
+ withLock {
25
+ let result = storage.resetChannel()
26
+ if case .success = result {
27
+ clearNextLaunch()
28
+ }
29
+ return result
30
+ }
31
+ }
32
+
33
+ /// Commits `selection` in `storage`. Committing anything but a bundle, such
34
+ /// as the built-in bundle, deletes every downloaded bundle, so it runs
35
+ /// under the lock as a channel reset does. Once it commits,
36
+ /// `clearNextLaunch` runs under the lock, so React Native's next read picks
37
+ /// the built-in bundle.
38
+ func commitReleaseSelection(
39
+ _ selection: PersistedSelection,
40
+ in storage: BundleStorageService,
41
+ clearNextLaunch: () -> Void
42
+ ) -> Bool {
43
+ guard selection.kind != "BUNDLE" else {
44
+ return storage.commitReleaseSelection(selection)
45
+ }
46
+ return withLock {
47
+ let committed = storage.commitReleaseSelection(selection)
48
+ if committed {
49
+ clearNextLaunch()
50
+ }
51
+ return committed
52
+ }
53
+ }
54
+ }