/** * 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 { resetOPFSMock } from '../helpers/storage-harness'; beforeEach(() => { resetOPFSMock(); }); 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; function installLocksMock() { lockHolders = new Map(); // 保留 storage(OPFS 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) => { 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,保留 storage(OPFS 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(); }); });