import type { Optional } from "@libs/typing/types"
import type { Page, SandboxOptions } from "@astral/astral"
import { getLogger, type Logger } from "@logtape/logtape"
import { launch } from "@astral/astral"
import { env } from "@libs/toolbox/env"
export type { Logger, Page, SandboxOptions }
export type Permissions = NonNullable<Exclude<Required<SandboxOptions["sandbox"]>, boolean>>["permissions"]
export type BrowserOptions = {
logger?: string[]
fetch?: typeof globalThis.fetch
}
export type BrowserConfiguration = {
cache?: string
ua?: string
width?: number
height?: number
permissions?: Permissions
headless?: boolean
}
export class Browser {
constructor({ logger: category = ["browser"], fetch = globalThis.fetch }: BrowserOptions = {}, config: BrowserConfiguration = {}) {
this.#log = getLogger(category)
this.#fetch = fetch
this.#config = { width: 1280, height: 720, headless: true, permissions: "inherit", ...config }
}
readonly #log
readonly #fetch
readonly #config
#browser: Optional<Awaited<ReturnType<typeof launch>>>
async page(url: string): Promise<Page> {
if (!this.#browser) {
const args = []
if (this.#config.cache)
args.push(`--user-data-dir=${this.#config.cache}`)
this.#browser = await launch({
cache: this.#config.cache,
args,
headless: this.#config.headless,
userAgent: this.#config.ua,
launchPresets: { hardened: true, bgTransparent: true, windowSize: { width: this.#config.width, height: this.#config.height } },
})
const useragent = await this.#browser.userAgent()
const version = await this.#browser.version()
this.#log.info(`opened browser: ${version} ${useragent}`)
}
const page = await this.#browser.newPage(url, {
coverage: env("DENO_COVERAGE", { boolean: true }),
sandbox: { permissions: this.#config.permissions as Permissions },
interceptor: (request) => this.#fetch(request),
})
const close = page.close.bind(page)
page.close = async () => {
await close()
this.#log.debug(`closed page: ${url}`)
}
this.#log.info(`opened page: ${url}`)
return page
}
async [Symbol.asyncDispose](): Promise<void> {
await this.#browser?.close()
this.#browser = undefined
this.#log.info("closed browser")
}
}
|