release: v0.2.2 AriaEngine OPFS 自研存储后端 — 零依赖纯文件系统
CI / test (18.x) (push) Successful in 10m0s
CI / test (20.x) (push) Successful in 9m56s
CI / test (22.x) (push) Successful in 9m56s
CI / test (24.x) (push) Successful in 9m52s

This commit is contained in:
thzxx
2026-07-27 21:08:57 +08:00
parent eab58adc37
commit fef4db44ca
18 changed files with 450 additions and 73 deletions
@@ -1,54 +1,11 @@
# v0.2.1 生产加固方案 # v0.2.2 AriaEngine OPFS 自研存储后端
## 核心原则 ## 实现内容
- **所有新增测试方法必须确保不会导致 CI 卡死**(无 setTimeout/setInterval 泄漏,无 fake-indexeddb 滥用,无无限循环)
- 优先级 P0 → P1 → P2 顺序执行
## P0 — Critical4项) 1. **新增 `src/engine/aria/store/opfs_backend.ts`** — OPFSBackend 实现 IStorageBackend 接口,纯文件系统,零依赖
2. **`src/engine/aria/types.ts`** — storageBackend 类型增加 `'opfs'`
1. **Memory 引擎事务回滚修复**`src/engine/memory.ts` 的事务快照机制已存在但 TransactionManager 未正确调用。修复 `src/transaction/index.ts` 在 rollback 时调用引擎层 rollback。 3. **`src/engine/aria/index.ts`** — open() 增加 opfs 分支
4. **`src/index.ts`** — 导出 OPFSBackend
2. **WAL CRC 校验验证**`src/engine/aria/wal/log.ts` decodeAllRecords 跳过 CRC,恢复时验证 CRC 不匹配则丢弃 + 告警。 5. **`package.json`** — 版本 0.2.2
6. **README.md / CHANGELOG.md / site** — 更新版本号和特性说明
3. **Hybrid 引擎提交顺序**`src/hybrid/index.ts` commit 先写磁盘再写内存,磁盘失败回滚内存。 7. **测试** — OPFS 浏览器专属,用 mock 验证不卡死
4. **SSTableReader rangeScan 边界条件** — 修复多 block 场景下 startBlockIdx/endBlockIdx 可能越界。
## P1 — High6项)
5. **ColumnDef 约束激活**`src/table/schema.ts` checkFieldType 中激活 min/max/maxLength 校验。配套新增 schema 测试(纯同步,不会卡死)。
6. **查询结果上限**`DatabaseConfig` 增加 `maxRowsPerQuery` 默认 10000,所有引擎 find() 中截断。测试用 memory 引擎(纯同步)。
7. **`onError` 回调接入** — `src/core.ts` CRUD 操作 catch 块调用 `this.config.onError`。测试用 jest.fn() 验证(同步断言)。
8. **OPFS 引擎基础测试** — 补 5 个测试:open/close、createTable、insert、find、count。由于 OPFS 浏览器专属,测试用内存 fallback mock 方式,避免 jsdom 不具备 OPFS 导致卡死。
9. **MVCC gc() 定期调用** — AriaEngine `checkpointManager.tick()` 后每 N 次 op 调用一次 gc()。测试用 MVCC 单元测试(纯同步)。
10. **SSTable `get()` 返回 null 兜底** — 已修复(P0-4包含)
## P2 — Medium6项)
11. **LZ4 压缩修复 + 正确性测试** — 修复 compress/decompress 往返 bug,补 3 个往返正确性测试。**注意**:测试数据量控制在 500 字节以内,避免大循环。纯同步测试。
12. **debug 模式**`DatabaseConfig``debug: boolean`console.debug 输出关键操作日志。
13. **RESTRICT 外键行为修正** — cascadeDelete 检测到引用行时抛 `FOREIGN_KEY_VIOLATION` 而非静默保留孤儿。
14. **浏览器兼容表** — README 补充 Chrome 80+/Firefox 80+/Safari 14+/Edge 80+ 支持声明。
15. **React/Vue 集成去 @ts-nocheck** — 移除注释,补充完整类型声明。(测试仅验证编译不验证运行时,避免 jsdom 缺失 hooks 导致卡死)
16. **SQL 多语句防护** — Parser 检测到多余分号后还有有效 token 时报 `PARSE_ERROR`
## 严禁事项(新增测试必须遵守)
- ❌ 不使用 `setTimeout`/`setInterval` 除非配合 `jest.useFakeTimers()`
- ❌ 不使用 `fake-indexeddb` 进行多轮 open/close/reopen
- ❌ 不创建 IndexedDB 连接后不关闭
- ❌ 不使用 `done()` 回调模式
- ❌ 不对大数据集(>1000条)做 O(n²) 操作
- ✅ 优先使用 async/await + Memory Backend
- ✅ Mock 代替真实浏览器 API
- ✅ 每个 afterEach 确保资源清理
+14
View File
@@ -2,6 +2,20 @@
All notable changes to MetonaSqlark will be documented in this file. All notable changes to MetonaSqlark will be documented in this file.
## [0.2.2] - 2026-07-27
### Added
- **OPFS 自研存储后端** — AriaEngine 新增 `OPFSBackend`,纯浏览器文件系统 API,零 IndexedDB 依赖
- 每个 key 对应一个二进制文件,存储在 `navigator.storage.getDirectory()`
- 支持 read/write/delete/list/exists/clear 完整接口
- 页面文件(4KB)、WAL 日志、Schema、SSTable 全部存储为独立文件
- `{ dbName }/pg_1`, `{ dbName }/__wal_0`, `{ dbName }/__aria_schemas` ...
### Changed
- `AriaEngineConfig.storageBackend` 已支持 `'opfs'``AriaEngine.open()` 自动选择 OPFSBackend
---
## [0.2.1] - 2026-07-27 ## [0.2.1] - 2026-07-27
### Fixed ### Fixed
+2 -1
View File
@@ -1,7 +1,7 @@
# MetonaSqlark # MetonaSqlark
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/version-0.2.1-blue?style=flat-square" alt="version"> <img src="https://img.shields.io/badge/version-0.2.2-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/license-MIT-green?style=flat-square" alt="license">
<img src="https://img.shields.io/badge/coverage-91.0%25-brightgreen?style=flat-square" alt="coverage"> <img src="https://img.shields.io/badge/coverage-91.0%25-brightgreen?style=flat-square" alt="coverage">
<img src="https://img.shields.io/badge/tests-526%20passed-success?style=flat-square" alt="tests"> <img src="https://img.shields.io/badge/tests-526%20passed-success?style=flat-square" alt="tests">
@@ -271,6 +271,7 @@ const rows = await db.query('SELECT * FROM users');
| **Bloom Filter** | FNV-1a + Murmur 双哈希,快速否定 key | | **Bloom Filter** | FNV-1a + Murmur 双哈希,快速否定 key |
| **Slotted Page** | 4KB 固定页面,Slot Directory + Tuple 二进制序列化 | | **Slotted Page** | 4KB 固定页面,Slot Directory + Tuple 二进制序列化 |
| **Compaction** | Leveled Compaction,自动合并回收空间 | | **Compaction** | Leveled Compaction,自动合并回收空间 |
| **OPFS Backend** 🆕 | 自研文件存储后端,零外部依赖,纯二进制页面文件 |
--- ---
+88 -1
View File
@@ -2780,6 +2780,89 @@ class MemoryBackend {
} }
} }
class OPFSBackend {
constructor() {
this.root = null;
this.dbDir = null;
this.dbName = '';
}
async open(name) {
this.dbName = name;
this.root = await navigator.storage.getDirectory();
this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
}
async close() {
this.dbDir = null;
this.root = null;
}
isOpen() {
return this.dbDir !== null;
}
async read(key) {
if (!this.dbDir)
return null;
try {
const fh = await this.dbDir.getFileHandle(key);
const file = await fh.getFile();
return await file.arrayBuffer();
}
catch {
return null;
}
}
async write(key, data) {
if (!this.dbDir)
return;
const fh = await this.dbDir.getFileHandle(key, { create: true });
const writable = await fh.createWritable();
await writable.write(data);
await writable.close();
}
async delete(key) {
if (!this.dbDir)
return;
try {
await this.dbDir.removeEntry(key);
}
catch {
// 文件不存在则忽略
}
}
async listKeys() {
if (!this.dbDir)
return [];
const keys = [];
// FileSystemDirectoryHandle.entries() 返回 AsyncIterable,使用 any 绕过 dts 生成限制
const dir = this.dbDir;
for await (const [name] of dir.entries()) {
keys.push(name);
}
return keys;
}
async exists(key) {
if (!this.dbDir)
return false;
try {
await this.dbDir.getFileHandle(key);
return true;
}
catch {
return false;
}
}
async clear() {
if (!this.dbDir)
return;
const dir = this.dbDir;
for await (const [name] of dir.entries()) {
try {
await this.dbDir.removeEntry(name);
}
catch { /* ignore */ }
}
}
}
/** /**
* AriaEngine — 自研页面式存储引擎主类 * AriaEngine — 自研页面式存储引擎主类
* @module engine/aria/index * @module engine/aria/index
@@ -2817,7 +2900,10 @@ class AriaEngine {
return; return;
this.dbName = dbName; this.dbName = dbName;
// 1. 存储后端 // 1. 存储后端
if (this.config.storageBackend === 'indexeddb') { if (this.config.storageBackend === 'opfs') {
this.backend = new OPFSBackend();
}
else if (this.config.storageBackend === 'indexeddb') {
this.backend = new IndexedDBBackend(); this.backend = new IndexedDBBackend();
} }
else { else {
@@ -5825,6 +5911,7 @@ exports.IndexedDBEngine = IndexedDBEngine;
exports.MeSqlark = MeSqlark; exports.MeSqlark = MeSqlark;
exports.MemoryEngine = MemoryEngine; exports.MemoryEngine = MemoryEngine;
exports.MetonaSqlark = MetonaSqlark; exports.MetonaSqlark = MetonaSqlark;
exports.OPFSBackend = OPFSBackend;
exports.OPFSEngine = OPFSEngine; exports.OPFSEngine = OPFSEngine;
exports.Table = Table; exports.Table = Table;
exports.VERSION = VERSION; exports.VERSION = VERSION;
+1 -1
View File
File diff suppressed because one or more lines are too long
+54 -1
View File
@@ -851,6 +851,59 @@ interface Token {
/** 将 SQL 字符串解析为 Token 列表 */ /** 将 SQL 字符串解析为 Token 列表 */
declare function tokenize(sql: string): Token[]; declare function tokenize(sql: string): Token[];
/**
* AriaEngine Storage Backend — 存储后端抽象层
* @module engine/aria/store/backend
*
* 封装底层浏览器存储 APIIndexedDB / OPFS / Memory 回退),
* 供 Buffer Pool 的 PageIO 和 WAL 的 WALStore 使用。
*/
interface IStorageBackend {
/** 打开存储 */
open(name: string): Promise<void>;
/** 关闭存储 */
close(): Promise<void>;
/** 是否已打开 */
isOpen(): boolean;
/** 读取数据块 */
read(key: string): Promise<ArrayBuffer | null>;
/** 写入数据块 */
write(key: string, data: ArrayBuffer): Promise<void>;
/** 删除数据块 */
delete(key: string): Promise<void>;
/** 列出所有 key */
listKeys(): Promise<string[]>;
/** 检查 key 是否存在 */
exists(key: string): Promise<boolean>;
/** 清空所有数据 */
clear(): Promise<void>;
}
/**
* AriaEngine OPFS Backend — 基于 Origin Private File System 的自研存储后端
* @module engine/aria/store/opfs_backend
*
* 零外部依赖,纯浏览器文件系统 API。
* 每个 key 对应 OPFS 目录下的一个二进制文件。
*
* 浏览器要求:Chrome 102+ / Edge 102+
*/
declare class OPFSBackend implements IStorageBackend {
private root;
private dbDir;
private dbName;
open(name: string): Promise<void>;
close(): Promise<void>;
isOpen(): boolean;
read(key: string): Promise<ArrayBuffer | null>;
write(key: string, data: ArrayBuffer): Promise<void>;
delete(key: string): Promise<void>;
listKeys(): Promise<string[]>;
exists(key: string): Promise<boolean>;
clear(): Promise<void>;
}
/** /**
* metona-sqlark — 入口文件 * metona-sqlark — 入口文件
* @module metona-sqlark * @module metona-sqlark
@@ -896,4 +949,4 @@ declare global {
declare const MeSqlark: typeof MetonaSqlark; declare const MeSqlark: typeof MetonaSqlark;
export { AriaEngine, AriaEngineConfig, ColumnDef, DatabaseConfig, DeleteStatement, DiskEngine, FieldType, HybridEngine, IStorageEngine, IndexedDBEngine, InsertStatement, MeSqlark, MemoryEngine, MetonaSqlark, OPFSEngine, SelectStatement, Statement, StorageMode, Table, TableSchema, UpdateStatement, VERSION, api, create, api as default, parse, tokenize }; export { AriaEngine, AriaEngineConfig, ColumnDef, DatabaseConfig, DeleteStatement, DiskEngine, FieldType, HybridEngine, IStorageEngine, IndexedDBEngine, InsertStatement, MeSqlark, MemoryEngine, MetonaSqlark, OPFSBackend, OPFSEngine, SelectStatement, Statement, StorageMode, Table, TableSchema, UpdateStatement, VERSION, api, create, api as default, parse, tokenize };
+88 -2
View File
@@ -2776,6 +2776,89 @@ class MemoryBackend {
} }
} }
class OPFSBackend {
constructor() {
this.root = null;
this.dbDir = null;
this.dbName = '';
}
async open(name) {
this.dbName = name;
this.root = await navigator.storage.getDirectory();
this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
}
async close() {
this.dbDir = null;
this.root = null;
}
isOpen() {
return this.dbDir !== null;
}
async read(key) {
if (!this.dbDir)
return null;
try {
const fh = await this.dbDir.getFileHandle(key);
const file = await fh.getFile();
return await file.arrayBuffer();
}
catch {
return null;
}
}
async write(key, data) {
if (!this.dbDir)
return;
const fh = await this.dbDir.getFileHandle(key, { create: true });
const writable = await fh.createWritable();
await writable.write(data);
await writable.close();
}
async delete(key) {
if (!this.dbDir)
return;
try {
await this.dbDir.removeEntry(key);
}
catch {
// 文件不存在则忽略
}
}
async listKeys() {
if (!this.dbDir)
return [];
const keys = [];
// FileSystemDirectoryHandle.entries() 返回 AsyncIterable,使用 any 绕过 dts 生成限制
const dir = this.dbDir;
for await (const [name] of dir.entries()) {
keys.push(name);
}
return keys;
}
async exists(key) {
if (!this.dbDir)
return false;
try {
await this.dbDir.getFileHandle(key);
return true;
}
catch {
return false;
}
}
async clear() {
if (!this.dbDir)
return;
const dir = this.dbDir;
for await (const [name] of dir.entries()) {
try {
await this.dbDir.removeEntry(name);
}
catch { /* ignore */ }
}
}
}
/** /**
* AriaEngine — 自研页面式存储引擎主类 * AriaEngine — 自研页面式存储引擎主类
* @module engine/aria/index * @module engine/aria/index
@@ -2813,7 +2896,10 @@ class AriaEngine {
return; return;
this.dbName = dbName; this.dbName = dbName;
// 1. 存储后端 // 1. 存储后端
if (this.config.storageBackend === 'indexeddb') { if (this.config.storageBackend === 'opfs') {
this.backend = new OPFSBackend();
}
else if (this.config.storageBackend === 'indexeddb') {
this.backend = new IndexedDBBackend(); this.backend = new IndexedDBBackend();
} }
else { else {
@@ -5815,5 +5901,5 @@ if (typeof window !== 'undefined') {
// 别名 // 别名
const MeSqlark = MetonaSqlark; const MeSqlark = MetonaSqlark;
export { AriaEngine, HybridEngine, IndexedDBEngine, MeSqlark, MemoryEngine, MetonaSqlark, OPFSEngine, Table, VERSION, api, create, api as default, parse, tokenize }; export { AriaEngine, HybridEngine, IndexedDBEngine, MeSqlark, MemoryEngine, MetonaSqlark, OPFSBackend, OPFSEngine, Table, VERSION, api, create, api as default, parse, tokenize };
//# sourceMappingURL=metona-sqlark.esm.js.map //# sourceMappingURL=metona-sqlark.esm.js.map
+1 -1
View File
File diff suppressed because one or more lines are too long
+88 -1
View File
@@ -2782,6 +2782,89 @@
} }
} }
class OPFSBackend {
constructor() {
this.root = null;
this.dbDir = null;
this.dbName = '';
}
async open(name) {
this.dbName = name;
this.root = await navigator.storage.getDirectory();
this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
}
async close() {
this.dbDir = null;
this.root = null;
}
isOpen() {
return this.dbDir !== null;
}
async read(key) {
if (!this.dbDir)
return null;
try {
const fh = await this.dbDir.getFileHandle(key);
const file = await fh.getFile();
return await file.arrayBuffer();
}
catch {
return null;
}
}
async write(key, data) {
if (!this.dbDir)
return;
const fh = await this.dbDir.getFileHandle(key, { create: true });
const writable = await fh.createWritable();
await writable.write(data);
await writable.close();
}
async delete(key) {
if (!this.dbDir)
return;
try {
await this.dbDir.removeEntry(key);
}
catch {
// 文件不存在则忽略
}
}
async listKeys() {
if (!this.dbDir)
return [];
const keys = [];
// FileSystemDirectoryHandle.entries() 返回 AsyncIterable,使用 any 绕过 dts 生成限制
const dir = this.dbDir;
for await (const [name] of dir.entries()) {
keys.push(name);
}
return keys;
}
async exists(key) {
if (!this.dbDir)
return false;
try {
await this.dbDir.getFileHandle(key);
return true;
}
catch {
return false;
}
}
async clear() {
if (!this.dbDir)
return;
const dir = this.dbDir;
for await (const [name] of dir.entries()) {
try {
await this.dbDir.removeEntry(name);
}
catch { /* ignore */ }
}
}
}
/** /**
* AriaEngine 自研页面式存储引擎主类 * AriaEngine 自研页面式存储引擎主类
* @module engine/aria/index * @module engine/aria/index
@@ -2819,7 +2902,10 @@
return; return;
this.dbName = dbName; this.dbName = dbName;
// 1. 存储后端 // 1. 存储后端
if (this.config.storageBackend === 'indexeddb') { if (this.config.storageBackend === 'opfs') {
this.backend = new OPFSBackend();
}
else if (this.config.storageBackend === 'indexeddb') {
this.backend = new IndexedDBBackend(); this.backend = new IndexedDBBackend();
} }
else { else {
@@ -5827,6 +5913,7 @@
exports.MeSqlark = MeSqlark; exports.MeSqlark = MeSqlark;
exports.MemoryEngine = MemoryEngine; exports.MemoryEngine = MemoryEngine;
exports.MetonaSqlark = MetonaSqlark; exports.MetonaSqlark = MetonaSqlark;
exports.OPFSBackend = OPFSBackend;
exports.OPFSEngine = OPFSEngine; exports.OPFSEngine = OPFSEngine;
exports.Table = Table; exports.Table = Table;
exports.VERSION = VERSION; exports.VERSION = VERSION;
+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
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@metona-team/metona-sqlark", "name": "@metona-team/metona-sqlark",
"version": "0.2.1", "version": "0.2.2",
"description": "Frontend SQL database with in-memory and disk dual-mode storage", "description": "Frontend SQL database with in-memory and disk dual-mode storage",
"type": "module", "type": "module",
"main": "dist/metona-sqlark.js", "main": "dist/metona-sqlark.js",
+5 -5
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🧪 在线演示 — MetonaSqlark v0.2.1</title> <title>🧪 在线演示 — MetonaSqlark v0.2.2</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
<style> <style>
:root { :root {
@@ -83,13 +83,13 @@
<a href="docs.html">文档</a> <a href="docs.html">文档</a>
<a href="demo.html" class="nav-active">演示</a> <a href="demo.html" class="nav-active">演示</a>
</nav> </nav>
<div class="status"><span class="dot"></span> Memory 模式 — v0.2.1</div> <div class="status"><span class="dot"></span> Memory 模式 — v0.2.2</div>
</header> </header>
<div class="main"> <div class="main">
<div class="editor-panel"> <div class="editor-panel">
<div class="editor-area"> <div class="editor-area">
<textarea id="sql-input" placeholder="输入 SQL 语句...&#10;&#10;SELECT * FROM users;&#10;INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28);&#10;SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.2.1 在线演示 <textarea id="sql-input" placeholder="输入 SQL 语句...&#10;&#10;SELECT * FROM users;&#10;INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28);&#10;SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.2.2 在线演示
-- 已预置 users / orders / products 表数据 -- 已预置 users / orders / products 表数据
-- 新特性: AriaEngine · LSM-Tree · WAL · MVCC -- 新特性: AriaEngine · LSM-Tree · WAL · MVCC
@@ -469,7 +469,7 @@ LIMIT 5 OFFSET 0;
-- NOT LIKE 模糊排除 -- NOT LIKE 模糊排除
SELECT * FROM users SELECT * FROM users
WHERE name NOT LIKE 'A%' AND age > 20;`, WHERE name NOT LIKE 'A%' AND age > 20;`,
aria: `-- 🌲 AriaEngine 演示 (v0.2.1) aria: `-- 🌲 AriaEngine 演示 (v0.2.2)
-- AriaEngine: LSM-Tree 自研存储引擎 -- AriaEngine: LSM-Tree 自研存储引擎
-- 支持 WAL 崩溃恢复 + MVCC 快照隔离 -- 支持 WAL 崩溃恢复 + MVCC 快照隔离
@@ -522,7 +522,7 @@ document.addEventListener('keydown', e => {
// Boot // Boot
initDB().then(() => { initDB().then(() => {
console.log('✅ MetonaSqlark v0.2.1 demo ready'); console.log('✅ MetonaSqlark v0.2.2 demo ready');
setTimeout(runQuery, 300); setTimeout(runQuery, 300);
}).catch(err => { }).catch(err => {
renderError('初始化失败: ' + err.message); renderError('初始化失败: ' + err.message);
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>📖 API 文档 — MetonaSqlark v0.2.1</title> <title>📖 API 文档 — MetonaSqlark v0.2.2</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
<style> <style>
:root { :root {
@@ -629,7 +629,7 @@ db.<span class="f">emit</span>(<span class="s">'users'</span>, { <span class="s"
<h2 id="aria-engine">🌲 AriaEngine 自研存储引擎</h2> <h2 id="aria-engine">🌲 AriaEngine 自研存储引擎</h2>
<p><strong>v0.2.0 新增</strong> — AriaEngine 是专为 MetonaSqlark 设计的页面式存储引擎,对标 SQLite 设计理念。<br> <p><strong>v0.2.0 新增</strong> — AriaEngine 是专为 MetonaSqlark 设计的页面式存储引擎,对标 SQLite 设计理念。<br>
<strong>v0.2.1 生产加固</strong>WAL CRC 完整性校验、约束激活、RESTRICT 外键修正、Hybrid 提交顺序修复</p> <strong>v0.2.2 OPFS 自研后端</strong>新增 OPFSBackend,纯浏览器文件系统,零 IndexedDB 依赖,完全自研存储</p>
<h3>核心特性</h3> <h3>核心特性</h3>
<table> <table>
+1 -1
View File
@@ -152,7 +152,7 @@
<!-- Hero --> <!-- Hero -->
<section class="hero"> <section class="hero">
<div class="container"> <div class="container">
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.2.1 生产加固 — WAL CRC校验 · 约束激活 · RESTRICT外键 · Hybrid提交修复 · 浏览器兼容声明</div> <div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.2.2 发布 — OPFS 自研存储后端 · 零依赖纯文件系统 · AriaEngine 完全自研</div>
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1> <h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
<p>TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL + MVCC。</p> <p>TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL + MVCC。</p>
<div class="actions"> <div class="actions">
+4 -1
View File
@@ -25,6 +25,7 @@ import { WAL } from './wal/log';
import { WALRecordType, type WALRecord } from './types'; import { WALRecordType, type WALRecord } from './types';
import { CheckpointManager } from './wal/checkpoint'; import { CheckpointManager } from './wal/checkpoint';
import { IndexedDBBackend, MemoryBackend, type IStorageBackend } from './store/backend'; import { IndexedDBBackend, MemoryBackend, type IStorageBackend } from './store/backend';
import { OPFSBackend } from './store/opfs_backend';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// AriaEngine // AriaEngine
@@ -63,7 +64,9 @@ export class AriaEngine implements IStorageEngine {
this.dbName = dbName; this.dbName = dbName;
// 1. 存储后端 // 1. 存储后端
if (this.config.storageBackend === 'indexeddb') { if (this.config.storageBackend === 'opfs') {
this.backend = new OPFSBackend();
} else if (this.config.storageBackend === 'indexeddb') {
this.backend = new IndexedDBBackend(); this.backend = new IndexedDBBackend();
} else { } else {
this.backend = new MemoryBackend(); this.backend = new MemoryBackend();
+88
View File
@@ -0,0 +1,88 @@
/**
* AriaEngine OPFS Backend Origin Private File System
* @module engine/aria/store/opfs_backend
*
* API
* key OPFS
*
* Chrome 102+ / Edge 102+
*/
import type { IStorageBackend } from './backend';
export class OPFSBackend implements IStorageBackend {
private root: FileSystemDirectoryHandle | null = null;
private dbDir: FileSystemDirectoryHandle | null = null;
private dbName = '';
async open(name: string): Promise<void> {
this.dbName = name;
this.root = await navigator.storage.getDirectory();
this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
}
async close(): Promise<void> {
this.dbDir = null;
this.root = null;
}
isOpen(): boolean {
return this.dbDir !== null;
}
async read(key: string): Promise<ArrayBuffer | null> {
if (!this.dbDir) return null;
try {
const fh = await this.dbDir.getFileHandle(key);
const file = await fh.getFile();
return await file.arrayBuffer();
} catch {
return null;
}
}
async write(key: string, data: ArrayBuffer): Promise<void> {
if (!this.dbDir) return;
const fh = await this.dbDir.getFileHandle(key, { create: true });
const writable = await fh.createWritable();
await writable.write(data);
await writable.close();
}
async delete(key: string): Promise<void> {
if (!this.dbDir) return;
try {
await this.dbDir.removeEntry(key);
} catch {
// 文件不存在则忽略
}
}
async listKeys(): Promise<string[]> {
if (!this.dbDir) return [];
const keys: string[] = [];
// FileSystemDirectoryHandle.entries() 返回 AsyncIterable,使用 any 绕过 dts 生成限制
const dir = this.dbDir as any;
for await (const [name] of dir.entries()) {
keys.push(name);
}
return keys;
}
async exists(key: string): Promise<boolean> {
if (!this.dbDir) return false;
try {
await this.dbDir.getFileHandle(key);
return true;
} catch {
return false;
}
}
async clear(): Promise<void> {
if (!this.dbDir) return;
const dir = this.dbDir as any;
for await (const [name] of dir.entries()) {
try { await this.dbDir!.removeEntry(name); } catch { /* ignore */ }
}
}
}
+2 -1
View File
@@ -86,5 +86,6 @@ export { Table } from './table/table';
export { parse } from './sql/parser'; export { parse } from './sql/parser';
export { tokenize } from './sql/lexer'; export { tokenize } from './sql/lexer';
// AriaEngine 类型 // AriaEngine 类型 & 后端
export type { AriaEngineConfig } from './engine/aria/types'; export type { AriaEngineConfig } from './engine/aria/types';
export { OPFSBackend } from './engine/aria/store/opfs_backend';