@scalebun/react-native 1.11.0 → 1.11.1
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/replaysdk/tracking/InteractionTracker.kt +0 -1
- package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +15 -3
- package/dist/scalebun.full.js +232 -69
- package/dist/scalebun.slim.js +232 -69
- package/ios/Ota/OtaSlotManager.swift +19 -5
- package/lib/commonjs/core/constants/version.js +1 -1
- package/lib/commonjs/features/ota/OtaOrchestrator.js +174 -48
- package/lib/commonjs/features/ota/OtaTypes.js +4 -0
- package/lib/commonjs/features/ota/useOtaUpdate.js +11 -2
- package/lib/commonjs/public/ScaleBunFacade.js +115 -2
- package/lib/module/core/constants/version.js +1 -1
- package/lib/module/features/ota/OtaOrchestrator.js +174 -48
- package/lib/module/features/ota/OtaTypes.js +1 -1
- package/lib/module/features/ota/useOtaUpdate.js +11 -2
- package/lib/module/public/ScaleBunFacade.js +115 -2
- package/lib/typescript/core/constants/version.d.ts +1 -1
- package/lib/typescript/features/ota/OtaEventEmitter.d.ts +15 -1
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts +22 -3
- package/lib/typescript/features/ota/OtaTypes.d.ts +29 -32
- package/lib/typescript/public/ScaleBunFacade.d.ts +27 -0
- package/package.json +2 -2
- package/src/core/constants/version.ts +1 -1
- package/src/features/ota/OtaEventEmitter.ts +12 -0
- package/src/features/ota/OtaOrchestrator.ts +209 -62
- package/src/features/ota/OtaTypes.ts +37 -39
- package/src/features/ota/useOtaUpdate.ts +11 -2
- package/src/public/ScaleBunFacade.ts +127 -3
|
@@ -179,10 +179,18 @@ class SlotManager(private val filesDir: File) {
|
|
|
179
179
|
if (reason != null) {
|
|
180
180
|
runCatching {
|
|
181
181
|
otaRoot.mkdirs()
|
|
182
|
+
// Stamp WHICH bundle is being reverted away from, read before
|
|
183
|
+
// current/ is deleted below. Without it JS had nothing to name:
|
|
184
|
+
// slot meta carries no bundleId and the previous slot is gone by
|
|
185
|
+
// the time JS looks, so every AUTO_ROLLBACK reported bundleId
|
|
186
|
+
// "unknown" and the server dropped it as unresolvable — leaving
|
|
187
|
+
// the crash guard's own rollback signal permanently at zero.
|
|
188
|
+
val failingSha = readMeta(currentSlot)?.optString("sha256", "").orEmpty()
|
|
182
189
|
revertRecord.writeText(
|
|
183
190
|
JSONObject()
|
|
184
191
|
.put("reason", reason)
|
|
185
192
|
.put("at", System.currentTimeMillis())
|
|
193
|
+
.apply { if (failingSha.isNotEmpty()) put("sha256", failingSha) }
|
|
186
194
|
.toString(),
|
|
187
195
|
)
|
|
188
196
|
}
|
|
@@ -237,7 +245,7 @@ class SlotManager(private val filesDir: File) {
|
|
|
237
245
|
val metaFile = File(currentSlot, META_FILENAME)
|
|
238
246
|
if (!metaFile.exists()) {
|
|
239
247
|
Log.w(TAG, "Boot marker present but no current/meta.json — auto-reverting")
|
|
240
|
-
revert()
|
|
248
|
+
revert(reason = "missing_meta")
|
|
241
249
|
return
|
|
242
250
|
}
|
|
243
251
|
|
|
@@ -259,7 +267,7 @@ class SlotManager(private val filesDir: File) {
|
|
|
259
267
|
}
|
|
260
268
|
if (currentId != markerId) {
|
|
261
269
|
Log.w(TAG, "Boot marker sha256 mismatch — auto-reverting")
|
|
262
|
-
revert()
|
|
270
|
+
revert(reason = "hash_mismatch")
|
|
263
271
|
return
|
|
264
272
|
}
|
|
265
273
|
|
|
@@ -290,7 +298,7 @@ class SlotManager(private val filesDir: File) {
|
|
|
290
298
|
Log.i(TAG, "Boot guard: attempt $attempts/$MAX_BOOT_ATTEMPTS for this bundle")
|
|
291
299
|
} catch (e: Exception) {
|
|
292
300
|
Log.e(TAG, "checkBootGuard() failed — reverting to be safe", e)
|
|
293
|
-
revert()
|
|
301
|
+
revert(reason = "boot_guard_error")
|
|
294
302
|
}
|
|
295
303
|
}
|
|
296
304
|
|
|
@@ -312,6 +320,10 @@ class SlotManager(private val filesDir: File) {
|
|
|
312
320
|
state.put("bootGuardReverted", true)
|
|
313
321
|
state.put("bootGuardRevertReason", rec.optString("reason", "boot_crash_guard"))
|
|
314
322
|
state.put("bootGuardRevertedAt", rec.optLong("at", 0L))
|
|
323
|
+
// The bundle reverted AWAY from. JS joins it back to a bundle id
|
|
324
|
+
// through its install record so the rollback can be reported.
|
|
325
|
+
val sha = rec.optString("sha256", "")
|
|
326
|
+
if (sha.isNotEmpty()) state.put("bootGuardRevertedSha256", sha)
|
|
315
327
|
}
|
|
316
328
|
revertRecord.delete()
|
|
317
329
|
}
|
package/dist/scalebun.full.js
CHANGED
|
@@ -605,7 +605,7 @@ var SDK_VERSION;
|
|
|
605
605
|
var init_version = __esm({
|
|
606
606
|
"lib/module/core/constants/version.js"() {
|
|
607
607
|
"use strict";
|
|
608
|
-
SDK_VERSION = "1.11.
|
|
608
|
+
SDK_VERSION = "1.11.1";
|
|
609
609
|
}
|
|
610
610
|
});
|
|
611
611
|
|
|
@@ -10592,6 +10592,42 @@ var init_bootstrap = __esm({
|
|
|
10592
10592
|
}
|
|
10593
10593
|
});
|
|
10594
10594
|
|
|
10595
|
+
// lib/module/core/lifecycle/appLifecycle.js
|
|
10596
|
+
var appLifecycle_exports = {};
|
|
10597
|
+
__export(appLifecycle_exports, {
|
|
10598
|
+
appLifecycle: () => appLifecycle
|
|
10599
|
+
});
|
|
10600
|
+
var import_react_native13, AppLifecycle, appLifecycle;
|
|
10601
|
+
var init_appLifecycle = __esm({
|
|
10602
|
+
"lib/module/core/lifecycle/appLifecycle.js"() {
|
|
10603
|
+
"use strict";
|
|
10604
|
+
import_react_native13 = require("react-native");
|
|
10605
|
+
init_internalLogger();
|
|
10606
|
+
AppLifecycle = class {
|
|
10607
|
+
listeners = [];
|
|
10608
|
+
currentState = import_react_native13.AppState.currentState;
|
|
10609
|
+
constructor() {
|
|
10610
|
+
import_react_native13.AppState.addEventListener("change", this.handleStateChange);
|
|
10611
|
+
}
|
|
10612
|
+
handleStateChange = (nextState) => {
|
|
10613
|
+
__DEV__ && logger.debug(`App state changed: ${this.currentState} -> ${nextState}`);
|
|
10614
|
+
this.currentState = nextState;
|
|
10615
|
+
this.listeners.forEach((l) => l(nextState));
|
|
10616
|
+
};
|
|
10617
|
+
addListener(listener) {
|
|
10618
|
+
this.listeners.push(listener);
|
|
10619
|
+
}
|
|
10620
|
+
removeListener(listener) {
|
|
10621
|
+
this.listeners = this.listeners.filter((l) => l !== listener);
|
|
10622
|
+
}
|
|
10623
|
+
getCurrentState() {
|
|
10624
|
+
return this.currentState;
|
|
10625
|
+
}
|
|
10626
|
+
};
|
|
10627
|
+
appLifecycle = new AppLifecycle();
|
|
10628
|
+
}
|
|
10629
|
+
});
|
|
10630
|
+
|
|
10595
10631
|
// lib/module/features/crash/CrashReporter.js
|
|
10596
10632
|
function getCrashReporter() {
|
|
10597
10633
|
if (!_reporter) _reporter = new CrashReporter();
|
|
@@ -12176,6 +12212,11 @@ async function deliverOtaEvents(params) {
|
|
|
12176
12212
|
kind: "ota_event",
|
|
12177
12213
|
type: e.type,
|
|
12178
12214
|
bundleId: e.bundleId,
|
|
12215
|
+
// Without this the backend stored a null releaseId on every row it ingested,
|
|
12216
|
+
// while serving the release id on every check — so the delivery funnel could
|
|
12217
|
+
// only ever be grouped by bundle, and a bundle re-promoted under a second
|
|
12218
|
+
// release merged the two into one indistinguishable series.
|
|
12219
|
+
releaseId: e.releaseId,
|
|
12179
12220
|
installationId: params.installationId,
|
|
12180
12221
|
// OTA bundles are compiled per platform, so this genuinely is ios|android. Narrowed via
|
|
12181
12222
|
// resolveMobileOS so a non-mobile RN target is skipped rather than served an Android bundle.
|
|
@@ -12277,20 +12318,31 @@ var init_OtaOrchestrator = __esm({
|
|
|
12277
12318
|
this.bootGuardConfig = config ?? {};
|
|
12278
12319
|
this.signatureConfig = config?.signature;
|
|
12279
12320
|
__DEV__ && logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? "unknown"}${this.environment.bridgeless ? ", bridgeless" : ""}${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? "?"}` : ""})`);
|
|
12280
|
-
this.
|
|
12321
|
+
const slotState = this.readSlotState();
|
|
12322
|
+
this.hydrateCurrentBundleFromSlots(slotState);
|
|
12281
12323
|
void prefetchDeviceCountry();
|
|
12324
|
+
this.checkBootGuardRecovery(slotState);
|
|
12282
12325
|
this.verifyRunningBundleIdentity();
|
|
12283
|
-
this.checkBootGuardRecovery();
|
|
12284
12326
|
});
|
|
12285
12327
|
}
|
|
12286
12328
|
/**
|
|
12287
12329
|
* Read the active slot back into `currentBundle` so the next check reports
|
|
12288
12330
|
* what this device is genuinely running.
|
|
12289
12331
|
*/
|
|
12290
|
-
|
|
12291
|
-
|
|
12332
|
+
/**
|
|
12333
|
+
* Parse the native slot state once. Returns null when the module is absent or
|
|
12334
|
+
* the payload is unreadable — every caller treats that as "factory bundle".
|
|
12335
|
+
*/
|
|
12336
|
+
readSlotState() {
|
|
12337
|
+
if (!NativeScaleBunOta_default) return null;
|
|
12338
|
+
try {
|
|
12339
|
+
return JSON.parse(NativeScaleBunOta_default.getSlotState());
|
|
12340
|
+
} catch {
|
|
12341
|
+
return null;
|
|
12342
|
+
}
|
|
12343
|
+
}
|
|
12344
|
+
hydrateCurrentBundleFromSlots(state) {
|
|
12292
12345
|
try {
|
|
12293
|
-
const state = JSON.parse(NativeScaleBunOta_default.getSlotState());
|
|
12294
12346
|
const current = state?.current;
|
|
12295
12347
|
if (!current?.sha256) return;
|
|
12296
12348
|
const record4 = this.readInstallRecord();
|
|
@@ -12298,6 +12350,7 @@ var init_OtaOrchestrator = __esm({
|
|
|
12298
12350
|
this.currentBundle = {
|
|
12299
12351
|
id: record4.bundleId,
|
|
12300
12352
|
version: record4.version,
|
|
12353
|
+
releaseId: record4.releaseId ?? void 0,
|
|
12301
12354
|
sha256: record4.sha256
|
|
12302
12355
|
};
|
|
12303
12356
|
__DEV__ && logger.debug(`[OTA] Running bundle v${record4.version} (${record4.bundleId})`);
|
|
@@ -12328,29 +12381,23 @@ var init_OtaOrchestrator = __esm({
|
|
|
12328
12381
|
verifyRunningBundleIdentity() {
|
|
12329
12382
|
if (!this.currentBundle) return;
|
|
12330
12383
|
const running = readRunningBundleMarker();
|
|
12331
|
-
const
|
|
12332
|
-
if (
|
|
12384
|
+
const record4 = this.readInstallRecord();
|
|
12385
|
+
if (!record4) return;
|
|
12386
|
+
if (record4.sha256 !== this.currentBundle.sha256) {
|
|
12333
12387
|
this.clearInstallExpectation();
|
|
12334
12388
|
return;
|
|
12335
12389
|
}
|
|
12336
|
-
if (
|
|
12337
|
-
|
|
12338
|
-
|
|
12339
|
-
|
|
12340
|
-
return;
|
|
12341
|
-
}
|
|
12342
|
-
logger.error(`[OTA] INSTALL DID NOT TAKE EFFECT \u2014 bundle ${this.currentBundle.id} was installed and carries a known identity marker, but the running bundle reports ${running ?? "no marker at all"}. The app is executing different code than the slot manager believes. Check that the host app resolves the OTA bundle path at launch (see the ScaleBunOta integration for your React Native version).`);
|
|
12343
|
-
otaEventEmitter.emitSimple("APPLY_FAILED", this.currentBundle.id, {
|
|
12344
|
-
error: `install_not_effective \u2014 expected ${expected.identityToken}, running ${running ?? "none"}`
|
|
12345
|
-
});
|
|
12390
|
+
if (!record4.identityToken) return;
|
|
12391
|
+
if (running === record4.identityToken) {
|
|
12392
|
+
__DEV__ && logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
|
|
12393
|
+
this.retireIdentityToken(record4);
|
|
12346
12394
|
return;
|
|
12347
12395
|
}
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
}
|
|
12396
|
+
logger.error(`[OTA] INSTALL DID NOT TAKE EFFECT \u2014 bundle ${this.currentBundle.id} was installed and carries a known identity marker, but the running bundle reports ${running ?? "no marker at all"}. The app is executing different code than the slot manager believes. Check that the host app resolves the OTA bundle path at launch (see the ScaleBunOta integration for your React Native version).`);
|
|
12397
|
+
otaEventEmitter.emitSimple("APPLY_FAILED", this.currentBundle.id, {
|
|
12398
|
+
releaseId: this.currentBundle.releaseId,
|
|
12399
|
+
error: `install_not_effective \u2014 expected ${record4.identityToken}, running ${running ?? "none"}`
|
|
12400
|
+
});
|
|
12354
12401
|
}
|
|
12355
12402
|
// ── Install expectation ────────────────────────────────────────────────────
|
|
12356
12403
|
// Written just before the restart that activates a bundle, read on the next
|
|
@@ -12362,6 +12409,11 @@ var init_OtaOrchestrator = __esm({
|
|
|
12362
12409
|
this.storage().set(_OtaOrchestrator.INSTALL_EXPECTATION_KEY, JSON.stringify({
|
|
12363
12410
|
bundleId: bundle.id,
|
|
12364
12411
|
version: bundle.version,
|
|
12412
|
+
// Carried so telemetry emitted on a LATER launch (a boot-guard
|
|
12413
|
+
// rollback, an ineffective install) can still be attributed to the
|
|
12414
|
+
// release, not merely the bundle. The check response is long gone by
|
|
12415
|
+
// then; this record is the only thing that remembers.
|
|
12416
|
+
releaseId: bundle.releaseId ?? null,
|
|
12365
12417
|
// The join key back to the native slot, which records sha256 and
|
|
12366
12418
|
// nothing else identifying.
|
|
12367
12419
|
sha256: bundle.sha256,
|
|
@@ -12380,12 +12432,26 @@ var init_OtaOrchestrator = __esm({
|
|
|
12380
12432
|
return null;
|
|
12381
12433
|
}
|
|
12382
12434
|
}
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12435
|
+
/**
|
|
12436
|
+
* Drop the identity token once the install has been proven, keeping the rest
|
|
12437
|
+
* of the record.
|
|
12438
|
+
*
|
|
12439
|
+
* The record does two jobs: it proves an install took effect (once), and it
|
|
12440
|
+
* maps the native slot's sha256 back to a bundle id (for the life of that
|
|
12441
|
+
* bundle). Only the first job is finished after a successful verification, so
|
|
12442
|
+
* only the token is retired.
|
|
12443
|
+
*/
|
|
12444
|
+
retireIdentityToken(record4) {
|
|
12445
|
+
try {
|
|
12446
|
+
this.storage().set(_OtaOrchestrator.INSTALL_EXPECTATION_KEY, JSON.stringify({
|
|
12447
|
+
bundleId: record4.bundleId,
|
|
12448
|
+
version: record4.version,
|
|
12449
|
+
releaseId: record4.releaseId ?? null,
|
|
12450
|
+
sha256: record4.sha256,
|
|
12451
|
+
identityToken: null
|
|
12452
|
+
}));
|
|
12453
|
+
} catch {
|
|
12454
|
+
}
|
|
12389
12455
|
}
|
|
12390
12456
|
clearInstallExpectation() {
|
|
12391
12457
|
try {
|
|
@@ -12402,17 +12468,24 @@ var init_OtaOrchestrator = __esm({
|
|
|
12402
12468
|
* If getSlotState() shows bootMarkerPresent=false but we have a 'previous' slot
|
|
12403
12469
|
* and no 'current' OTA bundle, the native layer already reverted.
|
|
12404
12470
|
*/
|
|
12405
|
-
checkBootGuardRecovery() {
|
|
12406
|
-
if (!NativeScaleBunOta_default) return;
|
|
12471
|
+
checkBootGuardRecovery(state) {
|
|
12407
12472
|
try {
|
|
12408
|
-
|
|
12409
|
-
const
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
|
|
12473
|
+
if (!state?.bootGuardReverted) return;
|
|
12474
|
+
const record4 = this.readInstallRecord();
|
|
12475
|
+
const revertedSha = state.bootGuardRevertedSha256;
|
|
12476
|
+
const matchesRecord = !!record4 && (!revertedSha || record4.sha256 === revertedSha);
|
|
12477
|
+
const reason = state.bootGuardRevertReason || "boot_crash_guard";
|
|
12478
|
+
if (matchesRecord && record4) {
|
|
12479
|
+
logger.warn(`[OTA] Boot guard fired \u2014 reverted away from bundle ${record4.bundleId} (v${record4.version}); reason: ${reason}`);
|
|
12480
|
+
otaEventEmitter.emitSimple("AUTO_ROLLBACK", record4.bundleId, {
|
|
12481
|
+
releaseId: record4.releaseId ?? void 0,
|
|
12482
|
+
version: record4.version,
|
|
12483
|
+
reason
|
|
12414
12484
|
});
|
|
12485
|
+
this.clearInstallExpectation();
|
|
12486
|
+
return;
|
|
12415
12487
|
}
|
|
12488
|
+
logger.warn(`[OTA] Boot guard fired (reason: ${reason}) but the rolled-back bundle could not be identified locally \u2014 no install record. The rollback is not reported to the server.`);
|
|
12416
12489
|
} catch {
|
|
12417
12490
|
}
|
|
12418
12491
|
}
|
|
@@ -12503,6 +12576,12 @@ var init_OtaOrchestrator = __esm({
|
|
|
12503
12576
|
};
|
|
12504
12577
|
}
|
|
12505
12578
|
__DEV__ && logger.debug("[OTA] Sync started\u2026");
|
|
12579
|
+
if (this.currentBundle?.id) {
|
|
12580
|
+
otaEventEmitter.emitSimple("CHECK", this.currentBundle.id, {
|
|
12581
|
+
releaseId: this.currentBundle.releaseId,
|
|
12582
|
+
version: this.currentBundle.version
|
|
12583
|
+
});
|
|
12584
|
+
}
|
|
12506
12585
|
const checkRes = await this.checkForUpdate(params);
|
|
12507
12586
|
if (checkRes.action === "NONE") {
|
|
12508
12587
|
__DEV__ && logger.debug("[OTA] App is up to date");
|
|
@@ -12513,7 +12592,9 @@ var init_OtaOrchestrator = __esm({
|
|
|
12513
12592
|
}
|
|
12514
12593
|
if (checkRes.action === "ROLLBACK") {
|
|
12515
12594
|
logger.warn("[OTA] Server requested ROLLBACK \u2014 reverting to previous bundle");
|
|
12516
|
-
otaEventEmitter.emitSimple("MANUAL_ROLLBACK", this.currentBundle?.id ?? "unknown"
|
|
12595
|
+
otaEventEmitter.emitSimple("MANUAL_ROLLBACK", this.currentBundle?.id ?? "unknown", {
|
|
12596
|
+
releaseId: this.currentBundle?.releaseId
|
|
12597
|
+
});
|
|
12517
12598
|
const reverted = await NativeScaleBunOta_default.revertToPrevious();
|
|
12518
12599
|
if (reverted) {
|
|
12519
12600
|
this.currentBundle = null;
|
|
@@ -12537,9 +12618,14 @@ var init_OtaOrchestrator = __esm({
|
|
|
12537
12618
|
}
|
|
12538
12619
|
const bundle = checkRes.bundle;
|
|
12539
12620
|
let patchUsed = false;
|
|
12621
|
+
otaEventEmitter.emitSimple("OFFERED", bundle.id, {
|
|
12622
|
+
releaseId: bundle.releaseId,
|
|
12623
|
+
version: bundle.version
|
|
12624
|
+
});
|
|
12540
12625
|
const signatureOutcome = await verifyBundleSignature(bundle.sha256, bundle.signature, this.signatureConfig);
|
|
12541
12626
|
if (!signatureOutcome.ok) {
|
|
12542
12627
|
otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
|
|
12628
|
+
releaseId: bundle.releaseId,
|
|
12543
12629
|
error: `Signature check failed: ${signatureOutcome.reason}`,
|
|
12544
12630
|
version: bundle.version
|
|
12545
12631
|
});
|
|
@@ -12550,6 +12636,7 @@ var init_OtaOrchestrator = __esm({
|
|
|
12550
12636
|
};
|
|
12551
12637
|
}
|
|
12552
12638
|
otaEventEmitter.emitSimple("DOWNLOAD_STARTED", bundle.id, {
|
|
12639
|
+
releaseId: bundle.releaseId,
|
|
12553
12640
|
version: bundle.version
|
|
12554
12641
|
});
|
|
12555
12642
|
const downloadStart = Date.now();
|
|
@@ -12635,6 +12722,7 @@ var init_OtaOrchestrator = __esm({
|
|
|
12635
12722
|
postProgress(0, "FAILED");
|
|
12636
12723
|
logger.error("[OTA] Staging bundle failed after retries");
|
|
12637
12724
|
otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
|
|
12725
|
+
releaseId: bundle.releaseId,
|
|
12638
12726
|
error: "Staging failed \u2014 SHA-256 mismatch or download error",
|
|
12639
12727
|
version: bundle.version
|
|
12640
12728
|
});
|
|
@@ -12647,6 +12735,7 @@ var init_OtaOrchestrator = __esm({
|
|
|
12647
12735
|
postProgress(bundle.size, "COMPLETED");
|
|
12648
12736
|
const downloadDuration = Date.now() - downloadStart;
|
|
12649
12737
|
otaEventEmitter.emitSimple("DOWNLOAD_COMPLETE", bundle.id, {
|
|
12738
|
+
releaseId: bundle.releaseId,
|
|
12650
12739
|
version: bundle.version,
|
|
12651
12740
|
durationMs: downloadDuration,
|
|
12652
12741
|
patchUsed
|
|
@@ -12655,6 +12744,7 @@ var init_OtaOrchestrator = __esm({
|
|
|
12655
12744
|
if (!applied) {
|
|
12656
12745
|
logger.error("[OTA] Applying update failed");
|
|
12657
12746
|
otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
|
|
12747
|
+
releaseId: bundle.releaseId,
|
|
12658
12748
|
error: "Atomic slot swap failed",
|
|
12659
12749
|
version: bundle.version
|
|
12660
12750
|
});
|
|
@@ -12668,6 +12758,7 @@ var init_OtaOrchestrator = __esm({
|
|
|
12668
12758
|
this.isRestartRequiredState = true;
|
|
12669
12759
|
this.recordInstallExpectation(bundle);
|
|
12670
12760
|
otaEventEmitter.emitSimple("INSTALLED", bundle.id, {
|
|
12761
|
+
releaseId: bundle.releaseId,
|
|
12671
12762
|
version: bundle.version
|
|
12672
12763
|
});
|
|
12673
12764
|
__DEV__ && logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
|
|
@@ -12720,6 +12811,14 @@ var init_OtaOrchestrator = __esm({
|
|
|
12720
12811
|
NativeScaleBunOta_default?.markHealthy();
|
|
12721
12812
|
__DEV__ && logger.info("[OTA] Boot guard cleared \u2014 bundle marked healthy \u2713");
|
|
12722
12813
|
this.healthyTimer = null;
|
|
12814
|
+
const running = this.currentBundle;
|
|
12815
|
+
if (running?.id) {
|
|
12816
|
+
otaEventEmitter.emitSimple("BOOT_SUCCESS", running.id, {
|
|
12817
|
+
releaseId: running.releaseId,
|
|
12818
|
+
version: running.version,
|
|
12819
|
+
durationMs: healthyMs
|
|
12820
|
+
});
|
|
12821
|
+
}
|
|
12723
12822
|
});
|
|
12724
12823
|
}, healthyMs);
|
|
12725
12824
|
} catch {
|
|
@@ -15041,31 +15140,8 @@ var FlushScheduler = class {
|
|
|
15041
15140
|
}
|
|
15042
15141
|
};
|
|
15043
15142
|
|
|
15044
|
-
// lib/module/
|
|
15045
|
-
|
|
15046
|
-
init_internalLogger();
|
|
15047
|
-
var AppLifecycle = class {
|
|
15048
|
-
listeners = [];
|
|
15049
|
-
currentState = import_react_native13.AppState.currentState;
|
|
15050
|
-
constructor() {
|
|
15051
|
-
import_react_native13.AppState.addEventListener("change", this.handleStateChange);
|
|
15052
|
-
}
|
|
15053
|
-
handleStateChange = (nextState) => {
|
|
15054
|
-
__DEV__ && logger.debug(`App state changed: ${this.currentState} -> ${nextState}`);
|
|
15055
|
-
this.currentState = nextState;
|
|
15056
|
-
this.listeners.forEach((l) => l(nextState));
|
|
15057
|
-
};
|
|
15058
|
-
addListener(listener) {
|
|
15059
|
-
this.listeners.push(listener);
|
|
15060
|
-
}
|
|
15061
|
-
removeListener(listener) {
|
|
15062
|
-
this.listeners = this.listeners.filter((l) => l !== listener);
|
|
15063
|
-
}
|
|
15064
|
-
getCurrentState() {
|
|
15065
|
-
return this.currentState;
|
|
15066
|
-
}
|
|
15067
|
-
};
|
|
15068
|
-
var appLifecycle = new AppLifecycle();
|
|
15143
|
+
// lib/module/bootstrap/SDKBootstrapper.js
|
|
15144
|
+
init_appLifecycle();
|
|
15069
15145
|
|
|
15070
15146
|
// lib/module/bootstrap/FeatureRegistry.js
|
|
15071
15147
|
init_internalLogger();
|
|
@@ -20107,7 +20183,7 @@ async function captureInstallReferrerOnce(sink) {
|
|
|
20107
20183
|
}
|
|
20108
20184
|
|
|
20109
20185
|
// lib/module/public/ScaleBunFacade.js
|
|
20110
|
-
var ScaleBunFacade = class {
|
|
20186
|
+
var ScaleBunFacade = class _ScaleBunFacade {
|
|
20111
20187
|
initialized = false;
|
|
20112
20188
|
/** In-flight init promise — guards against a second init() racing before the first resolves. */
|
|
20113
20189
|
_initInFlight = null;
|
|
@@ -20267,6 +20343,7 @@ var ScaleBunFacade = class {
|
|
|
20267
20343
|
logger.warn("[ScaleBun] A signing key is pinned but no signature verifier is available. Signature checking is fail-closed: updates will be REJECTED until one exists. Install the optional peers `@noble/ed25519` + `@noble/hashes` (no further code needed), or supply `ota.verifySignature`.");
|
|
20268
20344
|
}
|
|
20269
20345
|
}
|
|
20346
|
+
const healthyAfterMs = typeof ota.healthyAfterMs === "number" ? ota.healthyAfterMs : typeof ota.healthyTimeoutMs === "number" ? ota.healthyTimeoutMs : void 0;
|
|
20270
20347
|
otaOrchestrator2.init({
|
|
20271
20348
|
...signingRequested ? {
|
|
20272
20349
|
signature: {
|
|
@@ -20276,15 +20353,99 @@ var ScaleBunFacade = class {
|
|
|
20276
20353
|
verifier
|
|
20277
20354
|
}
|
|
20278
20355
|
} : {},
|
|
20279
|
-
...
|
|
20280
|
-
|
|
20356
|
+
...healthyAfterMs !== void 0 ? {
|
|
20357
|
+
healthyAfterMs
|
|
20281
20358
|
} : {}
|
|
20282
20359
|
});
|
|
20283
20360
|
logger.info("[ScaleBun] OTA enabled from init config.");
|
|
20361
|
+
this._startOtaChecks(ota);
|
|
20284
20362
|
} catch (err) {
|
|
20285
20363
|
logger.warn(`[ScaleBun] OTA init failed: ${err?.message ?? err}`);
|
|
20286
20364
|
}
|
|
20287
20365
|
}
|
|
20366
|
+
/** Guards against overlapping config-driven OTA checks. */
|
|
20367
|
+
_otaCheckInFlight = false;
|
|
20368
|
+
/** Wall clock of the last config-driven check, for the foreground floor. */
|
|
20369
|
+
_otaLastCheckAt = 0;
|
|
20370
|
+
_otaForegroundListener = null;
|
|
20371
|
+
/**
|
|
20372
|
+
* Minimum gap between config-driven checks.
|
|
20373
|
+
*
|
|
20374
|
+
* A foreground transition is cheap to trigger — app switchers, permission
|
|
20375
|
+
* dialogs and share sheets all produce one — so an unthrottled check would
|
|
20376
|
+
* put a request on the hot path every time the user glanced away. Ten
|
|
20377
|
+
* minutes is well below any realistic release cadence and well above that
|
|
20378
|
+
* noise. A host that wants a check on demand calls `useOtaUpdate().sync()`,
|
|
20379
|
+
* which is never throttled.
|
|
20380
|
+
*/
|
|
20381
|
+
static OTA_MIN_CHECK_INTERVAL_MS = 10 * 60 * 1e3;
|
|
20382
|
+
/**
|
|
20383
|
+
* Drive OTA checks from init config: once at startup, then on each
|
|
20384
|
+
* foreground when `checkOnForeground` is on (the schema default).
|
|
20385
|
+
*
|
|
20386
|
+
* `appVersion` is resolved from the native bridge rather than asked of the
|
|
20387
|
+
* integrator, because it gates the server's `targetAppVersion` semver check
|
|
20388
|
+
* — sending a wrong or invented value is worse than sending none, and there
|
|
20389
|
+
* is no honest default. If it cannot be resolved, the check is skipped with
|
|
20390
|
+
* a warning instead of guessing.
|
|
20391
|
+
*/
|
|
20392
|
+
_startOtaChecks(ota) {
|
|
20393
|
+
if (__DEV__) {
|
|
20394
|
+
logger.info("[ScaleBun] OTA checks are skipped in debug builds (Metro owns the bundle).");
|
|
20395
|
+
return;
|
|
20396
|
+
}
|
|
20397
|
+
const runCheck = async (trigger) => {
|
|
20398
|
+
if (this._otaCheckInFlight) return;
|
|
20399
|
+
if (trigger === "foreground" && Date.now() - this._otaLastCheckAt < _ScaleBunFacade.OTA_MIN_CHECK_INTERVAL_MS) {
|
|
20400
|
+
return;
|
|
20401
|
+
}
|
|
20402
|
+
const clientKey = this._clientKey;
|
|
20403
|
+
const apiUrl = this._apiBaseUrl;
|
|
20404
|
+
if (!clientKey || !apiUrl) return;
|
|
20405
|
+
this._otaCheckInFlight = true;
|
|
20406
|
+
try {
|
|
20407
|
+
const info = await bridgeAdapter.getDeviceInfo();
|
|
20408
|
+
const appVersion = info?.appVersion;
|
|
20409
|
+
if (!appVersion) {
|
|
20410
|
+
logger.warn("[ScaleBun] OTA check skipped \u2014 the app version could not be read from the native bridge. Rebuild the native app, or drive checks yourself with useOtaUpdate({ appVersion }).");
|
|
20411
|
+
return;
|
|
20412
|
+
}
|
|
20413
|
+
this._otaLastCheckAt = Date.now();
|
|
20414
|
+
const {
|
|
20415
|
+
otaOrchestrator: otaOrchestrator2
|
|
20416
|
+
} = (init_OtaOrchestrator(), __toCommonJS(OtaOrchestrator_exports));
|
|
20417
|
+
await otaOrchestrator2.sync({
|
|
20418
|
+
apiUrl,
|
|
20419
|
+
clientKey,
|
|
20420
|
+
appVersion,
|
|
20421
|
+
// The documented option, finally connected. Omitted means the
|
|
20422
|
+
// server's `default` channel, exactly as before.
|
|
20423
|
+
channelName: typeof ota.channelOverride === "string" ? ota.channelOverride : void 0
|
|
20424
|
+
// Never forced from config: the release's own installMode
|
|
20425
|
+
// decides when the app restarts, and yanking the screen out
|
|
20426
|
+
// from under a user is not a decision this switch should make.
|
|
20427
|
+
});
|
|
20428
|
+
} catch (err) {
|
|
20429
|
+
logger.warn(`[ScaleBun] OTA check failed: ${err?.message ?? err}`);
|
|
20430
|
+
} finally {
|
|
20431
|
+
this._otaCheckInFlight = false;
|
|
20432
|
+
}
|
|
20433
|
+
};
|
|
20434
|
+
void runCheck("startup");
|
|
20435
|
+
if (ota.checkOnForeground === false) return;
|
|
20436
|
+
if (this._otaForegroundListener) return;
|
|
20437
|
+
try {
|
|
20438
|
+
const {
|
|
20439
|
+
appLifecycle: appLifecycle2
|
|
20440
|
+
} = (init_appLifecycle(), __toCommonJS(appLifecycle_exports));
|
|
20441
|
+
this._otaForegroundListener = (state) => {
|
|
20442
|
+
if (state === "active") void runCheck("foreground");
|
|
20443
|
+
};
|
|
20444
|
+
appLifecycle2.addListener(this._otaForegroundListener);
|
|
20445
|
+
} catch {
|
|
20446
|
+
this._otaForegroundListener = null;
|
|
20447
|
+
}
|
|
20448
|
+
}
|
|
20288
20449
|
_autoEnableDebug(debugConfig) {
|
|
20289
20450
|
try {
|
|
20290
20451
|
const dbgConfig = buildDebugConfigFromInitConfig(debugConfig);
|
|
@@ -21696,6 +21857,7 @@ init_internalLogger();
|
|
|
21696
21857
|
|
|
21697
21858
|
// lib/module/features/engage/engageTriggerEngine.js
|
|
21698
21859
|
init_internalLogger();
|
|
21860
|
+
init_appLifecycle();
|
|
21699
21861
|
init_AutoScreenDetector();
|
|
21700
21862
|
init_StorageBackend();
|
|
21701
21863
|
init_engageSignals();
|
|
@@ -22003,6 +22165,7 @@ function installEngageTriggerEngine(deps) {
|
|
|
22003
22165
|
}
|
|
22004
22166
|
|
|
22005
22167
|
// lib/module/features/engage/EngagePromptProvider.js
|
|
22168
|
+
init_appLifecycle();
|
|
22006
22169
|
init_deviceId();
|
|
22007
22170
|
init_api();
|
|
22008
22171
|
init_SessionManager();
|
|
@@ -34236,7 +34399,7 @@ function useOtaUpdate(options) {
|
|
|
34236
34399
|
setIsSyncing(true);
|
|
34237
34400
|
setDownloadProgress(0);
|
|
34238
34401
|
try {
|
|
34239
|
-
const result = await otaOrchestrator.sync(
|
|
34402
|
+
const result = await otaOrchestrator.sync(optionsRef.current);
|
|
34240
34403
|
setSyncResult(result);
|
|
34241
34404
|
if (result.isMandatory && result.status === "UPDATE_INSTALLED" && optionsRef.current.mandatoryBlocksUi) {
|
|
34242
34405
|
setMandatoryUpdatePending(true);
|
|
@@ -34245,7 +34408,7 @@ function useOtaUpdate(options) {
|
|
|
34245
34408
|
} finally {
|
|
34246
34409
|
setIsSyncing(false);
|
|
34247
34410
|
}
|
|
34248
|
-
}, [
|
|
34411
|
+
}, []);
|
|
34249
34412
|
const restart = (0, import_react16.useCallback)(() => {
|
|
34250
34413
|
setMandatoryUpdatePending(false);
|
|
34251
34414
|
otaOrchestrator.restart();
|