@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.
- package/HotUpdater.podspec +14 -2
- package/Package.swift +24 -3
- package/android/build.gradle +2 -6
- package/android/hot-updater-fingerprint.gradle +135 -0
- package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
- package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
- package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
- package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
- package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
- package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +493 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
- package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
- package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
- package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
- package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
- package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
- package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
- package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
- package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
- package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
- package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
- package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
- package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
- package/android/src/newarch/HotUpdater.kt +53 -15
- package/android/src/newarch/HotUpdaterModule.kt +42 -17
- package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
- package/android/src/oldarch/HotUpdater.kt +51 -12
- package/android/src/oldarch/HotUpdaterModule.kt +43 -17
- package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
- package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
- package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
- package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
- package/ios/HotUpdater/Internal/CohortService.swift +8 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
- package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
- package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
- package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
- package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
- package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
- package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
- package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
- package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
- package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
- package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
- package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
- package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
- package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
- package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
- package/ios/HotUpdater/Package.swift +11 -0
- package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
- package/ios/HotUpdater/Public/HotUpdater.h +21 -2
- package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
- package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
- package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
- package/lib/commonjs/checkForUpdate.js +3 -1
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +9 -12
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +24 -79
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/nativeUpdateEvents.js +66 -0
- package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/checkForUpdate.js +3 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +10 -13
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +19 -73
- package/lib/module/native.js.map +1 -1
- package/lib/module/nativeUpdateEvents.js +61 -0
- package/lib/module/nativeUpdateEvents.js.map +1 -0
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -14
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +9 -46
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
- package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -14
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +9 -46
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
- package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/checkForUpdate.ts +7 -1
- package/src/index.ts +9 -17
- package/src/native.ts +19 -91
- package/src/nativeUpdateEvents.ts +77 -0
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +20 -3
- package/android/libs/README.md +0 -50
- package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
- package/android/src/bridge/BridgeReactInstance.kt +0 -38
- package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
- package/android/src/main/java/com/hotupdater/ContentAppearedLog.kt +0 -31
- package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
- package/android/src/newarch/ReactIntegrationManager.kt +0 -212
- 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
|
+
}
|