@hot-updater/react-native 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/HotUpdater.podspec +24 -3
  2. package/Package.swift +100 -0
  3. package/android/build.gradle +3 -4
  4. package/android/hot-updater-builtin-assets.gradle +67 -0
  5. package/android/hot-updater-min-bundle-id.gradle +69 -0
  6. package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +85 -0
  7. package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +116 -11
  8. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +117 -12
  9. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +5 -52
  10. package/android/src/newarch/HotUpdaterModule.kt +46 -2
  11. package/android/src/oldarch/HotUpdaterModule.kt +46 -2
  12. package/ios/HotUpdater/Interface/HotUpdaterInterface.h +66 -0
  13. package/ios/HotUpdater/Interface/HotUpdaterInterface.m +3 -0
  14. package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +87 -0
  15. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +88 -13
  16. package/ios/HotUpdater/Internal/HotUpdater.mm +41 -90
  17. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +25 -11
  18. package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +26 -0
  19. package/ios/HotUpdater/Internal/HotUpdaterMinBundleId.m +10 -0
  20. package/ios/HotUpdater/Package.swift +88 -0
  21. package/ios/Plugins/HotUpdaterMinBundleIdPlugin/HotUpdaterMinBundleIdPlugin.swift +28 -0
  22. package/ios/scripts/generate-min-bundle-id.zsh +49 -0
  23. package/lib/commonjs/checkForUpdate.js +8 -0
  24. package/lib/commonjs/checkForUpdate.js.map +1 -1
  25. package/lib/commonjs/fetchUpdateResponse.js +5 -2
  26. package/lib/commonjs/fetchUpdateResponse.js.map +1 -1
  27. package/lib/commonjs/httpClient.js +64 -5
  28. package/lib/commonjs/httpClient.js.map +1 -1
  29. package/lib/commonjs/native.js +9 -0
  30. package/lib/commonjs/native.js.map +1 -1
  31. package/lib/commonjs/sdkVersion.js +1 -1
  32. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  33. package/lib/module/checkForUpdate.js +8 -0
  34. package/lib/module/checkForUpdate.js.map +1 -1
  35. package/lib/module/fetchUpdateResponse.js +4 -1
  36. package/lib/module/fetchUpdateResponse.js.map +1 -1
  37. package/lib/module/httpClient.js +67 -7
  38. package/lib/module/httpClient.js.map +1 -1
  39. package/lib/module/native.js +9 -0
  40. package/lib/module/native.js.map +1 -1
  41. package/lib/module/sdkVersion.js +1 -1
  42. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  43. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts +2 -0
  45. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/httpClient.d.ts +18 -2
  47. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  50. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +13 -1
  51. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  52. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  53. package/lib/typescript/module/fetchUpdateResponse.d.ts +2 -0
  54. package/lib/typescript/module/fetchUpdateResponse.d.ts.map +1 -1
  55. package/lib/typescript/module/httpClient.d.ts +18 -2
  56. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  57. package/lib/typescript/module/native.d.ts.map +1 -1
  58. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  59. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +13 -1
  60. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  61. package/package.json +5 -4
  62. package/react-native.config.js +7 -0
  63. package/src/checkForUpdate.ts +11 -0
  64. package/src/fetchUpdateResponse.ts +4 -1
  65. package/src/httpClient.ts +84 -9
  66. package/src/native.ts +7 -0
  67. package/src/sdkVersion.ts +1 -1
  68. package/src/specs/NativeHotUpdater.ts +13 -1
@@ -285,7 +285,7 @@ public protocol BundleStorageService {
285
285
  func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection
286
286
 
287
287
  // Bundle update
288
- func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL?, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void)
288
+ func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL?, assets: [String: ChangedAssetDescriptor], fileResolver: ArtifactFileResolver?, progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void)
289
289
  func stageReleaseSelection(_ selection: PersistedSelection) -> Bool
290
290
  func acceptReleaseCatalog(catalogId: String, scopeKey: String, generation: Int64, catalogHash: String, channel: String, selectionContextHash: String) -> Bool
291
291
  func getActiveUpdateState() -> [String: Any]
