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:
@@ -202,19 +202,30 @@ const useNativeMobileChrome = (): void => {
|
||||
};
|
||||
|
||||
const useNativeMobileLifecycle = (onResume: () => void): void => {
|
||||
const wasInactiveRef = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isCapacitorMobileApp()) return;
|
||||
|
||||
let disposed = false;
|
||||
const cleanup: Array<() => void> = [];
|
||||
const resumeAfterInactive = () => {
|
||||
if (!wasInactiveRef.current) return;
|
||||
wasInactiveRef.current = false;
|
||||
onResume();
|
||||
};
|
||||
|
||||
void import('@capacitor/app').then(async ({ App }) => {
|
||||
if (disposed) return;
|
||||
const state = await App.addListener('appStateChange', ({ 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) {
|
||||
void state.remove();
|
||||
void resume.remove();
|
||||
|
||||
@@ -1169,6 +1169,10 @@ export async function fetchMessagesForSession(sessionID: string, directory?: str
|
||||
// can't repopulate (and un-evict) a session already navigated away from.
|
||||
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) => {
|
||||
const materialized = materializeSessionSnapshots(
|
||||
state,
|
||||
@@ -1179,6 +1183,7 @@ export async function fetchMessagesForSession(sessionID: string, directory?: str
|
||||
})),
|
||||
{ skipPartTypes: MESSAGE_REFETCH_SKIP_PARTS },
|
||||
)
|
||||
if (!materialized.messagesChanged && !materialized.partsChanged) return state
|
||||
return { message: materialized.message, part: materialized.part }
|
||||
})
|
||||
} catch {
|
||||
|
||||
@@ -277,15 +277,18 @@ async function materializeSessionFromServer(
|
||||
if (options?.isStale?.()) return
|
||||
|
||||
store.setState((state: DirectoryStore) => {
|
||||
const materialized = materializeSessionSnapshots(
|
||||
state,
|
||||
sessionID,
|
||||
const materialized = materializeSessionSnapshots(
|
||||
state,
|
||||
sessionID,
|
||||
records.map((record: { info: Message; parts?: Part[] }) => ({
|
||||
info: stripMessageDiffSnapshots(record.info),
|
||||
parts: record.parts ?? [],
|
||||
})),
|
||||
})),
|
||||
{ skipPartTypes: RECONNECT_SKIP_PARTS },
|
||||
)
|
||||
if (!materialized.messagesChanged && !materialized.partsChanged) {
|
||||
return state
|
||||
}
|
||||
return { message: materialized.message, part: materialized.part }
|
||||
})
|
||||
|
||||
@@ -1705,9 +1708,11 @@ export function SyncProvider(props: {
|
||||
const lastStatusPollAtByDirectoryRef = useRef(new Map<string, number>())
|
||||
const lastFullResyncAtByDirectoryRef = useRef(new Map<string, number>())
|
||||
const lastChildDiscoveryAtByDirectoryRef = useRef(new Map<string, number>())
|
||||
const resyncingDirectoriesRef = useRef(new Set<string>())
|
||||
const statusPollingDirectoriesRef = useRef(new Set<string>())
|
||||
const pipelineReconnectRef = useRef<((reason?: string) => void) | null>(null)
|
||||
const resyncingDirectoriesRef = useRef(new Set<string>())
|
||||
const statusPollingDirectoriesRef = useRef(new Set<string>())
|
||||
const pipelineReconnectRef = useRef<((reason?: string) => void) | null>(null)
|
||||
const pipelineHasConnectedRef = useRef(false)
|
||||
const pipelineDisconnectedBeforeFirstConnectRef = useRef(false)
|
||||
|
||||
const system = useMemo<SyncSystem>(
|
||||
() => ({
|
||||
@@ -1903,23 +1908,31 @@ export function SyncProvider(props: {
|
||||
}
|
||||
handleEvent(directory, payload, childStores, routingIndex)
|
||||
},
|
||||
onReconnect: () => {
|
||||
useConfigStore.setState({
|
||||
isConnected: true,
|
||||
hasEverConnected: true,
|
||||
connectionPhase: "connected",
|
||||
})
|
||||
if (isRecentBoot()) {
|
||||
return
|
||||
}
|
||||
onReconnect: () => {
|
||||
useConfigStore.setState({
|
||||
isConnected: true,
|
||||
hasEverConnected: true,
|
||||
connectionPhase: "connected",
|
||||
})
|
||||
const isFirstConnect = !pipelineHasConnectedRef.current
|
||||
pipelineHasConnectedRef.current = true
|
||||
if (isFirstConnect && !pipelineDisconnectedBeforeFirstConnectRef.current) {
|
||||
return
|
||||
}
|
||||
if (isRecentBoot()) {
|
||||
return
|
||||
}
|
||||
for (const dir of childStores.children.keys()) {
|
||||
triggerDirectoryResync(dir, "stream-reconnect")
|
||||
}
|
||||
},
|
||||
onDisconnect: (reason) => {
|
||||
const { hasEverConnected } = useConfigStore.getState()
|
||||
useConfigStore.setState({
|
||||
isConnected: false,
|
||||
},
|
||||
onDisconnect: (reason) => {
|
||||
if (!pipelineHasConnectedRef.current) {
|
||||
pipelineDisconnectedBeforeFirstConnectRef.current = true
|
||||
}
|
||||
const { hasEverConnected } = useConfigStore.getState()
|
||||
useConfigStore.setState({
|
||||
isConnected: false,
|
||||
connectionPhase: hasEverConnected ? "reconnecting" : "connecting",
|
||||
lastDisconnectReason: reason,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user