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 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();
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -277,15 +277,18 @@ async function materializeSessionFromServer(
|
|||||||
if (options?.isStale?.()) return
|
if (options?.isStale?.()) return
|
||||||
|
|
||||||
store.setState((state: DirectoryStore) => {
|
store.setState((state: DirectoryStore) => {
|
||||||
const materialized = materializeSessionSnapshots(
|
const materialized = materializeSessionSnapshots(
|
||||||
state,
|
state,
|
||||||
sessionID,
|
sessionID,
|
||||||
records.map((record: { info: Message; parts?: Part[] }) => ({
|
records.map((record: { info: Message; parts?: Part[] }) => ({
|
||||||
info: stripMessageDiffSnapshots(record.info),
|
info: stripMessageDiffSnapshots(record.info),
|
||||||
parts: record.parts ?? [],
|
parts: record.parts ?? [],
|
||||||
})),
|
})),
|
||||||
{ 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 }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1705,9 +1708,11 @@ export function SyncProvider(props: {
|
|||||||
const lastStatusPollAtByDirectoryRef = useRef(new Map<string, number>())
|
const lastStatusPollAtByDirectoryRef = useRef(new Map<string, number>())
|
||||||
const lastFullResyncAtByDirectoryRef = useRef(new Map<string, number>())
|
const lastFullResyncAtByDirectoryRef = useRef(new Map<string, number>())
|
||||||
const lastChildDiscoveryAtByDirectoryRef = useRef(new Map<string, number>())
|
const lastChildDiscoveryAtByDirectoryRef = useRef(new Map<string, number>())
|
||||||
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>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -1903,23 +1908,31 @@ export function SyncProvider(props: {
|
|||||||
}
|
}
|
||||||
handleEvent(directory, payload, childStores, routingIndex)
|
handleEvent(directory, payload, childStores, routingIndex)
|
||||||
},
|
},
|
||||||
onReconnect: () => {
|
onReconnect: () => {
|
||||||
useConfigStore.setState({
|
useConfigStore.setState({
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
hasEverConnected: true,
|
hasEverConnected: true,
|
||||||
connectionPhase: "connected",
|
connectionPhase: "connected",
|
||||||
})
|
})
|
||||||
if (isRecentBoot()) {
|
const isFirstConnect = !pipelineHasConnectedRef.current
|
||||||
return
|
pipelineHasConnectedRef.current = true
|
||||||
}
|
if (isFirstConnect && !pipelineDisconnectedBeforeFirstConnectRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isRecentBoot()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
for (const dir of childStores.children.keys()) {
|
for (const dir of childStores.children.keys()) {
|
||||||
triggerDirectoryResync(dir, "stream-reconnect")
|
triggerDirectoryResync(dir, "stream-reconnect")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDisconnect: (reason) => {
|
onDisconnect: (reason) => {
|
||||||
const { hasEverConnected } = useConfigStore.getState()
|
if (!pipelineHasConnectedRef.current) {
|
||||||
useConfigStore.setState({
|
pipelineDisconnectedBeforeFirstConnectRef.current = true
|
||||||
isConnected: false,
|
}
|
||||||
|
const { hasEverConnected } = useConfigStore.getState()
|
||||||
|
useConfigStore.setState({
|
||||||
|
isConnected: false,
|
||||||
connectionPhase: hasEverConnected ? "reconnecting" : "connecting",
|
connectionPhase: hasEverConnected ? "reconnecting" : "connecting",
|
||||||
lastDisconnectReason: reason,
|
lastDisconnectReason: reason,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user