import { Context, Data, Effect, type Layer } from "effect"; import * as Binding from "./Binding"; import type { WorkerEnvironment } from "./Environment"; const expectedBrowserRenderingBinding = "Browser Rendering binding resource"; /** Error raised when a Browser Rendering operation fails. */ export class BrowserRenderingOperationError extends Data.TaggedError( "BrowserRenderingOperationError", )<{ readonly binding: string; readonly operation: string; readonly cause: unknown; }> {} /** Typed Browser Rendering binding definition. */ export interface BrowserRenderingDefinition { /** Binding name as configured in `wrangler.jsonc`. */ readonly binding: string; } export type BrowserRenderingBinding = object; export type BrowserRenderingLaunch = ( binding: RawBinding, options?: LaunchOptions, ) => Promise; export type BrowserRenderingConnect = ( binding: RawBinding, options?: ConnectOptions, ) => Promise; export interface BrowserRenderingBrowserLike { readonly newPage: () => Promise; readonly close?: () => Promise; readonly disconnect?: () => void; readonly version?: () => Promise; } export interface BrowserRenderingPageLike { readonly goto?: (url: string, options?: unknown) => Promise; readonly setContent?: (html: string, options?: unknown) => Promise; readonly content?: () => Promise; readonly screenshot?: (options?: unknown) => Promise; readonly pdf?: (options?: unknown) => Promise; readonly evaluate?: (pageFunction: unknown, ...args: ReadonlyArray) => Promise; readonly close?: (options?: unknown) => Promise; } export interface BrowserRenderingPageClient { readonly raw: Page; readonly goto: ( url: string, options?: Parameters>[1], ) => Effect.Effect< Awaited>>, BrowserRenderingOperationError >; readonly setContent: ( html: string, options?: Parameters>[1], ) => Effect.Effect; readonly content: Effect.Effect; readonly screenshot: ( options?: Parameters>[0], ) => Effect.Effect; readonly pdf: ( options?: Parameters>[0], ) => Effect.Effect; readonly evaluate: ( pageFunction: Parameters>[0], ...args: ReadonlyArray ) => Effect.Effect; readonly close: ( options?: Parameters>[0], ) => Effect.Effect; } export interface BrowserRenderingBrowserClient< Browser extends BrowserRenderingBrowserLike, Page extends BrowserRenderingPageLike, > { readonly raw: Browser; readonly newPage: Effect.Effect, BrowserRenderingOperationError>; readonly version: Effect.Effect; readonly close: Effect.Effect; readonly disconnect: Effect.Effect; } export interface BrowserRenderingClient< RawBinding extends BrowserRenderingBinding = BrowserRenderingBinding, > { readonly launchWith: < Browser extends BrowserRenderingBrowserLike, Page extends BrowserRenderingPageLike = BrowserRenderingPageLike, LaunchOptions = unknown, >( launch: BrowserRenderingLaunch, options?: LaunchOptions, ) => Effect.Effect, BrowserRenderingOperationError>; readonly connectWith: < Browser extends BrowserRenderingBrowserLike, Page extends BrowserRenderingPageLike = BrowserRenderingPageLike, ConnectOptions = unknown, >( connect: BrowserRenderingConnect, options?: ConnectOptions, ) => Effect.Effect, BrowserRenderingOperationError>; readonly unsafeRaw: Effect.Effect; readonly definition: BrowserRenderingDefinition; } declare const BrowserRenderingServiceTypeId: unique symbol; /** Nominal service marker for Browser Rendering services created with {@link make}. */ export interface BrowserRenderingService { readonly [BrowserRenderingServiceTypeId]: { readonly id: Id; }; } export type LayerOptions = { readonly binding: string; }; export interface TagClass extends Context.ServiceClass< Self, Id, BrowserRenderingClient > { readonly id: Id; readonly layer: ( options: LayerOptions, ) => Layer.Layer< Self, Binding.BindingNotFoundError | Binding.BindingValidationError, WorkerEnvironment >; } const browserRenderingError = (binding: string, operation: string, cause: unknown) => new BrowserRenderingOperationError({ binding, operation, cause }); const tryBrowserRenderingPromise = ( binding: string, operation: string, evaluate: () => Promise, ): Effect.Effect => Effect.tryPromise({ try: evaluate, catch: (cause) => browserRenderingError(binding, operation, cause), }); const tryBrowserRenderingSync = ( binding: string, operation: string, evaluate: () => A, ): Effect.Effect => Effect.try({ try: evaluate, catch: (cause) => browserRenderingError(binding, operation, cause), }); const missingMethod = (binding: string, operation: string) => Effect.fail( browserRenderingError( binding, operation, new TypeError(`Browser Rendering object does not expose ${operation}()`), ), ); const wrapPage = ( binding: string, page: Page, ): BrowserRenderingPageClient => ({ raw: page, goto: (url, options) => (page.goto === undefined ? missingMethod(binding, "goto") : tryBrowserRenderingPromise(binding, "goto", () => page.goto!(url, options), )) as Effect.Effect< Awaited>>, BrowserRenderingOperationError >, setContent: (html, options) => page.setContent === undefined ? missingMethod(binding, "setContent") : tryBrowserRenderingPromise(binding, "setContent", () => page.setContent!(html, options)), content: page.content === undefined ? missingMethod(binding, "content") : tryBrowserRenderingPromise(binding, "content", () => page.content!()), screenshot: (options) => (page.screenshot === undefined ? missingMethod(binding, "screenshot") : tryBrowserRenderingPromise(binding, "screenshot", () => page.screenshot!(options), )) as Effect.Effect, pdf: (options) => (page.pdf === undefined ? missingMethod(binding, "pdf") : tryBrowserRenderingPromise(binding, "pdf", () => page.pdf!(options))) as Effect.Effect< never, BrowserRenderingOperationError >, evaluate: (pageFunction, ...args) => (page.evaluate === undefined ? missingMethod(binding, "evaluate") : tryBrowserRenderingPromise(binding, "evaluate", () => page.evaluate!(pageFunction, ...args), )) as Effect.Effect, close: (options) => page.close === undefined ? Effect.void : tryBrowserRenderingPromise(binding, "closePage", () => page.close!(options)), }); const wrapBrowser = < Browser extends BrowserRenderingBrowserLike, Page extends BrowserRenderingPageLike, >( binding: string, browser: Browser, ): BrowserRenderingBrowserClient => ({ raw: browser, newPage: tryBrowserRenderingPromise(binding, "newPage", () => browser.newPage()).pipe( Effect.map((page) => wrapPage(binding, page)), ), version: browser.version === undefined ? missingMethod(binding, "version") : tryBrowserRenderingPromise(binding, "version", () => browser.version!()), close: browser.close === undefined ? Effect.void : tryBrowserRenderingPromise(binding, "closeBrowser", () => browser.close!()), disconnect: browser.disconnect === undefined ? Effect.void : tryBrowserRenderingSync(binding, "disconnect", () => browser.disconnect!()), }); export const isBrowserRenderingBinding = (value: unknown): value is BrowserRenderingBinding => (typeof value === "object" || typeof value === "function") && value !== null; export const makeClient = ( definition: BrowserRenderingDefinition, ) => (binding: RawBinding): BrowserRenderingClient => ({ definition, launchWith: (launch, options) => tryBrowserRenderingPromise(definition.binding, "launch", () => launch(binding, options)).pipe( Effect.map((browser) => wrapBrowser(definition.binding, browser)), ), connectWith: (connect, options) => tryBrowserRenderingPromise(definition.binding, "connect", () => connect(binding, options), ).pipe(Effect.map((browser) => wrapBrowser(definition.binding, browser))), unsafeRaw: Effect.succeed(binding), }); export const layer = ( tag: Context.Service, definition: BrowserRenderingDefinition, ) => Binding.layer(tag, definition.binding, isBrowserRenderingBinding, makeClient(definition), { expected: expectedBrowserRenderingBinding, }); export const make = (id: Id) => Tag>()(id); export const Tag = () => (id: Id) => { const tag = Context.Service()(id); const makeLayer = (definition: LayerOptions) => layer(tag, definition); return Object.assign(tag, { id, layer: makeLayer, }) as TagClass; };