ScaleBun
Skip to article

Update lifecycle & events

react-nativeDeveloper

Exactly what happens from check to activation — install modes (restart / resume / immediate), the full event funnel, the boot-guard healthy window, mandatory updates, delta patches, and how to drive a custom update UI from events.

Updated

This page is the detail behind "the SDK downloads and applies an update." If you're building an update UI, wiring telemetry, or reasoning about when code actually swaps, this is the contract.

The event funnel#

Every update walks a fixed sequence of events, emitted on otaEventEmitter. Each is a stage in a funnel — the earlier ones always fire, the later ones only if the update progresses:

EventMeaning
CHECKA check was made against the server — the denominator of the funnel
OFFEREDThe server has a bundle for this device — the offer, before any bytes move
DOWNLOAD_STARTEDDownload began
DOWNLOAD_PROGRESSProgress tick (01); high-frequency
DOWNLOAD_COMPLETEBytes are down and verified
INSTALLEDStaged and swapped — emitted optimistically, before the bundle has booted
BOOT_SUCCESSThe bundle booted and survived to the healthy mark — the honest activation signal
APPLY_FAILEDStaging/applying failed
AUTO_ROLLBACKThe boot guard reverted a bundle that failed to become healthy
MANUAL_ROLLBACKAn operator rolled the release back
TypeScript
import { otaEventEmitter } from '@scalebun/react-native';
const off = otaEventEmitter.on('BOOT_SUCCESS', (e) => analytics.track('ota_activated', e));otaEventEmitter.on('AUTO_ROLLBACK', (e) => analytics.track('ota_rolled_back', e));// later: off();

Install modes — when an update activates#

A staged bundle never swaps mid-session. When it activates is set per release via its install mode (absent means ON_NEXT_RESTART):

Install modeBehaviour
ON_NEXT_RESTART (default)Nothing happens now; the next cold start loads the new bundle
ON_NEXT_RESUMEThe JS runtime restarts the next time the app returns to the foreground
IMMEDIATEThe JS runtime restarts right after install

You can also force an immediate restart from the client regardless of the release's mode, with autoRestart on the hook (or restart()):

TypeScript
useOtaUpdate({ /* … */ autoRestart: true }); // apply as soon as it's staged

The boot guard & the healthy window#

When a new bundle activates, the SDK arms a boot guard: it waits for the bundle to prove it's healthy before committing to it. If the bundle crashes on startup before that, the SDK reverts to the last-good bundle on the next launch — a broken release can't brick the app.

The healthy window is configurable when the orchestrator initializes:

TypeScript
import { otaOrchestrator } from '@scalebun/react-native';
// healthyAfterMs: how long the new bundle must run before it's marked healthy.otaOrchestrator.init({ healthyAfterMs: 10_000 }); // default 10s
  • Survive healthyAfterMs after boot → the marker clears, BOOT_SUCCESS fires, the bundle is committed.

  • Crash before that → AUTO_ROLLBACK, and the device reports it so the rollback is visible in the dashboard's recovery view.

The boot-attempt limit (how many crashed launches before reverting) lives in native code — the guard is enforced below JS so a JS crash can't disable it.

Mandatory updates#

A release can be marked mandatory. The hook surfaces it, and with mandatoryBlocksUi the SDK blocks the UI until it's applied:

TSX
const { mandatoryUpdatePending, downloadProgress } = useOtaUpdate({ /* … */ mandatoryBlocksUi: true });if (mandatoryUpdatePending) return <BlockingUpdate progress={downloadProgress} />;

Use this sparingly — a mandatory update that blocks the UI is a hard gate on your users. It's the right tool for a critical fix, not routine releases.

Delta (patch) updates#

When the server can produce a binary diff between the device's current bundle and the new one, the SDK downloads the patch instead of the whole bundle and reconstructs it locally — far less data on the wire. It's automatic; you don't opt in. The DOWNLOAD_COMPLETE event tells you whether a patch was used:

TypeScript
otaEventEmitter.on('DOWNLOAD_COMPLETE', (e) => {  console.log(e.patchUsed ? 'delta patch applied' : 'full bundle downloaded');});

Runtime compatibility (why targeting matters)#

A bundle carries the RN version and Hermes bytecode (HBC) version it was built against. The device checks these before loading — an OTA bundle built for a Hermes engine the installed binary doesn't have would fail to load. This is why:

  • Publish with the same JS engine as your store build (--hermes, the default).

  • Scope releases to compatible native versions with --target-version — a JS bundle that needs a native module from app 1.2.0 must not reach a device on 1.1.0.

See Channels & rollouts → targeting.

Error handling#

sync() resolves to a SyncResult whose status is one of UP_TO_DATE | UPDATE_INSTALLED | ROLLED_BACK | ERROR. Treat ERROR (and the APPLY_FAILED event) as non-fatal — the app keeps running the current bundle; log it and let the next check retry:

TypeScript
const r = await sync();if (r.status === 'ERROR') logger.warn('OTA sync failed', r.error); // current bundle keeps running

Next#