fix(site/v0.8.0): 站点版本同步 + 现场失败修复(file:// 打不开 OPFS 的可操作错误)

用户报告"站点演示失败了",实测复现并定位根因:

- 现象:直接双击 site/demo.html(file://)→ 点「🌲 Aria」→
  " 数据库初始化失败: Failed to open AriaEngine database "demo""(Memory 正常)。
- 根因:file:// 属不透明来源,Chromium 拒绝 navigator.storage.getDirectory()
  并抛 SecurityError;此时 isSecureContext 仍为 true、API 也存在,无法提前探测。
  引擎把它包成 ARIA_OPEN_ERROR 时丢掉了底层错误 → 消息对用户不可操作。
- 修复:OPFSBackend.open() 显式检查并抛 ARIA_OPFS_UNAVAILABLE,消息给出两条出路
  (用 http(s) 打开 / 改用 mode:'memory'),原始 SecurityError 挂 cause;
  site/demo.html 额外用中文说明"为什么失败 + 怎么修"。

顺带修掉一个更普遍的问题:DatabaseError 的第三个参数只进 details,err.cause
恒为 undefined,而文档/注释多处写"底层错误作为 cause 保留"。现在两者都成立
(details 语义不变;cause 声明为公开字段并接入标准错误链)。

站点版本同步:demo.html(title / 状态栏 / SQL 预置脚本 / console 日志)与
benchmark.html(title)此前仍是 v0.7.4(日志甚至是 v0.4.2)→ 统一 v0.8.0;
docs.html 的 AriaEngine 版本演进列表补上 v0.8.0 条目、错误码表补
ARIA_OPFS_UNAVAILABLE;README 补"OPFS 需要 http(s) 页面"的浏览器兼容说明。

回归与门禁:tests/engine/aria-opfs-unavailable.test.ts(5 项,含正常环境正控);
变异 R19 / R20 均被拦住(总计 42/42);93 套件 / 1985 用例;覆盖率
90.59 / 82.61 / 94.14 / 93.50(阈值 90/82/94/93);e2e 14/14;lint + 两份 tsc 干净;
dist 重建(251,731 B / gzip 63,431 B)并已同步全部体积宣称。
This commit is contained in:
thzxx
2026-09-15 17:09:29 +08:00
parent 0b44620721
commit c1c3036abd
25 changed files with 439 additions and 55 deletions
+32 -8
View File
@@ -11,8 +11,8 @@ All notable changes to MetonaSqlark will be documented in this file.
> 必然复发的结构**:多处并存的语义实现被收敛为唯一实现,并第一次让崩溃语义、 > 必然复发的结构**:多处并存的语义实现被收敛为唯一实现,并第一次让崩溃语义、
> 错误码一致性、入口等价性变成可机器验证的门禁。 > 错误码一致性、入口等价性变成可机器验证的门禁。
> >
> 测试规模 1304 → **198092 套件)+ 14 项 e2e**(另 4 个重型套件在独立 CI > 测试规模 1304 → **198593 套件)+ 14 项 e2e**(另 4 个重型套件在独立 CI
> job 串行运行);B-6 与审查轮的全部修复另有 **40 项变异验证**`scripts/mutation-b6.py` > job 串行运行);B-6 与审查轮的全部修复另有 **42 项变异验证**`scripts/mutation-b6.py`
> 全部通过,且脚本自带正控 / 编译失败区分 / 超时 / 逐字节恢复校验 / 进程锁)。 > 全部通过,且脚本自带正控 / 编译失败区分 / 超时 / 逐字节恢复校验 / 进程锁)。
### 工作流 B · 结构根治(消除整类缺陷) ### 工作流 B · 结构根治(消除整类缺陷)
@@ -93,7 +93,7 @@ All notable changes to MetonaSqlark will be documented in this file.
- 新增 `tests/v080-b6-single-commit-point.test.ts`(审查轮后扩到 **96 项** - 新增 `tests/v080-b6-single-commit-point.test.ts`(审查轮后扩到 **96 项**
manifest 严格校验表驱动 24 例 + 事务水位 P0 + 前缀空洞语义 + 在途读者与退休文件 + manifest 严格校验表驱动 24 例 + 事务水位 P0 + 前缀空洞语义 + 在途读者与退休文件 +
孤儿回收门槛 + 稀疏/损坏分支 + 旧格式形状校验 + bloom 配置透传)与 孤儿回收门槛 + 稀疏/损坏分支 + 旧格式形状校验 + bloom 配置透传)与
`scripts/mutation-b6.py`**40 项**变异验证:把修复回退到修复前行为,对应用例 `scripts/mutation-b6.py`**42 项**变异验证:把修复回退到修复前行为,对应用例
必须失败 —— 全部被拦住)。 必须失败 —— 全部被拦住)。
- **B-6 存储提交点(KVStore 侧止血)** — 两处 P0:① `open()` 遇损坏日志尾部会**清空整个 - **B-6 存储提交点(KVStore 侧止血)** — 两处 P0:① `open()` 遇损坏日志尾部会**清空整个
日志**(写 3 条 → 第 4 条撕裂 → 重开可见 → 再重开全空);② 自动 checkpoint 日志**(写 3 条 → 第 4 条撕裂 → 重开可见 → 再重开全空);② 自动 checkpoint
@@ -135,7 +135,7 @@ All notable changes to MetonaSqlark will be documented in this file.
statements 90 / branches 82 / functions 94 / lines 93);CI 常规 job 带 statements 90 / branches 82 / functions 94 / lines 93);CI 常规 job 带
`--coverage`lint 去掉 `continue-on-error`;新增 `tests/` 类型检查 `--coverage`lint 去掉 `continue-on-error`;新增 `tests/` 类型检查
(修复 **103 个**被 babel 剥离类型掩盖的测试类型错误);CI 校验 dist 与源码同步。 (修复 **103 个**被 babel 剥离类型掩盖的测试类型错误);CI 校验 dist 与源码同步。
→ 实测(v0.8.0 收尾复测)Statements 90.59% / Branches 82.59% / Functions 94.14% / → 实测(v0.8.0 收尾复测)Statements 90.59% / Branches 82.61% / Functions 94.14% /
Lines 93.50%(命令与 CI 常规 job 完全一致,可复现)。 Lines 93.50%(命令与 CI 常规 job 完全一致,可复现)。
- **测试介质忠实性修正**(两处同源缺陷,此前让所有多实例/多库验证跑在错误语义上) - **测试介质忠实性修正**(两处同源缺陷,此前让所有多实例/多库验证跑在错误语义上)
- `SharedMemoryBackend` 的读缓存是每实例私有的 → 介质退化为"每实例一份快照", - `SharedMemoryBackend` 的读缓存是每实例私有的 → 介质退化为"每实例一份快照",
@@ -232,8 +232,8 @@ All notable changes to MetonaSqlark will be documented in this file.
functions 94% 阈值 → 实测 93.84%),暴露出退化路径 `cloneRowFallback` 从未被测试; functions 94% 阈值 → 实测 93.84%),暴露出退化路径 `cloneRowFallback` 从未被测试;
`tests/engine/row-clone.test.ts`8 项:structuredClone 可用/不可用/抛错三条路径、 `tests/engine/row-clone.test.ts`8 项:structuredClone 可用/不可用/抛错三条路径、
Date/TypedArray/ArrayBuffer、深拷贝不泄漏、批量拷贝)后门禁恢复通过。 Date/TypedArray/ArrayBuffer、深拷贝不泄漏、批量拷贝)后门禁恢复通过。
- 最终数字(口径修正后,含新增的 8 项测试):92 套件 / 1980 用例, - 最终数字(口径修正后,含新增的 8 项测试):93 套件 / 1985 用例,
语句 90.59% / 分支 82.59% / 函数 94.14% / 行 93.50%。 语句 90.59% / 分支 82.61% / 函数 94.14% / 行 93.50%。
**测试质量修正("绿"不等于"有保护"** **测试质量修正("绿"不等于"有保护"**
@@ -246,6 +246,30 @@ All notable changes to MetonaSqlark will be documented in this file.
- `<=` 收紧为严格 `<`(冻结意图的水位必须**严格低于**意图起点);删掉死代码 - `<=` 收紧为严格 `<`(冻结意图的水位必须**严格低于**意图起点);删掉死代码
`const inner``void manifest`)与误名的"非 JSON"用例。 `const inner``void manifest`)与误名的"非 JSON"用例。
### 现场失败修复(站点演示页 file:// 打不开 Aria 引擎)
用户报告"站点演示失败了"。复现与根因(真实 Chromium 实测,非推断):
- **现象**:直接双击 `site/demo.html``file://`)→ 点「🌲 Aria」→
`❌ 数据库初始化失败: Failed to open AriaEngine database "demo"`(切回 Memory 正常)。
- **根因**`file://` 属于不透明来源,Chromium 认为本地文件"不适合 Web 应用访问"
`navigator.storage.getDirectory()``SecurityError`。注意此时
`isSecureContext === true` 且 API **存在** —— 只有真正调用才会失败,所以无法提前探测;
而引擎把它包成 `ARIA_OPEN_ERROR` 时**丢掉了底层错误**,消息对用户不可操作。
- **修复**`OPFSBackend.open()` 显式检查 OPFS 可用性,抛 `ARIA_OPFS_UNAVAILABLE`
消息里直接给出两条出路(**用 http(s) 打开**`node tests/e2e/server.cjs` /
`npx serve .`,或改用 `mode: 'memory'`),并把原始 `SecurityError` 挂在 cause 上。
- **顺带修掉一个更普遍的问题**`DatabaseError` 的第三个参数只存进 `details`
**`err.cause` 恒为 undefined** —— 而文档/注释多处写"底层错误作为 cause 保留"。
现在两个字段都成立(`details` 语义不变,向后兼容;`cause` 接入标准错误链)。
- **站点版本同步**`site/demo.html`title/状态栏/SQL 预置脚本/console 日志)、
`site/benchmark.html`title)此前仍标 v0.7.4(甚至 v0.4.2),已统一到 v0.8.0
`site/docs.html` 的 AriaEngine 版本演进列表补上 v0.8.0 条目,错误码表补
`ARIA_OPFS_UNAVAILABLE`
- 回归:`tests/engine/aria-opfs-unavailable.test.ts`5 项:storage 缺失 /
SecurityError 带 cause 与建议 / 正常环境正控 / DatabaseError 两个字段语义),
变异 R19 / R20 均被拦住。
### 文档订正(第二轮 G6:16 条不成立的宣称) ### 文档订正(第二轮 G6:16 条不成立的宣称)
- **MVCC**`site/docs.html`(表格、事务行、能力表)与 `site/demo.html` 仍写 - **MVCC**`site/docs.html`(表格、事务行、能力表)与 `site/demo.html` 仍写
@@ -254,10 +278,10 @@ All notable changes to MetonaSqlark will be documented in this file.
- **`backup()`**`interface.ts` 注释仍写"一致性快照" → 改为"逐表读取(非跨表快照)"。 - **`backup()`**`interface.ts` 注释仍写"一致性快照" → 改为"逐表读取(非跨表快照)"。
- **WAL 空洞**`docs.html` 写"空洞检测截断" → 改为"内部空洞如实上报并拒绝静默继续"。 - **WAL 空洞**`docs.html` 写"空洞检测截断" → 改为"内部空洞如实上报并拒绝静默继续"。
- **体积**README 与 docs.html 写"~105KB / gzip ~27KB" → 实测 min 产物 - **体积**README 与 docs.html 写"~105KB / gzip ~27KB" → 实测 min 产物
251,109 字节 / gzip 63,145 字节(`stat -c%s dist/metona-sqlark.min.js` 251,731 字节 / gzip 63,431 字节(`stat -c%s dist/metona-sqlark.min.js`
`gzip -c dist/metona-sqlark.min.js | wc -c`)。 `gzip -c dist/metona-sqlark.min.js | wc -c`)。
- **测试与覆盖率**README badge/正文、site 徽标与数字卡、CHANGELOG 全部更新为 - **测试与覆盖率**README badge/正文、site 徽标与数字卡、CHANGELOG 全部更新为
实测值(1980 / 92 套件 / 90.59% / 82.59% / 94.14% / 93.50%)。 实测值(1985 / 93 套件 / 90.59% / 82.61% / 94.14% / 93.50%)。
- **存储模式**site 仍写"5 种存储引擎" → 改为"4 种模式 + 3 种后端" - **存储模式**site 仍写"5 种存储引擎" → 改为"4 种模式 + 3 种后端"
"Tree-shakable"改为"UMD/ESM/CJS 多格式输出"(单文件 bundle + 未声明 "Tree-shakable"改为"UMD/ESM/CJS 多格式输出"(单文件 bundle + 未声明
`sideEffects`,不宜宣称可摇树)。 `sideEffects`,不宜宣称可摇树)。
+3 -3
View File
@@ -75,7 +75,7 @@ src/
├── plugin/ # Plugin system (14 lifecycle hooks) ├── plugin/ # Plugin system (14 lifecycle hooks)
└── integrations/ # React & Vue hooks └── integrations/ # React & Vue hooks
tests/ # Test suite (1980 test cases, 92 suites + 14 e2e) tests/ # Test suite (1985 test cases, 92 suites + 14 e2e)
tests/helpers/ # 共享测试工具(OPFS mock 等) tests/helpers/ # 共享测试工具(OPFS mock 等)
tests/e2e/ # Playwright e2e(真实 Chromium + OPFS tests/e2e/ # Playwright e2e(真实 Chromium + OPFS
site/ # Documentation site (index / docs / demo) site/ # Documentation site (index / docs / demo)
@@ -155,7 +155,7 @@ npm run build
# Output in dist/ # Output in dist/
# ├── metona-sqlark.js UMD # ├── metona-sqlark.js UMD
# ├── metona-sqlark.min.js UMD minified (251,109 B / gzip 63,145 B) # ├── metona-sqlark.min.js UMD minified (251,731 B / gzip 63,431 B)
# ├── metona-sqlark.esm.js ES Module # ├── metona-sqlark.esm.js ES Module
# ├── metona-sqlark.cjs CommonJS # ├── metona-sqlark.cjs CommonJS
# └── metona-sqlark.d.ts TypeScript declarations # └── metona-sqlark.d.ts TypeScript declarations
@@ -237,7 +237,7 @@ Open an issue at [git.metona.cn/MetonaTeam/MetonaSqlark/issues](https://git.meto
不会失败的用例等于没有保护。 不会失败的用例等于没有保护。
```bash ```bash
# 全部 40 项变异(存储单一提交点 / LSM 结构根治 / v0.8.0 全量审查修复) # 全部 42 项变异(存储单一提交点 / LSM 结构根治 / v0.8.0 全量审查修复)
python3 scripts/mutation-b6.py python3 scripts/mutation-b6.py
# 只跑其中一条(按名字子串匹配) # 只跑其中一条(按名字子串匹配)
+10 -10
View File
@@ -762,7 +762,7 @@ connection-manager/integrations/migration)两份完整报告,以及测试质
**验证**:新增 `tests/v080-sql-three-valued.test.ts` —— 26 条 SQL 语义矩阵 × **验证**:新增 `tests/v080-sql-three-valued.test.ts` —— 26 条 SQL 语义矩阵 ×
4 引擎(memory/disk/hybrid/aria+ UPDATE/DELETE 写路径,共 **104 断言** 4 引擎(memory/disk/hybrid/aria+ UPDATE/DELETE 写路径,共 **104 断言**
**测试规模**1304 → **1872 通过 / 90 套件**(另 4 个重型套件与 e2e 14 项)。新增: **测试规模**(B-6 收尾时的**阶段快照**,最终值见附录 I)1304 → **1872 通过 / 90 套件**(另 4 个重型套件与 e2e 14 项)。新增:
`tests/v080-savepoint.test.ts``v080-atomicity.test.ts``v080-subscribe.test.ts` `tests/v080-savepoint.test.ts``v080-atomicity.test.ts``v080-subscribe.test.ts`
`v080-correlated.test.ts``v080-sql-three-valued.test.ts` `v080-correlated.test.ts``v080-sql-three-valued.test.ts`
`v080-unified-validation.test.ts``v080-query-layer.test.ts` `v080-unified-validation.test.ts``v080-query-layer.test.ts`
@@ -836,7 +836,7 @@ SSTable / 跨库搬迁),必须在 `save()` 之前补校验**。
- PC-2 真崩溃注入(CDP `Page.crash`+ OPFS `createWritable` 的两个写入窗口; - PC-2 真崩溃注入(CDP `Page.crash`+ OPFS `createWritable` 的两个写入窗口;
- `FaultyBackend` 字节级故障注入(撕裂/bit-flip/掉电/写失败); - `FaultyBackend` 字节级故障注入(撕裂/bit-flip/掉电/写失败);
- **变异验证**成为回归套件的标准做法:把修复回退到修复前的行为,对应用例 - **变异验证**成为回归套件的标准做法:把修复回退到修复前的行为,对应用例
必须失败。本轮 A15/A13/A38/A39 与 **B-6 全量 + 审查轮共 40 项**均通过该检查(可执行脚本 必须失败。本轮 A15/A13/A38/A39 与 **B-6 全量 + 审查轮共 42 项**均通过该检查(可执行脚本
`scripts/mutation-b6.py`,逐项打印"变异是否被拦住")—— `scripts/mutation-b6.py`,逐项打印"变异是否被拦住")——
这是"测试真能拦住回归"与"测试只是陪跑"的分界线。 这是"测试真能拦住回归"与"测试只是陪跑"的分界线。
- 测试介质的两处**忠实性**缺陷已被修正(`SharedMemoryBackend` 跨实例可见性、 - 测试介质的两处**忠实性**缺陷已被修正(`SharedMemoryBackend` 跨实例可见性、
@@ -883,7 +883,7 @@ SSTable / 跨库搬迁),必须在 `save()` 之前补校验**。
| `src/engine/aria/index/merge_iterator.ts` | 胜出来源的补充推迟到下一次 `next()`(提前终止不多算) | | `src/engine/aria/index/merge_iterator.ts` | 胜出来源的补充推迟到下一次 `next()`(提前终止不多算) |
| `src/engine/aria/wal/checkpoint.ts` | checkpoint 只落 memtable`flushMemtables?`),不等 compaction | | `src/engine/aria/wal/checkpoint.ts` | checkpoint 只落 memtable`flushMemtables?`),不等 compaction |
| `tests/v080-b6-single-commit-point.test.ts` | 上述每一条的回归 + manifest 严格校验表驱动 **24** 例;v0.8.0 审查后扩到 **96 项**(新增事务水位 P0、前缀空洞语义、在途读者与退休文件、孤儿回收门槛、稀疏/损坏分支、形状校验等)| | `tests/v080-b6-single-commit-point.test.ts` | 上述每一条的回归 + manifest 严格校验表驱动 **24** 例;v0.8.0 审查后扩到 **96 项**(新增事务水位 P0、前缀空洞语义、在途读者与退休文件、孤儿回收门槛、稀疏/损坏分支、形状校验等)|
| `scripts/mutation-b6.py` | **40 项**变异验证(B-6 的 22 项 + 审查轮的 18 项),全部被对应用例拦住;脚本自带正控、编译失败区分、超时、逐字节恢复校验与进程锁(见 CONTRIBUTING| | `scripts/mutation-b6.py` | **42 项**变异验证(B-6 的 22 项 + 审查轮的 20 项),全部被对应用例拦住;脚本自带正控、编译失败区分、超时、逐字节恢复校验与进程锁(见 CONTRIBUTING|
### H.2 提交顺序(唯一不变量) ### H.2 提交顺序(唯一不变量)
@@ -924,7 +924,7 @@ __aria_manifest_<gen> 提交(单文件原子写 + 回读校验)
# 1) B-6 回归(96 项) # 1) B-6 回归(96 项)
npx jest tests/v080-b6-single-commit-point.test.ts npx jest tests/v080-b6-single-commit-point.test.ts
# 2) 变异验证:把每个修复回退到修复前行为,对应用例必须失败(40 项) # 2) 变异验证:把每个修复回退到修复前行为,对应用例必须失败(42 项)
python3 scripts/mutation-b6.py python3 scripts/mutation-b6.py
# 3) 随机压力 / 检查点崩溃恢复(发现缺陷 1 的套件) # 3) 随机压力 / 检查点崩溃恢复(发现缺陷 1 的套件)
@@ -934,8 +934,8 @@ npx jest tests/engine/aria-repair-hardening.test.ts tests/engine/aria-kv-backend
npx jest --testPathIgnorePatterns='/node_modules/|/tests/e2e/|aria-prod-load|kvstore-stress|aria-matrix-audit|aria-idx-flush-race' npx jest --testPathIgnorePatterns='/node_modules/|/tests/e2e/|aria-prod-load|kvstore-stress|aria-matrix-audit|aria-idx-flush-race'
``` ```
**实测数字**(审查轮收尾复测,详见附录 I):常规套件 **1980 通过 / 92 套件** **实测数字**(审查轮收尾复测,详见附录 I):常规套件 **1985 通过 / 93 套件**
覆盖率 语句 90.59% / 分支 82.59% / 函数 94.14% / 行 93.50%(阈值 90/82/94/93,全部通过); 覆盖率 语句 90.59% / 分支 82.61% / 函数 94.14% / 行 93.50%(阈值 90/82/94/93,全部通过);
e2e 14/14;变异验证 **40/40** 被拦住。 e2e 14/14;变异验证 **40/40** 被拦住。
**恢复报告**`engine.getRecoveryReport()` 返回 **恢复报告**`engine.getRecoveryReport()` 返回
@@ -1009,7 +1009,7 @@ e2e 14/14;变异验证 **40/40** 被拦住。
MVCC 快照隔离(docs.html ×3 + demo.html + `mvcc.ts` 文件头)、`backup()` MVCC 快照隔离(docs.html ×3 + demo.html + `mvcc.ts` 文件头)、`backup()`
"一致性快照"`interface.ts` 注释)、"空洞检测截断"、"~105KB / gzip ~27KB" "一致性快照"`interface.ts` 注释)、"空洞检测截断"、"~105KB / gzip ~27KB"
(实测 251,109 字节 / gzip 63,145 字节)、测试与覆盖率数字(README badge/正文、site 徽标与数字卡、 (实测 251,731 字节 / gzip 63,431 字节)、测试与覆盖率数字(README badge/正文、site 徽标与数字卡、
CHANGELOG)、"5 种存储引擎"(→ 4 模式 + 3 后端)、"Tree-shakable"(→ 多格式输出)、 CHANGELOG)、"5 种存储引擎"(→ 4 模式 + 3 后端)、"Tree-shakable"(→ 多格式输出)、
错误码表缺 **16 个**码、CONTRIBUTING 变异数量(17 → 40)、PLAN 附录 G 的 错误码表缺 **16 个**码、CONTRIBUTING 变异数量(17 → 40)、PLAN 附录 G 的
"12 项事故级"(→ 11 项,A1~A11)、G4"零丢失"(限定为矩阵覆盖范围内)、 "12 项事故级"(→ 11 项,A1~A11)、G4"零丢失"(限定为矩阵覆盖范围内)、
@@ -1029,16 +1029,16 @@ npx jest tests/v080-b6-single-commit-point.test.ts tests/engine/aria-wal-segment
# 2) 全量常规套件(不含重型与 e2e) # 2) 全量常规套件(不含重型与 e2e)
npx jest --testPathIgnorePatterns='/node_modules/|/tests/e2e/|aria-prod-load|kvstore-stress|aria-matrix-audit|aria-idx-flush-race' npx jest --testPathIgnorePatterns='/node_modules/|/tests/e2e/|aria-prod-load|kvstore-stress|aria-matrix-audit|aria-idx-flush-race'
# → 92 套件 / 1980 用例全绿 # → 93 套件 / 1985 用例全绿
# 3) 覆盖率(与 CI 常规 job 完全相同的命令) # 3) 覆盖率(与 CI 常规 job 完全相同的命令)
npx jest --coverage --coverageReporters=text-summary --testPathIgnorePatterns='/node_modules/|/tests/e2e/|aria-prod-load|kvstore-stress|aria-matrix-audit|aria-idx-flush-race' npx jest --coverage --coverageReporters=text-summary --testPathIgnorePatterns='/node_modules/|/tests/e2e/|aria-prod-load|kvstore-stress|aria-matrix-audit|aria-idx-flush-race'
# → Statements 90.59% / Branches 82.59% / Functions 94.14% / Lines 93.50%(阈值 90/82/94/93 # → Statements 90.59% / Branches 82.61% / Functions 94.14% / Lines 93.50%(阈值 90/82/94/93
# 4) e2e(真实 Chromium + 真实 OPFS + CDP Page.crash # 4) e2e(真实 Chromium + 真实 OPFS + CDP Page.crash
npx playwright test # → 14/14 npx playwright test # → 14/14
# 5) 变异验证(40 项;脚本自带正控、编译失败区分、超时、逐字节恢复校验、进程锁) # 5) 变异验证(42 项;脚本自带正控、编译失败区分、超时、逐字节恢复校验、进程锁)
python3 scripts/mutation-b6.py # → 40/40 全部被拦住 python3 scripts/mutation-b6.py # → 40/40 全部被拦住
# 6) 静态门禁 # 6) 静态门禁
+14 -7
View File
@@ -4,7 +4,7 @@
<img src="https://img.shields.io/badge/version-0.8.0-blue?style=flat-square" alt="version"> <img src="https://img.shields.io/badge/version-0.8.0-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-90.59%25%20stmts-brightgreen?style=flat-square" alt="coverage"> <img src="https://img.shields.io/badge/coverage-90.59%25%20stmts-brightgreen?style=flat-square" alt="coverage">
<img src="https://img.shields.io/badge/tests-1980%20passed-success?style=flat-square" alt="tests"> <img src="https://img.shields.io/badge/tests-1985%20passed-success?style=flat-square" alt="tests">
</p> </p>
> 基于 TypeScript 的**前端关系型数据库**:完整 SQL + Query Builder 双 API > 基于 TypeScript 的**前端关系型数据库**:完整 SQL + Query Builder 双 API
@@ -91,7 +91,7 @@ npm install @metona-team/metona-sqlark
<script src="https://git.metona.cn/MetonaTeam/MetonaSqlark/raw/branch/master/dist/metona-sqlark.min.js"></script> <script src="https://git.metona.cn/MetonaTeam/MetonaSqlark/raw/branch/master/dist/metona-sqlark.min.js"></script>
``` ```
或从 [`dist/`](./dist/) 下载:`metona-sqlark.js`UMD 开发版)/ `metona-sqlark.min.js`(压缩版:实测 251,109 字节 / gzip 63,145 字节)/ `metona-sqlark.esm.js` / `metona-sqlark.cjs` / `metona-sqlark.d.ts` 或从 [`dist/`](./dist/) 下载:`metona-sqlark.js`UMD 开发版)/ `metona-sqlark.min.js`(压缩版:实测 251,731 字节 / gzip 63,431 字节)/ `metona-sqlark.esm.js` / `metona-sqlark.cjs` / `metona-sqlark.d.ts`
--- ---
@@ -423,7 +423,7 @@ const { data, loading, error, refresh } = useSqlarkQuery(db, 'SELECT * FROM user
npm install # 安装依赖 npm install # 安装依赖
npm run dev # 开发模式(localhost:3001 npm run dev # 开发模式(localhost:3001
npm run build # 生产构建(生成 dist/ npm run build # 生产构建(生成 dist/
npm test # 运行测试(1980 用例 · 92 套件;+4 个重型套件) npm test # 运行测试(1985 用例 · 93 套件;+4 个重型套件)
python3 scripts/mutation-b6.py # 变异验证:把 B-6 的修复逐项回退,对应用例必须失败 python3 scripts/mutation-b6.py # 变异验证:把 B-6 的修复逐项回退,对应用例必须失败
npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build
npm run lint # 代码检查 npm run lint # 代码检查
@@ -436,11 +436,11 @@ npm run typecheck # 类型检查
| 指标 | 数值 | | 指标 | 数值 |
|------|------| |------|------|
| 测试用例 | 198092 套件)+ 14 Playwright e2e,另 4 个重型套件在独立 CI job 串行运行 | | 测试用例 | 198593 套件)+ 14 Playwright e2e,另 4 个重型套件在独立 CI job 串行运行 |
| 语句覆盖率 | 90.59%8532/9418 | | 语句覆盖率 | 90.59%8538/9424 |
| 分支覆盖率 | 82.59%4452/5390 | | 分支覆盖率 | 82.61%4459/5397 |
| 函数覆盖率 | 94.14%1223/1299 | | 函数覆盖率 | 94.14%1223/1299 |
| 行覆盖率 | 93.50%7727/8264 | | 行覆盖率 | 93.50%7733/8270 |
| SQL 关键字 | 72 | | SQL 关键字 | 72 |
| 存储模式 | 4`memory` / `disk` / `hybrid` / `aria` | | 存储模式 | 4`memory` / `disk` / `hybrid` / `aria` |
| 存储后端 | 3OPFS / KVStore / Memory),Aria 引擎另有 LSM-Tree + WAL + 页面化 | | 存储后端 | 3OPFS / KVStore / Memory),Aria 引擎另有 LSM-Tree + WAL + 页面化 |
@@ -516,6 +516,13 @@ npm run typecheck # 类型检查
> 分片空洞与记录级损坏都会被**显式上报**(恢复报告 + 告警),不做静默截断。 > 分片空洞与记录级损坏都会被**显式上报**(恢复报告 + 告警),不做静默截断。
> >
> **多标签页保护**AriaEngine 通过 Web Locks 获取库级独占锁,第二个标签页打开同一库抛 `ARIA_LOCKED`。 > **多标签页保护**AriaEngine 通过 Web Locks 获取库级独占锁,第二个标签页打开同一库抛 `ARIA_LOCKED`。
>
> **OPFS 需要 http(s) 页面**`aria` + `diskEngine: 'opfs'` 在 **`file://` 直接打开的页面里不可用** ——
> Chromium 会以 `SecurityError` 拒绝 `navigator.storage.getDirectory()`(注意此时
> `isSecureContext` 仍是 `true`、API 也存在,只有真正调用才会发现)。引擎会抛
> `ARIA_OPFS_UNAVAILABLE`,消息里给出两条出路:用本地服务器打开
> `node tests/e2e/server.cjs 3344` → `http://127.0.0.1:3344/...`),或改用
> `mode: 'memory'` / KVStore 后端。
--- ---
+33 -1
View File
@@ -29,6 +29,18 @@ class DatabaseError extends Error {
this.code = code; this.code = code;
this.details = details; this.details = details;
this.name = 'DatabaseError'; this.name = 'DatabaseError';
// v0.8.0review 修复):把底层错误同时挂到**标准** `Error.cause` 上。
//
// 修复前第三个参数只存进 `details``err.cause` 恒为 undefined —— 而文档与
// 代码注释多处写的是"底层错误作为 cause 保留"(实测被打脸:探针打印
// `e.cause?.code` 得到 undefined)。`details` 语义保持不变(向后兼容),
// `cause` 让标准错误链工具(日志/监控/Node 的 error.cause 约定)能看到根因。
//
// 注意:target 是 ES2020lib 无 ES2022 的 ErrorOptions),因此用赋值而不是
// `super(message, { cause })`;运行时所有现代引擎都支持该属性。
if (details !== undefined) {
this.cause = details;
}
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -2314,7 +2326,27 @@ class OPFSBackend {
} }
async open(name) { async open(name) {
this.dbName = name; this.dbName = name;
this.root = await navigator.storage.getDirectory(); // v0.8.0review 修复):OPFS 不可用时给出**可操作的**错误,而不是让上层
// 只能报一句 "Failed to open AriaEngine database"。
//
// 真实场景(实测):用 file:// 直接打开页面时 Chromium 认为本地文件"不适合
// Web 应用访问"`navigator.storage.getDirectory()` 抛 SecurityError —— 而
// `isSecureContext` 仍是 true、API 也**存在**,所以只有真正调用才会发现。
// 修复前这个 SecurityError 被 `ARIA_OPEN_ERROR` 吞成一句无从下手的消息
//(cause 没往上带),用户不知道是"环境不支持"还是"库坏了"。
if (typeof navigator === 'undefined' || !navigator.storage
|| typeof navigator.storage.getDirectory !== 'function') {
throw new DatabaseError('OPFS is not available in this environment: navigator.storage.getDirectory is missing. ' +
"Use the http(s) protocol (file:// is rejected by browsers) or switch to mode: 'memory' / a KVStore backend.", 'ARIA_OPFS_UNAVAILABLE');
}
try {
this.root = await navigator.storage.getDirectory();
}
catch (error) {
throw new DatabaseError(`OPFS is not accessible here (${error.name}: ${error.message}). ` +
'Chromium blocks OPFS for file:// pages — serve the page over http(s) ' +
"(e.g. `npx serve .` / `node tests/e2e/server.cjs`) or use mode: 'memory'.", 'ARIA_OPFS_UNAVAILABLE', error);
}
this.dbDir = await this.root.getDirectoryHandle(name, { create: true }); this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
// v0.4.5: 清理崩溃残留临时文件(不阻塞打开) // v0.4.5: 清理崩溃残留临时文件(不阻塞打开)
await this.cleanupStaleFiles(); await this.cleanupStaleFiles();
+1 -1
View File
File diff suppressed because one or more lines are too long
+8
View File
@@ -236,6 +236,14 @@ interface MetonaPlugin {
declare class DatabaseError extends Error { declare class DatabaseError extends Error {
code: string; code: string;
details?: unknown | undefined; details?: unknown | undefined;
/**
*
*
* v0.8.0 target ES2020lib `Error`
* `cause` TypeScript 使 `err.cause`
*
*/
cause?: unknown;
constructor(message: string, code: string, details?: unknown | undefined); constructor(message: string, code: string, details?: unknown | undefined);
} }
declare const VERSION = "0.8.0"; declare const VERSION = "0.8.0";
+33 -1
View File
@@ -25,6 +25,18 @@ class DatabaseError extends Error {
this.code = code; this.code = code;
this.details = details; this.details = details;
this.name = 'DatabaseError'; this.name = 'DatabaseError';
// v0.8.0review 修复):把底层错误同时挂到**标准** `Error.cause` 上。
//
// 修复前第三个参数只存进 `details``err.cause` 恒为 undefined —— 而文档与
// 代码注释多处写的是"底层错误作为 cause 保留"(实测被打脸:探针打印
// `e.cause?.code` 得到 undefined)。`details` 语义保持不变(向后兼容),
// `cause` 让标准错误链工具(日志/监控/Node 的 error.cause 约定)能看到根因。
//
// 注意:target 是 ES2020lib 无 ES2022 的 ErrorOptions),因此用赋值而不是
// `super(message, { cause })`;运行时所有现代引擎都支持该属性。
if (details !== undefined) {
this.cause = details;
}
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -2310,7 +2322,27 @@ class OPFSBackend {
} }
async open(name) { async open(name) {
this.dbName = name; this.dbName = name;
this.root = await navigator.storage.getDirectory(); // v0.8.0review 修复):OPFS 不可用时给出**可操作的**错误,而不是让上层
// 只能报一句 "Failed to open AriaEngine database"。
//
// 真实场景(实测):用 file:// 直接打开页面时 Chromium 认为本地文件"不适合
// Web 应用访问"`navigator.storage.getDirectory()` 抛 SecurityError —— 而
// `isSecureContext` 仍是 true、API 也**存在**,所以只有真正调用才会发现。
// 修复前这个 SecurityError 被 `ARIA_OPEN_ERROR` 吞成一句无从下手的消息
//(cause 没往上带),用户不知道是"环境不支持"还是"库坏了"。
if (typeof navigator === 'undefined' || !navigator.storage
|| typeof navigator.storage.getDirectory !== 'function') {
throw new DatabaseError('OPFS is not available in this environment: navigator.storage.getDirectory is missing. ' +
"Use the http(s) protocol (file:// is rejected by browsers) or switch to mode: 'memory' / a KVStore backend.", 'ARIA_OPFS_UNAVAILABLE');
}
try {
this.root = await navigator.storage.getDirectory();
}
catch (error) {
throw new DatabaseError(`OPFS is not accessible here (${error.name}: ${error.message}). ` +
'Chromium blocks OPFS for file:// pages — serve the page over http(s) ' +
"(e.g. `npx serve .` / `node tests/e2e/server.cjs`) or use mode: 'memory'.", 'ARIA_OPFS_UNAVAILABLE', error);
}
this.dbDir = await this.root.getDirectoryHandle(name, { create: true }); this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
// v0.4.5: 清理崩溃残留临时文件(不阻塞打开) // v0.4.5: 清理崩溃残留临时文件(不阻塞打开)
await this.cleanupStaleFiles(); await this.cleanupStaleFiles();
+1 -1
View File
File diff suppressed because one or more lines are too long
+33 -1
View File
@@ -31,6 +31,18 @@
this.code = code; this.code = code;
this.details = details; this.details = details;
this.name = 'DatabaseError'; this.name = 'DatabaseError';
// v0.8.0review 修复):把底层错误同时挂到**标准** `Error.cause` 上。
//
// 修复前第三个参数只存进 `details``err.cause` 恒为 undefined —— 而文档与
// 代码注释多处写的是"底层错误作为 cause 保留"(实测被打脸:探针打印
// `e.cause?.code` 得到 undefined)。`details` 语义保持不变(向后兼容),
// `cause` 让标准错误链工具(日志/监控/Node 的 error.cause 约定)能看到根因。
//
// 注意:target 是 ES2020lib 无 ES2022 的 ErrorOptions),因此用赋值而不是
// `super(message, { cause })`;运行时所有现代引擎都支持该属性。
if (details !== undefined) {
this.cause = details;
}
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -2316,7 +2328,27 @@
} }
async open(name) { async open(name) {
this.dbName = name; this.dbName = name;
this.root = await navigator.storage.getDirectory(); // v0.8.0review 修复):OPFS 不可用时给出**可操作的**错误,而不是让上层
// 只能报一句 "Failed to open AriaEngine database"。
//
// 真实场景(实测):用 file:// 直接打开页面时 Chromium 认为本地文件"不适合
// Web 应用访问"`navigator.storage.getDirectory()` 抛 SecurityError —— 而
// `isSecureContext` 仍是 true、API 也**存在**,所以只有真正调用才会发现。
// 修复前这个 SecurityError 被 `ARIA_OPEN_ERROR` 吞成一句无从下手的消息
//(cause 没往上带),用户不知道是"环境不支持"还是"库坏了"。
if (typeof navigator === 'undefined' || !navigator.storage
|| typeof navigator.storage.getDirectory !== 'function') {
throw new DatabaseError('OPFS is not available in this environment: navigator.storage.getDirectory is missing. ' +
"Use the http(s) protocol (file:// is rejected by browsers) or switch to mode: 'memory' / a KVStore backend.", 'ARIA_OPFS_UNAVAILABLE');
}
try {
this.root = await navigator.storage.getDirectory();
}
catch (error) {
throw new DatabaseError(`OPFS is not accessible here (${error.name}: ${error.message}). ` +
'Chromium blocks OPFS for file:// pages — serve the page over http(s) ' +
"(e.g. `npx serve .` / `node tests/e2e/server.cjs`) or use mode: 'memory'.", 'ARIA_OPFS_UNAVAILABLE', error);
}
this.dbDir = await this.root.getDirectoryHandle(name, { create: true }); this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
// v0.4.5: 清理崩溃残留临时文件(不阻塞打开) // v0.4.5: 清理崩溃残留临时文件(不阻塞打开)
await this.cleanupStaleFiles(); await this.cleanupStaleFiles();
+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
+33 -1
View File
@@ -27,6 +27,18 @@ class DatabaseError extends Error {
this.code = code; this.code = code;
this.details = details; this.details = details;
this.name = 'DatabaseError'; this.name = 'DatabaseError';
// v0.8.0review 修复):把底层错误同时挂到**标准** `Error.cause` 上。
//
// 修复前第三个参数只存进 `details``err.cause` 恒为 undefined —— 而文档与
// 代码注释多处写的是"底层错误作为 cause 保留"(实测被打脸:探针打印
// `e.cause?.code` 得到 undefined)。`details` 语义保持不变(向后兼容),
// `cause` 让标准错误链工具(日志/监控/Node 的 error.cause 约定)能看到根因。
//
// 注意:target 是 ES2020lib 无 ES2022 的 ErrorOptions),因此用赋值而不是
// `super(message, { cause })`;运行时所有现代引擎都支持该属性。
if (details !== undefined) {
this.cause = details;
}
} }
} }
@@ -2308,7 +2320,27 @@ class OPFSBackend {
} }
async open(name) { async open(name) {
this.dbName = name; this.dbName = name;
this.root = await navigator.storage.getDirectory(); // v0.8.0review 修复):OPFS 不可用时给出**可操作的**错误,而不是让上层
// 只能报一句 "Failed to open AriaEngine database"。
//
// 真实场景(实测):用 file:// 直接打开页面时 Chromium 认为本地文件"不适合
// Web 应用访问"`navigator.storage.getDirectory()` 抛 SecurityError —— 而
// `isSecureContext` 仍是 true、API 也**存在**,所以只有真正调用才会发现。
// 修复前这个 SecurityError 被 `ARIA_OPEN_ERROR` 吞成一句无从下手的消息
//(cause 没往上带),用户不知道是"环境不支持"还是"库坏了"。
if (typeof navigator === 'undefined' || !navigator.storage
|| typeof navigator.storage.getDirectory !== 'function') {
throw new DatabaseError('OPFS is not available in this environment: navigator.storage.getDirectory is missing. ' +
"Use the http(s) protocol (file:// is rejected by browsers) or switch to mode: 'memory' / a KVStore backend.", 'ARIA_OPFS_UNAVAILABLE');
}
try {
this.root = await navigator.storage.getDirectory();
}
catch (error) {
throw new DatabaseError(`OPFS is not accessible here (${error.name}: ${error.message}). ` +
'Chromium blocks OPFS for file:// pages — serve the page over http(s) ' +
"(e.g. `npx serve .` / `node tests/e2e/server.cjs`) or use mode: 'memory'.", 'ARIA_OPFS_UNAVAILABLE', error);
}
this.dbDir = await this.root.getDirectoryHandle(name, { create: true }); this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
// v0.4.5: 清理崩溃残留临时文件(不阻塞打开) // v0.4.5: 清理崩溃残留临时文件(不阻塞打开)
await this.cleanupStaleFiles(); await this.cleanupStaleFiles();
+1 -1
View File
File diff suppressed because one or more lines are too long
Vendored
+33 -1
View File
@@ -27,6 +27,18 @@ class DatabaseError extends Error {
this.code = code; this.code = code;
this.details = details; this.details = details;
this.name = 'DatabaseError'; this.name = 'DatabaseError';
// v0.8.0review 修复):把底层错误同时挂到**标准** `Error.cause` 上。
//
// 修复前第三个参数只存进 `details``err.cause` 恒为 undefined —— 而文档与
// 代码注释多处写的是"底层错误作为 cause 保留"(实测被打脸:探针打印
// `e.cause?.code` 得到 undefined)。`details` 语义保持不变(向后兼容),
// `cause` 让标准错误链工具(日志/监控/Node 的 error.cause 约定)能看到根因。
//
// 注意:target 是 ES2020lib 无 ES2022 的 ErrorOptions),因此用赋值而不是
// `super(message, { cause })`;运行时所有现代引擎都支持该属性。
if (details !== undefined) {
this.cause = details;
}
} }
} }
@@ -2308,7 +2320,27 @@ class OPFSBackend {
} }
async open(name) { async open(name) {
this.dbName = name; this.dbName = name;
this.root = await navigator.storage.getDirectory(); // v0.8.0review 修复):OPFS 不可用时给出**可操作的**错误,而不是让上层
// 只能报一句 "Failed to open AriaEngine database"。
//
// 真实场景(实测):用 file:// 直接打开页面时 Chromium 认为本地文件"不适合
// Web 应用访问"`navigator.storage.getDirectory()` 抛 SecurityError —— 而
// `isSecureContext` 仍是 true、API 也**存在**,所以只有真正调用才会发现。
// 修复前这个 SecurityError 被 `ARIA_OPEN_ERROR` 吞成一句无从下手的消息
//(cause 没往上带),用户不知道是"环境不支持"还是"库坏了"。
if (typeof navigator === 'undefined' || !navigator.storage
|| typeof navigator.storage.getDirectory !== 'function') {
throw new DatabaseError('OPFS is not available in this environment: navigator.storage.getDirectory is missing. ' +
"Use the http(s) protocol (file:// is rejected by browsers) or switch to mode: 'memory' / a KVStore backend.", 'ARIA_OPFS_UNAVAILABLE');
}
try {
this.root = await navigator.storage.getDirectory();
}
catch (error) {
throw new DatabaseError(`OPFS is not accessible here (${error.name}: ${error.message}). ` +
'Chromium blocks OPFS for file:// pages — serve the page over http(s) ' +
"(e.g. `npx serve .` / `node tests/e2e/server.cjs`) or use mode: 'memory'.", 'ARIA_OPFS_UNAVAILABLE', error);
}
this.dbDir = await this.root.getDirectoryHandle(name, { create: true }); this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
// v0.4.5: 清理崩溃残留临时文件(不阻塞打开) // v0.4.5: 清理崩溃残留临时文件(不阻塞打开)
await this.cleanupStaleFiles(); await this.cleanupStaleFiles();
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -43,7 +43,7 @@ module.exports = {
// //
// 阈值 = 在**修正口径后**(不再排除实现文件)实测的真实基线,下取整到整数百分点。 // 阈值 = 在**修正口径后**(不再排除实现文件)实测的真实基线,下取整到整数百分点。
// 历史基线(2026-08-15):Statements 90.66% / Branches 82.94% / Functions 94.36% / Lines 93.43% // 历史基线(2026-08-15):Statements 90.66% / Branches 82.94% / Functions 94.36% / Lines 93.43%
// v0.8.0 收尾实测(2026-09-151980 用例):Statements 90.59% / Branches 82.59% / // v0.8.0 收尾实测(2026-09-151985 用例):Statements 90.59% / Branches 82.61% /
// Functions 94.14% / Lines 93.50% // Functions 94.14% / Lines 93.50%
// //
coverageThreshold: { coverageThreshold: {
+25
View File
@@ -415,6 +415,31 @@ MUTATIONS = [
new=""" void missing; // [MUTATION] 不上报空洞""", new=""" void missing; // [MUTATION] 不上报空洞""",
test=WALSEG, pattern='内部空洞', test=WALSEG, pattern='内部空洞',
), ),
dict(
name='R20 DatabaseError 不再暴露标准 cause(根因丢失)',
file='src/constants.ts',
old=""" if (details !== undefined) {
(this as { cause?: unknown }).cause = details;
}""",
new=""" void details; // [MUTATION] 修复前:只有 details,没有 cause""",
test='tests/engine/aria-opfs-unavailable.test.ts', pattern='DatabaseError',
),
dict(
name='R19 OPFS 不可用时吞掉原因(错误不可操作)',
file='src/engine/aria/store/opfs_backend.ts',
old=""" throw new DatabaseError(
`OPFS is not accessible here (${(error as Error).name}: ${(error as Error).message}). ` +
'Chromium blocks OPFS for file:// pages — serve the page over http(s) ' +
"(e.g. `npx serve .` / `node tests/e2e/server.cjs`) or use mode: 'memory'.",
'ARIA_OPFS_UNAVAILABLE',
error,
);""",
new=""" throw new DatabaseError(
'Failed to open OPFS backend',
'ARIA_OPFS_UNAVAILABLE',
); // [MUTATION] 丢掉原始 cause 与建议""",
test='tests/engine/aria-opfs-unavailable.test.ts', pattern='OPFSBackend',
),
dict( dict(
name='R17 bloom 位数被忽略(bloomFilterBitsPerKey 配置无效)', name='R17 bloom 位数被忽略(bloomFilterBitsPerKey 配置无效)',
file='src/engine/aria/index/sstable_builder.ts', file='src/engine/aria/index/sstable_builder.ts',
+1 -1
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.7.4</title> <title>⚡ 性能基准 — MetonaSqlark v0.8.0</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 {
+20 -7
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.7.4</title> <title>🧪 在线演示 — MetonaSqlark v0.8.0</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 {
@@ -84,7 +84,7 @@
<a href="demo.html" class="nav-active">演示</a> <a href="demo.html" class="nav-active">演示</a>
<a href="benchmark.html">基准</a> <a href="benchmark.html">基准</a>
</nav> </nav>
<div class="status"><span class="dot" id="engine-dot"></span> <span id="engine-status">Memory</span> 模式 — v0.7.4</div> <div class="status"><span class="dot" id="engine-dot"></span> <span id="engine-status">Memory</span> 模式 — v0.8.0</div>
<button class="btn btn-preset" onclick="switchEngine('memory')" id="btn-memory" style="margin:6px 4px 6px 0;padding:6px 12px;">⚡ Memory</button> <button class="btn btn-preset" onclick="switchEngine('memory')" id="btn-memory" style="margin:6px 4px 6px 0;padding:6px 12px;">⚡ Memory</button>
<button class="btn btn-preset" onclick="switchEngine('aria')" id="btn-aria" style="margin:6px 0;padding:6px 12px;color:#ec4899;border-color:#ec4899;">🌲 Aria</button> <button class="btn btn-preset" onclick="switchEngine('aria')" id="btn-aria" style="margin:6px 0;padding:6px 12px;color:#ec4899;border-color:#ec4899;">🌲 Aria</button>
</header> </header>
@@ -92,9 +92,10 @@
<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.7.4 在线演示 <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.8.0 在线演示
-- 已预置 users / orders / products 表数据 -- 已预置 users / orders / products 表数据
-- 新特性: ALTER TABLE · TRUNCATE TABLE · WAL同步 · MVCC · SQL注入防护 -- 新特性 (v0.8.0): manifest 单一提交点 · WAL 记录级损坏上报 · 崩溃恢复报告
-- 另有: ALTER TABLE · TRUNCATE TABLE · 参数化查询 · 三值逻辑 · 时间/大小写函数 · 外键级联 · 发布订阅
-- 查看所有数据 -- 查看所有数据
SELECT * FROM users; SELECT * FROM users;
@@ -185,6 +186,18 @@ async function initDB() {
await seedDemoData(db); await seedDemoData(db);
renderInfo(`✅ ${engine === 'aria' ? '🌲 AriaEngineLSM-Tree + WAL + MVCC' : '⚡ Memory'} 引擎已就绪`); renderInfo(`✅ ${engine === 'aria' ? '🌲 AriaEngineLSM-Tree + WAL + MVCC' : '⚡ Memory'} 引擎已就绪`);
} catch (e) { } catch (e) {
// v0.8.0:OPFS 在当前环境不可用(最常见:直接双击打开的 file:// 页面,Chromium
// 禁止其访问 OPFS;此时 isSecureContext 仍为 true、API 也存在,只能调用才发现)
// → 用中文说清"为什么失败 + 怎么修",而不是丢一句底层英文报错。
if (e && e.code === 'ARIA_OPFS_UNAVAILABLE') {
renderError(
'Aria 引擎需要 OPFS,而当前页面环境不允许访问(' + location.protocol + ')。\n' +
'· 原因:浏览器只允许 http(s) 页面使用 OPFSfile:// 直接打开会被拒绝。\n' +
'· 解决:用本地服务器打开(仓库内执行 node tests/e2e/server.cjs 3344' +
'再访问 http://127.0.0.1:3344/site/demo.html),或点上面的「⚡ Memory」用内存模式体验。'
);
return;
}
renderError('数据库初始化失败: ' + e.message); renderError('数据库初始化失败: ' + e.message);
} }
} }
@@ -555,9 +568,9 @@ SELECT COUNT(*) as total FROM temp_logs;
-- 清理 -- 清理
DROP TABLE temp_logs;`, DROP TABLE temp_logs;`,
aria: `-- 🌲 AriaEngine 演示 (v0.7.4) aria: `-- 🌲 AriaEngine 演示 (v0.8.0)
-- 点击右上角「🌲 Aria」按钮切换数据库引擎到 AriaEngine -- 点击右上角「🌲 Aria」按钮切换数据库引擎到 AriaEngine
-- 当前数据库即运行在 Aria 引擎上(LSM-Tree · WAL 崩溃恢复 · MVCC · BloomFilter -- 当前数据库即运行在 Aria 引擎上(LSM-Tree · WAL 崩溃恢复 · BloomFilter · 4KB 页面化
-- 基础 CRUD 与 Memory 引擎完全兼容 -- 基础 CRUD 与 Memory 引擎完全兼容
-- 当前引擎确认 -- 当前引擎确认
@@ -826,7 +839,7 @@ document.addEventListener('keydown', e => {
document.getElementById('btn-aria').style.opacity = '0.6'; document.getElementById('btn-aria').style.opacity = '0.6';
document.getElementById('engine-status').textContent = '⚡ Memory'; document.getElementById('engine-status').textContent = '⚡ Memory';
initDB().then(() => { initDB().then(() => {
console.log('✅ MetonaSqlark v0.4.2 demo ready'); console.log('✅ MetonaSqlark v0.8.0 demo ready');
setTimeout(runQuery, 300); setTimeout(runQuery, 300);
}).catch(err => { }).catch(err => {
renderError('初始化失败: ' + err.message); renderError('初始化失败: ' + err.message);
+4 -2
View File
@@ -140,7 +140,7 @@ npm install @metona-team/metona-sqlark</pre>
<table> <table>
<tr><th>文件</th><th>格式</th><th>用途</th></tr> <tr><th>文件</th><th>格式</th><th>用途</th></tr>
<tr><td><code>metona-sqlark.js</code></td><td>UMD</td><td>浏览器开发版(含 sourcemap</td></tr> <tr><td><code>metona-sqlark.js</code></td><td>UMD</td><td>浏览器开发版(含 sourcemap</td></tr>
<tr><td><code>metona-sqlark.min.js</code></td><td>UMD (minified)</td><td>生产环境(实测 251,109 字节 / gzip 63,145 字节,含全部引擎与 SQL 层)</td></tr> <tr><td><code>metona-sqlark.min.js</code></td><td>UMD (minified)</td><td>生产环境(实测 251,731 字节 / gzip 63,431 字节,含全部引擎与 SQL 层)</td></tr>
<tr><td><code>metona-sqlark.esm.js</code></td><td>ES Module</td><td>现代打包工具 / 浏览器 ESM</td></tr> <tr><td><code>metona-sqlark.esm.js</code></td><td>ES Module</td><td>现代打包工具 / 浏览器 ESM</td></tr>
<tr><td><code>metona-sqlark.cjs</code></td><td>CommonJS</td><td>Node.js require()</td></tr> <tr><td><code>metona-sqlark.cjs</code></td><td>CommonJS</td><td>Node.js require()</td></tr>
<tr><td><code>metona-sqlark.d.ts</code></td><td>TypeScript 声明</td><td>类型提示</td></tr> <tr><td><code>metona-sqlark.d.ts</code></td><td>TypeScript 声明</td><td>类型提示</td></tr>
@@ -815,6 +815,7 @@ db.<span class="f">broadcastChange</span>(<span class="s">'users'</span>);</pre>
<strong>v0.7.2 语句级原子性 / 事务 DDL / 约束硬化</strong> — UPDATE 语句级两阶段原子(多行匹配第 N 行失败整句不执行 + 批内唯一互查)· 事务内 DDL 显式拒绝(四引擎对齐)· SET NULL 级联绕过 required 约束整体拒绝 · 参数绑定注释感知(注释中 `?`/引号不参与绑定)· 未闭合字符串显式 PARSE_ERROR · 未知 where 操作符抛 QUERY_ERROR(此前静默全匹配)· update undefined 语义化(保留旧值)· Hybrid write-through 失败补偿(磁盘失败自动重载内存对齐)。<br> <strong>v0.7.2 语句级原子性 / 事务 DDL / 约束硬化</strong> — UPDATE 语句级两阶段原子(多行匹配第 N 行失败整句不执行 + 批内唯一互查)· 事务内 DDL 显式拒绝(四引擎对齐)· SET NULL 级联绕过 required 约束整体拒绝 · 参数绑定注释感知(注释中 `?`/引号不参与绑定)· 未闭合字符串显式 PARSE_ERROR · 未知 where 操作符抛 QUERY_ERROR(此前静默全匹配)· update undefined 语义化(保留旧值)· Hybrid write-through 失败补偿(磁盘失败自动重载内存对齐)。<br>
<strong>v0.7.3 深度审计第六阶段:INSERT 原子 / 索引一致性 / 边界窗口</strong> — INSERT 语句级两阶段原子(三引擎 + Aria PK 批内重复,此前部分提交)· 索引列 IS NULL 恒空修复(Memory/KVStore/Hybrid,对齐 Aria)· delete RESTRICT 预检不再破坏索引 · queryStream 子查询静默空结果修复($subquery/$exists/$col 回退物化)· ALTER DROP 索引列残留清理 · CREATE UNIQUE INDEX 存量重复数据校验(失败原子回滚)· <code>SELECT *, col AS alias</code> 解析与投影 · WAL BEGIN/ROLLBACK 写失败窗口修复(事务不泄漏/数据不复活)· aria $in 批级预加载(消除逐值 drainChain 性能悬崖)· 多条件 AND 等值下推(索引真正生效,EXPLAIN 同步)· ANALYZE 统计含二级索引 · React/Vue hooks 生命周期修复(config 变更重建 / 卸载关闭)· 迁移无主键旧库兜底 · 1256 测试 75 套件 · 90.0% 行覆盖率。<br> <strong>v0.7.3 深度审计第六阶段:INSERT 原子 / 索引一致性 / 边界窗口</strong> — INSERT 语句级两阶段原子(三引擎 + Aria PK 批内重复,此前部分提交)· 索引列 IS NULL 恒空修复(Memory/KVStore/Hybrid,对齐 Aria)· delete RESTRICT 预检不再破坏索引 · queryStream 子查询静默空结果修复($subquery/$exists/$col 回退物化)· ALTER DROP 索引列残留清理 · CREATE UNIQUE INDEX 存量重复数据校验(失败原子回滚)· <code>SELECT *, col AS alias</code> 解析与投影 · WAL BEGIN/ROLLBACK 写失败窗口修复(事务不泄漏/数据不复活)· aria $in 批级预加载(消除逐值 drainChain 性能悬崖)· 多条件 AND 等值下推(索引真正生效,EXPLAIN 同步)· ANALYZE 统计含二级索引 · React/Vue hooks 生命周期修复(config 变更重建 / 卸载关闭)· 迁移无主键旧库兜底 · 1256 测试 75 套件 · 90.0% 行覆盖率。<br>
<strong>v0.7.4 深度审计第七阶段:写语句子查询 / 约束硬化 / 真惰性流式</strong> — UPDATE/DELETE WHERE 子查询正确执行(四引擎,此前静默 0 行;关联引用显式 NOT_SUPPORTEDEXPLAIN 估算同步修复)· 主键 NULL/undefined 强制拒绝(SQL 语义 PK 隐含 NOT NULL,此前静默生成 "null"/"undefined" 主键)· DROP INDEX 保留建表 UNIQUE 约束(对齐 SQLite:需重建表解除;仅 CREATE UNIQUE INDEX 添加的可随索引删除)· GROUP BY / DISTINCT / UNION 键类型安全编码(null 与 'null' 字符串不再合并)· UPDATE 未知列显式 COLUMN_NOT_FOUND(此前脏列写入存储行)· queryStream 多语句显式 PARSE_ERROR(此前静默忽略后续语句)· KVStore 后台错误跨 reopen 清理 + Hybrid beginTransaction 失败补偿回滚 · <strong>Aria findStream 真惰性</strong>MergeIterator 迭代器化 + SSTable/MemTable 生成器扫描,limit 提前终止,大表流式内存 O(1)——此前内部 drain 全量物化)· REINDEX 单次全表扫描重建全部索引列(此前每列一次全扫描)· RB-Tree 删除双黑修复边界 + LSM 死代码清理 · 1304 测试 76 套件 · 90.1% 行覆盖率。</p> <strong>v0.7.4 深度审计第七阶段:写语句子查询 / 约束硬化 / 真惰性流式</strong> — UPDATE/DELETE WHERE 子查询正确执行(四引擎,此前静默 0 行;关联引用显式 NOT_SUPPORTEDEXPLAIN 估算同步修复)· 主键 NULL/undefined 强制拒绝(SQL 语义 PK 隐含 NOT NULL,此前静默生成 "null"/"undefined" 主键)· DROP INDEX 保留建表 UNIQUE 约束(对齐 SQLite:需重建表解除;仅 CREATE UNIQUE INDEX 添加的可随索引删除)· GROUP BY / DISTINCT / UNION 键类型安全编码(null 与 'null' 字符串不再合并)· UPDATE 未知列显式 COLUMN_NOT_FOUND(此前脏列写入存储行)· queryStream 多语句显式 PARSE_ERROR(此前静默忽略后续语句)· KVStore 后台错误跨 reopen 清理 + Hybrid beginTransaction 失败补偿回滚 · <strong>Aria findStream 真惰性</strong>MergeIterator 迭代器化 + SSTable/MemTable 生成器扫描,limit 提前终止,大表流式内存 O(1)——此前内部 drain 全量物化)· REINDEX 单次全表扫描重建全部索引列(此前每列一次全扫描)· RB-Tree 删除双黑修复边界 + LSM 死代码清理 · 1304 测试 76 套件 · 90.1% 行覆盖率。</p>
<strong>v0.8.0 根治性迭代:统一语义 / 消灭复发结构 / 验证基础设施</strong> — 三份行校验实现收敛为唯一 choke point(未知列/NaN 显式拒绝)· 唯一值比较与编码原语 · SQL 与 TABLE API 单管线(QueryBuilder 只产 AST)· CASE 表达式改 token 流递归下降 · 输出列序号与分隔标识符 · <strong>`__aria_manifest_&lt;generation&gt;` 单一提交点</strong>(页面水位 + SSTable 元数据 + 表结构 + WAL 水位 + 冻结表意图一次原子提交;头部/载荷双 CRC、先写后验、保留两代;顺序固定为<strong>数据落盘 → manifest 提交 → 才允许截断 WAL / 删除旧文件</strong>)· 元数据损坏不再静默空库(`ARIA_MANIFEST_CORRUPT` / `ARIA_LEGACY_META_CORRUPT`)· WAL LSN 全库单调 + 按水位删除分片 + 空洞与记录级损坏<strong>如实上报</strong>`ARIA_WAL_GAP` / `droppedWALRecords`)· LSM 冻结表一等状态、compaction 不再摘整层、底部层原地合并回收墓碑、按层 `compacting`、退休表 + 读者 epoch · 读路径"快照 + 结构版本乐观重试"(删除全部 prefetch 依赖)· checkpoint 不等 compaction(根治 "8~11s 悬崖")· 介质读故障与"文件不存在"分离(`ARIA_SSTABLE_READ_FAILED`,不误删元数据)· 恢复报告 `getRecoveryReport()` · 覆盖率门禁 + 变异验证(40 项)成为标准做法 ✅ v0.8.0
<h3>存储模式对比</h3> <h3>存储模式对比</h3>
<table> <table>
@@ -932,7 +933,8 @@ db.<span class="f">broadcastChange</span>(<span class="s">'users'</span>);</pre>
<tr><td><code>ARIA_MANIFEST_NOT_LOADED</code></td><td><code>load()</code> 就提交 manifest:拒绝写坏介质 🆕 v0.8.0</td></tr> <tr><td><code>ARIA_MANIFEST_NOT_LOADED</code></td><td><code>load()</code> 就提交 manifest:拒绝写坏介质 🆕 v0.8.0</td></tr>
<tr><td><code>ARIA_SSTABLE_SAVE_CONTRACT</code></td><td>存储实现违反 <code>save()</code> 契约(既不抛错也不返回结果):不把"写调用返回了"当成落盘成功 🆕 v0.8.0</td></tr> <tr><td><code>ARIA_SSTABLE_SAVE_CONTRACT</code></td><td>存储实现违反 <code>save()</code> 契约(既不抛错也不返回结果):不把"写调用返回了"当成落盘成功 🆕 v0.8.0</td></tr>
<tr><td><code>ARIA_DB_NOT_OPEN</code></td><td><code>open()</code> 之前使用 Aria 存储层(如加密后端) 🆕 v0.8.0</td></tr> <tr><td><code>ARIA_DB_NOT_OPEN</code></td><td><code>open()</code> 之前使用 Aria 存储层(如加密后端) 🆕 v0.8.0</td></tr>
<tr><td><code>ARIA_OPEN_ERROR</code></td><td>AriaEngine 打开失败(底层错误作为 cause 保留) 🆕 v0.8.0</td></tr> <tr><td><code>ARIA_OPEN_ERROR</code></td><td>AriaEngine 打开失败(底层错误保留<code>err.details</code> 与标准 <code>err.cause</code> 上,便于定位根因 🆕 v0.8.0</td></tr>
<tr><td><code>ARIA_OPFS_UNAVAILABLE</code></td><td>OPFS 在当前环境不可用(如 <code>file://</code> 页面被 Chromium 禁止访问 OPFS):错误信息给出可操作建议(改用 http(s) 或 <code>mode:'memory'</code> 🆕 v0.8.0</td></tr>
<tr><td><code>DB_NOT_OPEN</code></td><td>数据库未打开(KVStoreEngine 操作前未 <code>open()</code></td></tr> <tr><td><code>DB_NOT_OPEN</code></td><td>数据库未打开(KVStoreEngine 操作前未 <code>open()</code></td></tr>
<tr><td><code>KV_SCHEMA_ERROR</code></td><td>KVStore 中的表结构记录损坏</td></tr> <tr><td><code>KV_SCHEMA_ERROR</code></td><td>KVStore 中的表结构记录损坏</td></tr>
<tr><td><code>KV_LOG_ERROR</code> / <code>KV_BACKGROUND_ERROR</code></td><td>KVStore 日志写入失败 / 后台刷盘失败</td></tr> <tr><td><code>KV_LOG_ERROR</code> / <code>KV_BACKGROUND_ERROR</code></td><td>KVStore 日志写入失败 / 后台刷盘失败</td></tr>
+4 -4
View File
@@ -153,7 +153,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.8.0 根治性迭代 — 1980 测试 92 套件 · 语句/分支/函数/行覆盖率 90.59% / 82.59% / 94.14% / 93.50% · UPDATE/DELETE 子查询正确执行 · 主键非空强制 · DROP INDEX 保留 UNIQUE 约束 · findStream 真惰性 · 参数化查询 · 自研 KVStore 事务引擎 · AriaEngine LSM+WAL+MVCC · 崩溃恢复</div> <div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.8.0 根治性迭代 — 1985 测试 93 套件 · 语句/分支/函数/行覆盖率 90.59% / 82.61% / 94.14% / 93.50% · UPDATE/DELETE 子查询正确执行 · 主键非空强制 · DROP INDEX 保留 UNIQUE 约束 · findStream 真惰性 · 参数化查询 · 自研 KVStore 事务引擎 · AriaEngine LSM+WAL+MVCC · 崩溃恢复</div>
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1> <h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
<p>TypeScript 原生构建,4 种存储模式 + 3 种存储后端,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。</p> <p>TypeScript 原生构建,4 种存储模式 + 3 种存储后端,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。</p>
<div class="actions"> <div class="actions">
@@ -279,7 +279,7 @@ npm install @metona-team/metona-sqlark
<div class="feature-card"> <div class="feature-card">
<div class="icon">📦</div> <div class="icon">📦</div>
<h3>零运行时依赖</h3> <h3>零运行时依赖</h3>
<p>纯 TypeScript 实现,不依赖任何第三方库。UMD/ESM/CJS 多格式输出,min 产物实测 251,109 字节 / gzip 63,145 字节。</p> <p>纯 TypeScript 实现,不依赖任何第三方库。UMD/ESM/CJS 多格式输出,min 产物实测 251,731 字节 / gzip 63,431 字节。</p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<div class="icon">📊</div> <div class="icon">📊</div>
@@ -414,12 +414,12 @@ npm install @metona-team/metona-sqlark
<p>MetonaSqlark 的核心指标</p> <p>MetonaSqlark 的核心指标</p>
</div> </div>
<div class="stats"> <div class="stats">
<div class="stat-card"><div class="num">1980</div><div class="label">测试用例(+14 e2e</div></div> <div class="stat-card"><div class="num">1985</div><div class="label">测试用例(+14 e2e</div></div>
<div class="stat-card"><div class="num">93.50%</div><div class="label">行覆盖率</div></div> <div class="stat-card"><div class="num">93.50%</div><div class="label">行覆盖率</div></div>
<div class="stat-card"><div class="num">63KB</div><div class="label">gzip 体积</div></div> <div class="stat-card"><div class="num">63KB</div><div class="label">gzip 体积</div></div>
<div class="stat-card"><div class="num">4+3</div><div class="label">存储模式+后端</div></div> <div class="stat-card"><div class="num">4+3</div><div class="label">存储模式+后端</div></div>
<div class="stat-card"><div class="num">72</div><div class="label">SQL 关键字</div></div> <div class="stat-card"><div class="num">72</div><div class="label">SQL 关键字</div></div>
<div class="stat-card"><div class="num">91</div><div class="label">测试套件</div></div> <div class="stat-card"><div class="num">93</div><div class="label">测试套件</div></div>
</div> </div>
</div> </div>
</section> </section>
+21
View File
@@ -214,6 +214,15 @@ export interface MetonaPlugin {
/** 数据库错误 */ /** 数据库错误 */
export class DatabaseError extends Error { export class DatabaseError extends Error {
/**
*
*
* v0.8.0 target ES2020lib `Error`
* `cause` TypeScript 使 `err.cause`
*
*/
cause?: unknown;
constructor( constructor(
message: string, message: string,
public code: string, public code: string,
@@ -221,6 +230,18 @@ export class DatabaseError extends Error {
) { ) {
super(message); super(message);
this.name = 'DatabaseError'; this.name = 'DatabaseError';
// v0.8.0review 修复):把底层错误同时挂到**标准** `Error.cause` 上。
//
// 修复前第三个参数只存进 `details``err.cause` 恒为 undefined —— 而文档与
// 代码注释多处写的是"底层错误作为 cause 保留"(实测被打脸:探针打印
// `e.cause?.code` 得到 undefined)。`details` 语义保持不变(向后兼容),
// `cause` 让标准错误链工具(日志/监控/Node 的 error.cause 约定)能看到根因。
//
// 注意:target 是 ES2020lib 无 ES2022 的 ErrorOptions),因此用赋值而不是
// `super(message, { cause })`;运行时所有现代引擎都支持该属性。
if (details !== undefined) {
(this as { cause?: unknown }).cause = details;
}
} }
} }
+28 -1
View File
@@ -18,6 +18,7 @@
* Chrome 102+ / Edge 102+Safari 15.2+ / Firefox 111+ OPFS * Chrome 102+ / Edge 102+Safari 15.2+ / Firefox 111+ OPFS
*/ */
import type { IStorageBackend } from './backend'; import type { IStorageBackend } from './backend';
import { DatabaseError } from '../../../constants';
/** 崩溃残留临时文件后缀(createWritable 底层实现可能留下) */ /** 崩溃残留临时文件后缀(createWritable 底层实现可能留下) */
const STALE_SUFFIXES = ['.crswap', '.tmp']; const STALE_SUFFIXES = ['.crswap', '.tmp'];
@@ -30,7 +31,33 @@ export class OPFSBackend implements IStorageBackend {
async open(name: string): Promise<void> { async open(name: string): Promise<void> {
this.dbName = name; this.dbName = name;
this.root = await navigator.storage.getDirectory(); // v0.8.0review 修复):OPFS 不可用时给出**可操作的**错误,而不是让上层
// 只能报一句 "Failed to open AriaEngine database"。
//
// 真实场景(实测):用 file:// 直接打开页面时 Chromium 认为本地文件"不适合
// Web 应用访问"`navigator.storage.getDirectory()` 抛 SecurityError —— 而
// `isSecureContext` 仍是 true、API 也**存在**,所以只有真正调用才会发现。
// 修复前这个 SecurityError 被 `ARIA_OPEN_ERROR` 吞成一句无从下手的消息
//(cause 没往上带),用户不知道是"环境不支持"还是"库坏了"。
if (typeof navigator === 'undefined' || !navigator.storage
|| typeof navigator.storage.getDirectory !== 'function') {
throw new DatabaseError(
'OPFS is not available in this environment: navigator.storage.getDirectory is missing. ' +
"Use the http(s) protocol (file:// is rejected by browsers) or switch to mode: 'memory' / a KVStore backend.",
'ARIA_OPFS_UNAVAILABLE',
);
}
try {
this.root = await navigator.storage.getDirectory();
} catch (error) {
throw new DatabaseError(
`OPFS is not accessible here (${(error as Error).name}: ${(error as Error).message}). ` +
'Chromium blocks OPFS for file:// pages — serve the page over http(s) ' +
"(e.g. `npx serve .` / `node tests/e2e/server.cjs`) or use mode: 'memory'.",
'ARIA_OPFS_UNAVAILABLE',
error,
);
}
this.dbDir = await this.root.getDirectoryHandle(name, { create: true }); this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
// v0.4.5: 清理崩溃残留临时文件(不阻塞打开) // v0.4.5: 清理崩溃残留临时文件(不阻塞打开)
await this.cleanupStaleFiles(); await this.cleanupStaleFiles();
@@ -0,0 +1,97 @@
/**
* OPFS "可操作"
*
* v0.8.0 review file:// 直接打开站点演示页 → 切到 Aria
* `navigator.storage.getDirectory()` SecurityErrorChromium
* Web 访 `ARIA_OPEN_ERROR`**cause **
* "Failed to open AriaEngine database"
* `isSecureContext` trueAPI ****
* "API 是否存在"
*/
import { describe, it, expect, afterEach } from '@jest/globals';
import { OPFSBackend } from '../../src/engine/aria/store/opfs_backend';
const realNavigator = globalThis.navigator;
afterEach(() => {
Object.defineProperty(globalThis, 'navigator', { value: realNavigator, configurable: true });
});
function stubStorage(storage: unknown): void {
Object.defineProperty(globalThis, 'navigator', {
value: { ...(realNavigator as object), storage },
configurable: true,
});
}
describe('OPFSBackend.open — 环境不可用时给可操作的错误', () => {
it('navigator.storage 缺失(老浏览器 / 非安全上下文)→ ARIA_OPFS_UNAVAILABLE', async () => {
stubStorage(undefined);
const backend = new OPFSBackend();
await expect(backend.open('demo')).rejects.toMatchObject({ code: 'ARIA_OPFS_UNAVAILABLE' });
try {
await backend.open('demo');
} catch (error) {
const message = (error as Error).message;
expect(message).toContain('navigator.storage.getDirectory is missing');
expect(message).toContain('http(s)'); // 必须告诉用户怎么修
expect(message).toContain('memory');
}
});
it('getDirectory 抛 SecurityErrorfile:// 页面)→ 带原始 cause + 可操作建议', async () => {
const security = new Error('It was determined that certain files are unsafe for access within a Web application');
security.name = 'SecurityError';
stubStorage({ getDirectory: async () => { throw security; } });
const backend = new OPFSBackend();
const error = await backend.open('demo').then(() => null, (e: unknown) => e) as
(Error & { code?: string; cause?: unknown }) | null;
expect(error).not.toBeNull();
expect(error!.code).toBe('ARIA_OPFS_UNAVAILABLE');
expect(error!.cause).toBe(security); // 原始错误必须保留(cause 非枚举,直接断言)
expect(error!.message).toContain('SecurityError');
expect(error!.message).toContain('file://'); // 点明最可能的成因
expect(error!.message).toContain('http(s)');
expect(error!.message).toContain('memory');
});
it('正常环境照旧可用(正控:不能把好环境也拒了)', async () => {
const files = new Map<string, unknown>();
const dirHandle = {
async getFileHandle(name: string) {
return { async createWritable() { return { async write() {}, async close() { files.set(name, 1); } }; } };
},
async getDirectoryHandle() { return dirHandle; },
async removeEntry(name: string) { files.delete(name); },
async *entries() { /* 无残留文件 */ },
};
stubStorage({ getDirectory: async () => dirHandle });
const backend = new OPFSBackend();
await backend.open('demo');
expect(backend.isOpen()).toBe(true);
await backend.close();
expect(backend.isOpen()).toBe(false);
});
});
describe('DatabaseError — 根因必须可通过标准 cause 取到', () => {
it('第三个参数同时进 details 与 Error.cause(两者语义都成立)', async () => {
const { DatabaseError } = await import('../../src/constants');
const root = new Error('underlying failure');
const err = new DatabaseError('wrapper message', 'WRAP_CODE', root);
expect(err.code).toBe('WRAP_CODE');
expect(err.details).toBe(root); // 既有契约(文档示例打印 err.details
expect(err.cause).toBe(root); // 标准错误链(v0.8.0 修复前恒为 undefined
expect(err.message).toBe('wrapper message');
expect(err.name).toBe('DatabaseError');
expect(err instanceof Error).toBe(true);
});
it('没有底层错误时不制造 cause(保持 undefined', async () => {
const { DatabaseError } = await import('../../src/constants');
const err = new DatabaseError('plain', 'PLAIN_CODE');
expect(err.details).toBeUndefined();
expect(err.cause).toBeUndefined();
});
});