perf(stores): defer safeStorage writes off the interaction path (#1941)

* perf(stores): defer safeStorage writes off the interaction path

Session switches funnel every persisted store slice through safeStorage.setItem,
and doing those large JSON.stringify writes synchronously blocked the main
thread for over a second. Add a write-behind buffer that:

- Defers each setItem/removeItem to a later task via setTimeout(0) so the
  click-to-paint path is not blocked.
- Coalesces repeated writes to the same key into a single backing flush.
- Serves pending values from memory so read-after-write stays consistent
  within the deferral window.
- Flushes synchronously on pagehide/beforeunload/visibilitychange/freeze so
  deferred state survives tab close, reload, and the mobile freeze lifecycle.

Adds a test covering write deferral, coalescing, and pending read serving.

* fix(stores): defer persisted JSON serialization

* fix(stores): defer direct safeStorage writes

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Tom Rochette
2026-06-30 11:47:52 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 088a70fe5a
commit 1505274f94
32 changed files with 406 additions and 83 deletions
+3 -3
View File
@@ -4,8 +4,8 @@
*/
import { create } from "zustand"
import { persist, createJSONStorage } from "zustand/middleware"
import { getSafeStorage } from "@/stores/utils/safeStorage"
import { persist } from "zustand/middleware"
import { createDeferredSafeJSONStorage } from "@/stores/utils/safeStorage"
type ModelSelection = { providerId: string; modelId: string }
type LastUsedProvider = { providerID: string; modelID: string }
@@ -126,7 +126,7 @@ export const useSelectionStore = create<SelectionState>()(
{
name: "selection-store",
version: 1,
storage: createJSONStorage(() => getSafeStorage()),
storage: createDeferredSafeJSONStorage(),
partialize: (state) => {
// Convert Maps to arrays and slice to keep only the most recent MAX_PERSISTED_SESSIONS
const models = Array.from(state.sessionModelSelections.entries()).slice(-MAX_PERSISTED_SESSIONS)
+4 -4
View File
@@ -25,7 +25,7 @@ import { useDirectoryStore } from "@/stores/useDirectoryStore"
import { useSessionFoldersStore } from "@/stores/useSessionFoldersStore"
import { useCommandsStore } from "@/stores/useCommandsStore"
import { useSkillsStore } from "@/stores/useSkillsStore"
import { getSafeStorage } from "@/stores/utils/safeStorage"
import { getDeferredSafeStorage } from "@/stores/utils/safeStorage"
import { markPendingUserSendAnimation } from "@/lib/userSendAnimation"
import { flattenAssistantTextParts } from "@/lib/messages/messageText"
import { composeForkSessionMessage } from "@/lib/messages/executionMeta"
@@ -327,7 +327,7 @@ const resolveDirectoryKey = (session: Session): string | null => {
?? normalizePath(sessionRecord.project?.worktree ?? null)
}
const safeStorage = getSafeStorage()
const safeStorage = getDeferredSafeStorage()
const DRAFT_TARGET_STORAGE_KEY = "oc.chatInput.lastDraftTarget"
type PersistedDraftTarget = { projectId: string | null; directory: string | null }
@@ -515,7 +515,7 @@ const WORKTREE_MAP_STORAGE_KEY = 'oc.worktreeMap'
const loadPersistedWorktreeMap = (): Map<string, WorktreeMetadata[]> => {
try {
const raw = getSafeStorage().getItem(WORKTREE_MAP_STORAGE_KEY)
const raw = getDeferredSafeStorage().getItem(WORKTREE_MAP_STORAGE_KEY)
if (!raw) return new Map()
const entries = JSON.parse(raw) as Array<[string, WorktreeMetadata[]]>
if (!Array.isArray(entries)) return new Map()
@@ -529,7 +529,7 @@ const loadPersistedWorktreeMap = (): Map<string, WorktreeMetadata[]> => {
const persistWorktreeMap = (map: Map<string, WorktreeMetadata[]>): void => {
try {
getSafeStorage().setItem(WORKTREE_MAP_STORAGE_KEY, JSON.stringify([...map.entries()]))
getDeferredSafeStorage().setItem(WORKTREE_MAP_STORAGE_KEY, JSON.stringify([...map.entries()]))
} catch {
// quota / serialization error — ignore; discovery still refreshes at runtime
}