Skip to content

feat(lifecycle): one native-owned app state for Go and JS - #29673

Merged
chrisnojima merged 4 commits into
nojima/HOTPOT-as-02-linksfrom
nojima/HOTPOT-js-03-appstate
Sep 23, 2026
Merged

chrisnojima merged 4 commits into
nojima/HOTPOT-as-02-linksfrom
nojima/HOTPOT-js-03-appstate

Conversation

@chrisnojima

@chrisnojima chrisnojima commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Stack 1/7 on #29650.

Why

  • Two sources of app state. Native reports lifecycle to Go, while JS reads React Native's AppState separately. The two can disagree, and JS's copy gates mark-read and focus.
  • Android reports one activity's lifecycle as the app's. A rotation calls 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

  • react-native-kb: a new onAppLifecycle event and a synchronous getAppLifecycleState(). Native stores the latest state, so JS can seed it at startup.
  • iOS: every callback that reports to Go also emits to JS. SceneDelegate tells AppDelegate when the scene disconnects, and the resign snapshot view no longer stacks up.
  • Android: AppLifecycleForwarder observes ProcessLifecycleOwner.
    • ON_START and ON_RESUME report foreground.
    • ON_STOP runs appDidEnterBackground, then starts a background task if Go asks for one.
    • appWillExit runs only when the activity is really finishing.
  • JS: RN AppState is gone. applyMobileAppState is the only writer of mobileAppState, fed by the native event and logged as [AppState] native: <state>. Dark mode and contact permissions follow that one value.

Judgment calls

  • iOS resign-active still reports INACTIVE to Go. Resign-active covers Control Center, Notification Center, system alerts and the app switcher. On INACTIVE, gregor disconnects (test(gregor): failing tests for app-state and connection lifecycle bugs #29664) and the local http server stops (test(kbhttp): failing tests for server lifetime across app states #29665), so chat drops its connection and images stop loading until the app is active again. Go gets the true state, and the two call sites carry a comment about these side effects until the Go fixes land.
  • Android reports the process lifecycle. JS stays active under system dialogs, permission prompts and multi-window focus loss. Background is reported about 700 ms after the last activity stops (ProcessLifecycleOwner's debounce), not at onPause.
  • Android background drops master's else setAppStateBackground(). appDidEnterBackground already reports BACKGROUND and flushes when it returns false, so the else ran a second full compaction.
  • MainActivity.onResume still 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.
  • JS seeds mobileAppState from native at startup rather than waiting for the first change. The store's subscribers therefore fire once on first mount.
  • Not changed: iOS calls notifyAppState twice 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-launch ios.log shows 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 RN AppState events are ignored, unsubscribing stops updates, and desktop is unaffected. The seed, ignore and unsubscribe cases were mutation-checked.

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
chrisnojima merged commit 1dd35a3 into master Sep 23, 2026
1 check passed
@chrisnojima
chrisnojima deleted the nojima/HOTPOT-js-03-appstate branch September 23, 2026 20:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant