From 3a6d49d3fc6be341e66918bb4a67d1ee4e72ed75 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Tue, 11 Mar 2025 10:07:24 +0800 Subject: [PATCH] feat(ReduxService): Implement comprehensive Redux state management service for main process --- src/main/services/ReduxService.ts | 219 ++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 src/main/services/ReduxService.ts diff --git a/src/main/services/ReduxService.ts b/src/main/services/ReduxService.ts new file mode 100644 index 00000000..2cba6b5a --- /dev/null +++ b/src/main/services/ReduxService.ts @@ -0,0 +1,219 @@ +import { ipcMain } from 'electron' +import { EventEmitter } from 'events' + +import { windowService } from './WindowService' + +type StoreValue = any +type Unsubscribe = () => void + +export class ReduxService extends EventEmitter { + private stateCache: any = {} + private isReady = false + + constructor() { + super() + this.setupIpcHandlers() + } + + private setupIpcHandlers() { + // 监听 store 就绪事件 + ipcMain.handle('redux-store-ready', () => { + this.isReady = true + this.emit('ready') + }) + + // 监听 store 状态变化 + ipcMain.on('redux-state-change', (_, newState) => { + this.stateCache = newState + this.emit('stateChange', newState) + }) + } + + private async waitForStoreReady(webContents: Electron.WebContents, timeout = 10000): Promise { + 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(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(selector: string): Promise { + try { + // 如果已经准备就绪,先尝试从缓存中获取 + if (this.isReady) { + const cachedValue = this.selectSync(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 { + 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 { + 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('redux-state-change', 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('stateChange', handler) + return () => { + this.off('stateChange', handler) + } + } + + // 获取整个状态树 + async getState(): Promise { + 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 { + for (const action of actions) { + await this.dispatch(action) + } + } +} + +export const reduxService = new ReduxService() + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +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) + } +}