cherry-studio/src/main/services/ReduxService.ts
Hamm 10f4fde0e3 refactor(Constants): 优化一些常量和枚举值 (#3773)
* refactor(main): 使用枚举管理 IPC 通道

- 新增 IpcChannel 枚举,用于统一管理所有的 IPC 通道
- 修改相关代码,使用 IpcChannel 枚举替代硬编码的字符串通道名称
- 此改动有助于提高代码的可维护性和可读性,避免因通道名称变更导致的错误

* refactor(ipc): 将字符串通道名称替换为 IpcChannel 枚举

- 在多个文件中将硬编码的字符串通道名称替换为 IpcChannel 枚举值
- 更新了相关文件的导入,增加了对 IpcChannel 的引用
- 通过使用枚举来管理 IPC 通道名称,提高了代码的可维护性和可读性

* refactor(ipc): 调整 IPC 通道枚举和预加载脚本

- 移除了 IpcChannel 枚举中的未使用注释
- 更新了预加载脚本中 IpcChannel 的导入路径

* refactor(ipc): 更新 IpcChannel导入路径

- 将 IpcChannel 的导入路径从 @main/enum/IpcChannel 修改为 @shared/IpcChannel
- 此修改涉及多个文件,包括 AppUpdater、BackupManager、EditMcpJsonPopup 等
- 同时移除了 tsconfig.web.json 中对 src/main/**/* 的引用

* refactor(ipc): 添加 ReduxStoreReady 事件并更新事件监听

- 在 IpcChannel 枚举中添加 ReduxStoreReady 事件
- 更新 ReduxService 中的事件监听,使用新的枚举值

* refactor(main): 重构 ReduxService 中的状态变化事件处理

- 将状态变化事件名称定义为常量 STATUS_CHANGE_EVENT
- 更新事件监听和触发使用新的常量
- 优化了代码结构,提高了可维护性

* refactor(i18n): 优化国际化配置和语言选择逻辑

- 在多个文件中引入 defaultLanguage 常量,统一默认语言设置
- 调整 i18n 初始化和语言变更逻辑,使用新配置
- 更新相关组件和 Hook 中的语言选择逻辑

* refactor(ConfigManager): 重构配置管理器

- 添加 ConfigKeys 枚举,用于统一配置项的键名
- 引入 defaultLanguage,作为默认语言设置
- 重构 get 和 set 方法,使用 ConfigKeys 枚举作为键名
- 优化类型定义和方法签名,提高代码可读性和可维护性

* refactor(ConfigManager): 重命名配置键 ZoomFactor

将配置键 zoomFactor 重命名为 ZoomFactor,以符合命名规范。
更新了相关方法和属性以反映这一变更。

* refactor(shared): 重构常量定义并优化文件大小格式化逻辑

- 在 constant.ts 中添加 KB、MB、GB 常量定义
- 将 defaultLanguage 移至 constant.ts
- 更新 ConfigManager、useAppInit、i18n、GeneralSettings 等文件中的导入路径
- 优化 formatFileSize 函数,使用新定义的常量

* refactor(FileSize): 使用 GB/MB/KB 等常量处理文件大小计算

* refactor(ipc): 将字符串通道名称替换为 IpcChannel 枚举

- 在多个文件中将硬编码的字符串通道名称替换为 IpcChannel 枚举值
- 更新了相关文件的导入,增加了对 IpcChannel 的引用
- 通过使用枚举来管理 IPC 通道名称,提高了代码的可维护性和可读性

* refactor(ipc): 更新 IpcChannel导入路径

- 将 IpcChannel 的导入路径从 @main/enum/IpcChannel 修改为 @shared/IpcChannel
- 此修改涉及多个文件,包括 AppUpdater、BackupManager、EditMcpJsonPopup 等
- 同时移除了 tsconfig.web.json 中对 src/main/**/* 的引用

* refactor(i18n): 优化国际化配置和语言选择逻辑

- 在多个文件中引入 defaultLanguage 常量,统一默认语言设置
- 调整 i18n 初始化和语言变更逻辑,使用新配置
- 更新相关组件和 Hook 中的语言选择逻辑

* refactor(shared): 重构常量定义并优化文件大小格式化逻辑

- 在 constant.ts 中添加 KB、MB、GB 常量定义
- 将 defaultLanguage 移至 constant.ts
- 更新 ConfigManager、useAppInit、i18n、GeneralSettings 等文件中的导入路径
- 优化 formatFileSize 函数,使用新定义的常量

* refactor: 移除重复的导入语句

- 在 HomeWindow.tsx 和 useAppInit.ts 文件中移除了重复的 defaultLanguage导入语句
- 这个改动简化了代码结构,提高了代码的可读性和维护性
2025-04-04 19:07:23 +08:00

228 lines
6.1 KiB
TypeScript

import { ipcMain } from 'electron'
import { EventEmitter } from 'events'
import { windowService } from './WindowService'
import { IpcChannel } from '@shared/IpcChannel'
type StoreValue = any
type Unsubscribe = () => void
export class ReduxService extends EventEmitter {
private stateCache: any = {}
private isReady = false
private readonly STATUS_CHANGE_EVENT = 'statusChange'
constructor() {
super()
this.setupIpcHandlers()
}
private setupIpcHandlers() {
// 监听 store 就绪事件
ipcMain.handle(IpcChannel.ReduxStoreReady, () => {
this.isReady = true
this.emit('ready')
})
// 监听 store 状态变化
ipcMain.on(IpcChannel.ReduxStateChange, (_, newState) => {
this.stateCache = newState
this.emit(this.STATUS_CHANGE_EVENT, newState)
})
}
private async waitForStoreReady(webContents: Electron.WebContents, timeout = 10000): Promise<void> {
if (this.isReady) return
const startTime = Date.now()
while (Date.now() - startTime < timeout) {
try {
const isReady = await webContents.executeJavaScript(`
!!window.store && typeof window.store.getState === 'function'
`)
if (isReady) {
this.isReady = true
return
}
} catch (error) {
// 忽略错误,继续等待
}
await new Promise((resolve) => setTimeout(resolve, 100))
}
throw new Error('Timeout waiting for Redux store to be ready')
}
// 添加同步获取状态的方法
getStateSync() {
return this.stateCache
}
// 添加同步选择器方法
selectSync<T = StoreValue>(selector: string): T | undefined {
try {
// 使用 Function 构造器来安全地执行选择器
const selectorFn = new Function('state', `return ${selector}`)
return selectorFn(this.stateCache)
} catch (error) {
console.error('Failed to select from cache:', error)
return undefined
}
}
// 修改 select 方法,优先使用缓存
async select<T = StoreValue>(selector: string): Promise<T> {
try {
// 如果已经准备就绪,先尝试从缓存中获取
if (this.isReady) {
const cachedValue = this.selectSync<T>(selector)
if (cachedValue !== undefined) {
return cachedValue
}
}
// 如果缓存中没有,再从渲染进程获取
const mainWindow = windowService.getMainWindow()
if (!mainWindow) {
throw new Error('Main window is not available')
}
await this.waitForStoreReady(mainWindow.webContents)
return await mainWindow.webContents.executeJavaScript(`
(() => {
const state = window.store.getState();
return ${selector};
})()
`)
} catch (error) {
console.error('Failed to select store value:', error)
throw error
}
}
// 派发 action
async dispatch(action: any): Promise<void> {
const mainWindow = windowService.getMainWindow()
if (!mainWindow) {
throw new Error('Main window is not available')
}
await this.waitForStoreReady(mainWindow.webContents)
try {
await mainWindow.webContents.executeJavaScript(`
window.store.dispatch(${JSON.stringify(action)})
`)
} catch (error) {
console.error('Failed to dispatch action:', error)
throw error
}
}
// 订阅状态变化
async subscribe(selector: string, callback: (newValue: any) => void): Promise<Unsubscribe> {
const mainWindow = windowService.getMainWindow()
if (!mainWindow) {
throw new Error('Main window is not available')
}
await this.waitForStoreReady(mainWindow.webContents)
// 在渲染进程中设置监听
await mainWindow.webContents.executeJavaScript(
`
if (!window._storeSubscriptions) {
window._storeSubscriptions = new Set();
// 设置全局状态变化监听
const unsubscribe = window.store.subscribe(() => {
const state = window.store.getState();
window.electron.ipcRenderer.send('` +
IpcChannel.ReduxStateChange +
`', state);
});
window._storeSubscriptions.add(unsubscribe);
}
`
)
// 在主进程中处理回调
const handler = async () => {
try {
const newValue = await this.select(selector)
callback(newValue)
} catch (error) {
console.error('Error in subscription handler:', error)
}
}
this.on(this.STATUS_CHANGE_EVENT, handler)
return () => {
this.off(this.STATUS_CHANGE_EVENT, handler)
}
}
// 获取整个状态树
async getState(): Promise<any> {
const mainWindow = windowService.getMainWindow()
if (!mainWindow) {
throw new Error('Main window is not available')
}
await this.waitForStoreReady(mainWindow.webContents)
try {
return await mainWindow.webContents.executeJavaScript(`
window.store.getState()
`)
} catch (error) {
console.error('Failed to get state:', error)
throw error
}
}
// 批量执行 actions
async batch(actions: any[]): Promise<void> {
for (const action of actions) {
await this.dispatch(action)
}
}
}
export const reduxService = new ReduxService()
/** example
async function example() {
try {
// 读取状态
const settings = await reduxService.select('state.settings')
console.log('settings', settings)
// 派发 action
await reduxService.dispatch({
type: 'settings/updateApiKey',
payload: 'new-api-key'
})
// 订阅状态变化
const unsubscribe = await reduxService.subscribe('state.settings.apiKey', (newValue) => {
console.log('API key changed:', newValue)
})
// 批量执行 actions
await reduxService.batch([
{ type: 'action1', payload: 'data1' },
{ type: 'action2', payload: 'data2' }
])
// 同步方法虽然可能不是最新的数据,但响应更快
const apiKey = reduxService.selectSync('state.settings.apiKey')
console.log('apiKey', apiKey)
// 处理保证是最新的数据
const apiKey1 = await reduxService.select('state.settings.apiKey')
console.log('apiKey1', apiKey1)
// 取消订阅
unsubscribe()
} catch (error) {
console.error('Error:', error)
}
}
*/