@hot-updater/react-native 0.36.12 → 0.36.14
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 +19 -0
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +3 -0
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +65 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +18 -1
- package/ios/HotUpdater/Internal/BundleMetadata.swift +15 -0
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/package.json +6 -6
- package/src/sdkVersion.ts +1 -1
|
@@ -485,6 +485,7 @@ class BundleFileStorageService(
|
|
|
485
485
|
checkAndCleanupIfIsolationKeyChanged()
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
+
private var hasPreparedLaunch = false
|
|
488
489
|
private var currentLaunchReport: LaunchReport? = null
|
|
489
490
|
|
|
490
491
|
@Volatile
|
|
@@ -930,6 +931,7 @@ class BundleFileStorageService(
|
|
|
930
931
|
stableBundleId = currentVerifiedBundleId,
|
|
931
932
|
stagingBundleId = bundleId,
|
|
932
933
|
verificationPending = true,
|
|
934
|
+
launchInProgress = false,
|
|
933
935
|
updatedAt = System.currentTimeMillis(),
|
|
934
936
|
)
|
|
935
937
|
}
|
|
@@ -956,6 +958,7 @@ class BundleFileStorageService(
|
|
|
956
958
|
stableBundleId = null,
|
|
957
959
|
stagingBundleId = fallbackBundleId,
|
|
958
960
|
verificationPending = false,
|
|
961
|
+
launchInProgress = false,
|
|
959
962
|
updatedAt = System.currentTimeMillis(),
|
|
960
963
|
)
|
|
961
964
|
saveMetadata(updatedMetadata)
|
|
@@ -1052,6 +1055,7 @@ class BundleFileStorageService(
|
|
|
1052
1055
|
saveMetadata(
|
|
1053
1056
|
metadata.copy(
|
|
1054
1057
|
verificationPending = false,
|
|
1058
|
+
launchInProgress = false,
|
|
1055
1059
|
updatedAt = System.currentTimeMillis(),
|
|
1056
1060
|
),
|
|
1057
1061
|
)
|
|
@@ -1106,8 +1110,22 @@ class BundleFileStorageService(
|
|
|
1106
1110
|
override fun prepareLaunch(pendingRecovery: PendingCrashRecovery?): LaunchSelection {
|
|
1107
1111
|
saveLaunchReport(null)
|
|
1108
1112
|
applyPendingRecoveryIfNeeded(pendingRecovery)
|
|
1113
|
+
// Only a new storage instance consumes an unfinished launch. Repeated bundle
|
|
1114
|
+
// lookups or bridge reloads in this process must not reject its own launch.
|
|
1115
|
+
if (!hasPreparedLaunch) {
|
|
1116
|
+
val metadata = loadMetadataOrNull()
|
|
1117
|
+
if (metadata?.verificationPending == true && metadata.launchInProgress) {
|
|
1118
|
+
metadata.stagingBundleId?.let { rollbackPendingBundle(it) }
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
hasPreparedLaunch = true
|
|
1109
1122
|
|
|
1110
1123
|
val selection = selectLaunch()
|
|
1124
|
+
if (selection.shouldRollbackOnCrash) {
|
|
1125
|
+
loadMetadataOrNull()?.let { metadata ->
|
|
1126
|
+
saveMetadata(metadata.copy(launchInProgress = true))
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1111
1129
|
Log.d(
|
|
1112
1130
|
TAG,
|
|
1113
1131
|
"prepareLaunch: bundleId=${selection.launchedBundleId} shouldRollback=${selection.shouldRollbackOnCrash} url=${selection.bundleUrl}",
|
|
@@ -1937,6 +1955,7 @@ class BundleFileStorageService(
|
|
|
1937
1955
|
stableBundleId = null,
|
|
1938
1956
|
stagingBundleId = null,
|
|
1939
1957
|
verificationPending = false,
|
|
1958
|
+
launchInProgress = false,
|
|
1940
1959
|
)
|
|
1941
1960
|
|
|
1942
1961
|
if (!saveMetadata(clearedMetadata)) {
|
|
@@ -14,6 +14,7 @@ data class BundleMetadata(
|
|
|
14
14
|
val stableBundleId: String? = null,
|
|
15
15
|
val stagingBundleId: String? = null,
|
|
16
16
|
val verificationPending: Boolean = false,
|
|
17
|
+
val launchInProgress: Boolean = false,
|
|
17
18
|
val updatedAt: Long = System.currentTimeMillis(),
|
|
18
19
|
) {
|
|
19
20
|
companion object {
|
|
@@ -43,6 +44,7 @@ data class BundleMetadata(
|
|
|
43
44
|
null
|
|
44
45
|
},
|
|
45
46
|
verificationPending = json.optBoolean("verificationPending", false),
|
|
47
|
+
launchInProgress = json.optBoolean("launchInProgress", false),
|
|
46
48
|
updatedAt = json.optLong("updatedAt", System.currentTimeMillis()),
|
|
47
49
|
)
|
|
48
50
|
|
|
@@ -86,6 +88,7 @@ data class BundleMetadata(
|
|
|
86
88
|
put("stableBundleId", stableBundleId ?: JSONObject.NULL)
|
|
87
89
|
put("stagingBundleId", stagingBundleId ?: JSONObject.NULL)
|
|
88
90
|
put("verificationPending", verificationPending)
|
|
91
|
+
put("launchInProgress", launchInProgress)
|
|
89
92
|
put("updatedAt", updatedAt)
|
|
90
93
|
}
|
|
91
94
|
|
|
@@ -23,6 +23,71 @@ class BundleFileStorageServiceTest {
|
|
|
23
23
|
@get:Rule
|
|
24
24
|
val temporaryFolder = TemporaryFolder()
|
|
25
25
|
|
|
26
|
+
@Test
|
|
27
|
+
fun `unfinished staging launch rolls back to stable on cold start`() {
|
|
28
|
+
assertStagingLaunchAcrossColdStart(hasStableBundle = true)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
fun `unfinished staging launch rolls back to built in on cold start`() {
|
|
33
|
+
assertStagingLaunchAcrossColdStart(hasStableBundle = false)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test
|
|
37
|
+
fun `completed staging launch remains trusted on cold start`() {
|
|
38
|
+
assertStagingLaunchAcrossColdStart(hasStableBundle = true, completesLaunch = true)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Test
|
|
42
|
+
fun `completed first OTA launch remains trusted on cold start`() {
|
|
43
|
+
assertStagingLaunchAcrossColdStart(hasStableBundle = false, completesLaunch = true)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private fun assertStagingLaunchAcrossColdStart(
|
|
47
|
+
hasStableBundle: Boolean,
|
|
48
|
+
completesLaunch: Boolean = false,
|
|
49
|
+
) {
|
|
50
|
+
val rootDir = temporaryFolder.newFolder()
|
|
51
|
+
val preferences = InMemoryPreferencesService()
|
|
52
|
+
val stableBundleId = if (hasStableBundle) "stable-bundle" else null
|
|
53
|
+
listOfNotNull(stableBundleId, "hung-bundle").forEach { bundleId ->
|
|
54
|
+
val directory = createBundleDir(rootDir, bundleId)
|
|
55
|
+
writeFile(directory, "index.android.bundle")
|
|
56
|
+
writeManifest(directory, listOf("index.android.bundle"))
|
|
57
|
+
}
|
|
58
|
+
writeMetadata(
|
|
59
|
+
rootDir,
|
|
60
|
+
BundleMetadata(
|
|
61
|
+
isolationKey = TEST_ISOLATION_KEY,
|
|
62
|
+
stableBundleId = stableBundleId,
|
|
63
|
+
stagingBundleId = "hung-bundle",
|
|
64
|
+
verificationPending = true,
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
val firstProcess = createService(rootDir, preferences)
|
|
68
|
+
val firstLaunch = firstProcess.prepareLaunch(null)
|
|
69
|
+
assertEquals("hung-bundle", firstLaunch.launchedBundleId)
|
|
70
|
+
assertTrue(firstLaunch.shouldRollbackOnCrash)
|
|
71
|
+
|
|
72
|
+
assertEquals("hung-bundle", firstProcess.prepareLaunch(null).launchedBundleId)
|
|
73
|
+
assertFalse(firstProcess.getCrashHistory().contains("hung-bundle"))
|
|
74
|
+
if (completesLaunch) {
|
|
75
|
+
firstProcess.markLaunchCompleted("hung-bundle")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Issue #1321: simulate process termination without a crash marker.
|
|
79
|
+
val secondProcess = createService(rootDir, preferences)
|
|
80
|
+
val nextLaunch = secondProcess.prepareLaunch(null)
|
|
81
|
+
assertEquals(if (completesLaunch) "hung-bundle" else stableBundleId, nextLaunch.launchedBundleId)
|
|
82
|
+
assertFalse(nextLaunch.shouldRollbackOnCrash)
|
|
83
|
+
assertEquals(!completesLaunch, secondProcess.getCrashHistory().contains("hung-bundle"))
|
|
84
|
+
if (!completesLaunch) {
|
|
85
|
+
assertEquals("RECOVERED", secondProcess.notifyAppReady()["status"])
|
|
86
|
+
}
|
|
87
|
+
val thirdProcess = createService(rootDir, preferences)
|
|
88
|
+
assertEquals(nextLaunch.launchedBundleId, thirdProcess.prepareLaunch(null).launchedBundleId)
|
|
89
|
+
}
|
|
90
|
+
|
|
26
91
|
@Test
|
|
27
92
|
fun `resolveBundleFile uses single manifest bundle at root`() {
|
|
28
93
|
val rootDir = temporaryFolder.newFolder("root-manifest-bundle")
|
|
@@ -349,6 +349,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
349
349
|
|
|
350
350
|
private var activeTasks: [URLSessionTask] = []
|
|
351
351
|
|
|
352
|
+
private var hasPreparedLaunch = false
|
|
352
353
|
private var currentLaunchReport: LaunchReport?
|
|
353
354
|
private let activeBundleMetadataLock = NSLock()
|
|
354
355
|
private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot?
|
|
@@ -1609,7 +1610,22 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1609
1610
|
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection {
|
|
1610
1611
|
saveLaunchReport(nil)
|
|
1611
1612
|
applyPendingRecoveryIfNeeded(pendingRecovery)
|
|
1612
|
-
|
|
1613
|
+
// Only a new storage instance consumes an unfinished launch. Repeated bundle
|
|
1614
|
+
// lookups or bridge reloads in this process must not reject its own launch.
|
|
1615
|
+
if !hasPreparedLaunch,
|
|
1616
|
+
let metadata = loadMetadataOrNull(),
|
|
1617
|
+
metadata.verificationPending,
|
|
1618
|
+
metadata.launchInProgress,
|
|
1619
|
+
let stagingBundleId = metadata.stagingBundleId {
|
|
1620
|
+
rollbackPendingBundle(stagingBundleId)
|
|
1621
|
+
}
|
|
1622
|
+
hasPreparedLaunch = true
|
|
1623
|
+
let selection = selectLaunch(bundle: bundle)
|
|
1624
|
+
if selection.shouldRollbackOnCrash, var metadata = loadMetadataOrNull() {
|
|
1625
|
+
metadata.launchInProgress = true
|
|
1626
|
+
_ = saveMetadata(metadata)
|
|
1627
|
+
}
|
|
1628
|
+
return selection
|
|
1613
1629
|
}
|
|
1614
1630
|
|
|
1615
1631
|
// MARK: - Bundle Update
|
|
@@ -2535,6 +2551,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2535
2551
|
}
|
|
2536
2552
|
|
|
2537
2553
|
metadata.verificationPending = false
|
|
2554
|
+
metadata.launchInProgress = false
|
|
2538
2555
|
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
2539
2556
|
let _ = saveMetadata(metadata)
|
|
2540
2557
|
}
|
|
@@ -12,6 +12,7 @@ public struct BundleMetadata: Codable {
|
|
|
12
12
|
var stableBundleId: String?
|
|
13
13
|
var stagingBundleId: String?
|
|
14
14
|
var verificationPending: Bool
|
|
15
|
+
var launchInProgress: Bool
|
|
15
16
|
var updatedAt: Double
|
|
16
17
|
|
|
17
18
|
enum CodingKeys: String, CodingKey {
|
|
@@ -20,6 +21,7 @@ public struct BundleMetadata: Codable {
|
|
|
20
21
|
case stableBundleId = "stable_bundle_id"
|
|
21
22
|
case stagingBundleId = "staging_bundle_id"
|
|
22
23
|
case verificationPending = "verification_pending"
|
|
24
|
+
case launchInProgress = "launch_in_progress"
|
|
23
25
|
case updatedAt = "updated_at"
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -29,6 +31,7 @@ public struct BundleMetadata: Codable {
|
|
|
29
31
|
stableBundleId: String? = nil,
|
|
30
32
|
stagingBundleId: String? = nil,
|
|
31
33
|
verificationPending: Bool = false,
|
|
34
|
+
launchInProgress: Bool = false,
|
|
32
35
|
updatedAt: Double = Date().timeIntervalSince1970 * 1000
|
|
33
36
|
) {
|
|
34
37
|
self.schema = schema
|
|
@@ -36,9 +39,21 @@ public struct BundleMetadata: Codable {
|
|
|
36
39
|
self.stableBundleId = stableBundleId
|
|
37
40
|
self.stagingBundleId = stagingBundleId
|
|
38
41
|
self.verificationPending = verificationPending
|
|
42
|
+
self.launchInProgress = launchInProgress
|
|
39
43
|
self.updatedAt = updatedAt
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
public init(from decoder: Decoder) throws {
|
|
47
|
+
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
48
|
+
schema = try values.decode(String.self, forKey: .schema)
|
|
49
|
+
isolationKey = try values.decodeIfPresent(String.self, forKey: .isolationKey)
|
|
50
|
+
stableBundleId = try values.decodeIfPresent(String.self, forKey: .stableBundleId)
|
|
51
|
+
stagingBundleId = try values.decodeIfPresent(String.self, forKey: .stagingBundleId)
|
|
52
|
+
verificationPending = try values.decode(Bool.self, forKey: .verificationPending)
|
|
53
|
+
launchInProgress = try values.decodeIfPresent(Bool.self, forKey: .launchInProgress) ?? false
|
|
54
|
+
updatedAt = try values.decode(Double.self, forKey: .updatedAt)
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
static func load(from file: URL, expectedIsolationKey: String) -> BundleMetadata? {
|
|
43
58
|
guard FileManager.default.fileExists(atPath: file.path) else {
|
|
44
59
|
print("[BundleMetadata] Metadata file does not exist: \(file.path)")
|
|
@@ -4,5 +4,5 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.HOT_UPDATER_SDK_VERSION = void 0;
|
|
7
|
-
const HOT_UPDATER_SDK_VERSION = exports.HOT_UPDATER_SDK_VERSION = "0.36.
|
|
7
|
+
const HOT_UPDATER_SDK_VERSION = exports.HOT_UPDATER_SDK_VERSION = "0.36.14";
|
|
8
8
|
//# sourceMappingURL=sdkVersion.js.map
|
package/lib/module/sdkVersion.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const HOT_UPDATER_SDK_VERSION = "0.36.
|
|
1
|
+
export declare const HOT_UPDATER_SDK_VERSION = "0.36.14";
|
|
2
2
|
//# sourceMappingURL=sdkVersion.d.ts.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const HOT_UPDATER_SDK_VERSION = "0.36.
|
|
1
|
+
export declare const HOT_UPDATER_SDK_VERSION = "0.36.14";
|
|
2
2
|
//# sourceMappingURL=sdkVersion.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/react-native",
|
|
3
|
-
"version": "0.36.
|
|
3
|
+
"version": "0.36.14",
|
|
4
4
|
"description": "React Native OTA solution for self-hosted",
|
|
5
5
|
"source": "./src/index.ts",
|
|
6
6
|
"main": "./lib/commonjs/index.js",
|
|
@@ -131,14 +131,14 @@
|
|
|
131
131
|
"react-native-builder-bob": "0.41.0",
|
|
132
132
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
133
133
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
134
|
-
"hot-updater": "0.36.
|
|
134
|
+
"hot-updater": "0.36.14"
|
|
135
135
|
},
|
|
136
136
|
"dependencies": {
|
|
137
137
|
"use-sync-external-store": "1.5.0",
|
|
138
|
-
"@hot-updater/cli-tools": "0.36.
|
|
139
|
-
"@hot-updater/core": "0.36.
|
|
140
|
-
"@hot-updater/js": "0.36.
|
|
141
|
-
"@hot-updater/plugin-core": "0.36.
|
|
138
|
+
"@hot-updater/cli-tools": "0.36.14",
|
|
139
|
+
"@hot-updater/core": "0.36.14",
|
|
140
|
+
"@hot-updater/js": "0.36.14",
|
|
141
|
+
"@hot-updater/plugin-core": "0.36.14"
|
|
142
142
|
},
|
|
143
143
|
"scripts": {
|
|
144
144
|
"build": "node ./scripts/sync-sdk-version.mjs run -- bob build && tsc -p plugin/tsconfig.build.json",
|
package/src/sdkVersion.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const HOT_UPDATER_SDK_VERSION = "0.36.
|
|
1
|
+
export const HOT_UPDATER_SDK_VERSION = "0.36.14";
|