release: v0.1.14 生产加固 — IDB事务原子性/多标签页感知/幂等init
CI / test (18.x) (push) Successful in 9m54s
CI / test (20.x) (push) Successful in 9m52s
CI / test (22.x) (push) Successful in 9m52s
CI / test (24.x) (push) Successful in 9m48s

This commit is contained in:
thzxx
2026-07-26 17:20:31 +08:00
parent 71e238a5cc
commit 014e92884c
16 changed files with 165 additions and 52 deletions
+34 -9
View File
@@ -31,7 +31,7 @@ class DatabaseError extends Error {
// ---------------------------------------------------------------------------
// 版本
// ---------------------------------------------------------------------------
const VERSION = '0.1.13';
const VERSION = '0.1.14';
/**
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
@@ -204,7 +204,13 @@ class MemoryEngine {
this.snapshot = null;
}
// ---- 生命周期 ----
async open(_dbName, _version) { this.opened = true; }
async open(_dbName, _version) {
if (this.opened) {
// 幂等:已打开则忽略
return;
}
this.opened = true;
}
async close() {
this.tables.clear();
this.schemas.clear();
@@ -553,13 +559,26 @@ class IndexedDBEngine {
await this.memoryCache.open(dbName, version);
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version);
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onsuccess = () => {
this.db = request.result;
// 多标签页冲突处理:其他标签页升级版本时自动关闭当前连接
this.db.onversionchange = () => {
if (this.db) {
this.db.close();
this.db = null;
// eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Database "${dbName}" was upgraded in another tab. Connection closed. Please re-open.`);
}
};
resolve();
};
request.onerror = () => reject(new DatabaseError(`Failed to open IndexedDB "${dbName}"`, 'IDB_OPEN_ERROR', request.error));
request.onblocked = () => reject(new DatabaseError(`IndexedDB "${dbName}" is blocked`, 'IDB_BLOCKED'));
});
}
async close() {
if (this.db) {
this.db.onversionchange = null; // 清理监听器
this.db.close();
this.db = null;
}
@@ -765,16 +784,22 @@ class IndexedDBEngine {
tx.onerror = () => reject(new DatabaseError(`Clear failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
});
}
/** 将内存缓存中的所有表数据刷新到 IndexedDB */
/** 将内存缓存中的所有表数据原子性刷新到 IndexedDB */
async flushToIDB() {
const tableNames = await this.memoryCache.getTableNames();
const db = this.ensureDB();
// 每个表在一个单独的 IDB 事务中完成 clear+insert,保证原子性
for (const tableName of tableNames) {
const rows = await this.memoryCache.find(tableName, { table: tableName });
// Clear + bulk insert for simplicity
await this.idbClear(tableName);
if (rows.length > 0) {
await this.idbInsert(tableName, rows);
}
await new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
store.clear(); // 清空
for (const row of rows)
store.add(row); // 批量写入
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError(`Flush failed for "${tableName}"`, 'IDB_FLUSH_ERROR', tx.error));
});
}
}
ensureDB() {
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -110,7 +110,7 @@ interface MetonaPlugin {
/** 销毁 */
destroy(): void;
}
declare const VERSION = "0.1.13";
declare const VERSION = "0.1.14";
/**
* metona-sqlark Plugin — 插件系统
@@ -567,7 +567,7 @@ declare class IndexedDBEngine implements IStorageEngine {
private idbUpdate;
private idbDelete;
private idbClear;
/** 将内存缓存中的所有表数据刷新到 IndexedDB */
/** 将内存缓存中的所有表数据原子性刷新到 IndexedDB */
private flushToIDB;
private ensureDB;
}
+34 -9
View File
@@ -27,7 +27,7 @@ class DatabaseError extends Error {
// ---------------------------------------------------------------------------
// 版本
// ---------------------------------------------------------------------------
const VERSION = '0.1.13';
const VERSION = '0.1.14';
/**
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
@@ -200,7 +200,13 @@ class MemoryEngine {
this.snapshot = null;
}
// ---- 生命周期 ----
async open(_dbName, _version) { this.opened = true; }
async open(_dbName, _version) {
if (this.opened) {
// 幂等:已打开则忽略
return;
}
this.opened = true;
}
async close() {
this.tables.clear();
this.schemas.clear();
@@ -549,13 +555,26 @@ class IndexedDBEngine {
await this.memoryCache.open(dbName, version);
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version);
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onsuccess = () => {
this.db = request.result;
// 多标签页冲突处理:其他标签页升级版本时自动关闭当前连接
this.db.onversionchange = () => {
if (this.db) {
this.db.close();
this.db = null;
// eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Database "${dbName}" was upgraded in another tab. Connection closed. Please re-open.`);
}
};
resolve();
};
request.onerror = () => reject(new DatabaseError(`Failed to open IndexedDB "${dbName}"`, 'IDB_OPEN_ERROR', request.error));
request.onblocked = () => reject(new DatabaseError(`IndexedDB "${dbName}" is blocked`, 'IDB_BLOCKED'));
});
}
async close() {
if (this.db) {
this.db.onversionchange = null; // 清理监听器
this.db.close();
this.db = null;
}
@@ -761,16 +780,22 @@ class IndexedDBEngine {
tx.onerror = () => reject(new DatabaseError(`Clear failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
});
}
/** 将内存缓存中的所有表数据刷新到 IndexedDB */
/** 将内存缓存中的所有表数据原子性刷新到 IndexedDB */
async flushToIDB() {
const tableNames = await this.memoryCache.getTableNames();
const db = this.ensureDB();
// 每个表在一个单独的 IDB 事务中完成 clear+insert,保证原子性
for (const tableName of tableNames) {
const rows = await this.memoryCache.find(tableName, { table: tableName });
// Clear + bulk insert for simplicity
await this.idbClear(tableName);
if (rows.length > 0) {
await this.idbInsert(tableName, rows);
}
await new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
store.clear(); // 清空
for (const row of rows)
store.add(row); // 批量写入
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError(`Flush failed for "${tableName}"`, 'IDB_FLUSH_ERROR', tx.error));
});
}
}
ensureDB() {
+1 -1
View File
File diff suppressed because one or more lines are too long
+34 -9
View File
@@ -33,7 +33,7 @@
// ---------------------------------------------------------------------------
// 版本
// ---------------------------------------------------------------------------
const VERSION = '0.1.13';
const VERSION = '0.1.14';
/**
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
@@ -206,7 +206,13 @@
this.snapshot = null;
}
// ---- 生命周期 ----
async open(_dbName, _version) { this.opened = true; }
async open(_dbName, _version) {
if (this.opened) {
// 幂等:已打开则忽略
return;
}
this.opened = true;
}
async close() {
this.tables.clear();
this.schemas.clear();
@@ -555,13 +561,26 @@
await this.memoryCache.open(dbName, version);
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version);
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onsuccess = () => {
this.db = request.result;
// 多标签页冲突处理:其他标签页升级版本时自动关闭当前连接
this.db.onversionchange = () => {
if (this.db) {
this.db.close();
this.db = null;
// eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Database "${dbName}" was upgraded in another tab. Connection closed. Please re-open.`);
}
};
resolve();
};
request.onerror = () => reject(new DatabaseError(`Failed to open IndexedDB "${dbName}"`, 'IDB_OPEN_ERROR', request.error));
request.onblocked = () => reject(new DatabaseError(`IndexedDB "${dbName}" is blocked`, 'IDB_BLOCKED'));
});
}
async close() {
if (this.db) {
this.db.onversionchange = null; // 清理监听器
this.db.close();
this.db = null;
}
@@ -767,16 +786,22 @@
tx.onerror = () => reject(new DatabaseError(`Clear failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
});
}
/** 将内存缓存中的所有表数据刷新到 IndexedDB */
/** 将内存缓存中的所有表数据原子性刷新到 IndexedDB */
async flushToIDB() {
const tableNames = await this.memoryCache.getTableNames();
const db = this.ensureDB();
// 每个表在一个单独的 IDB 事务中完成 clear+insert,保证原子性
for (const tableName of tableNames) {
const rows = await this.memoryCache.find(tableName, { table: tableName });
// Clear + bulk insert for simplicity
await this.idbClear(tableName);
if (rows.length > 0) {
await this.idbInsert(tableName, rows);
}
await new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
store.clear(); // 清空
for (const row of rows)
store.add(row); // 批量写入
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError(`Flush failed for "${tableName}"`, 'IDB_FLUSH_ERROR', tx.error));
});
}
}
ensureDB() {
+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