fix(chat): save previous-session anchor in microtask with bail check

When switching sessions, the previous session's viewport anchor save
was deferred via setTimeout(..., 0). This races with the new session's
restoreSnapshot effect: the timer can fire after React has flushed
the new session's render and before the restore effect runs, leaving
the saved anchor and the restored scroll position fighting over the
same viewport store entry. The save reads messages (can be expensive)
on the same tick as the new session's skeleton render.

Replace setTimeout(..., 0) with queueMicrotask() so the save runs
immediately after the current synchronous call stack and before the
next macrotask / paint. This guarantees the save completes before the
new session's restoreSnapshot effect fires.

Add a bail check: if the user switched sessions again between the
microtask scheduling and execution (rapid switching), the save is
now stale. Comparing the captured newId to the current currentSessionId
at microtask runtime avoids clobbering the in-flight session's anchor
with data from a session that is no longer "previous".

This is the queueMicrotask + bail change acknowledged as 'great' in
the review of #1675, extracted as a focused single-file PR.
This commit is contained in:
herjarsa
2026-07-09 21:55:57 +02:00
parent a1aae30e66
commit 83d4bc7b59
+11 -2
View File
@@ -617,7 +617,16 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
// skeleton to render and reads messages which can be expensive.
if (previousSessionId && previousSessionId !== id) {
const prevId = previousSessionId
setTimeout(() => {
const newId = id
// queueMicrotask runs after the current synchronous call stack (and
// before the next macrotask / setTimeout(0) / paint), so the previous
// session's anchor is saved before the new session's restoreSnapshot
// effect fires. This eliminates the race where save and restore
// interleave against the same viewport store entry.
queueMicrotask(() => {
// Bail if the user already switched again — save is now stale.
const current = get().currentSessionId
if (current !== newId) return
const memState = getViewportSessionMemory(prevId)
if (!memState?.isStreaming) {
const prevMessages = getSyncMessages(prevId)
@@ -625,7 +634,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
useViewportStore.getState().updateViewportAnchor(prevId, prevMessages.length - 1)
}
}
}, 0)
});
}
// Mark session viewed in notification store + update active session ref