Migrate from IndexedDB to file-based storage with cross-instance sync
This commit is contained in:
@@ -2,6 +2,7 @@ import { app, BrowserWindow, dialog, ipcMain } from "electron"
|
||||
import { join } from "path"
|
||||
import { createApplicationMenu } from "./menu"
|
||||
import { setupInstanceIPC } from "./ipc"
|
||||
import { setupStorageIPC } from "./storage"
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
@@ -27,6 +28,7 @@ function createWindow() {
|
||||
|
||||
createApplicationMenu(mainWindow)
|
||||
setupInstanceIPC(mainWindow)
|
||||
setupStorageIPC()
|
||||
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow = null
|
||||
|
||||
121
electron/main/storage.ts
Normal file
121
electron/main/storage.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { app, ipcMain } from "electron"
|
||||
import { join } from "path"
|
||||
import { readFile, writeFile, mkdir, unlink, stat } from "fs/promises"
|
||||
import { existsSync } from "fs"
|
||||
|
||||
const CONFIG_DIR = join(app.getPath("home"), ".config", "opencode-client")
|
||||
const CONFIG_FILE = join(CONFIG_DIR, "config.json")
|
||||
const INSTANCES_DIR = join(CONFIG_DIR, "instances")
|
||||
|
||||
// File watching for config changes
|
||||
let configWatchers = new Set<number>()
|
||||
let configLastModified = 0
|
||||
let configCache: string | null = null
|
||||
|
||||
async function ensureDirectories() {
|
||||
try {
|
||||
await mkdir(CONFIG_DIR, { recursive: true })
|
||||
await mkdir(INSTANCES_DIR, { recursive: true })
|
||||
} catch (error) {
|
||||
console.error("Failed to create directories:", error)
|
||||
}
|
||||
}
|
||||
|
||||
async function readConfigWithCache(): Promise<string> {
|
||||
try {
|
||||
const stats = await stat(CONFIG_FILE)
|
||||
const currentModified = stats.mtime.getTime()
|
||||
|
||||
// If file hasn't been modified since last read, return cache
|
||||
if (configCache && configLastModified >= currentModified) {
|
||||
return configCache
|
||||
}
|
||||
|
||||
const content = await readFile(CONFIG_FILE, "utf-8")
|
||||
configCache = content
|
||||
configLastModified = currentModified
|
||||
return content
|
||||
} catch (error) {
|
||||
// File doesn't exist or can't be read
|
||||
configCache = null
|
||||
configLastModified = 0
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function invalidateConfigCache() {
|
||||
configCache = null
|
||||
configLastModified = 0
|
||||
}
|
||||
|
||||
export function setupStorageIPC() {
|
||||
ensureDirectories()
|
||||
|
||||
ipcMain.handle("storage:getConfigPath", () => CONFIG_FILE)
|
||||
ipcMain.handle("storage:getInstancesDir", () => INSTANCES_DIR)
|
||||
|
||||
ipcMain.handle("storage:readConfigFile", async () => {
|
||||
try {
|
||||
return await readConfigWithCache()
|
||||
} catch (error) {
|
||||
// Return empty config if file doesn't exist
|
||||
return JSON.stringify({ preferences: { showThinkingBlocks: false }, recentFolders: [] }, null, 2)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle("storage:writeConfigFile", async (_, content: string) => {
|
||||
try {
|
||||
await writeFile(CONFIG_FILE, content, "utf-8")
|
||||
invalidateConfigCache()
|
||||
|
||||
// Notify other renderer processes about config change
|
||||
const windows = require("electron").BrowserWindow.getAllWindows()
|
||||
windows.forEach((win: any) => {
|
||||
if (win.webContents && !win.webContents.isDestroyed()) {
|
||||
win.webContents.send("storage:configChanged")
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to write config file:", error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle("storage:readInstanceFile", async (_, filename: string) => {
|
||||
const instanceFile = join(INSTANCES_DIR, `${filename}.json`)
|
||||
try {
|
||||
return await readFile(instanceFile, "utf-8")
|
||||
} catch (error) {
|
||||
// Return empty instance data if file doesn't exist
|
||||
return JSON.stringify({ messageHistory: [] }, null, 2)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle("storage:writeInstanceFile", async (_, filename: string, content: string) => {
|
||||
const instanceFile = join(INSTANCES_DIR, `${filename}.json`)
|
||||
try {
|
||||
await writeFile(instanceFile, content, "utf-8")
|
||||
} catch (error) {
|
||||
console.error(`Failed to write instance file for ${filename}:`, error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle("storage:deleteInstanceFile", async (_, filename: string) => {
|
||||
const instanceFile = join(INSTANCES_DIR, `${filename}.json`)
|
||||
try {
|
||||
if (existsSync(instanceFile)) {
|
||||
await unlink(instanceFile)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete instance file for ${filename}:`, error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Clean up on app quit
|
||||
app.on("before-quit", () => {
|
||||
configCache = null
|
||||
configLastModified = 0
|
||||
})
|
||||
@@ -15,6 +15,14 @@ export interface ElectronAPI {
|
||||
) => void
|
||||
onNewInstance: (callback: () => void) => void
|
||||
scanDirectory: (workspaceFolder: string) => Promise<string[]>
|
||||
// Storage operations
|
||||
getConfigPath: () => string
|
||||
getInstancesDir: () => string
|
||||
readConfigFile: () => Promise<string>
|
||||
writeConfigFile: (content: string) => Promise<void>
|
||||
readInstanceFile: (instanceId: string) => Promise<string>
|
||||
writeInstanceFile: (instanceId: string, content: string) => Promise<void>
|
||||
deleteInstanceFile: (instanceId: string) => Promise<void>
|
||||
}
|
||||
|
||||
const electronAPI: ElectronAPI = {
|
||||
@@ -37,6 +45,18 @@ const electronAPI: ElectronAPI = {
|
||||
ipcRenderer.on("menu:newInstance", () => callback())
|
||||
},
|
||||
scanDirectory: (workspaceFolder: string) => ipcRenderer.invoke("fs:scanDirectory", workspaceFolder),
|
||||
// Storage operations
|
||||
getConfigPath: () => ipcRenderer.invoke("storage:getConfigPath"),
|
||||
getInstancesDir: () => ipcRenderer.invoke("storage:getInstancesDir"),
|
||||
readConfigFile: () => ipcRenderer.invoke("storage:readConfigFile"),
|
||||
writeConfigFile: (content: string) => ipcRenderer.invoke("storage:writeConfigFile", content),
|
||||
readInstanceFile: (filename: string) => ipcRenderer.invoke("storage:readInstanceFile", filename),
|
||||
writeInstanceFile: (filename: string, content: string) =>
|
||||
ipcRenderer.invoke("storage:writeInstanceFile", filename, content),
|
||||
deleteInstanceFile: (filename: string) => ipcRenderer.invoke("storage:deleteInstanceFile", filename),
|
||||
onConfigChanged: (callback: () => void) => {
|
||||
ipcRenderer.on("storage:configChanged", () => callback())
|
||||
},
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld("electronAPI", electronAPI)
|
||||
|
||||
Reference in New Issue
Block a user