feat(lifecycle): one native-owned app state for Go and JS - #29673
Merged
chrisnojima merged 4 commits intoSep 23, 2026
Merged
Conversation
Native forwards the app state it reports to Go (SetAppState*) to JS through a react-native-kb onAppLifecycle event, with getAppLifecycleState as the startup seed. JS drops RN AppState; applyMobileAppState is the only writer of the shell store's mobileAppState, and each event logs "[AppState] native: <state>". Android reports from ProcessLifecycleOwner instead of MainActivity pause, so moving between our own activities no longer looks like a trip to the background. iOS reports BACKGROUNDACTIVE instead of INACTIVE on resign-active: master's gregor and kbhttp tear down on INACTIVE. Flush audit (calls that run a leveldb flush on master), every one replaces an existing call on the same transition: - Android appDidEnterBackground / appBeginBackgroundTaskNonblock / setAppStateBackground: moved from MainActivity.onPause to process ON_STOP (fires once when no activity is started, never more often than onPause). - Android appWillExit: MainActivity.onDestroy now only when isFinishing. - iOS KeybaseSetAppStateBackground in notifyAppState: same call, same case. - iOS KeybaseSetAppStateBackgroundActive replaces KeybaseSetAppStateInactive (neither flushes).
AppDidEnterBackground already sets BACKGROUND (and flushes) when it returns false, so the extra setAppStateBackground ran a second full leveldb compaction back-to-back.
chrisnojima
added this pull request to stack #29680
September 23, 2026 19:54
This was referenced Sep 23, 2026
chrisnojima
removed this pull request from stack #29680
September 23, 2026 19:56
chrisnojima
added this pull request to stack #29681
September 23, 2026 19:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack 1/7 on #29650.
Why
AppStateseparately. The two can disagree, and JS's copy gates mark-read and focus.appWillExit, a picker or permission dialog reports background, and every background ran two full leveldb compactions (test(libkb): failing tests for leveldb flush, open and cleaner lifecycle #29669).What this changes
onAppLifecycleevent and a synchronousgetAppLifecycleState(). Native stores the latest state, so JS can seed it at startup.SceneDelegatetellsAppDelegatewhen the scene disconnects, and the resign snapshot view no longer stacks up.AppLifecycleForwarderobservesProcessLifecycleOwner.appDidEnterBackground, then starts a background task if Go asks for one.appWillExitruns only when the activity is really finishing.AppStateis gone.applyMobileAppStateis the only writer ofmobileAppState, fed by the native event and logged as[AppState] native: <state>. Dark mode and contact permissions follow that one value.Judgment calls
activeunder system dialogs, permission prompts and multi-window focus loss. Background is reported about 700 ms after the last activity stops (ProcessLifecycleOwner's debounce), not atonPause.else setAppStateBackground().appDidEnterBackgroundalready reports BACKGROUND and flushes when it returns false, so the else ran a second full compaction.MainActivity.onResumestill reports foreground alongside the process ON_START and ON_RESUME. Go may not be initialized at the first process ON_START, and a repeated report of the same state does nothing.mobileAppStatefrom native at startup rather than waiting for the first change. The store's subscribers therefore fire once on first mount.notifyAppStatetwice during a cold launch, as it already does on fix(links): route devices and /phone-app through the linking config #29650. If both calls land on.background, that is two flushes; one cold-launchios.logshows whether they do.Tests
constants/init/app-state.test.ts: states arrive through the native event in order, the store is seeded from native, each event is logged, unmapped states and RNAppStateevents are ignored, unsubscribing stops updates, and desktop is unaffected. The seed, ignore and unsubscribe cases were mutation-checked.