perf+fix: sprint B — async I/O, before-quit, immutable getState, lucide tree-shake

#2  atomicWrite spin-loop → async setTimeout. Раньше при retry на
    EBUSY/EPERM (антивирус, OneDrive) main process замораживался на
    50/200/800ms × до 3 итераций ≈ секунда залипания UI. Сейчас async
    sleep — event-loop живёт. Сохранён atomicWriteSync для flushNow
    (вызывается из before-quit когда event-loop уже умирает).
    Аналогичный фикс в games/steam-launch-options.ts.
#5  before-quit теперь дожидается stopGamesRegistry через
    e.preventDefault() + app.exit(0). Раньше GSI HTTP server не успевал
    closeAllConnections до exit, и следующий запуск получал
    EADDRINUSE на port 4701 (TIME_WAIT) — GSI молча не работал.
#10 IPC.getState возвращает поверхностную копию settings вместо мутации
    кэша. Раньше startWithWindows писалось напрямую в state.settings,
    разъезжаясь с persisted-disk-значением до следующего mutation.
#19 lib/icon.tsx: `import * as Lucide` (wildcard, ~500KB в bundle,
    1500+ иконок) → explicit named imports + ICON_MAP. В bundle
    остаются только 18 ICON_CHOICES.
This commit is contained in:
AnRil
2026-05-22 01:15:31 +07:00
parent a41dce511b
commit 4745f5e091
5 changed files with 137 additions and 35 deletions

View File

@@ -123,20 +123,19 @@ function writeBackup(path: string): void {
}
}
function atomicWrite(path: string, contents: string): void {
async function atomicWrite(path: string, contents: string): Promise<void> {
// Write to temp then rename (atomic on Windows for same directory). Retry a
// few times on transient EBUSY/EPERM (AV scanners and OneDrive sometimes
// hold a handle briefly during a Steam config rewrite).
//
// Раньше тут был busy-loop sleep — Steam-конфиги пишутся редко, но из main
// process, и при попадании на занятый файл (Steam ещё держит handle) морозили
// весь UI на 250мс. Заменили на async setTimeout-sleep.
const tmp = path + '.exr.tmp'
const delays = [0, 50, 200]
let lastErr: unknown
for (const delay of delays) {
if (delay > 0) {
const until = Date.now() + delay
while (Date.now() < until) {
/* spin */
}
}
if (delay > 0) await new Promise<void>((r) => setTimeout(r, delay))
try {
writeFileSync(tmp, contents, 'utf-8')
renameSync(tmp, path)
@@ -148,11 +147,11 @@ function atomicWrite(path: string, contents: string): void {
throw lastErr
}
function modifyLaunchOptions(
async function modifyLaunchOptions(
configPath: string,
appId: string,
fn: (current: string) => string | null
): boolean {
): Promise<boolean> {
let raw: string
try {
raw = readFileSync(configPath, 'utf-8')
@@ -188,7 +187,7 @@ function modifyLaunchOptions(
writeBackup(configPath)
try {
atomicWrite(configPath, stringifyVdf(parsed))
await atomicWrite(configPath, stringifyVdf(parsed))
} catch {
return false
}
@@ -225,7 +224,7 @@ async function applyOptionToAllConfigs(
): Promise<void> {
const paths = await getLocalConfigPaths()
for (const p of paths) {
modifyLaunchOptions(p, appId, (current) => {
await modifyLaunchOptions(p, appId, (current) => {
if (current.includes(option)) return current
return current.length > 0 ? `${current} ${option}` : option
})
@@ -238,7 +237,7 @@ async function removeOptionFromAllConfigs(
): Promise<void> {
const paths = await getLocalConfigPaths()
for (const p of paths) {
modifyLaunchOptions(p, appId, (current) => {
await modifyLaunchOptions(p, appId, (current) => {
if (!current.includes(option)) return current
return current
.split(/\s+/)