feat(browser): W4-C BrowserProvider contract and camofox-browser adapter skeleton
- contracts/browser.ts: provider kind, per-capability {supported, verified} flags,
session/tab/control-lease rows, NavigationBudget, redacted BrowserSnapshot,
DownloadArtifact with blob/secret refs, BrowserErrorKind
- adapters/browser: BrowserProvider interface, camofox client (health, capabilities
static + /openapi.json probe, openSession/navigate/snapshot/screenshot; rest
not_implemented for M2), none provider, redactSnapshot, profile serial lock,
domain allowlist budget; Bearer access key via BrowserSecretResolver
- testkit/browser: mock camofox server (auth, fixtures with secrets, faults)
- db 0023: browser_provider/session/tab/control_lease with one-active-lease and
one-live-session-per-profile partial unique indexes; types regenerated
- docs/design/browser-provider.md: contract, camofox OpenAPI 1.17.0 mapping,
unverified items and M2 prototype checklist; Playwright is not a product browser
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
269a2269a1
commit
effaa32b70
23 files changed
+2290
No files matched your search
@@ -0,0 +1,66 @@
|
||||
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<T>(key: string, fn: () => Promise<T>, opts?: { waitMs?: number }): Promise<T>;
|
||||
/** 当前持有或排队中的 key 数量(诊断用) */
|
||||
size(): number;
|
||||
}
|
||||
|
||||
interface Waiter {
|
||||
resolve: () => void;
|
||||
reject: (e: unknown) => void;
|
||||
timer?: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
export class InMemoryProfileLock implements ProfileLock {
|
||||
private readonly held = new Map<string, Waiter[]>();
|
||||
constructor(private readonly defaultWaitMs = 30_000) {}
|
||||
|
||||
size(): number {
|
||||
return this.held.size;
|
||||
}
|
||||
|
||||
async run<T>(key: string, fn: () => Promise<T>, opts: { waitMs?: number } = {}): Promise<T> {
|
||||
await this.acquire(key, opts.waitMs ?? this.defaultWaitMs);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
this.release(key);
|
||||
}
|
||||
}
|
||||
|
||||
private acquire(key: string, waitMs: number): Promise<void> {
|
||||
const queue = this.held.get(key);
|
||||
if (!queue) {
|
||||
this.held.set(key, []);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise<void>((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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user