import { BrowserError } from "./errors.ts"; /** * 同一 profile(站点/账号)的浏览器操作串行(REQ §4.4)。 * 内存实现只在单进程内有效;多进程/多实例部署需换成持久化实现(PG advisory lock 或 * browser_control_lease 表),接口保持不变。 */ export interface ProfileLock { /** 在持有 key 锁期间执行 fn;等待超过 waitMs 抛 BrowserError('lock_timeout') */ run(key: string, fn: () => Promise, opts?: { waitMs?: number }): Promise; /** 当前持有或排队中的 key 数量(诊断用) */ size(): number; } interface Waiter { resolve: () => void; reject: (e: unknown) => void; timer?: ReturnType; } export class InMemoryProfileLock implements ProfileLock { private readonly held = new Map(); constructor(private readonly defaultWaitMs = 30_000) {} size(): number { return this.held.size; } async run(key: string, fn: () => Promise, opts: { waitMs?: number } = {}): Promise { await this.acquire(key, opts.waitMs ?? this.defaultWaitMs); try { return await fn(); } finally { this.release(key); } } private acquire(key: string, waitMs: number): Promise { const queue = this.held.get(key); if (!queue) { this.held.set(key, []); return Promise.resolve(); } return new Promise((resolve, reject) => { const w: Waiter = { resolve, reject }; w.timer = setTimeout(() => { const i = queue.indexOf(w); if (i >= 0) queue.splice(i, 1); reject(new BrowserError("lock_timeout", `profile 锁等待超时(${waitMs}ms)`)); }, waitMs); queue.push(w); }); } private release(key: string): void { const queue = this.held.get(key); if (!queue) return; const next = queue.shift(); if (!next) { this.held.delete(key); return; } if (next.timer) clearTimeout(next.timer); next.resolve(); } }