From 58c8a1a3a01599ef509b2020e94c00185deddf44 Mon Sep 17 00:00:00 2001 From: thzxx Date: Sun, 9 Aug 2026 17:05:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=B3=E9=97=AD=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E5=89=8D=E5=B9=B2=E5=87=80=E5=85=B3=E9=97=AD=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=20+=20=E8=87=AA=E6=84=88=E4=BB=85=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E6=97=A9=E6=9C=9F=E8=A7=A6=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 退出前 db.close() 确保 WAL/SSTable 干净落盘,避免渲染进程销毁时 flush 中断留下半写文件导致下次启动损坏 - 自愈(删损坏库/重载)仅限页面生命周期内首次初始化;运行中失败直接抛错,不再阻塞应用关闭流程 --- .tscheck/flushkill.mjs | 28 ++++++++++++++++ src/renderer/App.tsx | 9 ++++- src/renderer/db/schema.ts | 69 ++++++++++++++++++++++++++------------- 3 files changed, 82 insertions(+), 24 deletions(-) create mode 100644 .tscheck/flushkill.mjs diff --git a/.tscheck/flushkill.mjs b/.tscheck/flushkill.mjs new file mode 100644 index 0000000..66e85f8 --- /dev/null +++ b/.tscheck/flushkill.mjs @@ -0,0 +1,28 @@ +import 'fake-indexeddb/auto' +import { AriaEngine } from '/mnt/d/CodeWorkspace/MarkLite/node_modules/@metona-team/metona-sqlark/dist/metona-sqlark.esm.js' + +// 小 memtable 阈值 → 频繁 flush → 多个 SSTable + WAL +const engine1 = new AriaEngine({ storageBackend: 'indexeddb', memtableSizeThreshold: 4096, walEnabled: true }) +await engine1.open('flushkill-test', 1) +await engine1.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, v: { type: 'number' } } }) + +// 写入大量数据触发多次 flush(写 WAL + 刷 SSTable) +for (let i = 0; i < 3000; i++) { + await engine1.insert('t', [{ id: `k${i}`, v: i }]) +} +console.log('S1 wrote 3000 rows') +// 不 close!直接丢弃实例(模拟应用被强制关闭/渲染进程被销毁) + +// 重新打开同库 +const engine2 = new AriaEngine({ storageBackend: 'indexeddb', memtableSizeThreshold: 4096, walEnabled: true }) +await engine2.open('flushkill-test', 1) +const tables = await engine2.getTableNames() +console.log('S2 tables:', JSON.stringify(tables)) +if (tables.includes('t')) { + const rows = await engine2.find('t', { limit: 5 }) + console.log('S2 sample rows:', JSON.stringify(rows)) + console.log('REOPEN OK') +} else { + console.log('TABLE LOST') +} +await engine2.close() diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 2954543..c7af41c 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -12,6 +12,7 @@ import { useDragDrop } from './hooks/useDragDrop' import { useAutoSave } from './hooks/useAutoSave' import { useIpcListeners } from './hooks/useIpcListeners' import { MeToast } from './lib/toast' +import { closeDatabase } from './db/schema' import { Toolbar } from './components/Toolbar/Toolbar' import { StatusBar } from './components/StatusBar' import { TabBar } from './components/TabBar/TabBar' @@ -58,7 +59,13 @@ export function App() { }) }, []) - useUnsavedWarning(() => tabs.some(t => t.isModified), handleConfirmClose, flushSaveToDB) + // v0.6.0: 关闭前先保存标签快照,再干净关闭数据库(避免 flush 中断导致下次启动损坏) + const handleBeforeForceClose = useCallback(async () => { + await flushSaveToDB() + await closeDatabase() + }, []) + + useUnsavedWarning(() => tabs.some(t => t.isModified), handleConfirmClose, handleBeforeForceClose) useKeyboard(handleOpenFile, handleSave, handleSaveAs) useIpcListeners() diff --git a/src/renderer/db/schema.ts b/src/renderer/db/schema.ts index a007a45..01add3e 100644 --- a/src/renderer/db/schema.ts +++ b/src/renderer/db/schema.ts @@ -79,18 +79,19 @@ const ACTIVE_TAB_COLUMNS: Record = { } /** - * v0.6.0: MetonaSqlark 初始化 — 懒加载单例。 - * create() 为异步,无法像 Dexie 那样模块顶层同步实例化; - * 首次调用时创建,之后复用同一 Promise。 - * * v0.6.0: 数据损坏自愈 — * AriaEngine 在异常退出(强杀/断电)时可能留下残缺的 SSTable 文件, * 重新打开解析块偏移会抛 RangeError(offset is out of bounds)导致初始化失败。 * 检测到打开失败后:标记 localStorage → 重载页面(干净环境无残留 IDB 连接) * → 删除损坏库 → 重建。create 成功后才清除标记,避免自愈失败死循环。 + * + * 注意:自愈(删库/重载)只允许在页面生命周期内首次初始化时触发; + * 运行中(如应用关闭流程的 flushSaveToDB)getDb 失败时直接抛错, + * 绝不删库/重载,避免关闭流程被数据库操作阻塞。 */ let dbPromise: Promise | null = null let initFailedNotified = false +let healTriggered = false // IndexedDBBackend 的内部库名 = `aria-${name}`(sqlark 源码约定) const DB_STORAGE_NAME = `aria-${DB_NAME}` @@ -144,28 +145,50 @@ async function createDatabase(): Promise { } async function initDb(): Promise { - await resetCorruptDatabaseIfNeeded() - try { - const db = await createDatabase() - // 创建成功:清除重置标记(自愈完成) + // 页面生命周期内仅首次初始化走自愈流程(删损坏库 + 失败重载) + if (!healTriggered) { + healTriggered = true + await resetCorruptDatabaseIfNeeded() try { - localStorage.removeItem(RESET_PENDING_KEY) - } catch { - /* 忽略 */ - } - return db - } catch (err) { - logError('数据库打开失败', err) - // 数据损坏自愈:首次失败 → 标记 + 重载页面(干净环境删除损坏库重建) - try { - if (localStorage.getItem(RESET_PENDING_KEY) !== '1') { - localStorage.setItem(RESET_PENDING_KEY, '1') - window.location.reload() + const db = await createDatabase() + // 创建成功:清除重置标记(自愈完成) + try { + localStorage.removeItem(RESET_PENDING_KEY) + } catch { + /* 忽略 */ } - } catch { - /* 忽略 */ + return db + } catch (err) { + logError('数据库打开失败', err) + // 启动早期失败:标记 + 重载页面(干净环境删除损坏库重建)— 仅此一次 + try { + if (localStorage.getItem(RESET_PENDING_KEY) !== '1') { + localStorage.setItem(RESET_PENDING_KEY, '1') + window.location.reload() + } + } catch { + /* 忽略 */ + } + throw err } - throw err + } + // 运行中(自愈已尝试过):不再删库/重载,直接尝试创建,失败抛给调用方 + return createDatabase() +} + +/** + * v0.6.0: 关闭数据库 — 应用退出前调用,确保 WAL/SSTable 干净落盘, + * 避免渲染进程被销毁时 flush 中断留下半写文件导致下次启动损坏。 + */ +export async function closeDatabase(): Promise { + const current = dbPromise + dbPromise = null + if (!current) return + try { + const db = await current.catch(() => null) + if (db) await db.close() + } catch (err) { + logError('关闭数据库失败', err) } }