160 lines · 5.1 KB · TypeScript
RawDownload
import { getGlobalSingleton } from "../instances";import { HostedPage, type PageState, type ScreencastFrame } from "./hosted-page";import { playwrightManager } from "./playwright"; export type { PageState, ScreencastFrame }; const TEXT_CAP_BYTES = 500 * 1024;const HTML_CAP_BYTES = 1024 * 1024;const NAVIGATION_TIMEOUT_MS = 8_000; const normalizeUrl = (value: string): string =>  /^[a-z][a-z0-9+.-]*:/i.test(value) ? value : `https://${value}`; const capString = (value: string, maximum: number): string =>  value.length > maximum ? value.slice(0, maximum) : value; class BrowserHost {  private pages = new Map<string, HostedPage>();  private activeId: string | null = null;   isAvailable(): boolean {    return playwrightManager.isAvailable();  }   async page(pageId?: string): Promise<HostedPage> {    const targetId = pageId ?? this.activeId;    const cached = targetId ? this.pages.get(targetId) : undefined;    if (cached && !cached.closed) {      this.activeId = cached.id;      return cached;    }    if (cached) this.pages.delete(cached.id);     const context = await playwrightManager.ensure();    const rawPage =      context        .pages()        .find((candidate) =>          Array.from(this.pages.values()).every((hosted) => !hosted.matches(candidate)),        ) ?? (await context.newPage());    const hosted = HostedPage.attach(rawPage);    this.pages.set(hosted.id, hosted);    this.activeId = hosted.id;    return hosted;  }   async navigate(url: string, pageId?: string): Promise<{ url: string; title: string }> {    const page = await this.page(pageId);    await page.navigate(normalizeUrl(url), NAVIGATION_TIMEOUT_MS);    const state = await page.readState();    return { url: state.url, title: state.title };  }   async getUrl(pageId?: string): Promise<{ url: string; title: string }> {    const state = await (await this.page(pageId)).readState();    return { url: state.url, title: state.title };  }   async getState(pageId?: string): Promise<PageState> {    return (await this.page(pageId)).readState();  }   async peekState(): Promise<PageState | null> {    const page = this.activeId ? this.pages.get(this.activeId) : undefined;    if (!page || page.closed) return null;    return page.readState();  }   async goBack(pageId?: string): Promise<void> {    await (await this.page(pageId)).goBack(NAVIGATION_TIMEOUT_MS);  }   async goForward(pageId?: string): Promise<void> {    await (await this.page(pageId)).goForward(NAVIGATION_TIMEOUT_MS);  }   async reload(pageId?: string): Promise<void> {    await (await this.page(pageId)).reload(NAVIGATION_TIMEOUT_MS);  }   async getText(pageId?: string): Promise<string> {    return capString(await (await this.page(pageId)).text(), TEXT_CAP_BYTES);  }   async getHtml(pageId?: string): Promise<string> {    return capString(await (await this.page(pageId)).html(), HTML_CAP_BYTES);  }   async click(args: { selector: string }, pageId?: string): Promise<{ found: boolean }> {    const page = await this.page(pageId);    return { found: await page.click(args.selector) };  }   async fill(    args: { selector: string; value: string },    pageId?: string,  ): Promise<{ found: boolean }> {    const page = await this.page(pageId);    return { found: await page.fill(args.selector, args.value) };  }   async scroll(    args: { deltaY: number; deltaX?: number },    pageId?: string,  ): Promise<{ deltaX: number; deltaY: number; scrollY: number }> {    const deltaY = clampDelta(args.deltaY);    const deltaX = clampDelta(args.deltaX ?? 0);    const scrollY = await (await this.page(pageId)).scroll(deltaX, deltaY);    return { deltaX, deltaY, scrollY };  }   async screenshot(pageId?: string): Promise<string> {    const data = await (await this.page(pageId)).screenshot("png");    return `data:image/png;base64,${data}`;  }   async setViewport(width: number, height: number, pageId?: string): Promise<void> {    await (await this.page(pageId)).setViewport(width, height);  }   async pollFrame(pageId?: string): Promise<{ frame: ScreencastFrame | null; state: PageState }> {    const page = await this.page(pageId);    const [frame, state] = await Promise.all([page.captureFrame(), page.readState()]);    return { frame, state };  }   async dispatchMouse(args: MouseInput, pageId?: string): Promise<void> {    await (await this.page(pageId)).dispatchMouse(args);  }   async dispatchKey(args: KeyInput, pageId?: string): Promise<void> {    await (await this.page(pageId)).dispatchKey(args);  }   stop(): void {    for (const page of this.pages.values()) page.close();    this.pages.clear();    this.activeId = null;    playwrightManager.stop();  }} export type MouseInput = {  type: "down" | "up" | "move" | "wheel";  x: number;  y: number;  button?: "left" | "right" | "middle";  clickCount?: number;  deltaX?: number;  deltaY?: number;}; export type KeyInput = { type: "down" | "up"; key: string; code: string }; const clampDelta = (value: number): number => {  if (!Number.isFinite(value)) return 0;  return Math.max(-10_000, Math.min(10_000, Math.trunc(value)));}; export const browserHost = getGlobalSingleton("browserHost", () => new BrowserHost());