@hot-updater/react-native 1.1.4 → 1.2.0-rc.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/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +136 -14
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +26 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +13 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +129 -1
- package/android/src/newarch/HotUpdaterModule.kt +39 -0
- package/android/src/oldarch/HotUpdaterModule.kt +40 -0
- package/android/src/oldarch/HotUpdaterSpec.kt +5 -0
- package/ios/HotUpdater/Interface/HotUpdaterInterface.h +7 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +147 -22
- package/ios/HotUpdater/Internal/HotUpdater.mm +18 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +31 -0
- package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +14 -0
- package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +37 -0
- package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +52 -0
- package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +124 -0
- package/lib/commonjs/checkForUpdate.js +101 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/native.js +22 -2
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/sdkVersion.js.map +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/checkForUpdate.js +102 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/native.js +19 -0
- package/lib/module/native.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/sdkVersion.js.map +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/native.d.ts +7 -0
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +8 -0
- 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/native.d.ts +7 -0
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +8 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/checkForUpdate.ts +133 -0
- package/src/native.ts +21 -0
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +9 -0
|
@@ -344,6 +344,14 @@ public protocol BundleStorageService {
|
|
|
344
344
|
* Restores the original bundle and clears downloaded bundle state.
|
|
345
345
|
*/
|
|
346
346
|
func resetChannel() -> Result<Bool, Error>
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Resets the channel only while the named Release selection is current,
|
|
350
|
+
* checked and reset in one critical section with the catalog checks, so
|
|
351
|
+
* a newer catalog accepted meanwhile keeps the channel. Success is false
|
|
352
|
+
* for a stale selection.
|
|
353
|
+
*/
|
|
354
|
+
func resetChannelIfSelectionCurrent(catalogId: String, scopeKey: String, generation: Int64, catalogHash: String, channel: String, selectionContextHash: String) -> Result<Bool, Error>
|
|
347
355
|
}
|
|
348
356
|
|
|
349
357
|
public extension BundleStorageService {
|
|
@@ -356,10 +364,12 @@ public extension BundleStorageService {
|
|
|
356
364
|
func getActiveUpdateState() -> [String: Any] { [:] }
|
|
357
365
|
func isReleaseSelectionCurrent(catalogId _: String, scopeKey _: String, generation _: Int64, catalogHash _: String, channel _: String, selectionContextHash _: String) -> Bool { false }
|
|
358
366
|
func commitReleaseSelection(_: PersistedSelection) -> Bool { false }
|
|
367
|
+
func resetChannelIfSelectionCurrent(catalogId _: String, scopeKey _: String, generation _: Int64, catalogHash _: String, channel _: String, selectionContextHash _: String) -> Result<Bool, Error> { .success(false) }
|
|
359
368
|
}
|
|
360
369
|
|
|
361
370
|
class BundleFileStorageService: BundleStorageService {
|
|
362
371
|
private static let maximumSafeByteSize: Int64 = 9_007_199_254_740_991
|
|
372
|
+
private static let nilUUID = "00000000-0000-0000-0000-000000000000"
|
|
363
373
|
/// Files downloaded at once when the archive is not used.
|
|
364
374
|
static let parallelFileDownloads = 16
|
|
365
375
|
/// What one request costs beyond the first `parallelFileDownloads`, in
|
|
@@ -564,10 +574,75 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
564
574
|
return (patchDirectory as NSString).appendingPathComponent("\(safeName).bsdiff")
|
|
565
575
|
}
|
|
566
576
|
|
|
577
|
+
/// Copies, with no OTA bundle installed, the packaged file each built-in
|
|
578
|
+
/// patch was made from, when its bytes are the patch's base: store
|
|
579
|
+
/// processing may have changed them. Only these patches are priced and
|
|
580
|
+
/// applied; the others fall back to their files.
|
|
581
|
+
private func prepareBuiltInPatchBases(
|
|
582
|
+
downloads: [String: ChangedAssetDescriptor],
|
|
583
|
+
currentBundleId: String?,
|
|
584
|
+
tempDirectory: String
|
|
585
|
+
) -> [String: String] {
|
|
586
|
+
guard currentBundleId == nil else { return [:] }
|
|
587
|
+
var bases: [String: String] = [:]
|
|
588
|
+
for (assetPath, descriptor) in downloads {
|
|
589
|
+
guard let patch = descriptor.patch,
|
|
590
|
+
isPatchForRunningBundle(patch, currentBundleId: nil) else {
|
|
591
|
+
continue
|
|
592
|
+
}
|
|
593
|
+
let basePath = patchBaseTempPath(
|
|
594
|
+
tempDirectory: tempDirectory,
|
|
595
|
+
assetPath: assetPath
|
|
596
|
+
)
|
|
597
|
+
if builtInAssetResolver.copyIfMatches(
|
|
598
|
+
assetPath: assetPath,
|
|
599
|
+
expectedHash: patch.baseFileHash,
|
|
600
|
+
destination: basePath
|
|
601
|
+
) {
|
|
602
|
+
bases[assetPath] = basePath
|
|
603
|
+
} else {
|
|
604
|
+
try? fileSystem.removeItem(atPath: basePath)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return bases
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/// Where the built-in bundle's file is copied to serve as a patch's base.
|
|
611
|
+
private func patchBaseTempPath(
|
|
612
|
+
tempDirectory: String,
|
|
613
|
+
assetPath: String
|
|
614
|
+
) -> String {
|
|
615
|
+
let safeName = assetPath
|
|
616
|
+
.replacingOccurrences(of: "/", with: "__")
|
|
617
|
+
.replacingOccurrences(of: "\\", with: "__")
|
|
618
|
+
let patchDirectory = (tempDirectory as NSString).appendingPathComponent("patches")
|
|
619
|
+
_ = fileSystem.createDirectory(atPath: patchDirectory)
|
|
620
|
+
return (patchDirectory as NSString).appendingPathComponent("\(safeName).base")
|
|
621
|
+
}
|
|
622
|
+
|
|
567
623
|
private func patchDownloadPath(assetPath: String) -> String {
|
|
568
624
|
return "\(assetPath).bsdiff"
|
|
569
625
|
}
|
|
570
626
|
|
|
627
|
+
/// Whether `patch` was made against the running bundle: the OTA bundle
|
|
628
|
+
/// `currentBundleId`, or with none, the built-in bundle, whose ID is
|
|
629
|
+
/// MIN_BUNDLE_ID once its native build is registered.
|
|
630
|
+
private func isPatchForRunningBundle(
|
|
631
|
+
_ patch: BsdiffPatchDescriptor,
|
|
632
|
+
currentBundleId: String?
|
|
633
|
+
) -> Bool {
|
|
634
|
+
guard patch.algorithm == "bsdiff" else { return false }
|
|
635
|
+
if let currentBundleId {
|
|
636
|
+
return currentBundleId == patch.baseBundleId
|
|
637
|
+
}
|
|
638
|
+
// A Debug build reports the nil UUID, and has no built-in bundle.
|
|
639
|
+
guard let builtInBundleId = resolveBuiltInBundleId(),
|
|
640
|
+
builtInBundleId != Self.nilUUID else {
|
|
641
|
+
return false
|
|
642
|
+
}
|
|
643
|
+
return builtInBundleId == patch.baseBundleId
|
|
644
|
+
}
|
|
645
|
+
|
|
571
646
|
private func downloadPath(
|
|
572
647
|
assetPath: String,
|
|
573
648
|
changedAsset: ChangedAssetDescriptor
|
|
@@ -609,6 +684,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
609
684
|
changedAsset: ChangedAssetDescriptor,
|
|
610
685
|
currentBundleId: String?,
|
|
611
686
|
currentBundleDir: String?,
|
|
687
|
+
builtInBasePath: String?,
|
|
612
688
|
destinationPath: String,
|
|
613
689
|
expectedHash: String,
|
|
614
690
|
tempDirectory: String,
|
|
@@ -616,29 +692,34 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
616
692
|
progressHandler: @escaping (UpdateProgressPayload) -> Void
|
|
617
693
|
) -> Bool {
|
|
618
694
|
guard let patch = changedAsset.patch,
|
|
619
|
-
patch
|
|
620
|
-
currentBundleId == patch.baseBundleId,
|
|
621
|
-
let currentBundleDir
|
|
695
|
+
isPatchForRunningBundle(patch, currentBundleId: currentBundleId)
|
|
622
696
|
else {
|
|
623
697
|
return false
|
|
624
698
|
}
|
|
625
699
|
|
|
626
700
|
let sourcePath: String
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
701
|
+
if currentBundleId == nil {
|
|
702
|
+
// Copied and checked before the plan priced the patch.
|
|
703
|
+
guard let builtInBasePath else { return false }
|
|
704
|
+
sourcePath = builtInBasePath
|
|
705
|
+
} else {
|
|
706
|
+
guard let currentBundleDir else { return false }
|
|
707
|
+
do {
|
|
708
|
+
sourcePath = try FileUtilities.fileURL(
|
|
709
|
+
for: assetPath,
|
|
710
|
+
destinationRoot: currentBundleDir
|
|
711
|
+
).path
|
|
712
|
+
} catch {
|
|
713
|
+
return false
|
|
714
|
+
}
|
|
715
|
+
guard self.fileSystem.fileExists(atPath: sourcePath),
|
|
716
|
+
HashUtils.verifyHash(
|
|
717
|
+
fileURL: URL(fileURLWithPath: sourcePath),
|
|
718
|
+
expectedHash: patch.baseFileHash
|
|
719
|
+
)
|
|
720
|
+
else {
|
|
721
|
+
return false
|
|
722
|
+
}
|
|
642
723
|
}
|
|
643
724
|
|
|
644
725
|
let patchPath = patchTempPath(
|
|
@@ -648,6 +729,9 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
648
729
|
|
|
649
730
|
defer {
|
|
650
731
|
try? self.fileSystem.removeItem(atPath: patchPath)
|
|
732
|
+
if currentBundleId == nil {
|
|
733
|
+
try? self.fileSystem.removeItem(atPath: sourcePath)
|
|
734
|
+
}
|
|
651
735
|
if self.fileSystem.fileExists(atPath: destinationPath),
|
|
652
736
|
!HashUtils.verifyHash(
|
|
653
737
|
fileURL: URL(fileURLWithPath: destinationPath),
|
|
@@ -1165,7 +1249,9 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1165
1249
|
archive: ParsedArchive?,
|
|
1166
1250
|
archiveUrl: URL?,
|
|
1167
1251
|
downloads: [String: ChangedAssetDescriptor],
|
|
1168
|
-
assets: [String: ParsedManifestAsset]
|
|
1252
|
+
assets: [String: ParsedManifestAsset],
|
|
1253
|
+
currentBundleId: String?,
|
|
1254
|
+
builtInPatchBases: [String: String]
|
|
1169
1255
|
) -> Bool {
|
|
1170
1256
|
guard downloads.count >= 2,
|
|
1171
1257
|
archive != nil,
|
|
@@ -1181,7 +1267,11 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1181
1267
|
return false
|
|
1182
1268
|
}
|
|
1183
1269
|
let plannedByteSize: Int64
|
|
1184
|
-
|
|
1270
|
+
// Only a patch this device applies saves bytes.
|
|
1271
|
+
if let patch = descriptor.patch,
|
|
1272
|
+
currentBundleId == nil
|
|
1273
|
+
? builtInPatchBases[path] != nil
|
|
1274
|
+
: isPatchForRunningBundle(patch, currentBundleId: currentBundleId) {
|
|
1185
1275
|
hasOfferedPatch = true
|
|
1186
1276
|
guard let patchByteSize = patch.byteSize,
|
|
1187
1277
|
patchByteSize >= 0,
|
|
@@ -2228,6 +2318,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2228
2318
|
changedAsset: ChangedAssetDescriptor,
|
|
2229
2319
|
currentBundleId: String?,
|
|
2230
2320
|
currentBundleDir: String?,
|
|
2321
|
+
builtInBasePath: String?,
|
|
2231
2322
|
destinationPath: String,
|
|
2232
2323
|
tempDirectory: String,
|
|
2233
2324
|
progressFile: UpdateProgressPayload.DiffProgressFileSnapshot,
|
|
@@ -2240,6 +2331,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2240
2331
|
changedAsset: changedAsset,
|
|
2241
2332
|
currentBundleId: currentBundleId,
|
|
2242
2333
|
currentBundleDir: currentBundleDir,
|
|
2334
|
+
builtInBasePath: builtInBasePath,
|
|
2243
2335
|
destinationPath: destinationPath,
|
|
2244
2336
|
expectedHash: expectedHash,
|
|
2245
2337
|
tempDirectory: tempDirectory,
|
|
@@ -2374,7 +2466,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2374
2466
|
// A patch for the running bundle was offered, but the original
|
|
2375
2467
|
// file came instead; a patch for another base was never tried.
|
|
2376
2468
|
let patchWasTried = changedAsset.patch.map {
|
|
2377
|
-
$0
|
|
2469
|
+
isPatchForRunningBundle($0, currentBundleId: currentBundleId)
|
|
2378
2470
|
} ?? false
|
|
2379
2471
|
return .downloaded(patchFallback: patchWasTried)
|
|
2380
2472
|
case .failure(let error):
|
|
@@ -2850,11 +2942,18 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2850
2942
|
// The archive is chosen before any patch is tried, so only the
|
|
2851
2943
|
// per-file install can fall back from a patch.
|
|
2852
2944
|
var patchFallbacks = false
|
|
2945
|
+
let builtInPatchBases = prepareBuiltInPatchBases(
|
|
2946
|
+
downloads: downloads,
|
|
2947
|
+
currentBundleId: currentBundleId,
|
|
2948
|
+
tempDirectory: tempDirectory
|
|
2949
|
+
)
|
|
2853
2950
|
if shouldUseArchive(
|
|
2854
2951
|
archive: targetManifest.archive,
|
|
2855
2952
|
archiveUrl: archiveUrl,
|
|
2856
2953
|
downloads: downloads,
|
|
2857
|
-
assets: targetManifest.assets
|
|
2954
|
+
assets: targetManifest.assets,
|
|
2955
|
+
currentBundleId: currentBundleId,
|
|
2956
|
+
builtInPatchBases: builtInPatchBases
|
|
2858
2957
|
), let archive = targetManifest.archive, let archiveUrl {
|
|
2859
2958
|
do {
|
|
2860
2959
|
diffFiles = try UpdateFailureError.during(.storage, resource: .archive) {
|
|
@@ -2908,6 +3007,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2908
3007
|
let outcome = try self.downloadManifestAsset(
|
|
2909
3008
|
assetPath: assetPath, expectedAsset: expectedAsset, changedAsset: descriptor,
|
|
2910
3009
|
currentBundleId: currentBundleId, currentBundleDir: currentBundleDir,
|
|
3010
|
+
builtInBasePath: builtInPatchBases[assetPath],
|
|
2911
3011
|
destinationPath: destinationPath, tempDirectory: workerTemp, progressFile: progressFile,
|
|
2912
3012
|
progressHandler: { payload in
|
|
2913
3013
|
progressLock.lock()
|
|
@@ -3216,6 +3316,31 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
3216
3316
|
return getBundleMetadataSnapshot(bundleId: bundleId)?.manifest ?? [:]
|
|
3217
3317
|
}
|
|
3218
3318
|
|
|
3319
|
+
func resetChannelIfSelectionCurrent(
|
|
3320
|
+
catalogId: String,
|
|
3321
|
+
scopeKey: String,
|
|
3322
|
+
generation: Int64,
|
|
3323
|
+
catalogHash: String,
|
|
3324
|
+
channel: String,
|
|
3325
|
+
selectionContextHash: String
|
|
3326
|
+
) -> Result<Bool, Error> {
|
|
3327
|
+
// The catalog checks take the same recursive lock, so none accepts a
|
|
3328
|
+
// newer catalog between this check and the reset.
|
|
3329
|
+
releaseStateLock.lock()
|
|
3330
|
+
defer { releaseStateLock.unlock() }
|
|
3331
|
+
guard isReleaseSelectionCurrent(
|
|
3332
|
+
catalogId: catalogId,
|
|
3333
|
+
scopeKey: scopeKey,
|
|
3334
|
+
generation: generation,
|
|
3335
|
+
catalogHash: catalogHash,
|
|
3336
|
+
channel: channel,
|
|
3337
|
+
selectionContextHash: selectionContextHash
|
|
3338
|
+
) else {
|
|
3339
|
+
return .success(false)
|
|
3340
|
+
}
|
|
3341
|
+
return resetChannel()
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3219
3344
|
func resetChannel() -> Result<Bool, Error> {
|
|
3220
3345
|
releaseStateLock.lock()
|
|
3221
3346
|
defer { releaseStateLock.unlock() }
|
|
@@ -1213,6 +1213,15 @@ RCT_EXPORT_MODULE();
|
|
|
1213
1213
|
rejecter:reject];
|
|
1214
1214
|
}
|
|
1215
1215
|
|
|
1216
|
+
- (void)resetChannelIfSelectionCurrent:(NSDictionary *)params
|
|
1217
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1218
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
1219
|
+
[HotUpdaterNativeUpdates() resetChannelIfSelectionCurrentWithImpl:[self instanceImpl]
|
|
1220
|
+
params:params
|
|
1221
|
+
resolver:resolve
|
|
1222
|
+
rejecter:reject];
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1216
1225
|
- (facebook::react::ModuleConstants<JS::NativeHotUpdater::Constants::Builder>)constantsToExport {
|
|
1217
1226
|
return [self getConstants];
|
|
1218
1227
|
}
|
|
@@ -1394,6 +1403,15 @@ RCT_EXPORT_METHOD(resetChannel:(RCTPromiseResolveBlock)resolve
|
|
|
1394
1403
|
rejecter:reject];
|
|
1395
1404
|
}
|
|
1396
1405
|
|
|
1406
|
+
RCT_EXPORT_METHOD(resetChannelIfSelectionCurrent:(NSDictionary *)params
|
|
1407
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1408
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
1409
|
+
[HotUpdaterNativeUpdates() resetChannelIfSelectionCurrentWithImpl:[self instanceImpl]
|
|
1410
|
+
params:params
|
|
1411
|
+
resolver:resolve
|
|
1412
|
+
rejecter:reject];
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1397
1415
|
- (NSDictionary *)constantsToExport {
|
|
1398
1416
|
return [self _buildConstantsDictionary];
|
|
1399
1417
|
}
|
|
@@ -1062,6 +1062,37 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
1062
1062
|
}
|
|
1063
1063
|
}
|
|
1064
1064
|
|
|
1065
|
+
/// Resets the channel while the Release selection `params` names is
|
|
1066
|
+
/// current, as `resetChannel` does; resolves false for a stale one.
|
|
1067
|
+
@objc
|
|
1068
|
+
public func resetChannelIfSelectionCurrent(
|
|
1069
|
+
_ params: [AnyHashable: Any]?,
|
|
1070
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
1071
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
1072
|
+
) {
|
|
1073
|
+
guard let guardParams = ReleaseSelectionGuardParams(params) else {
|
|
1074
|
+
resolver(false)
|
|
1075
|
+
return
|
|
1076
|
+
}
|
|
1077
|
+
let result = launchPreparationLock.resetChannelIfSelectionCurrent(guardParams, in: bundleStorage) {
|
|
1078
|
+
do {
|
|
1079
|
+
try preferences.setItem(nil, forKey: Self.CHANNEL_STORAGE_KEY)
|
|
1080
|
+
} catch {
|
|
1081
|
+
NSLog("[HotUpdaterImpl] Failed to clear channel override: \(error)")
|
|
1082
|
+
}
|
|
1083
|
+
self.currentLaunchSelection = nil
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
switch result {
|
|
1087
|
+
case .success(let success):
|
|
1088
|
+
resolver(success)
|
|
1089
|
+
case .failure(let error):
|
|
1090
|
+
let normalizedCode = HotUpdaterImpl.normalizeErrorCode(from: error)
|
|
1091
|
+
let nsError = error as NSError
|
|
1092
|
+
reject(normalizedCode, nsError.localizedDescription, nsError)
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1065
1096
|
private func prepareLaunchIfNeeded(bundle: Bundle) -> LaunchSelection {
|
|
1066
1097
|
launchPreparationLock.withLock { prepareLaunchLocked(bundle: bundle) }
|
|
1067
1098
|
}
|
|
@@ -37,6 +37,20 @@ final class HotUpdaterNativeUpdatesAdapter: NSObject, HotUpdaterNativeUpdatesInt
|
|
|
37
37
|
HotUpdaterNativeUpdateBridge.resetChannel(impl: impl as! HotUpdaterImpl, resolver: resolve, rejecter: reject)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
func resetChannelIfSelectionCurrent(
|
|
41
|
+
withImpl impl: HotUpdaterImplInterface,
|
|
42
|
+
params: [AnyHashable: Any]?,
|
|
43
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
44
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
45
|
+
) {
|
|
46
|
+
HotUpdaterNativeUpdateBridge.resetChannelIfSelectionCurrent(
|
|
47
|
+
impl: impl as! HotUpdaterImpl,
|
|
48
|
+
params: params,
|
|
49
|
+
resolver: resolve,
|
|
50
|
+
rejecter: reject
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
func takeEvents(withImpl impl: HotUpdaterImplInterface) -> String? {
|
|
41
55
|
HotUpdaterNativeUpdateBridge.takeEvents(impl: impl as! HotUpdaterImpl)
|
|
42
56
|
}
|
|
@@ -80,6 +80,13 @@ final class HotUpdaterImplUpdateHost: NativeUpdateHost {
|
|
|
80
80
|
return (value as? Bool) ?? false
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
func resetChannelIfSelectionCurrent(_ guardParams: [String: Any]) async -> Bool {
|
|
84
|
+
let value = try? await promise { resolve, reject in
|
|
85
|
+
self.impl.resetChannelIfSelectionCurrent(guardParams, resolver: resolve, rejecter: reject)
|
|
86
|
+
}
|
|
87
|
+
return (value as? Bool) ?? false
|
|
88
|
+
}
|
|
89
|
+
|
|
83
90
|
func releaseCatalogCache(_ partition: String) -> String? {
|
|
84
91
|
impl.releaseCatalogCacheValue(partition)
|
|
85
92
|
}
|
|
@@ -216,6 +223,36 @@ public final class HotUpdaterNativeUpdateBridge: NSObject {
|
|
|
216
223
|
}
|
|
217
224
|
}
|
|
218
225
|
|
|
226
|
+
/// `resetChannelIfSelectionCurrent` for JavaScript: waits for a native
|
|
227
|
+
/// check or staging as `resetChannel` does, then resets only while the
|
|
228
|
+
/// Release selection `params` names is current.
|
|
229
|
+
@objc(resetChannelIfSelectionCurrentWithImpl:params:resolver:rejecter:)
|
|
230
|
+
public static func resetChannelIfSelectionCurrent(
|
|
231
|
+
impl: HotUpdaterImpl,
|
|
232
|
+
params: [AnyHashable: Any]?,
|
|
233
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
234
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
235
|
+
) {
|
|
236
|
+
let checker = checker(impl: impl)
|
|
237
|
+
Task {
|
|
238
|
+
await checker.whenIdle {
|
|
239
|
+
await withCheckedContinuation { (done: CheckedContinuation<Void, Never>) in
|
|
240
|
+
impl.resetChannelIfSelectionCurrent(params, resolver: { value in
|
|
241
|
+
// The reset deleted staged bundles; their queued downloads go too.
|
|
242
|
+
if (value as? Bool) == true {
|
|
243
|
+
NativeUpdateEvents.remove(name: "onBundleDownloaded", from: HotUpdaterImplUpdateHost(impl: impl))
|
|
244
|
+
}
|
|
245
|
+
resolve(value)
|
|
246
|
+
done.resume()
|
|
247
|
+
}, rejecter: { code, message, error in
|
|
248
|
+
reject(code, message, error)
|
|
249
|
+
done.resume()
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
219
256
|
/// The queued plugin events as a JSON array, cleared as they are taken.
|
|
220
257
|
@objc(takeEventsWithImpl:)
|
|
221
258
|
public static func takeEvents(impl: HotUpdaterImpl) -> String? {
|
|
@@ -30,6 +30,30 @@ final class LaunchPreparationLock {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/// Resets the channel in `storage` while the selection `guardParams` names
|
|
34
|
+
/// is current, as `resetChannel` does. Once it resets, `clearNextLaunch`
|
|
35
|
+
/// runs under the lock.
|
|
36
|
+
func resetChannelIfSelectionCurrent(
|
|
37
|
+
_ guardParams: ReleaseSelectionGuardParams,
|
|
38
|
+
in storage: BundleStorageService,
|
|
39
|
+
clearNextLaunch: () -> Void
|
|
40
|
+
) -> Result<Bool, Error> {
|
|
41
|
+
withLock {
|
|
42
|
+
let result = storage.resetChannelIfSelectionCurrent(
|
|
43
|
+
catalogId: guardParams.catalogId,
|
|
44
|
+
scopeKey: guardParams.scopeKey,
|
|
45
|
+
generation: guardParams.generation,
|
|
46
|
+
catalogHash: guardParams.catalogHash,
|
|
47
|
+
channel: guardParams.channel,
|
|
48
|
+
selectionContextHash: guardParams.selectionContextHash
|
|
49
|
+
)
|
|
50
|
+
if case .success(true) = result {
|
|
51
|
+
clearNextLaunch()
|
|
52
|
+
}
|
|
53
|
+
return result
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
/// Commits `selection` in `storage`. Committing anything but a bundle, such
|
|
34
58
|
/// as the built-in bundle, deletes every downloaded bundle, so it runs
|
|
35
59
|
/// under the lock as a channel reset does. Once it commits,
|
|
@@ -52,3 +76,31 @@ final class LaunchPreparationLock {
|
|
|
52
76
|
}
|
|
53
77
|
}
|
|
54
78
|
}
|
|
79
|
+
|
|
80
|
+
/// The Release selection a guarded change names, as JavaScript sends it.
|
|
81
|
+
struct ReleaseSelectionGuardParams {
|
|
82
|
+
let catalogId: String
|
|
83
|
+
let scopeKey: String
|
|
84
|
+
let generation: Int64
|
|
85
|
+
let catalogHash: String
|
|
86
|
+
let channel: String
|
|
87
|
+
let selectionContextHash: String
|
|
88
|
+
|
|
89
|
+
init?(_ params: [AnyHashable: Any]?) {
|
|
90
|
+
guard let data = params as? [String: Any],
|
|
91
|
+
let catalogId = data["catalogId"] as? String,
|
|
92
|
+
let scopeKey = data["scopeKey"] as? String,
|
|
93
|
+
let generation = data["generation"] as? NSNumber,
|
|
94
|
+
let catalogHash = data["catalogHash"] as? String,
|
|
95
|
+
let channel = data["channel"] as? String,
|
|
96
|
+
let selectionContextHash = data["selectionContextHash"] as? String else {
|
|
97
|
+
return nil
|
|
98
|
+
}
|
|
99
|
+
self.catalogId = catalogId
|
|
100
|
+
self.scopeKey = scopeKey
|
|
101
|
+
self.generation = generation.int64Value
|
|
102
|
+
self.catalogHash = catalogHash
|
|
103
|
+
self.channel = channel
|
|
104
|
+
self.selectionContextHash = selectionContextHash
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -379,6 +379,9 @@ protocol NativeUpdateHost: NativeUpdateEventStorage {
|
|
|
379
379
|
progress: @escaping (Double) -> Void
|
|
380
380
|
) async throws -> Any?
|
|
381
381
|
func resetChannel() async -> Bool
|
|
382
|
+
/// Resets the channel only while the Release selection `guardParams`
|
|
383
|
+
/// names is current, in one step with the catalog checks.
|
|
384
|
+
func resetChannelIfSelectionCurrent(_ guardParams: [String: Any]) async -> Bool
|
|
382
385
|
func releaseCatalogCache(_ partition: String) -> String?
|
|
383
386
|
func setReleaseCatalogCache(_ partition: String, value: String) -> Bool
|
|
384
387
|
func removeReleaseCatalogCache(_ partition: String)
|
|
@@ -955,6 +958,22 @@ final class NativeUpdateChecker {
|
|
|
955
958
|
/// The target the last staging committed, for another retained result of
|
|
956
959
|
/// the same target.
|
|
957
960
|
private var lastCommittedTargetKey: String?
|
|
961
|
+
/// The last switched-channel leave that reset the device, for another
|
|
962
|
+
/// retained result of the same catalog. Kept apart from the committed
|
|
963
|
+
/// target, which a later staging replaces.
|
|
964
|
+
private var lastLeftTargetKeyValue: String?
|
|
965
|
+
private var lastLeftTargetKey: String? {
|
|
966
|
+
get {
|
|
967
|
+
committedTargetLock.lock()
|
|
968
|
+
defer { committedTargetLock.unlock() }
|
|
969
|
+
return lastLeftTargetKeyValue
|
|
970
|
+
}
|
|
971
|
+
set {
|
|
972
|
+
committedTargetLock.lock()
|
|
973
|
+
lastLeftTargetKeyValue = newValue
|
|
974
|
+
committedTargetLock.unlock()
|
|
975
|
+
}
|
|
976
|
+
}
|
|
958
977
|
|
|
959
978
|
init(
|
|
960
979
|
host: NativeUpdateHost,
|
|
@@ -1173,6 +1192,7 @@ final class NativeUpdateChecker {
|
|
|
1173
1192
|
currentBundleId: currentBundleId,
|
|
1174
1193
|
minimumReleaseId: minimumReleaseId,
|
|
1175
1194
|
currentChannel: currentChannel,
|
|
1195
|
+
isSwitched: isSwitched,
|
|
1176
1196
|
targetChannel: targetChannel,
|
|
1177
1197
|
explicitChannel: explicitChannel,
|
|
1178
1198
|
cohort: cohort,
|
|
@@ -1208,6 +1228,7 @@ final class NativeUpdateChecker {
|
|
|
1208
1228
|
currentBundleId: String,
|
|
1209
1229
|
minimumReleaseId: String,
|
|
1210
1230
|
currentChannel: String,
|
|
1231
|
+
isSwitched: Bool,
|
|
1211
1232
|
targetChannel: String,
|
|
1212
1233
|
explicitChannel: String?,
|
|
1213
1234
|
cohort: String,
|
|
@@ -1286,6 +1307,20 @@ final class NativeUpdateChecker {
|
|
|
1286
1307
|
guard host.acceptReleaseCatalog(guardParams) else {
|
|
1287
1308
|
throw NativeUpdateCheckError.invalidResponse("Rejected a stale or inconsistent Release catalog")
|
|
1288
1309
|
}
|
|
1310
|
+
// The switched channel holds nothing for this build any more, as a
|
|
1311
|
+
// deleted channel does: the server answers its scope with an accepted
|
|
1312
|
+
// catalog without releases. A scope without a catalog proves nothing,
|
|
1313
|
+
// since a cache can still answer that for a channel deployed seconds ago.
|
|
1314
|
+
if isSwitched, targetChannel == currentChannel,
|
|
1315
|
+
catalog.releases.isEmpty, catalog.rollbackReleases.isEmpty {
|
|
1316
|
+
return leaveSwitchedChannel(
|
|
1317
|
+
options: options,
|
|
1318
|
+
currentBundleId: currentBundleId,
|
|
1319
|
+
minimumReleaseId: minimumReleaseId,
|
|
1320
|
+
channel: currentChannel,
|
|
1321
|
+
guardParams: guardParams
|
|
1322
|
+
)
|
|
1323
|
+
}
|
|
1289
1324
|
let stableInTargetScope = stable.flatMap { stable in
|
|
1290
1325
|
stable.catalogId.jsEquals(catalogId) && stable.scopeKey.jsEquals(scopeKey) ? stable : nil
|
|
1291
1326
|
}
|
|
@@ -1499,6 +1534,95 @@ final class NativeUpdateChecker {
|
|
|
1499
1534
|
)
|
|
1500
1535
|
}
|
|
1501
1536
|
|
|
1537
|
+
/// Leaves a switched channel that holds nothing for this build, such as a
|
|
1538
|
+
/// deleted one, as checkForUpdate.ts does: a rollback to the built-in
|
|
1539
|
+
/// bundle whose staging also clears the runtime channel, so the next check
|
|
1540
|
+
/// reads the build's default channel. The check itself resets nothing,
|
|
1541
|
+
/// since the reset removes the running bundle's files.
|
|
1542
|
+
private func leaveSwitchedChannel(
|
|
1543
|
+
options: NativeCheckRequest,
|
|
1544
|
+
currentBundleId: String,
|
|
1545
|
+
minimumReleaseId: String,
|
|
1546
|
+
channel: String,
|
|
1547
|
+
guardParams: [String: Any]
|
|
1548
|
+
) -> HotUpdaterUpdate {
|
|
1549
|
+
let runsBuiltIn = currentBundleId.jsEquals(minimumReleaseId)
|
|
1550
|
+
let errorScope = ErrorScope(
|
|
1551
|
+
channel: channel,
|
|
1552
|
+
bundleId: currentBundleId,
|
|
1553
|
+
updateStrategy: options.updateStrategy,
|
|
1554
|
+
targetBundleId: minimumReleaseId,
|
|
1555
|
+
targetReleaseId: nil
|
|
1556
|
+
)
|
|
1557
|
+
// With the catalog's freshness guard, as ordinary selections, so a
|
|
1558
|
+
// leave from an older catalog never joins a current one's staging.
|
|
1559
|
+
let targetKey = Self.jsonKey([
|
|
1560
|
+
"LEAVE_CHANNEL", channel, minimumReleaseId,
|
|
1561
|
+
guardParams["catalogId"] ?? NSNull(), guardParams["generation"] ?? NSNull(),
|
|
1562
|
+
guardParams["catalogHash"] ?? NSNull(), guardParams["selectionContextHash"] ?? NSNull(),
|
|
1563
|
+
])
|
|
1564
|
+
let install = { [self] (_: @escaping (Double) -> Void) async throws -> Bool in
|
|
1565
|
+
do {
|
|
1566
|
+
// This leave, from this or another result of the same catalog,
|
|
1567
|
+
// already happened: calling it again changes nothing.
|
|
1568
|
+
if lastLeftTargetKey == targetKey, host.channel() == host.defaultChannel() {
|
|
1569
|
+
return true
|
|
1570
|
+
}
|
|
1571
|
+
// A device that left or changed the channel meanwhile keeps it.
|
|
1572
|
+
guard host.channel() == channel else {
|
|
1573
|
+
throw NativeUpdateCheckError.staleReleaseCatalog
|
|
1574
|
+
}
|
|
1575
|
+
// The staging holds the gate, as `resetChannel()` waits for;
|
|
1576
|
+
// native checks the catalog again in the same step as the
|
|
1577
|
+
// reset, so a newer one accepted meanwhile keeps the channel.
|
|
1578
|
+
guard await host.resetChannelIfSelectionCurrent(guardParams) else {
|
|
1579
|
+
throw NativeUpdateCheckError.staleReleaseCatalog
|
|
1580
|
+
}
|
|
1581
|
+
lastLeftTargetKey = targetKey
|
|
1582
|
+
NativeUpdateEvents.remove(name: "onBundleDownloaded", from: host)
|
|
1583
|
+
return true
|
|
1584
|
+
} catch {
|
|
1585
|
+
reportUpdateError(error, stage: "download", resource: "artifact", scope: errorScope)
|
|
1586
|
+
throw error
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
let fromReleaseId = runningReleaseId(bundleId: currentBundleId, channel: channel)
|
|
1590
|
+
queueEvent("onUpdateCheck", [
|
|
1591
|
+
"status": "UPDATE_AVAILABLE",
|
|
1592
|
+
"channel": channel,
|
|
1593
|
+
"fromBundleId": currentBundleId,
|
|
1594
|
+
"fromReleaseId": fromReleaseId ?? NSNull(),
|
|
1595
|
+
"toBundleId": minimumReleaseId,
|
|
1596
|
+
"toReleaseId": NSNull(),
|
|
1597
|
+
"transitionKind": "USE_BUILTIN",
|
|
1598
|
+
"updateStatus": "ROLLBACK",
|
|
1599
|
+
"shouldForceUpdate": !runsBuiltIn,
|
|
1600
|
+
"updateStrategy": options.updateStrategy.name,
|
|
1601
|
+
])
|
|
1602
|
+
return HotUpdaterUpdate(
|
|
1603
|
+
id: minimumReleaseId,
|
|
1604
|
+
releaseId: nil,
|
|
1605
|
+
status: .rollback,
|
|
1606
|
+
shouldForceUpdate: !runsBuiltIn,
|
|
1607
|
+
message: nil,
|
|
1608
|
+
rolloutCohortCount: 1000,
|
|
1609
|
+
targetCohorts: [],
|
|
1610
|
+
bundleId: minimumReleaseId,
|
|
1611
|
+
transitionKind: "USE_BUILTIN",
|
|
1612
|
+
runsCurrentBundle: runsBuiltIn,
|
|
1613
|
+
targetKey: targetKey,
|
|
1614
|
+
checker: self,
|
|
1615
|
+
install: install,
|
|
1616
|
+
// Current while the device is still switched to the channel and
|
|
1617
|
+
// the catalog is current, and once the staging left it.
|
|
1618
|
+
isSelectionCurrent: { [host] in
|
|
1619
|
+
let current = host.channel()
|
|
1620
|
+
return current == host.defaultChannel()
|
|
1621
|
+
|| (current == channel && host.isReleaseSelectionCurrent(guardParams))
|
|
1622
|
+
}
|
|
1623
|
+
)
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1502
1626
|
// MARK: State
|
|
1503
1627
|
|
|
1504
1628
|
private static func jsonKey(_ fields: [Any]) -> String {
|