fix: v0.2.3 OPFS数据恢复 + Aria WAL事务边界修复
CI / test (18.x) (push) Successful in 9m58s
CI / test (22.x) (push) Successful in 9m54s
CI / test (20.x) (push) Successful in 10m0s
CI / test (24.x) (push) Successful in 9m52s

This commit is contained in:
thzxx
2026-07-27 21:30:35 +08:00
parent 4a3feac9ea
commit 2493209611
11 changed files with 224 additions and 22 deletions
+54 -3
View File
@@ -840,8 +840,10 @@ class OPFSEngine {
await this.memoryCache.open(dbName, version);
// 获取 OPFS 根目录
this.root = await navigator.storage.getDirectory();
// 创建数据库目录
// 创建/打开数据库目录
this.tablesDir = await this.root.getDirectoryHandle(dbName, { create: true });
// 从 OPFS 恢复已有表数据到内存缓存
await this.loadExistingTables();
}
async close() {
this.root = null;
@@ -967,6 +969,39 @@ class OPFSEngine {
return [];
}
}
/** 从 OPFS 加载已有表数据到内存缓存 */
async loadExistingTables() {
if (!this.tablesDir)
return;
const dir = this.tablesDir;
for await (const [name] of dir.entries()) {
if (!name.endsWith('.json'))
continue;
const tableName = name.replace('.json', '');
try {
const data = await this.readTableData(tableName);
// 从数据中推断 schema(简化:从第一行提取列信息)
if (data.length > 0) {
const firstRow = data[0];
const columns = {};
for (const key of Object.keys(firstRow)) {
const val = firstRow[key];
const type = typeof val === 'number' ? 'number' :
typeof val === 'boolean' ? 'boolean' :
typeof val === 'object' ? 'json' : 'string';
columns[key] = { type, primaryKey: key === 'id' };
}
await this.memoryCache.createTable({ name: tableName, columns });
for (const row of data) {
await this.memoryCache.insert(tableName, [row]);
}
}
}
catch {
// 单个文件损坏不影响其他表
}
}
}
/** 从 OPFS 加载表数据到内存缓存 */
async loadTableIntoMemory(tableName, schema) {
await this.memoryCache.createTable(schema);
@@ -3063,8 +3098,23 @@ class AriaEngine {
await this.loadSchemas();
// 6. 初始化 LSM(加载 SSTable 元数据)
await this.lsm.init();
// 7. WAL 恢复(恢复未刷盘的数据
await this.wal.recover((record) => this.applyWALRecord(record));
// 7. WAL 恢复(两阶段:先扫描事务边界,仅回放已提交事务
const committedTxns = new Set();
const allRecords = [];
await this.wal.recover((r) => allRecords.push(r));
// 第一遍:确定已提交事务
for (const r of allRecords) {
if (r.type === WALRecordType.COMMIT)
committedTxns.add(r.txnId);
if (r.type === WALRecordType.ROLLBACK)
committedTxns.delete(r.txnId);
}
// 第二遍:仅应用 txnId==0(非事务)或已提交事务的数据
for (const r of allRecords) {
if (r.txnId === 0 || committedTxns.has(r.txnId)) {
this.applyWALRecord(r);
}
}
// 8. Checkpoint ManagerBufferPool 暂简化,使用 flush 替代)
this.checkpointManager = new CheckpointManager(this.lsm, this.wal, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval);
this.opened = true;
@@ -3546,6 +3596,7 @@ class AriaEngine {
case WALRecordType.COMMIT:
case WALRecordType.ROLLBACK:
case WALRecordType.BEGIN:
case WALRecordType.DROP_TABLE:
break;
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -622,6 +622,8 @@ declare class OPFSEngine implements IStorageEngine {
private ensureDir;
private writeTableData;
private readTableData;
/** 从 OPFS 加载已有表数据到内存缓存 */
private loadExistingTables;
/** 从 OPFS 加载表数据到内存缓存 */
loadTableIntoMemory(tableName: string, schema: TableSchema): Promise<void>;
}
+54 -3
View File
@@ -836,8 +836,10 @@ class OPFSEngine {
await this.memoryCache.open(dbName, version);
// 获取 OPFS 根目录
this.root = await navigator.storage.getDirectory();
// 创建数据库目录
// 创建/打开数据库目录
this.tablesDir = await this.root.getDirectoryHandle(dbName, { create: true });
// 从 OPFS 恢复已有表数据到内存缓存
await this.loadExistingTables();
}
async close() {
this.root = null;
@@ -963,6 +965,39 @@ class OPFSEngine {
return [];
}
}
/** 从 OPFS 加载已有表数据到内存缓存 */
async loadExistingTables() {
if (!this.tablesDir)
return;
const dir = this.tablesDir;
for await (const [name] of dir.entries()) {
if (!name.endsWith('.json'))
continue;
const tableName = name.replace('.json', '');
try {
const data = await this.readTableData(tableName);
// 从数据中推断 schema(简化:从第一行提取列信息)
if (data.length > 0) {
const firstRow = data[0];
const columns = {};
for (const key of Object.keys(firstRow)) {
const val = firstRow[key];
const type = typeof val === 'number' ? 'number' :
typeof val === 'boolean' ? 'boolean' :
typeof val === 'object' ? 'json' : 'string';
columns[key] = { type, primaryKey: key === 'id' };
}
await this.memoryCache.createTable({ name: tableName, columns });
for (const row of data) {
await this.memoryCache.insert(tableName, [row]);
}
}
}
catch {
// 单个文件损坏不影响其他表
}
}
}
/** 从 OPFS 加载表数据到内存缓存 */
async loadTableIntoMemory(tableName, schema) {
await this.memoryCache.createTable(schema);
@@ -3059,8 +3094,23 @@ class AriaEngine {
await this.loadSchemas();
// 6. 初始化 LSM(加载 SSTable 元数据)
await this.lsm.init();
// 7. WAL 恢复(恢复未刷盘的数据
await this.wal.recover((record) => this.applyWALRecord(record));
// 7. WAL 恢复(两阶段:先扫描事务边界,仅回放已提交事务
const committedTxns = new Set();
const allRecords = [];
await this.wal.recover((r) => allRecords.push(r));
// 第一遍:确定已提交事务
for (const r of allRecords) {
if (r.type === WALRecordType.COMMIT)
committedTxns.add(r.txnId);
if (r.type === WALRecordType.ROLLBACK)
committedTxns.delete(r.txnId);
}
// 第二遍:仅应用 txnId==0(非事务)或已提交事务的数据
for (const r of allRecords) {
if (r.txnId === 0 || committedTxns.has(r.txnId)) {
this.applyWALRecord(r);
}
}
// 8. Checkpoint ManagerBufferPool 暂简化,使用 flush 替代)
this.checkpointManager = new CheckpointManager(this.lsm, this.wal, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval);
this.opened = true;
@@ -3542,6 +3592,7 @@ class AriaEngine {
case WALRecordType.COMMIT:
case WALRecordType.ROLLBACK:
case WALRecordType.BEGIN:
case WALRecordType.DROP_TABLE:
break;
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+54 -3
View File
@@ -842,8 +842,10 @@
await this.memoryCache.open(dbName, version);
// 获取 OPFS 根目录
this.root = await navigator.storage.getDirectory();
// 创建数据库目录
// 创建/打开数据库目录
this.tablesDir = await this.root.getDirectoryHandle(dbName, { create: true });
// 从 OPFS 恢复已有表数据到内存缓存
await this.loadExistingTables();
}
async close() {
this.root = null;
@@ -969,6 +971,39 @@
return [];
}
}
/** 从 OPFS 加载已有表数据到内存缓存 */
async loadExistingTables() {
if (!this.tablesDir)
return;
const dir = this.tablesDir;
for await (const [name] of dir.entries()) {
if (!name.endsWith('.json'))
continue;
const tableName = name.replace('.json', '');
try {
const data = await this.readTableData(tableName);
// 从数据中推断 schema(简化:从第一行提取列信息)
if (data.length > 0) {
const firstRow = data[0];
const columns = {};
for (const key of Object.keys(firstRow)) {
const val = firstRow[key];
const type = typeof val === 'number' ? 'number' :
typeof val === 'boolean' ? 'boolean' :
typeof val === 'object' ? 'json' : 'string';
columns[key] = { type, primaryKey: key === 'id' };
}
await this.memoryCache.createTable({ name: tableName, columns });
for (const row of data) {
await this.memoryCache.insert(tableName, [row]);
}
}
}
catch {
// 单个文件损坏不影响其他表
}
}
}
/** 从 OPFS 加载表数据到内存缓存 */
async loadTableIntoMemory(tableName, schema) {
await this.memoryCache.createTable(schema);
@@ -3065,8 +3100,23 @@
await this.loadSchemas();
// 6. 初始化 LSM(加载 SSTable 元数据)
await this.lsm.init();
// 7. WAL 恢复(恢复未刷盘的数据
await this.wal.recover((record) => this.applyWALRecord(record));
// 7. WAL 恢复(两阶段:先扫描事务边界,仅回放已提交事务
const committedTxns = new Set();
const allRecords = [];
await this.wal.recover((r) => allRecords.push(r));
// 第一遍:确定已提交事务
for (const r of allRecords) {
if (r.type === WALRecordType.COMMIT)
committedTxns.add(r.txnId);
if (r.type === WALRecordType.ROLLBACK)
committedTxns.delete(r.txnId);
}
// 第二遍:仅应用 txnId==0(非事务)或已提交事务的数据
for (const r of allRecords) {
if (r.txnId === 0 || committedTxns.has(r.txnId)) {
this.applyWALRecord(r);
}
}
// 8. Checkpoint ManagerBufferPool 暂简化,使用 flush 替代)
this.checkpointManager = new CheckpointManager(this.lsm, this.wal, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval);
this.opened = true;
@@ -3548,6 +3598,7 @@
case WALRecordType.COMMIT:
case WALRecordType.ROLLBACK:
case WALRecordType.BEGIN:
case WALRecordType.DROP_TABLE:
break;
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long