@hot-updater/react-native 0.27.1 → 0.29.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.
Files changed (87) hide show
  1. package/android/build.gradle +12 -0
  2. package/android/src/main/AndroidManifest.xml +3 -0
  3. package/android/src/main/AndroidManifestNew.xml +3 -0
  4. package/android/src/main/cpp/CMakeLists.txt +9 -0
  5. package/android/src/main/cpp/HotUpdaterRecovery.cpp +143 -0
  6. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +325 -210
  7. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +73 -16
  8. package/android/src/main/java/com/hotupdater/CohortService.kt +73 -0
  9. package/android/src/main/java/com/hotupdater/DecompressService.kt +28 -22
  10. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +1 -1
  11. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +51 -13
  12. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +533 -0
  13. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryReceiver.kt +14 -0
  14. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +55 -0
  15. package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +19 -7
  16. package/android/src/newarch/HotUpdaterModule.kt +16 -25
  17. package/android/src/oldarch/HotUpdaterModule.kt +20 -26
  18. package/android/src/oldarch/HotUpdaterSpec.kt +12 -2
  19. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +340 -232
  20. package/ios/HotUpdater/Internal/BundleMetadata.swift +61 -8
  21. package/ios/HotUpdater/Internal/CohortService.swift +63 -0
  22. package/ios/HotUpdater/Internal/DecompressService.swift +53 -30
  23. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +9 -1
  24. package/ios/HotUpdater/Internal/HotUpdater.mm +376 -70
  25. package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.h +7 -0
  26. package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.mm +4 -0
  27. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +321 -9
  28. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +24 -8
  29. package/lib/commonjs/DefaultResolver.js +3 -5
  30. package/lib/commonjs/DefaultResolver.js.map +1 -1
  31. package/lib/commonjs/checkForUpdate.js +2 -0
  32. package/lib/commonjs/checkForUpdate.js.map +1 -1
  33. package/lib/commonjs/index.js +13 -0
  34. package/lib/commonjs/index.js.map +1 -1
  35. package/lib/commonjs/native.js +211 -39
  36. package/lib/commonjs/native.js.map +1 -1
  37. package/lib/commonjs/native.spec.js +443 -0
  38. package/lib/commonjs/native.spec.js.map +1 -0
  39. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  40. package/lib/commonjs/types.js.map +1 -1
  41. package/lib/commonjs/wrap.js +4 -5
  42. package/lib/commonjs/wrap.js.map +1 -1
  43. package/lib/module/DefaultResolver.js +3 -5
  44. package/lib/module/DefaultResolver.js.map +1 -1
  45. package/lib/module/checkForUpdate.js +3 -1
  46. package/lib/module/checkForUpdate.js.map +1 -1
  47. package/lib/module/index.js +14 -1
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/native.js +204 -34
  50. package/lib/module/native.js.map +1 -1
  51. package/lib/module/native.spec.js +442 -0
  52. package/lib/module/native.spec.js.map +1 -0
  53. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  54. package/lib/module/types.js.map +1 -1
  55. package/lib/module/wrap.js +5 -6
  56. package/lib/module/wrap.js.map +1 -1
  57. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/index.d.ts +14 -1
  59. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/native.d.ts +43 -23
  61. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +32 -8
  63. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/types.d.ts +6 -3
  65. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/wrap.d.ts +3 -6
  67. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  68. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  69. package/lib/typescript/module/index.d.ts +14 -1
  70. package/lib/typescript/module/index.d.ts.map +1 -1
  71. package/lib/typescript/module/native.d.ts +43 -23
  72. package/lib/typescript/module/native.d.ts.map +1 -1
  73. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +32 -8
  74. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  75. package/lib/typescript/module/types.d.ts +6 -3
  76. package/lib/typescript/module/types.d.ts.map +1 -1
  77. package/lib/typescript/module/wrap.d.ts +3 -6
  78. package/lib/typescript/module/wrap.d.ts.map +1 -1
  79. package/package.json +6 -6
  80. package/src/DefaultResolver.ts +4 -4
  81. package/src/checkForUpdate.ts +4 -0
  82. package/src/index.ts +21 -0
  83. package/src/native.spec.ts +480 -0
  84. package/src/native.ts +285 -39
  85. package/src/specs/NativeHotUpdater.ts +36 -6
  86. package/src/types.ts +7 -3
  87. package/src/wrap.tsx +8 -12
