fix: EmptyState по центру и импорт SQLite без 413
Docker / build (push) Failing after 19s

Центрирование заглушек в графиках/Frame; лимит тела backup 100 MiB;
WAL checkpoint при экспорте и очистка -wal/-shm при импорте.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-19 04:41:42 +07:00
co-authored by Cursor
parent 2931d6ee0f
commit e079a61811
12 changed files with 284 additions and 62 deletions
+34 -2
View File
@@ -1,10 +1,12 @@
import Database from 'better-sqlite3'
import { drizzle, type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import * as schema from './schema/index.js'
import { ensureRuntimeSchema } from './runtime-migrate.js'
const SQLITE_HEADER = Buffer.from('SQLite format 3\0')
export type Db = BetterSQLite3Database<typeof schema>
let _db: Db | null = null
@@ -46,10 +48,40 @@ export function closeDb(): void {
}
}
function removeSidecarFiles(dbPath: string): void {
for (const suffix of ['-wal', '-shm'] as const) {
const side = `${dbPath}${suffix}`
if (existsSync(side)) unlinkSync(side)
}
}
/**
* Согласованный снимок файла БД: checkpoint WAL → чтение основного файла.
* Без checkpoint экспорт может быть неполным (данные только в *-wal).
*/
export function readDatabaseFileBuffer(): Buffer {
const sqlite = getSqlite()
sqlite.pragma('wal_checkpoint(TRUNCATE)')
return readFileSync(getDbPath())
}
/** Заменить файл SQLite и переоткрыть соединение. */
export function reloadDatabaseFromBuffer(buffer: Buffer): void {
if (!Buffer.isBuffer(buffer) || buffer.length < SQLITE_HEADER.length) {
throw new Error('Файл не похож на SQLite базу')
}
if (!buffer.subarray(0, SQLITE_HEADER.length).equals(SQLITE_HEADER)) {
throw new Error('Файл не похож на SQLite базу (неверный заголовок)')
}
const dbPath = getDbPath()
const dir = dirname(dbPath)
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
closeDb()
writeFileSync(getDbPath(), buffer)
// Старые WAL/SHM от предыдущей БД ломают открытие нового файла
removeSidecarFiles(dbPath)
writeFileSync(dbPath, buffer)
openDatabase()
}
+69
View File
@@ -0,0 +1,69 @@
import { afterEach, describe, expect, it } from 'vitest'
import Database from 'better-sqlite3'
import { mkdtempSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
closeDb,
getDb,
getSqlite,
readDatabaseFileBuffer,
reloadDatabaseFromBuffer,
} from './index.js'
describe('reloadDatabaseFromBuffer / readDatabaseFileBuffer', () => {
let dir: string
afterEach(() => {
closeDb()
delete process.env.DB_PATH
if (dir && existsSync(dir)) rmSync(dir, { recursive: true, force: true })
})
it('rejects non-sqlite buffer', () => {
dir = mkdtempSync(join(tmpdir(), 'vt-db-'))
process.env.DB_PATH = join(dir, 'app.db')
getDb()
expect(() => reloadDatabaseFromBuffer(Buffer.from('not-a-db'))).toThrow(/SQLite/)
})
it('replaces db and drops leftover wal/shm', () => {
dir = mkdtempSync(join(tmpdir(), 'vt-db-'))
const dbPath = join(dir, 'app.db')
process.env.DB_PATH = dbPath
getDb()
getSqlite().exec('CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY); INSERT INTO t VALUES (1);')
closeDb()
// Simulate stale sidecars after closed connection (Windows locks *-shm while open)
writeFileSync(`${dbPath}-wal`, Buffer.alloc(32))
writeFileSync(`${dbPath}-shm`, Buffer.alloc(32))
const donorPath = join(dir, 'donor.db')
const donor = new Database(donorPath)
donor.exec(`CREATE TABLE ok (x TEXT); INSERT INTO ok VALUES ('yes');`)
donor.close()
const buf = readFileSync(donorPath)
reloadDatabaseFromBuffer(buf)
const row = getSqlite().prepare('SELECT x FROM ok').get() as { x: string }
expect(row.x).toBe('yes')
// Старый мусорный WAL не должен подмешаться: таблица t из прошлой БД отсутствует
const tables = getSqlite()
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='t'`)
.all()
expect(tables).toHaveLength(0)
closeDb()
})
it('readDatabaseFileBuffer checkpoints wal', () => {
dir = mkdtempSync(join(tmpdir(), 'vt-db-'))
process.env.DB_PATH = join(dir, 'app.db')
getDb()
getSqlite().exec('CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY); INSERT INTO t VALUES (42);')
const buf = readDatabaseFileBuffer()
expect(buf.subarray(0, 15).toString('utf8')).toBe('SQLite format 3')
expect(buf.length).toBeGreaterThan(100)
})
})