docs: 重写 README 与 site 站点 — 剥离历史版本注释、统一信息架构(核心特性/快速开始/API速览/引擎对比/AriaEngine 专章/项目状态);更新 docs.html 存储模式对比表(移除 IndexedDB 遗留列)+ v0.6.1 修复记录;同步测试数 1093/70 套件/89.7%;修正 VERSION 常量 0.6.0→0.6.1 并重建 dist
CI / test (18.x) (push) Failing after 14m2s
CI / test (22.x) (push) Failing after 12m18s
CI / test (20.x) (push) Failing after 12m55s
CI / test (24.x) (push) Failing after 16m50s
CI / e2e (push) Successful in 9m53s

This commit is contained in:
thzxx
2026-08-10 21:50:49 +08:00
parent 9c109b4619
commit 0e192e0d84
12 changed files with 575 additions and 428 deletions
+101 -40
View File
@@ -34,7 +34,7 @@ class DatabaseError extends Error {
// ---------------------------------------------------------------------------
// 版本
// ---------------------------------------------------------------------------
const VERSION = '0.6.0';
const VERSION = '0.6.1';
/**
* metona-sqlark Shared WHERE Matcher 统一的条件匹配逻辑
@@ -978,12 +978,15 @@ class OPFSBackend {
*
* 仅用于测试与 Node 环境浏览器使用 OPFS 介质KVStore 默认自动选择
*/
/** 全局注册表:dbName → key → ArrayBuffer(跨实例共享,模拟持久化) */
/** 全局注册表:dbName → key → chunk 列表(跨实例共享,模拟持久化) */
const registry = new Map();
class SharedMemoryBackend {
constructor() {
this.dbName = '';
this.store = null;
/** 存储:key → chunk 列表(append O(1)read 时一次性拼接缓存) */
this.chunks = new Map();
/** 惰性拼接缓存(read 后缓存,write/append 失效) */
this.materialized = new Map();
}
/** 清空全局注册表(测试隔离用) */
static clearRegistry() {
@@ -994,61 +997,80 @@ class SharedMemoryBackend {
if (!registry.has(name)) {
registry.set(name, new Map());
}
this.store = registry.get(name);
// 从注册表恢复 chunks(持久化语义)
this.chunks = registry.get(name);
this.materialized = new Map();
}
/** close 不清除数据(持久化语义:重开同名库数据仍在) */
async close() {
this.store = null;
this.chunks = new Map();
this.materialized = new Map();
}
isOpen() {
return this.store !== null;
return this.dbName !== '';
}
async read(key) {
return this.store?.get(key) ?? null;
if (this.materialized.has(key))
return this.materialized.get(key);
const list = this.chunks.get(key);
if (!list || list.length === 0)
return null;
if (list.length === 1) {
this.materialized.set(key, list[0]);
return list[0];
}
const total = list.reduce((s2, c) => s2 + c.byteLength, 0);
const combined = new Uint8Array(total);
let off = 0;
for (const c of list) {
combined.set(new Uint8Array(c), off);
off += c.byteLength;
}
const buf = combined.buffer;
this.materialized.set(key, buf);
return buf;
}
async write(key, data) {
this.store?.set(key, data);
this.chunks.set(key, [data]);
this.materialized.set(key, data);
}
async append(key, data) {
if (!this.store)
return;
const existing = this.store.get(key);
if (existing) {
const combined = new Uint8Array(existing.byteLength + data.byteLength);
combined.set(new Uint8Array(existing), 0);
combined.set(new Uint8Array(data), existing.byteLength);
this.store.set(key, combined.buffer);
// O(1) 追加:只记录 chunkread 时惰性拼接
const list = this.chunks.get(key);
if (list) {
list.push(data);
}
else {
this.store.set(key, data);
this.chunks.set(key, [data]);
}
this.materialized.delete(key);
}
async writeMany(entries) {
if (!this.store)
return;
// 同步批量写入 = 原子(JS 单线程,无中间 await 点)
for (const [key, data] of Object.entries(entries)) {
this.store.set(key, data);
this.chunks.set(key, [data]);
this.materialized.set(key, data);
}
}
async delete(key) {
this.store?.delete(key);
this.chunks.delete(key);
this.materialized.delete(key);
}
async deleteMany(keys) {
if (!this.store)
return;
for (const key of keys) {
this.store.delete(key);
this.chunks.delete(key);
this.materialized.delete(key);
}
}
async listKeys() {
return this.store ? Array.from(this.store.keys()) : [];
return Array.from(this.chunks.keys());
}
async exists(key) {
return this.store?.has(key) ?? false;
return this.chunks.has(key);
}
async clear() {
this.store?.clear();
this.chunks.clear();
this.materialized.clear();
}
}
@@ -3282,7 +3304,11 @@ class SSTableReader {
if (this.indexEntries.length === 0)
return;
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey));
// v0.6.1-fix(P0): 索引键是"块内最后一个 key"builder 约定),
// locateBlockLE 返回最后一个 tail <= endKey 的块,但下一个块(tail > endKey
// 可能仍包含 < endKey 的条目(如末块 t5:k49425..k49995 尾 key 是 t6 前缀)→
// 被排除导致范围扫描漏读尾部数据。多扫一个块,条目级过滤保证不丢。
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey) + 1);
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx)
return;
const lenSize = this.lenFieldSize();
@@ -4084,16 +4110,21 @@ class LSM {
}
// v0.4.3-fix: 循环等待级联任务(flush 完成可能触发新的 compaction
await this.drainChain();
// 若仍有 frozen 数据未刷盘,在链尾追加
// v0.6.1-fix(P0): 剩余数据 flush 必须挂链串行 —— 此前直接 await 执行,
// 与链上 compaction 并发写 SSTable metacompaction 产物 saveMeta 后,
// memtable flush 的 saveMeta 读到中间态列表(含 compaction 产物)→ 覆盖产物
// 引用 → compaction 产物变孤儿 → 索引/主表数据静默丢失(优雅关闭后重开丢 75%)。
// 挂链后按序执行(memtable flush 在 compaction 之后),meta 无竞态。
if (this.immutableMemtable) {
const frozen = this.immutableMemtable;
await this.flushImmutableAsync(frozen);
this.flushChain = this.enqueueOnChain(() => this.flushImmutableAsync(frozen));
}
if (this.memtable.getEntryCount() > 0) {
this.freezeMemtable();
const frozen = this.immutableMemtable;
if (frozen)
await this.flushImmutableAsync(frozen);
if (frozen) {
this.flushChain = this.enqueueOnChain(() => this.flushImmutableAsync(frozen));
}
}
this.frozenMemtables = [];
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
@@ -5290,12 +5321,25 @@ class FileManager {
async init(dbName) {
this.dbName = dbName;
const meta = await this.backend.read('__aria_meta');
let nextPageId = 1;
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
const view = new DataView(meta);
this.nextPageId = view.getUint32(0, false);
nextPageId = new DataView(meta).getUint32(0, false);
}
else {
this.nextPageId = 1;
// v0.6.1-fix(P0): 崩溃一致性 —— allocatePageIds 的 saveMeta 可能在分配
// 页面后被崩溃中断(nextPageId 回退)。恢复时若按回退值继续分配,
// 页面 id 复用会覆盖旧页面;随后 compaction 按 meta.pageIds 删除"旧"SSTable
// 时误删被复用的新数据 → 崩溃恢复后静默丢数据(kv 后端 2 万行丢 75%)。
// 修复:以"现存最大页面 id + 1"为准(单调不回退,绝不复用已存在页面)。
const keys = await this.backend.listKeys();
for (const k of keys) {
if (k.startsWith('pg_')) {
const id = Number.parseInt(k.slice(3), 10);
if (!Number.isNaN(id) && id + 1 > nextPageId)
nextPageId = id + 1;
}
}
this.nextPageId = nextPageId;
if (!meta || !(meta instanceof ArrayBuffer) || meta.byteLength < 4 || nextPageId !== new DataView(meta).getUint32(0, false)) {
await this.saveMeta();
}
this.metaLoaded = true;
@@ -6181,7 +6225,17 @@ class AriaEngine {
},
getBufferedBytes: () => this.wal.getBufferedBytes(),
getBufferedCount: () => this.wal.getBufferedCount(),
}, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval, this.config.walSizeThreshold);
}, {
flushAll: async () => {
// v0.6.1-fix: checkpoint 必须同时落盘二级索引 LSM —
// 此前只 flush 主 LSMcheckpoint 截断 WAL 后崩溃时索引 memtable 未落盘、
// WAL 为空跳过重建 → 二级索引静默丢失最后一批条目(生产数据一致性问题)
await this.lsm.flush();
for (const idxLsm of this.secondaryIndexes.values()) {
await idxLsm.flush();
}
},
}, this.config.checkpointInterval, this.config.walSizeThreshold);
this.opened = true;
}
async close() {
@@ -6432,12 +6486,17 @@ class AriaEngine {
const pks = [];
// v0.3.1: 批量 WAL 写入(组提交),一次 insert 合并为一次落盘
const walRecords = [];
// v0.6.1-perf: 批量预加载本批 PK 涉及的 SSTable(一次 drainChain)。
// 此前循环内逐行 prefetchKeys —— 每行 await drainChain 排空后台链,
// 后台 compaction 在链上数秒时每行阻塞数秒 → 大数据量插入性能悬崖
// 10 万行 kv 后端从 5ms/批暴跌到 8~11s/批)。批内新数据在 memtable
// 或 flush 产物(自动入缓存),循环内 lsm.get 始终完整。
await this.lsm.prefetchKeys(rows.map((r) => `${tableName}:${String(r[pkCol])}`));
for (const row of rows) {
const validated = this.validateRow(schema, row);
const pkValue = String(validated[pkCol]);
const key = `${tableName}:${pkValue}`;
// Check duplicate in LSM + transaction snapshot
await this.lsm.prefetchKeys([key]);
const existing = this.currentTxnId
? (this.txnSnapshot?.get(key) ?? this.lsm.get(key))
: this.lsm.get(key);
@@ -7306,13 +7365,15 @@ class AriaEngine {
},
};
}
/** v0.4.5: 是否启用页面化物理存储(默认 OPFS 后端启用,显式配置可覆盖) */
/** v0.4.5: 是否启用页面化物理存储(默认 OPFS / KVStore 后端启用,显式配置可覆盖) */
isPageStorage() {
if (this.config.pageStorage === true)
return true;
if (this.config.pageStorage === false)
return false;
return this.config.storageBackend === 'opfs';
// v0.6.1: kv 后端同样页面化 — 整 value SSTable4MB)超过 1MB 页面缓存时
// 每次读取全量重载;拆 4KB 页面后缓存按页命中,大数据量读放大消除
return this.config.storageBackend === 'opfs' || this.config.storageBackend === 'kv';
}
// =======================================================================
// WAL 恢复