1import type * as cf from "@cloudflare/workers-types";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Stream from "effect/Stream";
5import type { RuntimeContext } from "../../RuntimeContext.ts";
6import * as Binding from "../Workers/Binding.ts";
7import type { ImagesBinding } from "./ImagesBinding.ts";
8
9const TypeId = "Cloudflare.Images.Images" as const;
10type TypeId = typeof TypeId;
11
12export class ImagesError extends Data.TaggedError("ImagesError")<{
13 message: string;
14 code?: number;
15 cause: unknown;
16}> {}
17
18/**
19 * A Cloudflare Images binding for image transformation inside Workers — a
20 * Worker-only binding with no backing cloud resource.
21 *
22 * `Images` is a single value that is at once the `Binding.Service` tag, the
23 * callable that produces an {@link ImagesBinding}, and the type. Declare it on a
24 * Worker's `env` (it flows through `InferEnv` → `cf.ImagesBinding`) or `yield*`
25 * it inside an Effect-native Worker to attach the binding and obtain the
26 * {@link ImagesClient}.
27 *
28 * ### Effect-style Worker (recommended)
29 * **Example:** Read image format and dimensions from the request body
30 * ```typescript
31 * import { HttpServerRequest } from "effect/http/HttpServerRequest";
32 *
33 * Cloudflare.Worker("ImageWorker", { main: import.meta.url },
34 * Effect.gen(function* () {
35 * const images = yield* Cloudflare.Images.Images("PIPELINE");
36 * return {
37 * fetch: Effect.gen(function* () {
38 * const request = yield* HttpServerRequest;
39 * const info = yield* images.info(request.stream);
40 * return yield* HttpServerResponse.json(info);
41 * }),
42 * };
43 * }).pipe(Effect.provide(Cloudflare.Images.ImagesBinding)),
44 * );
45 * ```
46 *
47 * ### Binding to a Worker (declarative)
48 * **Example:** Example
49 * ```typescript
50 * export const Worker = Cloudflare.Worker("Worker", {
51 * main: "./src/worker.ts",
52 * env: { MEDIA: Cloudflare.Images.Images("PIPELINE") },
53 * });
54 *
55 * export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
56 * // { MEDIA: ImagesBinding }
57 * ```
58 *
59 * ### Local development
60 * **Example:** Proxy to the real Images service in dev
61 * ```typescript
62 * // Default: transforms run locally via Sharp under `alchemy dev` and
63 * // hosted images are stored on disk. Alchemy.remote() opts the binding
64 * // into the real Images service instead — in an Effect-native Worker:
65 * const images = yield* Cloudflare.Images.Images("IMAGES").pipe(Alchemy.remote());
66 *
67 * // or declared on an async Worker's env:
68 * env: { IMAGES: Cloudflare.Images.Images("IMAGES").pipe(Alchemy.remote()) }
69 * ```
70 *
71 * @see https://developers.cloudflare.com/images/transform-images/bindings/
72 *
73 * @binding
74 * @product Images
75 * @category Media
76 */
77export interface Images extends Binding.Service<Images, TypeId, ImagesClient> {
78 /**
79 * @param name Binding name (logical id) — the `env` key it resolves to.
80 * @default "IMAGES"
81 */
82 (name?: string): ImagesBinding;
83}
84
85export const Images = Binding.Service<Images>({
86 id: TypeId,
87 defaultName: "IMAGES",
88 toWorkerBinding: (binding) => ({
89 type: "images",
90 name: binding.name,
91 }),
92});
93
94export const isImages = (value: unknown): value is ImagesBinding =>
95 Binding.isBinding(value) && value.kind === TypeId;
96
97/**
98 * Effect-native client for a Cloudflare Images binding. Wraps the runtime
99 * {@link cf.ImagesBinding} so each method returns an Effect tagged with
100 * {@link ImagesError}.
101 */
102export interface ImagesClient {
103 /** Effect resolving to the raw Cloudflare runtime binding. */
104 raw: Effect.Effect<cf.ImagesBinding, never, RuntimeContext>;
105 /**
106 * Read image format and dimensions from a stream of bytes. Fails with
107 * {@link ImagesError} (code 9412) if the input is not a recognized image.
108 */
109 info<E = never, R = never>(
110 stream: Stream.Stream<Uint8Array, E, R>,
111 options?: cf.ImageInputOptions,
112 ): Effect.Effect<cf.ImageInfoResponse, ImagesError, RuntimeContext | R>;
113 /**
114 * Begin a transformation pipeline. Subsequent `.transform()` / `.draw()`
115 * calls are pure; `.output(opts)` runs the pipeline.
116 */
117 input<E = never, R = never>(
118 stream: Stream.Stream<Uint8Array, E, R>,
119 options?: cf.ImageInputOptions,
120 ): Effect.Effect<ImageTransformer, never, RuntimeContext | R>;
121}
122
123/**
124 * Effect-native handle to the result of `input(...).output(...)`. Mirrors the
125 * runtime `ImageTransformationResult` but exposes side effects as sync effects.
126 */
127export interface ImageTransformationResult {
128 raw: cf.ImageTransformationResult;
129 response: Effect.Effect<cf.Response>;
130 contentType: Effect.Effect<string>;
131 image(
132 options?: cf.ImageTransformationOutputOptions,
133 ): Effect.Effect<cf.ReadableStream<Uint8Array>>;
134}
135
136/**
137 * Effect-native chainable transformer. `transform`/`draw` are pure; `output` is
138 * the only step that crosses into Cloudflare's runtime and returns an Effect.
139 */
140export interface ImageTransformer {
141 raw: cf.ImageTransformer;
142 transform(transform: cf.ImageTransform): ImageTransformer;
143 draw<E = never, R = never>(
144 image: Stream.Stream<Uint8Array, E, R> | ImageTransformer,
145 options?: cf.ImageDrawOptions,
146 ): Effect.Effect<ImageTransformer, never, R>;
147 output(
148 options: cf.ImageOutputOptions,
149 ): Effect.Effect<ImageTransformationResult, ImagesError>;
150}