@scalebun/react-native 2.0.6 → 2.0.8
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/scalebun/rn/ota/ScaleBunOtaModule.kt +12 -0
- package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +41 -0
- package/dist/scalebun.full.js +1 -1
- package/dist/scalebun.slim.js +1 -1
- package/ios/Ota/OtaSlotManager.swift +50 -0
- package/ios/Ota/ScaleBunOtaModule.swift +12 -0
- package/lib/commonjs/core/constants/version.js +1 -1
- package/lib/module/core/constants/version.js +1 -1
- package/lib/typescript/core/constants/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/core/constants/version.ts +1 -1
|
@@ -473,6 +473,18 @@ class ScaleBunOtaModule(reactContext: ReactApplicationContext) :
|
|
|
473
473
|
*/
|
|
474
474
|
@ReactMethod
|
|
475
475
|
override fun restartApp() {
|
|
476
|
+
// Tell the boot guard this next launch is ours.
|
|
477
|
+
//
|
|
478
|
+
// Hooked HERE rather than at the JS call sites so it cannot be forgotten:
|
|
479
|
+
// every restart the SDK performs goes through this method, and each one
|
|
480
|
+
// used to spend one of the bundle's two boot attempts as though it had
|
|
481
|
+
// crashed. Recorded before the restart is dispatched, because after it
|
|
482
|
+
// there is no "after".
|
|
483
|
+
try {
|
|
484
|
+
slotManager.markIntentionalRestart()
|
|
485
|
+
} catch (e: Throwable) {
|
|
486
|
+
android.util.Log.w(NAME, "restartApp: could not record intent", e)
|
|
487
|
+
}
|
|
476
488
|
val application = reactApplicationContext.applicationContext
|
|
477
489
|
com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
|
|
478
490
|
try {
|
|
@@ -49,6 +49,27 @@ class SlotManager(private val filesDir: File) {
|
|
|
49
49
|
private val revertRecord: File get() = File(otaRoot, REVERT_RECORD_FILENAME)
|
|
50
50
|
private val sequenceFile: File get() = File(otaRoot, "highest_sequence")
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Record that the next launch is a restart WE asked for.
|
|
54
|
+
*
|
|
55
|
+
* Consumed once by the boot guard so an intentional restart does not spend
|
|
56
|
+
* one of the bundle's two boot attempts. No-op when no boot marker is
|
|
57
|
+
* present, which is the normal steady state.
|
|
58
|
+
*/
|
|
59
|
+
fun markIntentionalRestart() {
|
|
60
|
+
try {
|
|
61
|
+
if (!bootMarker.exists()) return
|
|
62
|
+
val json = JSONObject(bootMarker.readText())
|
|
63
|
+
json.put("intentionalRestart", true)
|
|
64
|
+
bootMarker.writeText(json.toString())
|
|
65
|
+
FileOutputStream(bootMarker, true).use { it.fd.sync() }
|
|
66
|
+
} catch (e: Exception) {
|
|
67
|
+
// Best effort: failing to record intent only costs a boot attempt,
|
|
68
|
+
// which is strictly better than failing the restart itself.
|
|
69
|
+
Log.w(TAG, "Could not record intentional restart", e)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
/**
|
|
53
74
|
* Highest CONFIRMED release sequence (anti-downgrade floor, docs §15). Only
|
|
54
75
|
* updated once a bundle is marked healthy, so a failed/rolled-back higher
|
|
@@ -362,6 +383,26 @@ class SlotManager(private val filesDir: File) {
|
|
|
362
383
|
// that starts with the marker still present increments it. Reaching
|
|
363
384
|
// the limit means the bundle has been given MAX_BOOT_ATTEMPTS
|
|
364
385
|
// chances to call markHealthy() and never did → revert.
|
|
386
|
+
// A DELIBERATE restart is not a failed boot.
|
|
387
|
+
//
|
|
388
|
+
// The counter cannot tell "the bundle crashed on startup" from "the
|
|
389
|
+
// app restarted itself on purpose" — both simply arrive with the
|
|
390
|
+
// marker still present. So an update that installs and then restarts
|
|
391
|
+
// to activate spends one of its two lives immediately, and any second
|
|
392
|
+
// restart inside the healthy window spends the other and reverts a
|
|
393
|
+
// bundle that was never broken. Observed live: six full 19MB
|
|
394
|
+
// downloads and five reverts before one cycle happened to survive.
|
|
395
|
+
//
|
|
396
|
+
// restart() records its intent here first. That boot is expected, so
|
|
397
|
+
// it is not counted; the flag is one-shot, so a crash on the very
|
|
398
|
+
// next launch still counts and a genuinely broken bundle still goes.
|
|
399
|
+
if (markerJson.optBoolean("intentionalRestart", false)) {
|
|
400
|
+
markerJson.remove("intentionalRestart")
|
|
401
|
+
bootMarker.writeText(markerJson.toString())
|
|
402
|
+
FileOutputStream(bootMarker, true).use { it.fd.sync() }
|
|
403
|
+
Log.i(TAG, "Boot guard: expected restart, not counted as a failed boot")
|
|
404
|
+
return
|
|
405
|
+
}
|
|
365
406
|
val attempts = markerJson.optInt("bootAttempts", 0) + 1
|
|
366
407
|
if (attempts >= MAX_BOOT_ATTEMPTS) {
|
|
367
408
|
Log.w(
|
package/dist/scalebun.full.js
CHANGED
package/dist/scalebun.slim.js
CHANGED
|
@@ -44,6 +44,32 @@ class OtaSlotManager {
|
|
|
44
44
|
|
|
45
45
|
// MARK: - Public API
|
|
46
46
|
|
|
47
|
+
/// Record that the next bridge load is a restart WE asked for.
|
|
48
|
+
///
|
|
49
|
+
/// Consumed once by the boot guard so an intentional restart does not spend
|
|
50
|
+
/// one of the bundle's two boot attempts. No-op when no boot marker is
|
|
51
|
+
/// present, which is the normal steady state.
|
|
52
|
+
///
|
|
53
|
+
/// Mirrors SlotManager.markIntentionalRestart() on Android. It matters more
|
|
54
|
+
/// here: checkBootGuard() runs from getBundleURL(), which React Native calls
|
|
55
|
+
/// on every bridge creation — including the reload restartApp() triggers —
|
|
56
|
+
/// so without this the activation restart ALWAYS consumes an attempt.
|
|
57
|
+
func markIntentionalRestart() {
|
|
58
|
+
let fm = FileManager.default
|
|
59
|
+
guard fm.fileExists(atPath: bootMarkerFile.path) else { return }
|
|
60
|
+
do {
|
|
61
|
+
let data = try Data(contentsOf: bootMarkerFile)
|
|
62
|
+
guard var marker = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { return }
|
|
63
|
+
marker["intentionalRestart"] = true
|
|
64
|
+
let out = try JSONSerialization.data(withJSONObject: marker)
|
|
65
|
+
try out.write(to: bootMarkerFile, options: .atomic)
|
|
66
|
+
} catch {
|
|
67
|
+
// Best effort: failing to record intent only costs a boot attempt,
|
|
68
|
+
// which is strictly better than failing the restart itself.
|
|
69
|
+
NSLog("[ScaleBunOta] Could not record intentional restart: \(error)")
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
47
73
|
/// Returns the URL to the active JS bundle, or nil if no OTA bundle is installed.
|
|
48
74
|
func currentBundleURL() -> URL? {
|
|
49
75
|
let bundle = currentSlot.appendingPathComponent(OtaSlotManager.bundleFilename)
|
|
@@ -271,6 +297,30 @@ class OtaSlotManager {
|
|
|
271
297
|
// still finds it increments it, and reaching the limit means the
|
|
272
298
|
// bundle has had its chances and never became healthy.
|
|
273
299
|
var marker = markerJson
|
|
300
|
+
|
|
301
|
+
// The counter cannot tell "the bundle crashed on startup" from
|
|
302
|
+
// "the app restarted itself on purpose" — both simply arrive
|
|
303
|
+
// with the marker still present. An update that installs and
|
|
304
|
+
// then restarts to activate would spend one of its two lives
|
|
305
|
+
// immediately, and any second restart inside the healthy window
|
|
306
|
+
// would spend the other and revert a bundle that was never
|
|
307
|
+
// broken. That is exactly what happened on Android before
|
|
308
|
+
// markIntentionalRestart(): six full downloads, five reverts,
|
|
309
|
+
// one cycle that happened to survive.
|
|
310
|
+
//
|
|
311
|
+
// restartApp() records its intent first. That load is expected,
|
|
312
|
+
// so it is not counted; the flag is one-shot, so a crash on the
|
|
313
|
+
// very next launch still counts and a genuinely broken bundle
|
|
314
|
+
// still goes.
|
|
315
|
+
if marker["intentionalRestart"] as? Bool == true {
|
|
316
|
+
marker.removeValue(forKey: "intentionalRestart")
|
|
317
|
+
if let data = try? JSONSerialization.data(withJSONObject: marker) {
|
|
318
|
+
try? data.write(to: bootMarkerFile, options: .atomic)
|
|
319
|
+
}
|
|
320
|
+
NSLog("[ScaleBunOta] Boot guard: expected restart, not counted as a failed boot")
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
|
|
274
324
|
let attempts = (marker["bootAttempts"] as? Int ?? 0) + 1
|
|
275
325
|
if attempts >= OtaSlotManager.maxBootAttempts {
|
|
276
326
|
NSLog("[ScaleBunOta] Bundle failed to become healthy in \(attempts) launch(es) — reverting")
|
|
@@ -294,6 +294,18 @@ class ScaleBunOtaModule: NSObject, RCTBridgeModule {
|
|
|
294
294
|
*/
|
|
295
295
|
@objc(restartApp)
|
|
296
296
|
func restartApp() {
|
|
297
|
+
// Tell the boot guard this next bridge load is ours.
|
|
298
|
+
//
|
|
299
|
+
// Hooked HERE rather than at the JS call sites so it cannot be forgotten:
|
|
300
|
+
// every restart the SDK performs goes through this method. Recorded
|
|
301
|
+
// before the reload is dispatched, because after it there is no "after".
|
|
302
|
+
//
|
|
303
|
+
// On iOS this is not merely a nicety. checkBootGuard() runs from
|
|
304
|
+
// getBundleURL(), which React Native calls on every bridge creation —
|
|
305
|
+
// and the reload below creates one. Without this flag the activation
|
|
306
|
+
// restart always spends a boot attempt, leaving a healthy bundle one
|
|
307
|
+
// relaunch away from being reverted as though it had crashed.
|
|
308
|
+
ScaleBunOtaModule.slotManager.markIntentionalRestart()
|
|
297
309
|
DispatchQueue.main.async {
|
|
298
310
|
RCTTriggerReloadCommandListeners("ScaleBun OTA update")
|
|
299
311
|
}
|
|
@@ -14,5 +14,5 @@ exports.SDK_VERSION = void 0;
|
|
|
14
14
|
* value, so a stale one makes a rollout unobservable — which is the exact problem sending an SDK
|
|
15
15
|
* version was introduced to solve.
|
|
16
16
|
*/
|
|
17
|
-
const SDK_VERSION = exports.SDK_VERSION = '2.0.
|
|
17
|
+
const SDK_VERSION = exports.SDK_VERSION = '2.0.8';
|
|
18
18
|
//# sourceMappingURL=version.js.map
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
* value, so a stale one makes a rollout unobservable — which is the exact problem sending an SDK
|
|
9
9
|
* version was introduced to solve.
|
|
10
10
|
*/
|
|
11
|
-
export declare const SDK_VERSION = "2.0.
|
|
11
|
+
export declare const SDK_VERSION = "2.0.8";
|
|
12
12
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalebun/react-native",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
4
4
|
"description": "React Native SDK for ScaleBun",
|
|
5
5
|
"main": "lib/commonjs/index",
|
|
6
6
|
"module": "lib/module/index",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"@babel/runtime": "^7.25.0",
|
|
112
112
|
"@jridgewell/sourcemap-codec": "1.5.5",
|
|
113
113
|
"@jridgewell/trace-mapping": "0.3.31",
|
|
114
|
-
"@scalebun/cli": "^2.0.
|
|
114
|
+
"@scalebun/cli": "^2.0.8"
|
|
115
115
|
},
|
|
116
116
|
"codegenConfig": {
|
|
117
117
|
"name": "ScaleBunSpec",
|