fix(electron): close export database handles

Close the uncached readonly adapter after every Desktop export so Windows can delete the session without restarting. Add regression coverage for the successful export lifecycle.
This commit is contained in:
digua committed 2026-08-06 15:17:44 +08:00
1 parent 1a774592ff
commit 78e90abb3f
3 files changed
+93 -29

No files matched your search

@@ -4,7 +4,7 @@
*/
import type Database from 'better-sqlite3'
import { getDbPath, openRawDatabase, closeDatabase } from '../../core'
import { getDbPath, openRawDatabase, closeDatabase } from '../../core/dbCore'
// 重新导出 closeDatabase 供其他模块使用
export { closeDatabase }
@@ -0,0 +1,51 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import Database from 'better-sqlite3'
import { CHAT_DB_SCHEMA } from '@openchatlab/core'
import { BetterSqliteAdapter } from '@openchatlab/node-runtime/src/better-sqlite3-adapter'
import { exportFilterResultToFile } from './export'
test('closes the export database after writing the file', () => {
const outputDir = fs.mkdtempSync(path.join(process.env.CHATLAB_TEST_TMPDIR ?? os.tmpdir(), 'chatlab-desktop-export-'))
const rawDb = new Database(':memory:', {
nativeBinding: path.resolve('apps/cli/native/better_sqlite3.node'),
})
rawDb.exec(CHAT_DB_SCHEMA)
rawDb.exec(`
INSERT INTO meta (name, platform, type, imported_at)
VALUES ('Export test', 'qq', 'private', 1);
INSERT INTO member (id, platform_id, account_name)
VALUES (1, 'alice', 'Alice');
INSERT INTO message (sender_id, ts, type, content)
VALUES (1, 1, 0, 'hello');
`)
let closeCount = 0
const adapter = new BetterSqliteAdapter(rawDb)
const close = adapter.close.bind(adapter)
adapter.close = () => {
closeCount += 1
close()
}
try {
const result = exportFilterResultToFile(
{
sessionId: 'export-test',
sessionName: 'Export test',
outputDir,
},
() => adapter
)
assert.equal(result.success, true)
assert.equal(closeCount, 1)
assert.equal(fs.existsSync(result.filePath!), true)
} finally {
if (rawDb.open) rawDb.close()
fs.rmSync(outputDir, { recursive: true, force: true })
}
})
@@ -7,39 +7,52 @@
import * as fs from 'fs'
import * as path from 'path'
import { BetterSqliteAdapter } from '@openchatlab/node-runtime'
import { exportWithFormat } from '@openchatlab/node-runtime'
import type { ExportFormat } from '@openchatlab/node-runtime'
import type { DatabaseAdapter } from '@openchatlab/core'
import { BetterSqliteAdapter } from '@openchatlab/node-runtime/src/better-sqlite3-adapter'
import { exportWithFormat, type ExportFormat } from '@openchatlab/node-runtime/src/export/format-exporter'
import { openReadonlyDatabase } from './core'
export function exportFilterResultToFile(params: {
sessionId: string
sessionName: string
outputDir: string
format?: ExportFormat
timeFilter?: { startTs: number; endTs: number }
}): { success: boolean; filePath?: string; error?: string } {
function openExportDatabase(sessionId: string): DatabaseAdapter | null {
const rawDb = openReadonlyDatabase(sessionId)
return rawDb ? new BetterSqliteAdapter(rawDb) : null
}
export function exportFilterResultToFile(
params: {
sessionId: string
sessionName: string
outputDir: string
format?: ExportFormat
timeFilter?: { startTs: number; endTs: number }
},
openDatabase: (sessionId: string) => DatabaseAdapter | null = openExportDatabase
): {
success: boolean
filePath?: string
error?: string
} {
const format: ExportFormat = params.format || 'txt'
const openedDatabase = openDatabase(params.sessionId)
try {
const result = exportWithFormat(
{
sessionId: params.sessionId,
sessionName: params.sessionName,
format,
timeFilter: params.timeFilter,
},
() => openedDatabase
)
const result = exportWithFormat(
{
sessionId: params.sessionId,
sessionName: params.sessionName,
format,
timeFilter: params.timeFilter,
},
(sessionId) => {
const rawDb = openReadonlyDatabase(sessionId)
if (!rawDb) return null
return new BetterSqliteAdapter(rawDb)
if (!result.success) {
return { success: false, error: result.error }
}
)
if (!result.success) {
return { success: false, error: result.error }
const filePath = path.join(params.outputDir, result.filename)
fs.writeFileSync(filePath, result.content, 'utf8')
return { success: true, filePath }
} finally {
openedDatabase?.close()
}
const filePath = path.join(params.outputDir, result.filename)
fs.writeFileSync(filePath, result.content, 'utf8')
return { success: true, filePath }
}