Launch Safety & Rollback
A bad OTA update must never leave users stuck, and a good one must never be rolled back because of something that was not observed. Norrix follows the same model as expo-updates: every launch decision is a pure function of a persisted state document, and that document changes only when a concrete event was witnessed.
The state document
Each app keeps one file, written atomically, at Documents/norrix_ota/state.json on iOS and <filesDir>/norrix_ota/state.json on Android. It records:
| Field | Meaning |
|---|---|
binary | The store binary the state belongs to (version, build, fingerprint). A different binary discards everything. |
updates | Installed updates with successfulLaunches and failedLaunches counters. |
quarantined | Update ids that failed before ever succeeding on this binary. They are never launched or installed again. |
lastLaunch | What the loader booted for the current process and its outcome: started, displayed or failed. |
The native loader (NorrixOTA) and the JS SDK both read and write this document with the same rules.
Which bundle boots
At every launch, and before every in-process reload, the loader picks the newest installed update that:
- is not quarantined,
- has launched successfully at least once or has never failed, and
- has a complete install on disk (an
app/directory plus thenorrix-install.jsonmarker written at the end of the install).
If nothing qualifies, the store bundle boots. The loader records lastLaunch with outcome started and nothing else.
Launch outcomes
Only witnessed events change an outcome:
| Event | Outcome |
|---|---|
First content displayed (displayed, or a window’s contentLoaded on core 9.1+) | displayed; successfulLaunches increments. |
| Uncaught JS error before first display | failed; the update is quarantined and the app relaunches into the last known-good bundle. |
iOS bootstrap never reaches UIApplicationMain, or an uncaught native exception | failed, recorded natively; the next launch picks the last known-good bundle. |
| Android uncaught exception during startup | failed, recorded natively; the next launch picks the last known-good bundle. |
| Process ends any other way (user kill, low memory, watchdog, iOS prewarm, background) | Nothing. The next launch boots the same bundle again. |
An update that has succeeded before is not blamed for a later crash, so an established update is never quarantined by an unrelated failure.
Success is observed by the SDK at module evaluation, which is why @norrix/client-sdk should be imported at the top of your entry file. Initializing later only delays the confirmation; it can no longer cause a rollback.
Recovery
When a failed launch is observed in JS, the SDK:
- Marks the update failed and quarantines it.
- Waits up to five seconds for a newer update if one can be fetched.
- Relaunches into the newest launchable bundle: the previous update if it is still on disk, otherwise the store bundle. The relaunch happens in-process when the runtime exposes
NativeScriptRuntime.reloadApplication(the@nativescript/ios9.1.0 release does not; a 9.1.x patch adds it); otherwise the process restarts, and the cold launch reaches the same decision. - Reports
ota_launch_failedandota_recoveredtelemetry and emitsSyncStatus.ROLLED_BACKto your status callback.
Later update checks send the quarantined ids as excludeIds, so the server never offers the failed update to that device again. A new store binary clears the quarantine. Set retryFailedUpdates: true to opt out while debugging a rollout.
What you see in the dashboard
- Each update card shows how many distinct devices reported a failed launch of it.
- The OTA analytics page counts failed launches, recoveries and confirmed launches, and lists failures per update id.
Use getRunningUpdate() in the app to show the bundle that is actually executing, and getQuarantinedUpdateIds() to inspect a device’s quarantine. See the SDK usage guide for code.