Files
MetonaSqlark/tests/engine/aria-locks.test.ts
T
thzxx 24b3ca1079
CI / test (20.x) (push) Successful in 10m53s
CI / test (22.x) (push) Successful in 10m47s
CI / test (24.x) (push) Successful in 10m42s
CI / e2e (push) Successful in 10m27s
CI / test (18.x) (push) Successful in 11m1s
release: v0.6.0 — 完全移除 IndexedDB,自研 KVStore 事务存储引擎(多key原子写/快照日志恢复/CRC自愈)+ KVStoreEngine + 旧库迁移工具 + 10万级压力验证 + 崩溃注入e2e
2026-08-10 13:56:04 +08:00

180 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AriaEngine — 多标签页独占锁(Web Locks)测试
*
* 覆盖:
* 1. 第一个引擎持锁 → 第二个引擎打开抛 ARIA_LOCKED
* 2. 关闭后锁释放 → 可再次打开
* 3. 环境不支持 Web Locks → 降级打开(返回 false 语义 + 警告)
* 4. open 失败(如加密密码错误)→ 锁释放(其他标签页可打开)
* 5. 锁名按库隔离(不同库不互斥)
*/
import { AriaEngine } from '../../src/engine/aria/index';
import { DatabaseLock, lockName } from '../../src/engine/aria/locks';
import { createSchema } from '../../src/table/schema';
import { installOPFSMock } from '../helpers/opfs-mock';
beforeEach(() => { installOPFSMock(new Map()); });
let idbCounter = 0;
function uniqueDB(): string {
return `lock-${Date.now()}-${++idbCounter}-${Math.random().toString(36).slice(2, 8)}`;
}
// ===================================================================
// Web Locks mock:真实语义(ifAvailable / 回调结束锁自动释放)
// ===================================================================
let lockHolders: Map<string, true>;
function installLocksMock() {
lockHolders = new Map();
// 保留 storageOPFS mock 设置过),与 locks 共存
const existingNav = (globalThis as { navigator?: { storage?: unknown } }).navigator;
const storage = existingNav?.storage;
Object.defineProperty(globalThis, 'navigator', {
value: {
storage,
locks: {
request: async (_name: string, opts: { ifAvailable?: boolean }, cb: (lock: { name: string } | null) => Promise<void> | void) => {
const name = _name;
if (lockHolders.has(name)) {
if (opts?.ifAvailable) {
await cb(null);
return;
}
throw new Error('unreachable: non-ifAvailable request not used');
}
lockHolders.set(name, true);
try {
// 真实 Web Locks 语义:回调结束 → 锁自动释放
await cb({ name });
} finally {
lockHolders.delete(name);
}
},
},
},
configurable: true,
writable: true,
});
}
function removeLocksMock() {
// 只移除 locks,保留 storageOPFS mock 需要)
const existingNav = (globalThis as { navigator?: { storage?: unknown } }).navigator;
Object.defineProperty(globalThis, 'navigator', {
value: { storage: existingNav?.storage },
configurable: true,
writable: true,
});
}
// ===================================================================
// DatabaseLock 单元
// ===================================================================
describe('AriaEngine — DatabaseLock 单元', () => {
beforeEach(installLocksMock);
it('acquire 持锁 → 二次 acquire 抛 ARIA_LOCKED → release 后恢复', async () => {
const lock1 = new DatabaseLock();
expect(await lock1.acquire('db-a')).toBe(true);
expect(lock1.isAcquired()).toBe(true);
expect(lock1.isSupported()).toBe(true);
expect(lockHolders.has(lockName('db-a'))).toBe(true);
const lock2 = new DatabaseLock();
await expect(lock2.acquire('db-a')).rejects.toMatchObject({ code: 'ARIA_LOCKED' });
expect(lock2.isAcquired()).toBe(false);
await lock1.release();
expect(lock1.isAcquired()).toBe(false);
expect(lockHolders.has(lockName('db-a'))).toBe(false);
const lock3 = new DatabaseLock();
expect(await lock3.acquire('db-a')).toBe(true);
await lock3.release();
});
it('不同库名互不阻塞', async () => {
const l1 = new DatabaseLock();
const l2 = new DatabaseLock();
expect(await l1.acquire('db-x')).toBe(true);
expect(await l2.acquire('db-y')).toBe(true);
await l1.release();
await l2.release();
});
it('无 Web Locks 环境 → acquire 返回 false(降级)', async () => {
removeLocksMock();
const lock = new DatabaseLock();
expect(await lock.acquire('db-z')).toBe(false);
expect(lock.isSupported()).toBe(false);
expect(lock.isAcquired()).toBe(false);
});
it('重复 release 幂等', async () => {
const lock = new DatabaseLock();
await lock.acquire('db-r');
await lock.release();
await lock.release();
expect(lock.isAcquired()).toBe(false);
});
});
// ===================================================================
// AriaEngine 集成 — 多标签页锁
// ===================================================================
describe('AriaEngine — 多标签页锁(集成)', () => {
beforeEach(installLocksMock);
it('第二个标签页打开同一库 → ARIA_LOCKED', async () => {
const dbName = uniqueDB();
const engine1 = new AriaEngine({ storageBackend: 'opfs' });
await engine1.open(dbName, 1);
await engine1.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
const engine2 = new AriaEngine({ storageBackend: 'opfs' });
await expect(engine2.open(dbName, 1)).rejects.toMatchObject({ code: 'ARIA_LOCKED' });
// engine2 未持锁、未 opened
expect((engine2 as any).opened).toBe(false);
await engine1.close();
// 释放后可打开
const engine3 = new AriaEngine({ storageBackend: 'opfs' });
await engine3.open(dbName, 1);
expect(await engine3.count('t')).toBe(0);
await engine3.close();
});
it('open 失败(错误密码)→ 锁释放,其他标签页可打开', async () => {
const dbName = uniqueDB();
const engine1 = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'pw' } });
await engine1.open(dbName, 1);
await engine1.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
await engine1.close();
// 错误密码 → open 失败
const engineBad = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'wrong' } });
await expect(engineBad.open(dbName, 1)).rejects.toMatchObject({ code: 'ARIA_DECRYPT_ERROR' });
// 失败后锁已释放(未泄漏)
expect(lockHolders.has(lockName(dbName))).toBe(false);
// 正确密码可打开
const engineOk = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'pw' } });
await engineOk.open(dbName, 1);
await engineOk.close();
});
it('无 Web Locks 环境 → 降级打开(不抛错)', async () => {
removeLocksMock();
const dbName = uniqueDB();
const engine = new AriaEngine({ storageBackend: 'opfs' });
await engine.open(dbName, 1);
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
await engine.insert('t', [{ id: '1' }]);
expect(await engine.count('t')).toBe(1);
await engine.close();
});
});