@@ -336,6 +336,12 @@ public extension BundleStorageService {
336
336
 
337
337
  class BundleFileStorageService: BundleStorageService {
338
338
  private static let maximumSafeByteSize: Int64 = 9_007_199_254_740_991
339
+ /// Files downloaded at once when the archive is not used.
340
+ static let parallelFileDownloads = 16
341
+ /// What one request costs beyond the first `parallelFileDownloads`, in
342
+ /// bytes: each waits a round trip for a free slot, about 100 ms, which at
343
+ /// 5 MB/s over 16 slots is the time of 32 KiB.
344
+ private static let requestOverheadByteSize: Int64 = 32 * 1024
339
345
 
340
346
  private struct ActiveBundleMetadataSnapshot {
341
347
  let activeBundleId: String
@@ -1168,7 +1174,15 @@ class BundleFileStorageService: BundleStorageService {
1168
1174
  guard !overheadUnderflowed,
1169
1175
  tarOverhead >= 0 else { return false }
1170
1176
 
1171
- if archive.downloadByteSize <= individualLowerBound {
1177
+ // Many small files cost more in requests than in bytes.
1178
+ let requestOverhead = Int64(max(0, downloads.count - Self.parallelFileDownloads))
1179
+ * Self.requestOverheadByteSize
1180
+ let (individualCost, costOverflowed) = individualLowerBound
1181
+ .addingReportingOverflow(requestOverhead)
1182
+ guard !costOverflowed,
1183
+ individualCost <= Self.maximumSafeByteSize else { return false }
1184
+
1185
+ if archive.downloadByteSize <= individualCost {
1172
1186
  return true
1173
1187
  }
1174
1188
 
@@ -1176,7 +1190,7 @@ class BundleFileStorageService: BundleStorageService {
1176
1190
  !hasOfferedPatch else {
1177
1191
  return false
1178
1192
  }
1179
- let (archiveBound, boundOverflowed) = individualLowerBound
1193
+ let (archiveBound, boundOverflowed) = individualCost
1180
1194
  .addingReportingOverflow(tarOverhead)
1181
1195
  guard !boundOverflowed,
1182
1196
  archiveBound <= Self.maximumSafeByteSize else { return false }
@@ -2015,7 +2029,7 @@ class BundleFileStorageService: BundleStorageService {
2015
2029
  * @param progressHandler Callback for download and extraction progress (0.0 to 1.0)
2016
2030
  * @param completion Callback with result of the operation
2017
2031
  */
2018
- func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL? = nil, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void) {
2032
+ func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL? = nil, assets: [String: ChangedAssetDescriptor], fileResolver: ArtifactFileResolver? = nil, progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void) {
2019
2033
  // Check if bundle is in crashed history
2020
2034
  if getCrashHistory().contains(bundleId) {
2021
2035
  NSLog("[BundleStorage] Bundle '\(bundleId)' is in crashed history, rejecting update")
@@ -2038,6 +2052,7 @@ class BundleFileStorageService: BundleStorageService {
2038
2052
  manifestFileHash: manifestFileHash,
2039
2053
  archiveUrl: archiveUrl,
2040
2054
  changedAssets: assets,
2055
+ fileResolver: fileResolver,
2041
2056
  storeDir: storeDir,
2042
2057
  progressHandler: progressHandler,
2043
2058
  completion: completion
@@ -2045,6 +2060,35 @@ class BundleFileStorageService: BundleStorageService {
2045
2060
  }
2046
2061
  }
2047
2062
 
2063
+ /// Asks `fileResolver` for the URLs of `downloads`, in requests of at
2064
+ /// most `artifactFilesRequestLimit` paths.
2065
+ private func resolveFileUrls(
2066
+ fileResolver: ArtifactFileResolver,
2067
+ downloads: inout [String: ChangedAssetDescriptor]
2068
+ ) throws {
2069
+ let paths = downloads.keys.sorted()
2070
+ for start in stride(from: 0, to: paths.count, by: artifactFilesRequestLimit) {
2071
+ let chunk = Array(paths[start..<min(start + artifactFilesRequestLimit, paths.count)])
2072
+ let files: [String: ResolvedArtifactFile]
2073
+ do {
2074
+ files = try fileResolver.resolve(paths: chunk)
2075
+ } catch {
2076
+ throw BundleStorageError.downloadFailed(error)
2077
+ }
2078
+ for path in chunk {
2079
+ guard let file = files[path], let descriptor = downloads[path] else {
2080
+ throw BundleStorageError.invalidBundle
2081
+ }
2082
+ downloads[path] = ChangedAssetDescriptor(
2083
+ fileUrl: file.url,
2084
+ fileHash: descriptor.fileHash,
2085
+ fileCompression: file.compression,
2086
+ patch: descriptor.patch
2087
+ )
2088
+ }
2089
+ }
2090
+ }
2091
+
2048
2092
  /// How the per-file install produced one file.
2049
2093
  private enum AssetInstallOutcome {
2050
2094
  case patched
@@ -2490,6 +2534,7 @@ class BundleFileStorageService: BundleStorageService {
2490
2534
  manifestFileHash: String,
2491
2535
  archiveUrl: URL?,
2492
2536
  changedAssets: [String: ChangedAssetDescriptor],
2537
+ fileResolver: ArtifactFileResolver?,
2493
2538
  storeDir: String,
2494
2539
  progressHandler: @escaping (UpdateProgressPayload) -> Void,
2495
2540
  completion: @escaping (Result<UpdateDelivery, Error>) -> Void
@@ -2576,20 +2621,43 @@ class BundleFileStorageService: BundleStorageService {
2576
2621
  else {
2577
2622
  throw BundleStorageError.invalidBundle
2578
2623
  }
2579
- // Validate the entire wire contract even when every byte can be reused.
2580
- guard Set(targetManifest.assets.keys) == Set(changedAssets.keys) else {
2581
- throw BundleStorageError.invalidBundle
2624
+ if fileResolver == nil {
2625
+ // Legacy: artifact protocol v1, where every file comes with its URL.
2626
+ // Validate the entire wire contract even when every byte can be reused.
2627
+ guard Set(targetManifest.assets.keys) == Set(changedAssets.keys) else {
2628
+ throw BundleStorageError.invalidBundle
2629
+ }
2630
+ } else {
2631
+ // In v2, `assets` holds only the Hermes patch.
2632
+ guard Set(changedAssets.keys).isSubset(of: Set(targetManifest.assets.keys)) else {
2633
+ throw BundleStorageError.invalidBundle
2634
+ }
2582
2635
  }
2583
2636
  for (path, asset) in targetManifest.assets {
2584
2637
  guard FileUtilities.normalizedRelativePath(from: path) == path,
2585
- path != "manifest.json",
2586
- let descriptor = changedAssets[path],
2587
- descriptor.fileUrl != nil,
2588
- descriptor.fileHash.caseInsensitiveCompare(asset.fileHash) == .orderedSame else {
2638
+ path != "manifest.json" else {
2589
2639
  throw BundleStorageError.invalidBundle
2590
2640
  }
2641
+ if fileResolver == nil {
2642
+ guard let descriptor = changedAssets[path],
2643
+ descriptor.fileUrl != nil,
2644
+ descriptor.fileHash.caseInsensitiveCompare(asset.fileHash) == .orderedSame else {
2645
+ throw BundleStorageError.invalidBundle
2646
+ }
2647
+ }
2591
2648
  _ = try FileUtilities.fileURL(for: path, destinationRoot: tmpDir)
2592
2649
  }
2650
+ // In v2, the manifest names every file; URLs come once the install
2651
+ // knows which files it downloads.
2652
+ let plannedAssets: [String: ChangedAssetDescriptor] = fileResolver == nil
2653
+ ? changedAssets
2654
+ : targetManifest.assets.reduce(into: [:]) { planned, entry in
2655
+ planned[entry.key] = ChangedAssetDescriptor(
2656
+ fileUrl: nil,
2657
+ fileHash: entry.value.fileHash,
2658
+ patch: changedAssets[entry.key]?.patch
2659
+ )
2660
+ }
2593
2661
 
2594
2662
  // Downloads, patches, and extraction below classify their own errors.
2595
2663
  failurePhase = .storage
@@ -2642,7 +2710,7 @@ class BundleFileStorageService: BundleStorageService {
2642
2710
  }
2643
2711
  continue
2644
2712
  }
2645
- downloads[assetPath] = changedAssets[assetPath]
2713
+ downloads[assetPath] = plannedAssets[assetPath]
2646
2714
  }
2647
2715
 
2648
2716
  var archiveInstalled = false
@@ -2677,11 +2745,18 @@ class BundleFileStorageService: BundleStorageService {
2677
2745
  }
2678
2746
 
2679
2747
  if !archiveInstalled {
2748
+ if let fileResolver {
2749
+ failurePhase = .transfer
2750
+ failureResource = .file
2751
+ try resolveFileUrls(fileResolver: fileResolver, downloads: &downloads)
2752
+ failurePhase = .storage
2753
+ failureResource = nil
2754
+ }
2680
2755
  diffFiles = createDiffProgressFiles(changedAssets: downloads)
2681
2756
  emitDiffProgress(progressHandler: progressHandler, phase: "downloading", files: diffFiles)
2682
2757
  let progressLock = NSLock()
2683
2758
  let workers = OperationQueue()
2684
- workers.maxConcurrentOperationCount = 4
2759
+ workers.maxConcurrentOperationCount = Self.parallelFileDownloads
2685
2760
  var installError: Error?
2686
2761
  var installOutcomes: [AssetInstallOutcome] = []
2687
2762
  for progressFile in diffFiles {
@@ -9,17 +9,41 @@
9
9
  #include <signal.h>
10
10
  #include <unistd.h>
11
11
 
12
+ #if defined(SWIFT_PACKAGE)
13
+ // Swift Package Manager: the Swift classes, through the protocols they conform
14
+ // to (see HotUpdaterInterface.h).
15
+ #import "HotUpdaterInterface.h"
16
+ typedef NSObject<HotUpdaterImplInterface> HotUpdaterImpl;
17
+ typedef NSObject<HotUpdaterConfigInterface> HotUpdaterConfig;
18
+ #else
12
19
  #if __has_include("HotUpdater/HotUpdater-Swift.h")
13
20
  #import "HotUpdater/HotUpdater-Swift.h"
14
21
  #else
15
22
  #import "HotUpdater-Swift.h"
16
23
  #endif
17
24
 
25
+ static HotUpdaterImpl *HotUpdaterMakeImpl(void)
26
+ {
27
+ return [[HotUpdaterImpl alloc] init];
28
+ }
29
+
30
+ static NSString *HotUpdaterAppVersion(void)
31
+ {
32
+ return [HotUpdaterImpl appVersion];
33
+ }
34
+
35
+ static HotUpdaterConfig *HotUpdaterSharedConfig(void)
36
+ {
37
+ return [HotUpdaterConfig shared];
38
+ }
39
+ #endif
40
+
18
41
  @interface HotUpdater (InternalSharedImpl)
19
42
  + (HotUpdaterImpl *)sharedImpl;
20
- + (NSString *)generateUUIDv7FromTimestamp:(uint64_t)timestampMs;
21
43
  @end
22
44
 
45
+ extern "C" NSString *HotUpdaterBuildMinBundleId(void);
46
+
23
47
  namespace {
24
48
  constexpr size_t kHotUpdaterMaxBundleIdLength = 128;
25
49
  const int kHotUpdaterSignals[] = {SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGTRAP};
@@ -207,61 +231,11 @@ extern "C" BOOL HotUpdaterPerformRecoveryReload(void)
207
231
 
208
232
  extern "C" NSString *HotUpdaterGetMinBundleId(void)
209
233
  {
210
- static NSString *uuid = nil;
211
- static dispatch_once_t onceToken;
212
- dispatch_once(&onceToken, ^{
213
234
  #if DEBUG
214
- uuid = @"00000000-0000-0000-0000-000000000000";
235
+ return @"00000000-0000-0000-0000-000000000000";
215
236
  #else
216
- // Step 1: Use the HOT_UPDATER_MIN_BUNDLE_ID build setting, which the
217
- // hot-updater CLI passes to xcodebuild and the Info.plist slot it adds
218
- // (`$(HOT_UPDATER_MIN_BUNDLE_ID)`) carries into the app, as Android
219
- // takes -PMIN_BUNDLE_ID.
220
- NSString *customValue = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"HOT_UPDATER_MIN_BUNDLE_ID"];
221
- if ([customValue isKindOfClass:[NSString class]]) {
222
- NSString *trimmed = [customValue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
223
- // An unset build setting leaves the slot empty or unexpanded; anything
224
- // but a UUID falls back to the compile time below.
225
- NSUUID *parsed = trimmed.length > 0 ? [[NSUUID alloc] initWithUUIDString:trimmed] : nil;
226
- if (parsed) {
227
- uuid = parsed.UUIDString.lowercaseString;
228
- RCTLogInfo(@"[HotUpdater.mm] Using MIN_BUNDLE_ID from the HOT_UPDATER_MIN_BUNDLE_ID build setting: %@", uuid);
229
- return;
230
- }
231
- if (trimmed.length > 0 && ![trimmed isEqualToString:@"$(HOT_UPDATER_MIN_BUNDLE_ID)"]) {
232
- RCTLogWarn(@"[HotUpdater.mm] Ignoring HOT_UPDATER_MIN_BUNDLE_ID %@: not a UUID", trimmed);
233
- }
234
- }
235
-
236
- // Step 2: Fall back to the compile time (26-hour subtraction)
237
- RCTLogInfo(@"[HotUpdater.mm] No HOT_UPDATER_MIN_BUNDLE_ID build setting, using the compile time");
238
-
239
- NSString *compileDateStr = [NSString stringWithFormat:@"%s %s", __DATE__, __TIME__];
240
- NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
241
-
242
- // Parse __DATE__ __TIME__ as UTC to ensure consistent timezone handling across all build environments
243
- [formatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
244
- [formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
245
- [formatter setDateFormat:@"MMM d yyyy HH:mm:ss"];
246
- NSDate *buildDate = [formatter dateFromString:compileDateStr];
247
- if (!buildDate) {
248
- RCTLogWarn(@"[HotUpdater.mm] Could not parse build date: %@", compileDateStr);
249
- uuid = @"00000000-0000-0000-0000-000000000000";
250
- return;
251
- }
252
-
253
- // Subtract 26 hours (93600 seconds) to ensure MIN_BUNDLE_ID is always in the past
254
- // This guarantees that uuidv7-based bundleIds (generated at runtime) will always be newer than MIN_BUNDLE_ID
255
- // Why 26 hours? Global timezone range spans from UTC-12 to UTC+14 (total 26 hours)
256
- // By subtracting 26 hours, MIN_BUNDLE_ID becomes a safe "past timestamp" regardless of build timezone
257
- // Example: Build at 15:00 in any timezone -> parse as 15:00 UTC -> subtract 26h -> 13:00 UTC (previous day)
258
- NSTimeInterval adjustedTimestamp = [buildDate timeIntervalSince1970] - 93600.0;
259
- uint64_t buildTimestampMs = (uint64_t)(adjustedTimestamp * 1000.0);
260
-
261
- uuid = [HotUpdater generateUUIDv7FromTimestamp:buildTimestampMs];
237
+ return HotUpdaterBuildMinBundleId();
262
238
  #endif
263
- });
264
- return uuid;
265
239
  }
266
240
 
267
241
  extern "C" void *HotUpdaterCopyMinBundleId(void)
@@ -405,54 +379,22 @@ RCT_EXPORT_MODULE();
405
379
  static HotUpdaterImpl *_sharedImpl = nil;
406
380
  static dispatch_once_t onceToken;
407
381
  dispatch_once(&onceToken, ^{
408
- _sharedImpl = [[HotUpdaterImpl alloc] init];
382
+ _sharedImpl = HotUpdaterMakeImpl();
409
383
  });
410
384
  return _sharedImpl;
411
385
  }
412
386
 
413
387
  #pragma mark - React Native Constants (Keep getMinBundleId, delegate others)
414
388
 
415
- // Returns the minimum bundle ID string, either from Info.plist or generated from build timestamp
389
+ // Returns the bundle ID of the bundle this build ships (MIN_BUNDLE_ID)
416
390
  - (NSString *)getMinBundleId {
417
391
  return HotUpdaterGetMinBundleId();
418
392
  }
419
393
 
420
- // Helper method: Generate UUID v7 from timestamp (milliseconds)
421
- + (NSString *)generateUUIDv7FromTimestamp:(uint64_t)timestampMs {
422
- unsigned char bytes[16];
423
-
424
- // UUID v7 format: timestamp_ms (48 bits) + ver (4 bits) + random (12 bits) + variant (2 bits) + random (62 bits)
425
- bytes[0] = (timestampMs >> 40) & 0xFF;
426
- bytes[1] = (timestampMs >> 32) & 0xFF;
427
- bytes[2] = (timestampMs >> 24) & 0xFF;
428
- bytes[3] = (timestampMs >> 16) & 0xFF;
429
- bytes[4] = (timestampMs >> 8) & 0xFF;
430
- bytes[5] = timestampMs & 0xFF;
431
-
432
- // Version 7
433
- bytes[6] = 0x70;
434
- bytes[7] = 0x00;
435
-
436
- // Variant bits (10xxxxxx)
437
- bytes[8] = 0x80;
438
- bytes[9] = 0x00;
439
-
440
- // Remaining bytes (zeros for deterministic MIN_BUNDLE_ID)
441
- for (int i = 10; i < 16; i++) {
442
- bytes[i] = 0x00;
443
- }
444
-
445
- return [NSString stringWithFormat:
446
- @"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
447
- bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
448
- bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]];
449
- }
450
-
451
-
452
394
  - (NSDictionary *)_buildConstantsDictionary {
453
395
  return @{
454
396
  @"MIN_BUNDLE_ID": [self getMinBundleId] ?: [NSNull null],
455
- @"APP_VERSION": [HotUpdaterImpl appVersion] ?: [NSNull null],
397
+ @"APP_VERSION": HotUpdaterAppVersion() ?: [NSNull null],
456
398
  @"CHANNEL": [[HotUpdater sharedImpl] getChannel] ?: [NSNull null],
457
399
  @"DEFAULT_CHANNEL": [[HotUpdater sharedImpl] getDefaultChannel] ?: [NSNull null],
458
400
  @"FINGERPRINT_HASH": [[HotUpdater sharedImpl] getFingerprintHash] ?: [NSNull null]
@@ -484,7 +426,7 @@ RCT_EXPORT_MODULE();
484
426
  publicKey:(NSString *)publicKey
485
427
  channel:(NSString *)channel
486
428
  isolationKey:(NSString *)isolationKey {
487
- HotUpdaterConfig *config = [HotUpdaterConfig shared];
429
+ HotUpdaterConfig *config = HotUpdaterSharedConfig();
488
430
  [config configureWithFingerprintHash:fingerprintHash publicKey:publicKey channel:channel isolationKey:isolationKey];
489
431
  }
490
432
 
@@ -590,6 +532,15 @@ RCT_EXPORT_MODULE();
590
532
  if (params.selection()) {
591
533
  paramDict[@"selection"] = params.selection();
592
534
  }
535
+ if (params.filesUrl()) {
536
+ paramDict[@"filesUrl"] = params.filesUrl();
537
+ }
538
+ if (params.requestHeaders()) {
539
+ paramDict[@"requestHeaders"] = params.requestHeaders();
540
+ }
541
+ if (params.requestTimeout().has_value()) {
542
+ paramDict[@"requestTimeout"] = @(params.requestTimeout().value());
543
+ }
593
544
 
594
545
  HotUpdaterImpl *impl = [HotUpdater sharedImpl];
595
546
  [impl updateBundle:paramDict resolver:resolve rejecter:reject];
@@ -710,7 +661,7 @@ RCT_EXPORT_MODULE();
710
661
  HotUpdaterImpl *impl = [HotUpdater sharedImpl];
711
662
  return facebook::react::typedConstants<JS::NativeHotUpdater::Constants::Builder>({
712
663
  .MIN_BUNDLE_ID = [self getMinBundleId],
713
- .APP_VERSION = [HotUpdaterImpl appVersion],
664
+ .APP_VERSION = HotUpdaterAppVersion(),
714
665
  .CHANNEL = [impl getChannel],
715
666
  .DEFAULT_CHANNEL = [impl getDefaultChannel],
716
667
  .FINGERPRINT_HASH = [impl getFingerprintHash],
@@ -260,7 +260,7 @@ private func hotUpdaterGetMinBundleId() -> String {
260
260
  * @param reject Promise reject callback; download and install failures
261
261
  * carry `stage`, `reason`, and their details in the error's userInfo
262
262
  */
263
- public func updateBundle(_ params: NSDictionary?,
263
+ public func updateBundle(_ params: [AnyHashable: Any]?,
264
264
  resolver resolve: @escaping RCTPromiseResolveBlock,
265
265
  rejecter reject: @escaping RCTPromiseRejectBlock) {
266
266
 
@@ -316,11 +316,8 @@ private func hotUpdaterGetMinBundleId() -> String {
316
316
  return
317
317
  }
318
318
  let assets = assetsPayload.reduce(into: [String: ChangedAssetDescriptor]()) { partialResult, entry in
319
- guard let fileHash = entry.value["fileHash"] as? String,
320
- !fileHash.isEmpty
321
- else {
322
- return
323
- }
319
+ // In artifact protocol v2, an entry holds only the Hermes patch.
320
+ let fileHash = entry.value["fileHash"] as? String ?? ""
324
321
 
325
322
  let patchPayload = entry.value["patch"] as? [String: Any]
326
323
  let patch = patchPayload.flatMap { payload -> BsdiffPatchDescriptor? in
@@ -346,7 +343,8 @@ private func hotUpdaterGetMinBundleId() -> String {
346
343
  let filePayload = entry.value["file"] as? [String: Any]
347
344
  let fileUrl = (filePayload?["url"] as? String).flatMap { URL(string: $0) }
348
345
  let fileCompression = filePayload?["compression"] as? String
349
- guard fileUrl != nil || patch != nil else {
346
+ guard fileUrl != nil || patch != nil,
347
+ !fileHash.isEmpty || fileUrl == nil else {
350
348
  return
351
349
  }
352
350
 
@@ -357,6 +355,22 @@ private func hotUpdaterGetMinBundleId() -> String {
357
355
  patch: patch
358
356
  )
359
357
  }
358
+ var fileResolver: ArtifactFileResolver?
359
+ if let filesUrlString = data["filesUrl"] as? String {
360
+ guard let filesUrl = URL(string: filesUrlString) else {
361
+ let error = UpdateFailure.invalidResponse.with(resource: .file).error(
362
+ description: "Invalid 'filesUrl' provided: \(filesUrlString)"
363
+ )
364
+ reject("INVALID_FILE_URL", error.localizedDescription, error)
365
+ return
366
+ }
367
+ // The update check's headers, which the artifact files request repeats.
368
+ let headers = (data["requestHeaders"] as? [String: Any] ?? [:])
369
+ .compactMapValues { $0 as? String }
370
+ let timeout = (data["requestTimeout"] as? NSNumber).map { $0.doubleValue / 1000 }
371
+ .flatMap { $0 > 0 ? $0 : nil } ?? HTTPArtifactFileResolver.defaultTimeout
372
+ fileResolver = HTTPArtifactFileResolver(filesUrl: filesUrl, headers: headers, timeout: timeout)
373
+ }
360
374
  let selection = (data["selection"] as? [String: Any]).flatMap(Self.parseSelection)
361
375
  if let selection, !bundleStorage.stageReleaseSelection(selection) {
362
376
  // Not an update failure, so the rejection carries no stage or reason.
@@ -371,7 +385,7 @@ private func hotUpdaterGetMinBundleId() -> String {
371
385
  NSLog("[HotUpdaterImpl] updateBundle called with bundleId: \(bundleId), manifestUrl: \(manifestUrl.absoluteString)")
372
386
 
373
387
  // Heavy work is delegated to bundle storage service with safe error handling
374
- bundleStorage.updateBundle(bundleId: bundleId, manifestUrl: manifestUrl, manifestFileHash: manifestFileHash, archiveUrl: archiveUrl, assets: assets, progressHandler: { payload in
388
+ bundleStorage.updateBundle(bundleId: bundleId, manifestUrl: manifestUrl, manifestFileHash: manifestFileHash, archiveUrl: archiveUrl, assets: assets, fileResolver: fileResolver, progressHandler: { payload in
375
389
  DispatchQueue.main.async {
376
390
  NotificationCenter.default.post(
377
391
  name: .updateProgressDidChange,
@@ -448,7 +462,7 @@ private func hotUpdaterGetMinBundleId() -> String {
448
462
  )
449
463
  }
450
464
 
451
- public func acceptReleaseCatalog(_ params: NSDictionary?) -> Bool {
465
+ public func acceptReleaseCatalog(_ params: [AnyHashable: Any]?) -> Bool {
452
466
  guard let data = params as? [String: Any],
453
467
  let catalogId = data["catalogId"] as? String,
454
468
  let scopeKey = data["scopeKey"] as? String,
@@ -507,7 +521,7 @@ private func hotUpdaterGetMinBundleId() -> String {
507
521
  }
508
522
  }
509
523
 
510
- public func isReleaseSelectionCurrent(_ params: NSDictionary?) -> Bool {
524
+ public func isReleaseSelectionCurrent(_ params: [AnyHashable: Any]?) -> Bool {
511
525
  guard let data = params as? [String: Any],
512
526
  let catalogId = data["catalogId"] as? String,
513
527
  let scopeKey = data["scopeKey"] as? String,
@@ -528,7 +542,7 @@ private func hotUpdaterGetMinBundleId() -> String {
528
542
  }
529
543
 
530
544
  public func commitReleaseSelection(
531
- _ params: NSDictionary?,
545
+ _ params: [AnyHashable: Any]?,
532
546
  resolver resolve: @escaping RCTPromiseResolveBlock,
533
547
  rejecter reject: @escaping RCTPromiseRejectBlock
534
548
  ) {
@@ -0,0 +1,26 @@
1
+ #if SWIFT_PACKAGE
2
+ import Foundation
3
+ import HotUpdaterInterface
4
+
5
+ // Swift Package Manager only: the Objective-C++ module reaches these classes
6
+ // through the protocols in HotUpdaterInterface.h, and these conformances make
7
+ // the compiler check that the classes still match them.
8
+ extension HotUpdaterImpl: HotUpdaterImplInterface {}
9
+
10
+ extension HotUpdaterConfig: HotUpdaterConfigInterface {}
11
+
12
+ @_cdecl("HotUpdaterMakeImpl")
13
+ public func hotUpdaterMakeImpl() -> HotUpdaterImplInterface {
14
+ HotUpdaterImpl()
15
+ }
16
+
17
+ @_cdecl("HotUpdaterAppVersion")
18
+ public func hotUpdaterAppVersion() -> NSString? {
19
+ HotUpdaterImpl.appVersion as NSString?
20
+ }
21
+
22
+ @_cdecl("HotUpdaterSharedConfig")
23
+ public func hotUpdaterSharedConfig() -> HotUpdaterConfigInterface {
24
+ HotUpdaterConfig.shared
25
+ }
26
+ #endif
@@ -0,0 +1,10 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ // Generated before every compile by ios/scripts/generate-min-bundle-id.zsh,
4
+ // so this file is the only one rebuilt when the value changes.
5
+ #import "HotUpdaterBuildMinBundleId.h"
6
+
7
+ NSString *HotUpdaterBuildMinBundleId(void)
8
+ {
9
+ return @HOT_UPDATER_BUILD_MIN_BUNDLE_ID;
10
+ }
@@ -0,0 +1,88 @@
1
+ // swift-tools-version: 5.10
2
+ import PackageDescription
3
+
4
+ let coreSources = [
5
+ "ArtifactFileResolver.swift",
6
+ "FileUtilities.swift",
7
+ "BrotliFileDecompressor.swift",
8
+ "TarArchiveExtractor.swift",
9
+ "BundleFileStorageService.swift",
10
+ "BuiltInAssetResolver.swift",
11
+ "BundleMetadata.swift",
12
+ "FileManagerService.swift",
13
+ "HashUtils.swift",
14
+ "HotUpdaterConfig.swift",
15
+ "KeyValueStorageService.swift",
16
+ "NotificationExtension.swift",
17
+ "ReleaseCatalogCacheService.swift",
18
+ "SignatureVerifier.swift",
19
+ "UpdateFailure.swift",
20
+ "URLSessionDownloadService.swift",
21
+ "VersionedPreferencesService.swift",
22
+ ]
23
+
24
+ let coreExcludedFiles = [
25
+ "BsdiffPatchBridge.h",
26
+ "BsdiffPatchBridge.mm",
27
+ "CohortService.swift",
28
+ "HotUpdater-Bridging-Header.h",
29
+ "HotUpdater.mm",
30
+ "HotUpdaterImpl.swift",
31
+ "HotUpdaterInterface.swift",
32
+ "HotUpdaterMinBundleId.m",
33
+ ]
34
+
35
+ let bsdiffPatchBridgeExcludedFiles =
36
+ coreSources + coreExcludedFiles.filter {
37
+ $0 != "BsdiffPatchBridge.h" && $0 != "BsdiffPatchBridge.mm"
38
+ }
39
+
40
+ let package = Package(
41
+ name: "HotUpdater",
42
+ platforms: [
43
+ .iOS(.v13),
44
+ .macOS(.v10_15)
45
+ ],
46
+ products: [
47
+ .library(
48
+ name: "HotUpdaterCore",
49
+ targets: ["HotUpdaterCore"]
50
+ )
51
+ ],
52
+ dependencies: [],
53
+ targets: [
54
+ .target(
55
+ name: "HotUpdaterRecovery",
56
+ path: "Recovery",
57
+ exclude: ["HotUpdaterRecoveryHooks.mm"],
58
+ sources: ["HotUpdaterRecovery.mm"],
59
+ publicHeadersPath: ".",
60
+ cxxSettings: [.define("RCT_NEW_ARCH_ENABLED")]
61
+ ),
62
+ .target(
63
+ name: "HotUpdaterBsdiffPatch",
64
+ path: "Internal",
65
+ exclude: bsdiffPatchBridgeExcludedFiles,
66
+ sources: ["BsdiffPatchBridge.mm"],
67
+ publicHeadersPath: ".",
68
+ linkerSettings: [
69
+ .linkedFramework("Foundation"),
70
+ .linkedLibrary("bz2"),
71
+ ]
72
+ ),
73
+ // React Native's full native module cannot be built through SPM yet,
74
+ // but the pure-Swift update core can be.
75
+ .target(
76
+ name: "HotUpdaterCore",
77
+ dependencies: ["HotUpdaterBsdiffPatch"],
78
+ path: "Internal",
79
+ exclude: coreExcludedFiles,
80
+ sources: coreSources
81
+ ),
82
+ .testTarget(
83
+ name: "HotUpdaterTest",
84
+ dependencies: ["HotUpdaterCore", "HotUpdaterRecovery"],
85
+ path: "Test"
86
+ ),
87
+ ]
88
+ )
@@ -0,0 +1,28 @@
1
+ import Foundation
2
+ import PackagePlugin
3
+
4
+ // Runs ios/scripts/generate-min-bundle-id.zsh before every build of the target,
5
+ // the SwiftPM counterpart of the podspec's script phase. The script writes a
6
+ // Swift file defining HotUpdaterBuildMinBundleId(), which HotUpdater.mm calls.
7
+ @main
8
+ struct HotUpdaterMinBundleIdPlugin: BuildToolPlugin {
9
+ func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
10
+ let script = context.package.directoryURL
11
+ .appending(path: "ios/scripts/generate-min-bundle-id.zsh")
12
+ let outputDirectory = context.pluginWorkDirectoryURL
13
+ .appending(path: "MinBundleId")
14
+ var environment: [String: String] = [:]
15
+ if let minBundleId = ProcessInfo.processInfo.environment["HOT_UPDATER_MIN_BUNDLE_ID"] {
16
+ environment["HOT_UPDATER_MIN_BUNDLE_ID"] = minBundleId
17
+ }
18
+ return [
19
+ .prebuildCommand(
20
+ displayName: "[HotUpdater] Generate MIN_BUNDLE_ID",
21
+ executable: URL(filePath: "/bin/zsh"),
22
+ arguments: [script.path(percentEncoded: false), "--swift", outputDirectory.path(percentEncoded: false)],
23
+ environment: environment,
24
+ outputFilesDirectory: outputDirectory
25
+ ),
26
+ ]
27
+ }
28
+ }