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:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
088a70fe5a
commit
1505274f94
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user