mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
refactor: migrate tabs state from SQLite to localStorage cache
- Remove /app/state/:key API endpoint from apiSchemas - Remove appStateService handler from API handlers - Add Tab/TabsState types to persist cache schema - Refactor useTabs to use usePersistCache instead of useQuery/useMutation - Delete unused routeTree.gen.ts This change improves performance by avoiding unnecessary IPC + DB roundtrips for UI state that can be safely stored in localStorage with 200ms debounce. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
aae39e3365
commit
4348c8c4dc
@ -18,25 +18,6 @@ export type { ConcreteApiPaths } from './apiPaths'
|
||||
* enabling full TypeScript type checking across IPC boundaries.
|
||||
*/
|
||||
export interface ApiSchemas {
|
||||
/**
|
||||
* App State storage endpoint
|
||||
* @example GET /app/state/tabs_state
|
||||
* @example PUT /app/state/tabs_state { "value": {...} }
|
||||
*/
|
||||
'/app/state/:key': {
|
||||
/** Get app state by key */
|
||||
GET: {
|
||||
params: { key: string }
|
||||
response: any
|
||||
}
|
||||
/** Save app state */
|
||||
PUT: {
|
||||
params: { key: string }
|
||||
body: any
|
||||
response: { success: boolean }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test items collection endpoint
|
||||
* @example GET /test/items?page=1&limit=10&search=hello
|
||||
|
||||
24
packages/shared/data/cache/cacheSchemas.ts
vendored
24
packages/shared/data/cache/cacheSchemas.ts
vendored
@ -76,16 +76,36 @@ export const DefaultUseSharedCache: UseSharedCacheSchema = {
|
||||
'example-key': 'example default value'
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab type for browser-like tabs
|
||||
*/
|
||||
export type TabType = 'webview' | 'url' | 'browser'
|
||||
|
||||
export interface Tab {
|
||||
id: string
|
||||
type: TabType
|
||||
url: string
|
||||
title: string
|
||||
icon?: string
|
||||
isKeepAlive?: boolean
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface TabsState {
|
||||
tabs: Tab[]
|
||||
activeTabId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist cache schema defining allowed keys and their value types
|
||||
* This ensures type safety and prevents key conflicts
|
||||
*/
|
||||
export type RendererPersistCacheSchema = {
|
||||
'example-key': string
|
||||
'tabs_state': TabsState
|
||||
}
|
||||
|
||||
export const DefaultRendererPersistCache: RendererPersistCacheSchema = {
|
||||
'example-key': 'example default value'
|
||||
'tabs_state': { tabs: [], activeTabId: '' }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
* TypeScript will error if any endpoint is missing.
|
||||
*/
|
||||
|
||||
import { appStateService } from '@data/services/AppStateService'
|
||||
import { TestService } from '@data/services/TestService'
|
||||
import type { ApiImplementation } from '@shared/data/api/apiSchemas'
|
||||
|
||||
@ -17,18 +16,6 @@ const testService = TestService.getInstance()
|
||||
* Must implement every path+method combination from ApiSchemas
|
||||
*/
|
||||
export const apiHandlers: ApiImplementation = {
|
||||
'/app/state/:key': {
|
||||
GET: async ({ params }) => {
|
||||
const state = await appStateService.getState(params.key)
|
||||
return state
|
||||
},
|
||||
|
||||
PUT: async ({ params, body }) => {
|
||||
await appStateService.setState(params.key, body)
|
||||
return { success: true }
|
||||
}
|
||||
},
|
||||
|
||||
'/test/items': {
|
||||
GET: async ({ query }) => {
|
||||
return await testService.getItems({
|
||||
|
||||
@ -1,78 +1,28 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
import { useMutation, useQuery } from '../data/hooks/useDataApi'
|
||||
import { usePersistCache } from '../data/hooks/useCache'
|
||||
|
||||
const logger = loggerService.withContext('useTabs')
|
||||
|
||||
export type TabType = 'webview' | 'url' | 'browser'
|
||||
|
||||
export interface Tab {
|
||||
id: string
|
||||
type: TabType
|
||||
url: string
|
||||
title: string
|
||||
icon?: string
|
||||
isKeepAlive?: boolean
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
interface TabsState {
|
||||
tabs: Tab[]
|
||||
activeTabId: string
|
||||
}
|
||||
|
||||
const TABS_STORAGE_KEY = 'tabs_state'
|
||||
const DEFAULT_STATE: TabsState = { tabs: [], activeTabId: '' }
|
||||
// Re-export types from shared schema
|
||||
export type { Tab, TabsState, TabType } from '@shared/data/cache/cacheSchemas'
|
||||
import type { Tab } from '@shared/data/cache/cacheSchemas'
|
||||
|
||||
export function useTabs() {
|
||||
// Load state from DB
|
||||
// We cast the path because we haven't fully updated the concrete path types globally yet
|
||||
const {
|
||||
data: tabsState,
|
||||
mutate: mutateState,
|
||||
loading: isLoading
|
||||
} = useQuery(`/app/state/${TABS_STORAGE_KEY}` as any, {
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
fallbackData: DEFAULT_STATE
|
||||
}
|
||||
})
|
||||
const [tabsState, setTabsState] = usePersistCache('tabs_state')
|
||||
|
||||
// Ensure we always have a valid object structure
|
||||
const currentState: TabsState = useMemo(
|
||||
() => (tabsState && typeof tabsState === 'object' ? (tabsState as TabsState) : DEFAULT_STATE),
|
||||
[tabsState]
|
||||
)
|
||||
const tabs = useMemo(() => (Array.isArray(currentState.tabs) ? currentState.tabs : []), [currentState.tabs])
|
||||
const activeTabId = currentState.activeTabId || ''
|
||||
|
||||
// Mutation for saving
|
||||
const saveMutation = useMutation('PUT', `/app/state/${TABS_STORAGE_KEY}` as any)
|
||||
|
||||
// Unified update helper
|
||||
const updateState = useCallback(
|
||||
async (newState: TabsState) => {
|
||||
// 1. Optimistic update local cache
|
||||
await mutateState(newState, { revalidate: false })
|
||||
|
||||
// 2. Sync to DB
|
||||
saveMutation.mutate({ body: newState }).catch((err) => logger.error('Failed to save tabs state:', err))
|
||||
},
|
||||
[mutateState, saveMutation]
|
||||
)
|
||||
const tabs = useMemo(() => tabsState.tabs, [tabsState.tabs])
|
||||
const activeTabId = tabsState.activeTabId
|
||||
|
||||
const addTab = useCallback(
|
||||
(tab: Tab) => {
|
||||
const exists = tabs.find((t) => t.id === tab.id)
|
||||
if (exists) {
|
||||
updateState({ ...currentState, activeTabId: tab.id })
|
||||
setTabsState({ ...tabsState, activeTabId: tab.id })
|
||||
return
|
||||
}
|
||||
const newTabs = [...tabs, tab]
|
||||
updateState({ tabs: newTabs, activeTabId: tab.id })
|
||||
setTabsState({ tabs: newTabs, activeTabId: tab.id })
|
||||
},
|
||||
[tabs, currentState, updateState]
|
||||
[tabs, tabsState, setTabsState]
|
||||
)
|
||||
|
||||
const closeTab = useCallback(
|
||||
@ -81,42 +31,49 @@ export function useTabs() {
|
||||
let newActiveId = activeTabId
|
||||
|
||||
if (activeTabId === id) {
|
||||
// Activate adjacent tab
|
||||
const index = tabs.findIndex((t) => t.id === id)
|
||||
// Try to go left, then right
|
||||
const nextTab = newTabs[index - 1] || newTabs[index]
|
||||
newActiveId = nextTab ? nextTab.id : ''
|
||||
}
|
||||
|
||||
updateState({ tabs: newTabs, activeTabId: newActiveId })
|
||||
setTabsState({ tabs: newTabs, activeTabId: newActiveId })
|
||||
},
|
||||
[tabs, activeTabId, updateState]
|
||||
[tabs, activeTabId, setTabsState]
|
||||
)
|
||||
|
||||
const setActiveTab = useCallback(
|
||||
(id: string) => {
|
||||
if (id !== activeTabId) {
|
||||
updateState({ ...currentState, activeTabId: id })
|
||||
setTabsState({ ...tabsState, activeTabId: id })
|
||||
}
|
||||
},
|
||||
[activeTabId, currentState, updateState]
|
||||
[activeTabId, tabsState, setTabsState]
|
||||
)
|
||||
|
||||
const updateTab = useCallback(
|
||||
(id: string, updates: Partial<Tab>) => {
|
||||
const newTabs = tabs.map((t) => (t.id === id ? { ...t, ...updates } : t))
|
||||
updateState({ ...currentState, tabs: newTabs })
|
||||
setTabsState({ ...tabsState, tabs: newTabs })
|
||||
},
|
||||
[tabs, currentState, updateState]
|
||||
[tabs, tabsState, setTabsState]
|
||||
)
|
||||
|
||||
const setTabs = useCallback(
|
||||
(newTabs: Tab[] | ((prev: Tab[]) => Tab[])) => {
|
||||
const resolvedTabs = typeof newTabs === 'function' ? newTabs(tabs) : newTabs
|
||||
setTabsState({ ...tabsState, tabs: resolvedTabs })
|
||||
},
|
||||
[tabs, tabsState, setTabsState]
|
||||
)
|
||||
|
||||
return {
|
||||
tabs,
|
||||
activeTabId,
|
||||
isLoading,
|
||||
isLoading: false,
|
||||
addTab,
|
||||
closeTab,
|
||||
setActiveTab,
|
||||
updateTab
|
||||
updateTab,
|
||||
setTabs
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/'
|
||||
id: '__root__' | '/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute
|
||||
}
|
||||
export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>()
|
||||
Loading…
Reference in New Issue
Block a user