feat: implement store synchronization across windows (#5592)
- Added new IPC channels for store synchronization: StoreSync_Subscribe, StoreSync_Unsubscribe, StoreSync_OnUpdate, and StoreSync_BroadcastSync. - Integrated store sync service in various components, including the main IPC handler and renderer store. - Removed the MiniWindowReload IPC channel as it was no longer needed. - Updated the store configuration to support synchronization of specific state slices.
This commit is contained in:
+4
-1
@@ -24,6 +24,7 @@ import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import { ProxyConfig, proxyManager } from './services/ProxyManager'
|
||||
import { searchService } from './services/SearchService'
|
||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||
import storeSyncService from './services/StoreSyncService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { setOpenLinkExternal } from './services/WebviewService'
|
||||
import { windowService } from './services/WindowService'
|
||||
@@ -31,7 +32,6 @@ import { getResourcePath } from './utils'
|
||||
import { decrypt, encrypt } from './utils/aes'
|
||||
import { getConfigDir, getFilesDir } from './utils/file'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
|
||||
const fileManager = new FileStorage()
|
||||
const backupManager = new BackupManager()
|
||||
const exportService = new ExportService(fileManager)
|
||||
@@ -338,4 +338,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
|
||||
setOpenLinkExternal(webviewId, isExternal)
|
||||
)
|
||||
|
||||
// store sync
|
||||
storeSyncService.registerIpcHandler()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import type { StoreSyncAction } from '@types'
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
|
||||
/**
|
||||
* StoreSyncService class manages Redux store synchronization between multiple windows in the main process
|
||||
* It uses singleton pattern to ensure only one sync service instance exists in the application
|
||||
*
|
||||
* Main features:
|
||||
* 1. Manages window subscriptions for store sync
|
||||
* 2. Handles IPC communication for store sync between windows
|
||||
* 3. Broadcasts Redux actions from one window to all other windows
|
||||
* 4. Adds metadata to synced actions to prevent infinite sync loops
|
||||
*/
|
||||
export class StoreSyncService {
|
||||
private static instance: StoreSyncService
|
||||
private windowIds: number[] = []
|
||||
private isIpcHandlerRegistered = false
|
||||
|
||||
private constructor() {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance of StoreSyncService
|
||||
*/
|
||||
public static getInstance(): StoreSyncService {
|
||||
if (!StoreSyncService.instance) {
|
||||
StoreSyncService.instance = new StoreSyncService()
|
||||
}
|
||||
return StoreSyncService.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe a window to store sync
|
||||
* @param windowId ID of the window to subscribe
|
||||
*/
|
||||
public subscribe(windowId: number): void {
|
||||
if (!this.windowIds.includes(windowId)) {
|
||||
this.windowIds.push(windowId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe a window from store sync
|
||||
* @param windowId ID of the window to unsubscribe
|
||||
*/
|
||||
public unsubscribe(windowId: number): void {
|
||||
this.windowIds = this.windowIds.filter((id) => id !== windowId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register IPC handlers for store sync communication
|
||||
* Handles window subscription, unsubscription and action broadcasting
|
||||
*/
|
||||
public registerIpcHandler(): void {
|
||||
if (this.isIpcHandlerRegistered) return
|
||||
|
||||
ipcMain.handle(IpcChannel.StoreSync_Subscribe, (event) => {
|
||||
const windowId = BrowserWindow.fromWebContents(event.sender)?.id
|
||||
if (windowId) {
|
||||
this.subscribe(windowId)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.StoreSync_Unsubscribe, (event) => {
|
||||
const windowId = BrowserWindow.fromWebContents(event.sender)?.id
|
||||
if (windowId) {
|
||||
this.unsubscribe(windowId)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.StoreSync_OnUpdate, (event, action: StoreSyncAction) => {
|
||||
const sourceWindowId = BrowserWindow.fromWebContents(event.sender)?.id
|
||||
|
||||
if (!sourceWindowId) return
|
||||
|
||||
// Broadcast the action to all other windows
|
||||
this.broadcastToOtherWindows(sourceWindowId, action)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a Redux action to all other windows except the source
|
||||
* @param sourceWindowId ID of the window that originated the action
|
||||
* @param action Redux action to broadcast
|
||||
*/
|
||||
private broadcastToOtherWindows(sourceWindowId: number, action: StoreSyncAction): void {
|
||||
// Add metadata to indicate this action came from sync
|
||||
const syncAction = {
|
||||
...action,
|
||||
meta: {
|
||||
...action.meta,
|
||||
fromSync: true,
|
||||
source: `windowId:${sourceWindowId}`
|
||||
}
|
||||
}
|
||||
|
||||
// Send to all windows except the source
|
||||
this.windowIds.forEach((windowId) => {
|
||||
if (windowId !== sourceWindowId) {
|
||||
const targetWindow = BrowserWindow.fromId(windowId)
|
||||
if (targetWindow && !targetWindow.isDestroyed()) {
|
||||
targetWindow.webContents.send(IpcChannel.StoreSync_BroadcastSync, syncAction)
|
||||
} else {
|
||||
this.unsubscribe(windowId)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export default StoreSyncService.getInstance()
|
||||
@@ -3,7 +3,7 @@ import { isDev, isLinux, isMac, isWin } from '@main/constant'
|
||||
import { getFilesDir } from '@main/utils/file'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { ThemeMode } from '@types'
|
||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, nativeTheme, shell } from 'electron'
|
||||
import { app, BrowserWindow, Menu, MenuItem, nativeTheme, shell } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
@@ -484,10 +484,6 @@ export class WindowService {
|
||||
this.miniWindow?.webContents.send(IpcChannel.ShowMiniWindow)
|
||||
})
|
||||
|
||||
ipcMain.on(IpcChannel.MiniWindowReload, () => {
|
||||
this.miniWindow?.reload()
|
||||
})
|
||||
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
this.miniWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/src/windows/mini/index.html')
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user