@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.
- package/android/build.gradle +12 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +3 -0
- package/android/src/main/cpp/CMakeLists.txt +9 -0
- package/android/src/main/cpp/HotUpdaterRecovery.cpp +143 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +325 -210
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +73 -16
- package/android/src/main/java/com/hotupdater/CohortService.kt +73 -0
- package/android/src/main/java/com/hotupdater/DecompressService.kt +28 -22
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +1 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +51 -13
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +533 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryReceiver.kt +14 -0
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +55 -0
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +19 -7
- package/android/src/newarch/HotUpdaterModule.kt +16 -25
- package/android/src/oldarch/HotUpdaterModule.kt +20 -26
- package/android/src/oldarch/HotUpdaterSpec.kt +12 -2
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +340 -232
- package/ios/HotUpdater/Internal/BundleMetadata.swift +61 -8
- package/ios/HotUpdater/Internal/CohortService.swift +63 -0
- package/ios/HotUpdater/Internal/DecompressService.swift +53 -30
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +9 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +376 -70
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.h +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.mm +4 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +321 -9
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +24 -8
- package/lib/commonjs/DefaultResolver.js +3 -5
- package/lib/commonjs/DefaultResolver.js.map +1 -1
- package/lib/commonjs/checkForUpdate.js +2 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +13 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +211 -39
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +443 -0
- package/lib/commonjs/native.spec.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/wrap.js +4 -5
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/DefaultResolver.js +3 -5
- package/lib/module/DefaultResolver.js.map +1 -1
- package/lib/module/checkForUpdate.js +3 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +14 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +204 -34
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +442 -0
- package/lib/module/native.spec.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/wrap.js +5 -6
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +14 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +43 -23
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +32 -8
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +6 -3
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +3 -6
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +14 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +43 -23
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +32 -8
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +6 -3
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +3 -6
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/DefaultResolver.ts +4 -4
- package/src/checkForUpdate.ts +4 -0
- package/src/index.ts +21 -0
- package/src/native.spec.ts +480 -0
- package/src/native.ts +285 -39
- package/src/specs/NativeHotUpdater.ts +36 -6
- package/src/types.ts +7 -3
- 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
|
-
///
|
|
9
|
-
private let
|
|
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)
|
|
13
|
-
|
|
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
|
|
35
|
-
for strategy in
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
|
79
|
+
for strategy in signatureStrategies {
|
|
82
80
|
if strategy.isValid(file: file) {
|
|
83
81
|
return true
|
|
84
82
|
}
|
|
85
83
|
}
|
|
86
|
-
NSLog("[DecompressService] No
|
|
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
|
-
|
|
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 */
|