release: v0.6.0 — 完全移除 IndexedDB,自研 KVStore 事务存储引擎(多key原子写/快照日志恢复/CRC自愈)+ KVStoreEngine + 旧库迁移工具 + 10万级压力验证 + 崩溃注入e2e
This commit is contained in:
@@ -9,10 +9,12 @@
|
||||
|
||||
import { AriaEngine } from '../src/engine/aria/index';
|
||||
import { createSchema } from '../src/table/schema';
|
||||
import { OPFSEngine } from '../src/engine/opfs';
|
||||
import { MemTable } from '../src/engine/aria/index/memtable';
|
||||
import { MetonaSqlark } from '../src/core';
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import { installOPFSMock } from './helpers/opfs-mock';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
|
||||
let idbCounter = 0;
|
||||
function uniqueDB(): string {
|
||||
@@ -73,7 +75,7 @@ describe('P0 — 事务进行中 checkpoint 不得截断 WAL', () => {
|
||||
it('事务中触发 checkpoint → 崩溃恢复不丢事务数据', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const engine = new AriaEngine({
|
||||
storageBackend: 'indexeddb',
|
||||
storageBackend: 'opfs',
|
||||
checkpointInterval: 2, // 每 2 次操作即触发 checkpoint(事务中途)
|
||||
});
|
||||
await engine.open(dbName, 1);
|
||||
@@ -91,7 +93,7 @@ describe('P0 — 事务进行中 checkpoint 不得截断 WAL', () => {
|
||||
await (engine as any).backend.close();
|
||||
(engine as any).opened = false;
|
||||
|
||||
const engine2 = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 });
|
||||
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
|
||||
await engine2.open(dbName, 1);
|
||||
// 修复前:checkpoint 截断了事务的 WAL 记录 → 恢复后数据丢失
|
||||
expect(await engine2.count('t')).toBe(2);
|
||||
@@ -101,7 +103,7 @@ describe('P0 — 事务进行中 checkpoint 不得截断 WAL', () => {
|
||||
it('事务回滚后再 checkpoint 正常截断', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const engine = new AriaEngine({
|
||||
storageBackend: 'indexeddb',
|
||||
storageBackend: 'opfs',
|
||||
checkpointInterval: 1,
|
||||
});
|
||||
await engine.open(dbName, 1);
|
||||
@@ -115,7 +117,7 @@ describe('P0 — 事务进行中 checkpoint 不得截断 WAL', () => {
|
||||
await (engine as any).backend.close();
|
||||
(engine as any).opened = false;
|
||||
|
||||
const engine2 = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 });
|
||||
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
|
||||
await engine2.open(dbName, 1);
|
||||
expect(await engine2.count('t')).toBe(1);
|
||||
await engine2.close();
|
||||
@@ -168,7 +170,7 @@ describe('P1 — Aria DDL 索引清理', () => {
|
||||
|
||||
it('DROP_TABLE 崩溃恢复同样清理索引', async () => {
|
||||
const dbName = uniqueDB();
|
||||
const engine = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 });
|
||||
const engine = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
|
||||
await engine.open(dbName, 1);
|
||||
await engine.createTable(createSchema('t', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
@@ -180,7 +182,7 @@ describe('P1 — Aria DDL 索引清理', () => {
|
||||
await (engine as any).backend.close();
|
||||
(engine as any).opened = false;
|
||||
|
||||
const engine2 = new AriaEngine({ storageBackend: 'indexeddb', checkpointInterval: 100000 });
|
||||
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
|
||||
await engine2.open(dbName, 1);
|
||||
// 表不存在,且无索引残留
|
||||
expect(await engine2.hasTable('t')).toBe(false);
|
||||
@@ -189,88 +191,6 @@ describe('P1 — Aria DDL 索引清理', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ===================================================================
|
||||
// P1: OPFS 持久化与并发
|
||||
// ===================================================================
|
||||
|
||||
describe('P1 — OPFS 生产加固', () => {
|
||||
it('并发写不丢数据(内存快照总是最新,多写内容一致)', async () => {
|
||||
// 模拟真实 I/O 延迟:内存写同步、持久化异步 → 并发写内容均基于最新内存快照
|
||||
const files = mockOPFS(undefined, {
|
||||
ioDelay: 10,
|
||||
writeDelayFor: (data) => (data.length < 40 ? 30 : 5),
|
||||
});
|
||||
const engine = new OPFSEngine();
|
||||
await engine.open('opfs-race', 1);
|
||||
await engine.createTable(createSchema('users', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
name: { type: 'string' },
|
||||
}));
|
||||
// 两个写并发:内存同步写保证两个快照一致 → 无论完成顺序文件内容完整
|
||||
const p1 = engine.insert('users', [{ id: '1', name: 'A' }]);
|
||||
const p2 = engine.insert('users', [{ id: '2', name: 'B' }]);
|
||||
await Promise.all([p1, p2]);
|
||||
const rows = await engine.find('users', { table: 'users' });
|
||||
expect(rows).toHaveLength(2);
|
||||
// 文件内容完整(重启不丢)
|
||||
expect(files.get('users.json')).toContain('"B"');
|
||||
await engine.close();
|
||||
});
|
||||
|
||||
it('空表重启后保留(schema 持久化)', async () => {
|
||||
mockOPFS();
|
||||
const e1 = new OPFSEngine();
|
||||
await e1.open('opfs-schema', 1);
|
||||
await e1.createTable(createSchema('users', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
email: { type: 'string', index: true },
|
||||
}));
|
||||
await e1.close();
|
||||
|
||||
// 重启:空表也应恢复(修复前空表消失)+ 索引标记恢复
|
||||
const e2 = new OPFSEngine();
|
||||
await e2.open('opfs-schema', 1);
|
||||
expect(await e2.hasTable('users')).toBe(true);
|
||||
const schema = await e2.getTableSchema('users');
|
||||
expect(schema!.columns.email.index).toBe(true);
|
||||
await e2.close();
|
||||
});
|
||||
|
||||
it('有数据重启后索引查询可用', async () => {
|
||||
mockOPFS();
|
||||
const e1 = new OPFSEngine();
|
||||
await e1.open('opfs-idx', 1);
|
||||
await e1.createTable(createSchema('users', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
email: { type: 'string', index: true },
|
||||
}));
|
||||
await e1.insert('users', [{ id: '1', email: 'a@x.com' }]);
|
||||
await e1.close();
|
||||
|
||||
const e2 = new OPFSEngine();
|
||||
await e2.open('opfs-idx', 1);
|
||||
const byEmail = await e2.find('users', { table: 'users', where: { email: 'a@x.com' } });
|
||||
expect(byEmail).toHaveLength(1);
|
||||
await e2.close();
|
||||
});
|
||||
|
||||
it('dropTable 后重启无幽灵表', async () => {
|
||||
mockOPFS();
|
||||
const e1 = new OPFSEngine();
|
||||
await e1.open('opfs-drop', 1);
|
||||
await e1.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
await e1.insert('t', [{ id: '1' }]);
|
||||
await e1.dropTable('t');
|
||||
await e1.close();
|
||||
|
||||
const e2 = new OPFSEngine();
|
||||
await e2.open('opfs-drop', 1);
|
||||
expect(await e2.hasTable('t')).toBe(false);
|
||||
expect(await e2.getTableNames()).toEqual([]);
|
||||
await e2.close();
|
||||
});
|
||||
});
|
||||
|
||||
// ===================================================================
|
||||
// P2: Aria 内部状态
|
||||
// ===================================================================
|
||||
|
||||
Reference in New Issue
Block a user