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:
MyPrototypeWhat 2025-11-26 11:51:36 +08:00
parent aae39e3365
commit 4348c8c4dc
5 changed files with 49 additions and 159 deletions

View File

@ -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

View File

@ -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: '' }
}
/**

View File

@ -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({

View File

@ -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
}
}

View File

@ -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>()