fix: 关闭应用前干净关闭数据库 + 自愈仅启动早期触发
- 退出前 db.close() 确保 WAL/SSTable 干净落盘,避免渲染进程销毁时 flush 中断留下半写文件导致下次启动损坏 - 自愈(删损坏库/重载)仅限页面生命周期内首次初始化;运行中失败直接抛错,不再阻塞应用关闭流程
This commit is contained in:
@@ -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()
|
||||||
@@ -12,6 +12,7 @@ import { useDragDrop } from './hooks/useDragDrop'
|
|||||||
import { useAutoSave } from './hooks/useAutoSave'
|
import { useAutoSave } from './hooks/useAutoSave'
|
||||||
import { useIpcListeners } from './hooks/useIpcListeners'
|
import { useIpcListeners } from './hooks/useIpcListeners'
|
||||||
import { MeToast } from './lib/toast'
|
import { MeToast } from './lib/toast'
|
||||||
|
import { closeDatabase } from './db/schema'
|
||||||
import { Toolbar } from './components/Toolbar/Toolbar'
|
import { Toolbar } from './components/Toolbar/Toolbar'
|
||||||
import { StatusBar } from './components/StatusBar'
|
import { StatusBar } from './components/StatusBar'
|
||||||
import { TabBar } from './components/TabBar/TabBar'
|
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)
|
useKeyboard(handleOpenFile, handleSave, handleSaveAs)
|
||||||
useIpcListeners()
|
useIpcListeners()
|
||||||
|
|
||||||
|
|||||||
@@ -79,18 +79,19 @@ const ACTIVE_TAB_COLUMNS: Record<string, ColumnDef> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* v0.6.0: MetonaSqlark 初始化 — 懒加载单例。
|
|
||||||
* create() 为异步,无法像 Dexie 那样模块顶层同步实例化;
|
|
||||||
* 首次调用时创建,之后复用同一 Promise。
|
|
||||||
*
|
|
||||||
* v0.6.0: 数据损坏自愈 —
|
* v0.6.0: 数据损坏自愈 —
|
||||||
* AriaEngine 在异常退出(强杀/断电)时可能留下残缺的 SSTable 文件,
|
* AriaEngine 在异常退出(强杀/断电)时可能留下残缺的 SSTable 文件,
|
||||||
* 重新打开解析块偏移会抛 RangeError(offset is out of bounds)导致初始化失败。
|
* 重新打开解析块偏移会抛 RangeError(offset is out of bounds)导致初始化失败。
|
||||||
* 检测到打开失败后:标记 localStorage → 重载页面(干净环境无残留 IDB 连接)
|
* 检测到打开失败后:标记 localStorage → 重载页面(干净环境无残留 IDB 连接)
|
||||||
* → 删除损坏库 → 重建。create 成功后才清除标记,避免自愈失败死循环。
|
* → 删除损坏库 → 重建。create 成功后才清除标记,避免自愈失败死循环。
|
||||||
|
*
|
||||||
|
* 注意:自愈(删库/重载)只允许在页面生命周期内首次初始化时触发;
|
||||||
|
* 运行中(如应用关闭流程的 flushSaveToDB)getDb 失败时直接抛错,
|
||||||
|
* 绝不删库/重载,避免关闭流程被数据库操作阻塞。
|
||||||
*/
|
*/
|
||||||
let dbPromise: Promise<MetonaSqlark> | null = null
|
let dbPromise: Promise<MetonaSqlark> | null = null
|
||||||
let initFailedNotified = false
|
let initFailedNotified = false
|
||||||
|
let healTriggered = false
|
||||||
|
|
||||||
// IndexedDBBackend 的内部库名 = `aria-${name}`(sqlark 源码约定)
|
// IndexedDBBackend 的内部库名 = `aria-${name}`(sqlark 源码约定)
|
||||||
const DB_STORAGE_NAME = `aria-${DB_NAME}`
|
const DB_STORAGE_NAME = `aria-${DB_NAME}`
|
||||||
@@ -144,6 +145,9 @@ async function createDatabase(): Promise<MetonaSqlark> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function initDb(): Promise<MetonaSqlark> {
|
async function initDb(): Promise<MetonaSqlark> {
|
||||||
|
// 页面生命周期内仅首次初始化走自愈流程(删损坏库 + 失败重载)
|
||||||
|
if (!healTriggered) {
|
||||||
|
healTriggered = true
|
||||||
await resetCorruptDatabaseIfNeeded()
|
await resetCorruptDatabaseIfNeeded()
|
||||||
try {
|
try {
|
||||||
const db = await createDatabase()
|
const db = await createDatabase()
|
||||||
@@ -156,7 +160,7 @@ async function initDb(): Promise<MetonaSqlark> {
|
|||||||
return db
|
return db
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logError('数据库打开失败', err)
|
logError('数据库打开失败', err)
|
||||||
// 数据损坏自愈:首次失败 → 标记 + 重载页面(干净环境删除损坏库重建)
|
// 启动早期失败:标记 + 重载页面(干净环境删除损坏库重建)— 仅此一次
|
||||||
try {
|
try {
|
||||||
if (localStorage.getItem(RESET_PENDING_KEY) !== '1') {
|
if (localStorage.getItem(RESET_PENDING_KEY) !== '1') {
|
||||||
localStorage.setItem(RESET_PENDING_KEY, '1')
|
localStorage.setItem(RESET_PENDING_KEY, '1')
|
||||||
@@ -167,6 +171,25 @@ async function initDb(): Promise<MetonaSqlark> {
|
|||||||
}
|
}
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// 运行中(自愈已尝试过):不再删库/重载,直接尝试创建,失败抛给调用方
|
||||||
|
return createDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v0.6.0: 关闭数据库 — 应用退出前调用,确保 WAL/SSTable 干净落盘,
|
||||||
|
* 避免渲染进程被销毁时 flush 中断留下半写文件导致下次启动损坏。
|
||||||
|
*/
|
||||||
|
export async function closeDatabase(): Promise<void> {
|
||||||
|
const current = dbPromise
|
||||||
|
dbPromise = null
|
||||||
|
if (!current) return
|
||||||
|
try {
|
||||||
|
const db = await current.catch(() => null)
|
||||||
|
if (db) await db.close()
|
||||||
|
} catch (err) {
|
||||||
|
logError('关闭数据库失败', err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDb(): Promise<MetonaSqlark> {
|
export function getDb(): Promise<MetonaSqlark> {
|
||||||
|
|||||||
Reference in New Issue
Block a user