1import type * as cf from "@cloudflare/workers-types";
2import * as Data from "effect/Data";
3import type * as Effect from "effect/Effect";
4import type * as Stream from "effect/Stream";
5import type { RuntimeContext } from "../../RuntimeContext.ts";
6import * as Binding from "./Binding.ts";
7import type { BrowserBinding } from "./BrowserBinding.ts";
8
9const TypeId = "Cloudflare.Browser" as const;
10type TypeId = typeof TypeId;
11
12export class BrowserError extends Data.TaggedError("BrowserError")<{
13 message: string;
14 cause: unknown;
15}> {}
16
17/** The `Response` type returned by the Cloudflare Browser Rendering binding. */
18export type BrowserResponse = Awaited<ReturnType<cf.BrowserRun["fetch"]>>;
19
20/** An Effect produced by a {@link BrowserClient} operation. */
21type BrowserEffect<A> = Effect.Effect<A, BrowserError, RuntimeContext>;
22
23/** A byte stream produced by a binary {@link BrowserClient} action. */
24type BrowserByteStream = Stream.Stream<
25 Uint8Array,
26 BrowserError,
27 RuntimeContext
28>;
29
30// Quick action option types, re-exported so callers don't reach into the
31// `@cloudflare/workers-types` namespace directly.
32export type BrowserScreenshotOptions = cf.BrowserRunScreenshotOptions;
33export type BrowserPDFOptions = cf.BrowserRunPDFOptions;
34export type BrowserContentOptions = cf.BrowserRunContentOptions;
35export type BrowserScrapeOptions = cf.BrowserRunScrapeOptions;
36export type BrowserLinksOptions = cf.BrowserRunLinksOptions;
37export type BrowserSnapshotOptions = cf.BrowserRunSnapshotOptions;
38export type BrowserJsonOptions = cf.BrowserRunJsonOptions;
39export type BrowserMarkdownOptions = cf.BrowserRunMarkdownOptions;
40
41// Quick action success payloads.
42export type BrowserContentResult = cf.BrowserRunContentSuccessResponse;
43export type BrowserScrapeResult = cf.BrowserRunScrapeSuccessResponse;
44export type BrowserLinksResult = cf.BrowserRunLinksSuccessResponse;
45export type BrowserSnapshotResult = cf.BrowserRunSnapshotSuccessResponse;
46export type BrowserJsonResult = cf.BrowserRunJsonSuccessResponse;
47export type BrowserMarkdownResult = cf.BrowserRunMarkdownSuccessResponse;
48export type BrowserErrorResponse = cf.BrowserRunErrorResponse;
49
50/**
51 * Effect-native client for a Cloudflare Browser Rendering binding.
52 *
53 * Mirrors the runtime {@link cf.BrowserRun} binding, translating its shapes into
54 * Effect-native ones: JSON quick actions resolve to their parsed success
55 * payload, and binary actions (`screenshot`, `pdf`) resolve to a `Stream` of the
56 * response bytes. Non-success responses fail with {@link BrowserError}. The
57 * {@link raw} accessor and {@link fetch} are the promise-shaped escape hatches
58 * for libraries like `@cloudflare/puppeteer`.
59 */
60export interface BrowserClient {
61 /** Effect resolving to the raw Cloudflare Browser Rendering runtime binding. */
62 raw: Effect.Effect<cf.BrowserRun, never, RuntimeContext>;
63 /** Send a raw HTTP request to the Browser Run API. */
64 fetch(
65 ...args: Parameters<cf.BrowserRun["fetch"]>
66 ): BrowserEffect<BrowserResponse>;
67 /** Run a Browser Run quick action, resolving to the parsed payload. */
68 quickAction(
69 action: "screenshot",
70 options: BrowserScreenshotOptions,
71 ): BrowserByteStream;
72 quickAction(action: "pdf", options: BrowserPDFOptions): BrowserByteStream;
73 quickAction(
74 action: "content",
75 options: BrowserContentOptions,
76 ): BrowserEffect<BrowserContentResult>;
77 quickAction(
78 action: "scrape",
79 options: BrowserScrapeOptions,
80 ): BrowserEffect<BrowserScrapeResult>;
81 quickAction(
82 action: "links",
83 options: BrowserLinksOptions,
84 ): BrowserEffect<BrowserLinksResult>;
85 quickAction(
86 action: "snapshot",
87 options: BrowserSnapshotOptions,
88 ): BrowserEffect<BrowserSnapshotResult>;
89 quickAction(
90 action: "json",
91 options: BrowserJsonOptions,
92 ): BrowserEffect<BrowserJsonResult>;
93 quickAction(
94 action: "markdown",
95 options: BrowserMarkdownOptions,
96 ): BrowserEffect<BrowserMarkdownResult>;
97 /** Take a screenshot of a web page, streaming the raw image bytes. */
98 screenshot(options: BrowserScreenshotOptions): BrowserByteStream;
99 /** Generate a PDF of a web page, streaming the raw PDF bytes. */
100 pdf(options: BrowserPDFOptions): BrowserByteStream;
101 /** Get the HTML content of a web page. */
102 content(options: BrowserContentOptions): BrowserEffect<BrowserContentResult>;
103 /** Scrape elements from a web page by CSS selector. */
104 scrape(options: BrowserScrapeOptions): BrowserEffect<BrowserScrapeResult>;
105 /** Extract all links from a web page. */
106 links(options: BrowserLinksOptions): BrowserEffect<BrowserLinksResult>;
107 /** Get both the HTML content and a base64-encoded screenshot of a web page. */
108 snapshot(
109 options: BrowserSnapshotOptions,
110 ): BrowserEffect<BrowserSnapshotResult>;
111 /** Extract structured JSON data from a web page using AI. */
112 json(options: BrowserJsonOptions): BrowserEffect<BrowserJsonResult>;
113 /** Convert a web page to Markdown. */
114 markdown(
115 options: BrowserMarkdownOptions,
116 ): BrowserEffect<BrowserMarkdownResult>;
117}
118
119/**
120 * A Cloudflare Browser Rendering binding for launching headless browser sessions
121 * from Workers — a Worker-only binding with no backing cloud resource.
122 *
123 * `Browser` is a single value that is at once the `Binding.Service` tag, the
124 * callable that produces a {@link BrowserBinding}, and the type. Declare it on a
125 * Worker's `env` (it flows through `InferEnv` → `cf.BrowserRun`) or `yield*` it
126 * inside an Effect-native Worker to attach the binding and obtain the
127 * {@link BrowserClient}.
128 *
129 * ### Effect-style Worker (recommended)
130 * **Example:** Bind the runtime client and convert a page to Markdown
131 * ```typescript
132 * import * as Effect from "effect/Effect";
133 *
134 * Cloudflare.Worker(
135 * "BrowserWorker",
136 * { main: import.meta.url },
137 * Effect.gen(function* () {
138 * const browser = yield* Cloudflare.Browser("BROWSER");
139 *
140 * return {
141 * fetch: Effect.gen(function* () {
142 * return yield* browser.markdown({ url: "https://example.com" });
143 * }),
144 * };
145 * }).pipe(Effect.provide(Cloudflare.Workers.BrowserBinding)),
146 * );
147 * ```
148 *
149 * ### Worker binding metadata
150 * **Example:** Declare the binding on `env`
151 * ```typescript
152 * export const Worker = Cloudflare.Worker("Worker", {
153 * main: "./src/worker.ts",
154 * env: { BROWSER: Cloudflare.Browser() },
155 * });
156 *
157 * export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
158 * // { BROWSER: BrowserRun }
159 * ```
160 *
161 * ### Local development
162 * **Example:** Proxy to the real Browser Rendering service in dev
163 * ```typescript
164 * // Default: a real headless Chrome is launched locally and driven over
165 * // CDP under `alchemy dev`. Alchemy.remote() opts the binding into the
166 * // real Browser Rendering service instead — in an Effect-native Worker:
167 * const browser = yield* Cloudflare.Browser("BROWSER").pipe(Alchemy.remote());
168 *
169 * // or declared on an async Worker's env:
170 * env: { BROWSER: Cloudflare.Browser("BROWSER").pipe(Alchemy.remote()) }
171 * ```
172 *
173 * @see https://developers.cloudflare.com/browser-rendering/workers-binding-api/
174 *
175 * @binding
176 * @product Browser Rendering
177 * @category Developer Platform
178 */
179export interface Browser extends Binding.Service<
180 Browser,
181 TypeId,
182 BrowserClient
183> {
184 /**
185 * @param name Binding name (logical id) — the `env` key it resolves to.
186 * @default "BROWSER"
187 */
188 (name?: string): BrowserBinding;
189}
190
191export const Browser = Binding.Service<Browser>({
192 id: TypeId,
193 defaultName: "BROWSER",
194 toWorkerBinding: (binding) => ({
195 type: "browser",
196 name: binding.name,
197 }),
198});
199
200export const isBrowser = (value: unknown): value is BrowserBinding =>
201 Binding.isBinding(value) && value.kind === TypeId;