@@ -12,8 +12,6 @@ public struct BundleMetadata: Codable {
12
12
  var stableBundleId: String?
13
13
  var stagingBundleId: String?
14
14
  var verificationPending: Bool
15
- var verificationAttemptedAt: Double?
16
- var stagingExecutionCount: Int?
17
15
  var updatedAt: Double
18
16
 
19
17
  enum CodingKeys: String, CodingKey {
@@ -22,8 +20,6 @@ public struct BundleMetadata: Codable {
22
20
  case stableBundleId = "stable_bundle_id"
23
21
  case stagingBundleId = "staging_bundle_id"
24
22
  case verificationPending = "verification_pending"
25
- case verificationAttemptedAt = "verification_attempted_at"
26
- case stagingExecutionCount = "staging_execution_count"
27
23
  case updatedAt = "updated_at"
28
24
  }
29
25
 
@@ -33,8 +29,6 @@ public struct BundleMetadata: Codable {
33
29
  stableBundleId: String? = nil,
34
30
  stagingBundleId: String? = nil,
35
31
  verificationPending: Bool = false,
36
- verificationAttemptedAt: Double? = nil,
37
- stagingExecutionCount: Int? = nil,
38
32
  updatedAt: Double = Date().timeIntervalSince1970 * 1000
39
33
  ) {
40
34
  self.schema = schema
@@ -42,8 +36,6 @@ public struct BundleMetadata: Codable {
42
36
  self.stableBundleId = stableBundleId
43
37
  self.stagingBundleId = stagingBundleId
44
38
  self.verificationPending = verificationPending
45
- self.verificationAttemptedAt = verificationAttemptedAt
46
- self.stagingExecutionCount = stagingExecutionCount
47
39
  self.updatedAt = updatedAt
48
40
  }
49
41
 
@@ -191,3 +183,64 @@ public struct CrashedHistory: Codable {
191
183
  bundles.removeAll()
192
184
  }
193
185
  }
186
+
187
+ public struct PendingCrashRecovery {
188
+ let launchedBundleId: String?
189
+ let shouldRollback: Bool
190
+
191
+ static func from(json: [String: Any]) -> PendingCrashRecovery {
192
+ PendingCrashRecovery(
193
+ launchedBundleId: (json["bundleId"] as? String).flatMap { $0.isEmpty ? nil : $0 },
194
+ shouldRollback: json["shouldRollback"] as? Bool ?? false
195
+ )
196
+ }
197
+ }
198
+
199
+ public struct LaunchSelection {
200
+ let bundleURL: URL?
201
+ let launchedBundleId: String?
202
+ let shouldRollbackOnCrash: Bool
203
+ }
204
+
205
+ public struct LaunchReport: Codable {
206
+ static let launchReportFilename = "launch-report.json"
207
+
208
+ let status: String
209
+ let crashedBundleId: String?
210
+
211
+ init(status: String = "STABLE", crashedBundleId: String? = nil) {
212
+ self.status = status
213
+ self.crashedBundleId = crashedBundleId
214
+ }
215
+
216
+ static func load(from file: URL) -> LaunchReport? {
217
+ guard FileManager.default.fileExists(atPath: file.path) else {
218
+ return nil
219
+ }
220
+
221
+ do {
222
+ let data = try Data(contentsOf: file)
223
+ return try JSONDecoder().decode(LaunchReport.self, from: data)
224
+ } catch {
225
+ print("[LaunchReport] Failed to load launch report: \(error)")
226
+ return nil
227
+ }
228
+ }
229
+
230
+ func save(to file: URL) -> Bool {
231
+ do {
232
+ let encoder = JSONEncoder()
233
+ encoder.outputFormatting = .prettyPrinted
234
+ let data = try encoder.encode(self)
235
+ let directory = file.deletingLastPathComponent()
236
+ if !FileManager.default.fileExists(atPath: directory.path) {
237
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
238
+ }
239
+ try data.write(to: file)
240
+ return true
241
+ } catch {
242
+ print("[LaunchReport] Failed to save launch report: \(error)")
243
+ return false
244
+ }
245
+ }
246
+ }
@@ -0,0 +1,63 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ final class CohortService {
5
+ private let userDefaults: UserDefaults
6
+
7
+ // Keep the legacy key so existing custom cohorts continue to work.
8
+ private let cohortKey = "HotUpdater_CustomCohort"
9
+ private let fallbackIdentifierKey = "HotUpdater_FallbackCohortIdentifier"
10
+
11
+ init(userDefaults: UserDefaults = .standard) {
12
+ self.userDefaults = userDefaults
13
+ }
14
+
15
+ private func hashString(_ value: String) -> Int32 {
16
+ var hash: Int32 = 0
17
+
18
+ for scalar in value.unicodeScalars {
19
+ hash = (hash &* 31) &+ Int32(scalar.value)
20
+ }
21
+
22
+ return hash
23
+ }
24
+
25
+ private func defaultNumericCohort(for identifier: String) -> String {
26
+ let hash = Int64(hashString(identifier))
27
+ let normalized = Int((hash % 1000 + 1000) % 1000) + 1
28
+ return String(normalized)
29
+ }
30
+
31
+ private func fallbackIdentifier() -> String {
32
+ if let fallbackId = userDefaults.string(forKey: fallbackIdentifierKey), !fallbackId.isEmpty {
33
+ return fallbackId
34
+ }
35
+
36
+ let fallbackId = UUID().uuidString
37
+ userDefaults.set(fallbackId, forKey: fallbackIdentifierKey)
38
+ return fallbackId
39
+ }
40
+
41
+ func setCohort(_ cohort: String) {
42
+ if cohort.isEmpty {
43
+ return
44
+ }
45
+ userDefaults.set(cohort, forKey: cohortKey)
46
+ }
47
+
48
+ func getCohort() -> String {
49
+ if let cohort = userDefaults.string(forKey: cohortKey), !cohort.isEmpty {
50
+ return cohort
51
+ }
52
+
53
+ let initialCohort: String
54
+ if let idfv = UIDevice.current.identifierForVendor?.uuidString, !idfv.isEmpty {
55
+ initialCohort = defaultNumericCohort(for: idfv)
56
+ } else {
57
+ initialCohort = defaultNumericCohort(for: fallbackIdentifier())
58
+ }
59
+
60
+ userDefaults.set(initialCohort, forKey: cohortKey)
61
+ return initialCohort
62
+ }
63
+ }
@@ -5,16 +5,19 @@ import Foundation
5
5
  * Automatically detects format by trying each strategy's validation and delegates to appropriate decompression strategy.
6
6
  */
7
7
  class DecompressService {
8
- /// Array of available strategies in order of detection priority
9
- private let strategies: [DecompressionStrategy]
8
+ /// Strategies with reliable file signatures that can be validated cheaply.
9
+ private let signatureStrategies: [DecompressionStrategy]
10
+ /// TAR.BR has no reliable magic bytes, so it is attempted as the final fallback.
11
+ private let tarBrStrategy: DecompressionStrategy
10
12
 
11
13
  init() {
12
- // Order matters: Try ZIP first (clear magic bytes), then TAR.GZ (GZIP magic bytes), then TAR.BR (fallback)
13
- self.strategies = [
14
+ // Order matters: Try ZIP first (clear magic bytes), then TAR.GZ (GZIP magic bytes).
15
+ // TAR.BR is attempted only after signature-based formats are ruled out.
16
+ self.signatureStrategies = [
14
17
  ZipDecompressionStrategy(),
15
- TarGzDecompressionStrategy(),
16
- TarBrDecompressionStrategy()
18
+ TarGzDecompressionStrategy()
17
19
  ]
20
+ self.tarBrStrategy = TarBrDecompressionStrategy()
18
21
  }
19
22
 
20
23
  /**
@@ -31,8 +34,8 @@ class DecompressService {
31
34
  let fileName = fileURL.lastPathComponent
32
35
  let fileSize = (try? FileManager.default.attributesOfItem(atPath: file)[.size] as? UInt64) ?? 0
33
36
 
34
- // Try each strategy's validation
35
- for strategy in strategies {
37
+ // Try each signature-based strategy first.
38
+ for strategy in signatureStrategies {
36
39
  if strategy.isValid(file: file) {
37
40
  NSLog("[DecompressService] Using strategy for \(fileName)")
38
41
  try strategy.decompress(file: file, to: destination, progressHandler: progressHandler)
@@ -40,26 +43,21 @@ class DecompressService {
40
43
  }
41
44
  }
42
45
 
43
- // No valid strategy found - provide detailed error message
44
- let errorMessage = """
45
- Failed to decompress file: \(fileName) (\(fileSize) bytes)
46
-
47
- Tried strategies: ZIP (magic bytes 0x504B0304), TAR.GZ (magic bytes 0x1F8B), TAR.BR (file extension)
48
-
49
- Supported formats:
50
- - ZIP archives (.zip)
51
- - GZIP compressed TAR archives (.tar.gz)
52
- - Brotli compressed TAR archives (.tar.br)
46
+ NSLog("[DecompressService] No ZIP/TAR.GZ signature matched for \(fileName), trying TAR.BR fallback")
53
47
 
54
- Please verify the file is not corrupted and matches one of the supported formats.
55
- """
56
-
57
- NSLog("[DecompressService] \(errorMessage)")
58
- throw NSError(
59
- domain: "DecompressService",
60
- code: 1,
61
- userInfo: [NSLocalizedDescriptionKey: errorMessage]
62
- )
48
+ do {
49
+ try tarBrStrategy.decompress(file: file, to: destination, progressHandler: progressHandler)
50
+ NSLog("[DecompressService] Using TAR.BR fallback for \(fileName)")
51
+ return
52
+ } catch {
53
+ let invalidArchiveError = createInvalidArchiveError(
54
+ fileName: fileName,
55
+ fileSize: fileSize,
56
+ underlyingError: error
57
+ )
58
+ NSLog("[DecompressService] \(invalidArchiveError.localizedDescription)")
59
+ throw invalidArchiveError
60
+ }
63
61
  }
64
62
 
65
63
  /**
@@ -73,17 +71,42 @@ Please verify the file is not corrupted and matches one of the supported formats
73
71
  }
74
72
 
75
73
  /**
76
- * Validates if a file is a valid compressed archive.
74
+ * Validates if a file matches one of the signature-based archive formats.
77
75
  * @param file Path to the file to validate
78
76
  * @return true if the file is valid for any strategy
79
77
  */
80
78
  func isValid(file: String) -> Bool {
81
- for strategy in strategies {
79
+ for strategy in signatureStrategies {
82
80
  if strategy.isValid(file: file) {
83
81
  return true
84
82
  }
85
83
  }
86
- NSLog("[DecompressService] No valid strategy found for file: \(file)")
84
+ NSLog("[DecompressService] No ZIP/TAR.GZ signature matched for file: \(file). TAR.BR is handled during extraction fallback.")
87
85
  return false
88
86
  }
87
+
88
+ private func createInvalidArchiveError(fileName: String, fileSize: UInt64, underlyingError: Error? = nil) -> NSError {
89
+ let errorMessage = """
90
+ The downloaded bundle file is not a valid compressed archive: \(fileName) (\(fileSize) bytes)
91
+
92
+ Supported formats:
93
+ - ZIP archives (.zip)
94
+ - GZIP compressed TAR archives (.tar.gz)
95
+ - Brotli compressed TAR archives (.tar.br)
96
+ """
97
+
98
+ var userInfo: [String: Any] = [
99
+ NSLocalizedDescriptionKey: errorMessage
100
+ ]
101
+
102
+ if let underlyingError {
103
+ userInfo[NSUnderlyingErrorKey] = underlyingError
104
+ }
105
+
106
+ return NSError(
107
+ domain: "DecompressService",
108
+ code: 1,
109
+ userInfo: userInfo
110
+ )
111
+ }
89
112
  }
@@ -3,6 +3,14 @@
3
3
 
4
4
  #import "React/RCTBridgeModule.h"
5
5
  #import "React/RCTEventEmitter.h"
6
+ #import "React/RCTAssert.h"
7
+ #import "React/RCTConstants.h"
8
+ #import "React/RCTRootView.h"
6
9
  #import "React/RCTUtils.h" // Needed for RCTPromiseResolveBlock/RejectBlock in Swift
7
10
  #import <SSZipArchive/SSZipArchive.h>
8
- #endif /* HotUpdater_Bridging_Header_h */
11
+
12
+ @interface HotUpdaterRecoverySignalBridge : NSObject
13
+ + (void)installSignalHandlers:(NSString *)crashMarkerPath;
14
+ + (void)updateLaunchState:(NSString * _Nullable)bundleId shouldRollback:(BOOL)shouldRollback;
15
+ @end
16
+ #endif /* HotUpdater_Bridging_Header_h */