fix: prevent mobile session resync flicker

Avoid unnecessary resync on clean initial stream connect
Skip no-op message snapshot writes during recovery
Only trigger mobile resume sync after real app resume
This commit is contained in:
Bohdan Triapitsyn
2026-07-02 22:44:00 +03:00
parent 01e0905e8b
commit 3bd785a10a
3 changed files with 52 additions and 23 deletions
+13 -2
View File
@@ -202,19 +202,30 @@ const useNativeMobileChrome = (): void => {
}; };
const useNativeMobileLifecycle = (onResume: () => void): void => { const useNativeMobileLifecycle = (onResume: () => void): void => {
const wasInactiveRef = React.useRef(false);
React.useEffect(() => { React.useEffect(() => {
if (!isCapacitorMobileApp()) return; if (!isCapacitorMobileApp()) return;
let disposed = false; let disposed = false;
const cleanup: Array<() => void> = []; const cleanup: Array<() => void> = [];
const resumeAfterInactive = () => {
if (!wasInactiveRef.current) return;
wasInactiveRef.current = false;
onResume();
};
void import('@capacitor/app').then(async ({ App }) => { void import('@capacitor/app').then(async ({ App }) => {
if (disposed) return; if (disposed) return;
const state = await App.addListener('appStateChange', ({ isActive }) => { const state = await App.addListener('appStateChange', ({ isActive }) => {
document.documentElement.classList.toggle('oc-native-app-active', isActive); document.documentElement.classList.toggle('oc-native-app-active', isActive);
if (isActive) onResume(); if (!isActive) {
wasInactiveRef.current = true;
return;
}
resumeAfterInactive();
}); });
const resume = await App.addListener('resume', onResume); const resume = await App.addListener('resume', resumeAfterInactive);
if (disposed) { if (disposed) {
void state.remove(); void state.remove();
void resume.remove(); void resume.remove();
+5
View File
@@ -1169,6 +1169,10 @@ export async function fetchMessagesForSession(sessionID: string, directory?: str
// can't repopulate (and un-evict) a session already navigated away from. // can't repopulate (and un-evict) a session already navigated away from.
if (useSessionUIStore.getState().currentSessionId !== sessionID) return if (useSessionUIStore.getState().currentSessionId !== sessionID) return
const latestState = store.getState()
const latestStatus = getSessionMaterializationStatus(latestState, sessionID)
if (latestStatus.renderable && (latestState.message[sessionID]?.length ?? 0) >= records.length) return
store.setState((state) => { store.setState((state) => {
const materialized = materializeSessionSnapshots( const materialized = materializeSessionSnapshots(
state, state,
@@ -1179,6 +1183,7 @@ export async function fetchMessagesForSession(sessionID: string, directory?: str
})), })),
{ skipPartTypes: MESSAGE_REFETCH_SKIP_PARTS }, { skipPartTypes: MESSAGE_REFETCH_SKIP_PARTS },
) )
if (!materialized.messagesChanged && !materialized.partsChanged) return state
return { message: materialized.message, part: materialized.part } return { message: materialized.message, part: materialized.part }
}) })
} catch { } catch {
+13
View File
@@ -286,6 +286,9 @@ async function materializeSessionFromServer(
})), })),
{ skipPartTypes: RECONNECT_SKIP_PARTS }, { skipPartTypes: RECONNECT_SKIP_PARTS },
) )
if (!materialized.messagesChanged && !materialized.partsChanged) {
return state
}
return { message: materialized.message, part: materialized.part } return { message: materialized.message, part: materialized.part }
}) })
@@ -1708,6 +1711,8 @@ export function SyncProvider(props: {
const resyncingDirectoriesRef = useRef(new Set<string>()) const resyncingDirectoriesRef = useRef(new Set<string>())
const statusPollingDirectoriesRef = useRef(new Set<string>()) const statusPollingDirectoriesRef = useRef(new Set<string>())
const pipelineReconnectRef = useRef<((reason?: string) => void) | null>(null) const pipelineReconnectRef = useRef<((reason?: string) => void) | null>(null)
const pipelineHasConnectedRef = useRef(false)
const pipelineDisconnectedBeforeFirstConnectRef = useRef(false)
const system = useMemo<SyncSystem>( const system = useMemo<SyncSystem>(
() => ({ () => ({
@@ -1909,6 +1914,11 @@ export function SyncProvider(props: {
hasEverConnected: true, hasEverConnected: true,
connectionPhase: "connected", connectionPhase: "connected",
}) })
const isFirstConnect = !pipelineHasConnectedRef.current
pipelineHasConnectedRef.current = true
if (isFirstConnect && !pipelineDisconnectedBeforeFirstConnectRef.current) {
return
}
if (isRecentBoot()) { if (isRecentBoot()) {
return return
} }
@@ -1917,6 +1927,9 @@ export function SyncProvider(props: {
} }
}, },
onDisconnect: (reason) => { onDisconnect: (reason) => {
if (!pipelineHasConnectedRef.current) {
pipelineDisconnectedBeforeFirstConnectRef.current = true
}
const { hasEverConnected } = useConfigStore.getState() const { hasEverConnected } = useConfigStore.getState()
useConfigStore.setState({ useConfigStore.setState({
isConnected: false, isConnected: false,