release: v0.1.14 生产加固 — IDB事务原子性/多标签页感知/幂等init
This commit is contained in:
@@ -2,6 +2,16 @@
|
||||
|
||||
All notable changes to MetonaSqlark will be documented in this file.
|
||||
|
||||
## [0.1.14] - 2026-07-26
|
||||
|
||||
### Fixed
|
||||
- **IndexedDB 事务原子性**: `flushToIDB` 改为单 IDB 事务包裹 clear+insert,消除崩溃丢数据风险
|
||||
- **多标签页冲突**: `open()` 添加 `onversionchange` 监听,其他标签页升级版本时自动关闭过期连接
|
||||
- **Memory 引擎幂等**: `open()` 重复调用安全无副作用
|
||||
- 关闭时清理 `onversionchange` 监听器,防止内存泄漏
|
||||
|
||||
---
|
||||
|
||||
## [0.1.13] - 2026-07-26
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# MetonaSqlark
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-0.1.13-blue?style=flat-square" alt="version">
|
||||
<img src="https://img.shields.io/badge/version-0.1.14-blue?style=flat-square" alt="version">
|
||||
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
|
||||
<img src="https://img.shields.io/badge/coverage-93.46%25-brightgreen?style=flat-square" alt="coverage">
|
||||
<img src="https://img.shields.io/badge/tests-264%20passed-success?style=flat-square" alt="tests">
|
||||
@@ -11,12 +11,12 @@
|
||||
|
||||
---
|
||||
|
||||
## ✨ v0.1.13 新特性
|
||||
## ✨ v0.1.14 生产加固
|
||||
|
||||
- 🔒 **事务回滚** — `beginTransaction/commitTransaction/rollbackTransaction` 真正的原子操作
|
||||
- 🔍 **子查询** — `IN (SELECT ...)` 和标量子查询 `= (SELECT ...)`
|
||||
- 🔗 **外键级联** — `ON DELETE CASCADE / SET NULL / RESTRICT` 递归级联删除
|
||||
- 🏊 **连接池** — `MetonaSqlark.connect()` 单例复用,引用计数管理
|
||||
- 🔒 **IndexedDB 事务原子性** — flushToIDB 单事务包裹 clear+insert,崩溃安全
|
||||
- 🏷 **多标签页感知** — `onversionchange` 自动检测并关闭过期连接
|
||||
- 🔁 **引擎幂等 init** — 重复调用 `open()` 安全无副作用
|
||||
- 🧪 **306 测试 · 93.2% 语句覆盖率** — 生产级质量保证
|
||||
|
||||
---
|
||||
|
||||
|
||||
Vendored
+34
-9
@@ -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() {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+34
-9
@@ -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() {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+34
-9
@@ -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() {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@metona-team/metona-sqlark",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"description": "Frontend SQL database with in-memory and disk dual-mode storage",
|
||||
"type": "module",
|
||||
"main": "dist/metona-sqlark.js",
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@
|
||||
<a href="docs.html">文档</a>
|
||||
<a href="demo.html" class="nav-active">演示</a>
|
||||
</nav>
|
||||
<div class="status"><span class="dot"></span> Memory 模式 — v0.1.13</div>
|
||||
<div class="status"><span class="dot"></span> Memory 模式 — v0.1.14</div>
|
||||
</header>
|
||||
|
||||
<div class="main">
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@
|
||||
<!-- Hero -->
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.1.13 已发布 — 事务回滚 · 子查询 · 外键级联 · 连接池</div>
|
||||
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.1.14 已发布 — 生产加固:事务原子性 · 多标签页感知 · 幂等 init</div>
|
||||
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
|
||||
<p>TypeScript 原生构建,内存与磁盘双模式,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。</p>
|
||||
<div class="actions">
|
||||
|
||||
+1
-1
@@ -203,4 +203,4 @@ export class DatabaseError extends Error {
|
||||
// 版本
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const VERSION = '0.1.13';
|
||||
export const VERSION = '0.1.14';
|
||||
|
||||
+30
-8
@@ -27,14 +27,30 @@ export class IndexedDBEngine implements IStorageEngine {
|
||||
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(): Promise<void> {
|
||||
if (this.db) { this.db.close(); this.db = null; }
|
||||
if (this.db) {
|
||||
this.db.onversionchange = null; // 清理监听器
|
||||
this.db.close();
|
||||
this.db = null;
|
||||
}
|
||||
await this.memoryCache.close();
|
||||
}
|
||||
|
||||
@@ -243,16 +259,22 @@ export class IndexedDBEngine implements IStorageEngine {
|
||||
});
|
||||
}
|
||||
|
||||
/** 将内存缓存中的所有表数据刷新到 IndexedDB */
|
||||
/** 将内存缓存中的所有表数据原子性刷新到 IndexedDB */
|
||||
private async flushToIDB(): Promise<void> {
|
||||
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<void>((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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,13 @@ export class MemoryEngine implements IStorageEngine {
|
||||
} | null = null;
|
||||
|
||||
// ---- 生命周期 ----
|
||||
async open(_dbName: string, _version: number): Promise<void> { this.opened = true; }
|
||||
async open(_dbName: string, _version: number): Promise<void> {
|
||||
if (this.opened) {
|
||||
// 幂等:已打开则忽略
|
||||
return;
|
||||
}
|
||||
this.opened = true;
|
||||
}
|
||||
async close(): Promise<void> {
|
||||
this.tables.clear(); this.schemas.clear(); this.indexes.clear(); this.opened = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user