mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
perf(desktop): hand the window its persisted stores before the first render (#50113)
This commit is contained in:
@@ -3,6 +3,7 @@ import { marks } from "./lifecycle/marks"
|
||||
import { app } from "electron"
|
||||
import { acquireApplicationLock, configureApplication } from "./lifecycle/configure"
|
||||
import { startSidecarProbe } from "./service/sidecar-probe"
|
||||
import { registerStorageSnapshotHandler } from "./storage/snapshot"
|
||||
import { createEarlyWindow } from "./windows/early"
|
||||
import { rendererAssetsServed } from "./windows/protocol"
|
||||
import { registerRendererScheme } from "./windows/scheme"
|
||||
@@ -18,6 +19,7 @@ if (acquireApplicationLock()) {
|
||||
// module graph evaluates on the same thread Chromium needs to finish initialising.
|
||||
void app.whenReady().then(async () => {
|
||||
marks.ready = Date.now()
|
||||
registerStorageSnapshotHandler()
|
||||
createEarlyWindow()
|
||||
marks.window = Date.now()
|
||||
startSidecarProbe()
|
||||
|
||||
@@ -4,6 +4,7 @@ import { app, BrowserWindow } from "electron"
|
||||
import { Context, Effect, Layer, Path } from "effect"
|
||||
import { marks } from "../lifecycle/marks"
|
||||
import { openDatabase } from "./database"
|
||||
import { setStorageSnapshotProvider } from "./snapshot"
|
||||
import { createDraftStore } from "./drafts"
|
||||
import { importLegacyStores } from "./legacy"
|
||||
import { createStateStore } from "./state"
|
||||
@@ -41,6 +42,7 @@ export const layer = Layer.effect(
|
||||
storage.close()
|
||||
}),
|
||||
)
|
||||
setStorageSnapshotProvider((names) => Object.fromEntries(names.map((name) => [name, storage.state.items(name)])))
|
||||
marks.storage = Date.now()
|
||||
return Service.of(storage)
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { existsSync, readdirSync } from "node:fs"
|
||||
import path from "node:path"
|
||||
import { DatabaseSync } from "node:sqlite"
|
||||
import { app, ipcMain } from "electron"
|
||||
import { StorageSnapshotChannel, type StorageSnapshot } from "../../shared/ipc-transport"
|
||||
import { isRendererUrl } from "../windows/scheme"
|
||||
|
||||
// A window's preload asks for the namespaces its shell reads before the page runs, so the first
|
||||
// render is the hydrated one. Until the storage layer is up (the first window asks before it exists)
|
||||
// the answer comes from the database file; the layer takes over so later windows see queued writes.
|
||||
type Provider = (names: ReadonlyArray<string>) => StorageSnapshot
|
||||
|
||||
let provider: Provider = readFromDisk
|
||||
|
||||
export function setStorageSnapshotProvider(next: Provider) {
|
||||
provider = next
|
||||
}
|
||||
|
||||
export function registerStorageSnapshotHandler() {
|
||||
ipcMain.handle(StorageSnapshotChannel, (event, names: unknown): StorageSnapshot => {
|
||||
if (!isRendererUrl(event.senderFrame?.url)) return {}
|
||||
if (!Array.isArray(names)) return {}
|
||||
return provider(names.filter((name): name is string => typeof name === "string"))
|
||||
})
|
||||
}
|
||||
|
||||
// Nothing has been written in this process yet, so every namespace is at revision 0, as the
|
||||
// storage layer would report before its first update. Legacy electron-store files still waiting
|
||||
// to be imported would make the database stale for this launch, so the renderer asks the layer then.
|
||||
function readFromDisk(names: ReadonlyArray<string>): StorageSnapshot {
|
||||
const userData = app.getPath("userData")
|
||||
const file = path.join(userData, "drafts.sqlite")
|
||||
if (!existsSync(file)) return {}
|
||||
if (readdirSync(userData).some((name) => name === "default.dat" || /^opencode\..+\.dat$/.test(name))) return {}
|
||||
try {
|
||||
const db = new DatabaseSync(file)
|
||||
try {
|
||||
const rows = db.prepare("SELECT key, value FROM state WHERE name = ?")
|
||||
return Object.fromEntries(
|
||||
names.map((name) => [
|
||||
name,
|
||||
{
|
||||
items: Object.fromEntries(
|
||||
(rows.all(name) as { key: string; value: string }[]).map((row) => [row.key, row.value]),
|
||||
),
|
||||
revision: 0,
|
||||
},
|
||||
]),
|
||||
)
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { DesktopPaths } from "../paths"
|
||||
import { DesktopStorage } from "../storage"
|
||||
import { getStore } from "../storage/store"
|
||||
import { WINDOW_IDS_KEY } from "../storage/keys"
|
||||
import { windowDataFile } from "../../shared/ipc-transport"
|
||||
import { windowArguments } from "./bootstrap"
|
||||
import {
|
||||
getBackgroundColor,
|
||||
@@ -189,12 +190,4 @@ export const makeMainWindows = Effect.fn("Window.make")(function* () {
|
||||
return { create, restore }
|
||||
})
|
||||
|
||||
// Mirrors windowStorage() in packages/app/src/runtime/persistence/storage.ts; it is the state
|
||||
// namespace the renderer persists this window's tabs under.
|
||||
function windowDataFile(id: string) {
|
||||
return `opencode.window.${safeWindowID(id)}.dat`
|
||||
}
|
||||
|
||||
function safeWindowID(id: string) {
|
||||
return id.replace(/[^a-zA-Z0-9._-]/g, "-")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { contextBridge, ipcRenderer, webUtils } from "electron"
|
||||
import { DragCancelEvent, IpcTransportPort } from "../shared/ipc-transport"
|
||||
import {
|
||||
DragCancelEvent,
|
||||
IpcTransportPort,
|
||||
StorageSnapshotChannel,
|
||||
storageSnapshotNames,
|
||||
type StorageSnapshot,
|
||||
} from "../shared/ipc-transport"
|
||||
import { windowBootstrapFromArguments } from "../shared/window-bootstrap"
|
||||
|
||||
ipcRenderer.on(IpcTransportPort, (event) => {
|
||||
@@ -10,9 +16,14 @@ ipcRenderer.on(IpcTransportPort, (event) => {
|
||||
ipcRenderer.on(DragCancelEvent, () => window.dispatchEvent(new Event(DragCancelEvent)))
|
||||
|
||||
const bootstrap = windowBootstrapFromArguments(process.argv)
|
||||
// Asked before the page runs, so the stores the shell reads are hydrated on the first render.
|
||||
const storageSnapshot: Promise<StorageSnapshot> = ipcRenderer
|
||||
.invoke(StorageSnapshotChannel, storageSnapshotNames(bootstrap.id))
|
||||
.catch(() => ({}))
|
||||
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
windowID: bootstrap.id,
|
||||
bootstrap,
|
||||
storageSnapshot,
|
||||
getPathForFile: (file: File) => webUtils.getPathForFile(file),
|
||||
})
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { StorageSnapshot } from "../shared/ipc-transport"
|
||||
import type { WindowBootstrap } from "../shared/window-bootstrap"
|
||||
|
||||
export type ElectronNative = {
|
||||
windowID: string
|
||||
bootstrap: WindowBootstrap
|
||||
storageSnapshot: Promise<StorageSnapshot>
|
||||
getPathForFile(file: File): string
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ const updaterHandler = (state: UpdaterState) => {
|
||||
updaterCallbacks.forEach((callback) => callback(state))
|
||||
}
|
||||
|
||||
// One renderer-side copy: the bridge clones on every crossing, so consumption is tracked here.
|
||||
const seeded = window.electron.storageSnapshot.then((snapshot) => new Map(Object.entries(snapshot)))
|
||||
|
||||
export const api: ElectronAPI = {
|
||||
awaitInitialization: () => invoke("AppAwaitInitialization"),
|
||||
reconnectService: () => invoke("AppReconnectService"),
|
||||
@@ -99,7 +102,15 @@ export const api: ElectronAPI = {
|
||||
invoke("AppFinishFirstLaunchOnboarding", { createDefaultProject }),
|
||||
checkAppExists: (appName) => invoke("AppCheckAppExists", { appName }),
|
||||
resolveAppPath: (appName) => invoke("AppResolveAppPath", { appName }),
|
||||
storeItems: (name) => invoke("StorageItems", { name }).then(mutable),
|
||||
// The first read of a namespace the preload already fetched is served from that snapshot; later
|
||||
// reads (a window re-opening a namespace) go to the main process as usual.
|
||||
storeItems: (name) =>
|
||||
seeded.then((snapshot) => {
|
||||
const item = snapshot.get(name)
|
||||
if (!item) return invoke("StorageItems", { name }).then(mutable)
|
||||
snapshot.delete(name)
|
||||
return item
|
||||
}),
|
||||
storeUpdate: (name, insert, remove) => invoke("StorageUpdate", { name, insert, remove }),
|
||||
storeClear: (name) => invoke("StorageClear", { name }),
|
||||
onStoreChanged: (cb) =>
|
||||
|
||||
@@ -1,2 +1,16 @@
|
||||
export const IpcTransportPort = "desktop-rpc-port"
|
||||
export const DragCancelEvent = "opencode:drag-cancel"
|
||||
export const StorageSnapshotChannel = "desktop-storage-snapshot"
|
||||
|
||||
export type StorageSnapshot = Record<string, { items: Record<string, string>; revision: number }>
|
||||
|
||||
// The namespaces a window reads while its shell mounts. The preload asks for them before the page
|
||||
// runs so the first render already has them; mirrors windowStorage() in
|
||||
// packages/app/src/runtime/persistence/storage.ts.
|
||||
export function storageSnapshotNames(windowID: string) {
|
||||
return ["opencode.global.dat", "default.dat", windowDataFile(windowID)]
|
||||
}
|
||||
|
||||
export function windowDataFile(id: string) {
|
||||
return `opencode.window.${id.replace(/[^a-zA-Z0-9._-]/g, "-")}.dat`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user