@hot-updater/react-native 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +492 -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,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
|
-
|
|
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
|