1import type * as cf from "@cloudflare/workers-types";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import * as Binding from "../Workers/Binding.ts";
5import { makeBindingLayer } from "../Workers/BindingLayer.ts";
6import {
7 Images,
8 ImagesError,
9 type ImagesClient,
10 type ImageTransformationResult,
11 type ImageTransformer,
12} from "./Images.ts";
13
14/** The binding value produced by calling {@link Images} (declared on `env` or `yield*`-ed). */
15export type ImagesBinding = Binding.Binding<
16 Images["key"],
17 ImagesClient,
18 Images
19>;
20
21/**
22 * The layer that provides the Effect-native interface for the Cloudflare
23 * Workers Images binding.
24 *
25 * Provide it on the Worker effect (`Effect.provide(Cloudflare.Images.ImagesBinding)`)
26 * so that yielding an {@link Images} binding attaches the native `images`
27 * binding to the surrounding Worker at deploy time and, at runtime, resolves to
28 * the Effect-native {@link ImagesClient} (wrapping the raw `cf.ImagesBinding` so every
29 * `info` / `input(...).transform(...).output(...)` call returns an `Effect`).
30 */
31export const ImagesBinding = makeBindingLayer<
32 Images,
33 cf.ImagesBinding,
34 ImagesClient
35>(
36 Images,
37 (raw) =>
38 ({
39 raw,
40 info: (stream, options) =>
41 Effect.gen(function* () {
42 const binding = yield* raw;
43 const readable = yield* toCfReadable(stream);
44 return yield* tryPromise(() => binding.info(readable, options));
45 }),
46 input: (stream, options) =>
47 Effect.gen(function* () {
48 const binding = yield* raw;
49 const readable = yield* toCfReadable(stream);
50 return wrapTransformer(binding.input(readable, options));
51 }),
52 }) satisfies ImagesClient,
53);
54
55/**
56 * Wrap a runtime `ImageTransformer` as the Effect-native chainable client.
57 * `transform`/`draw` stay pure; `output` crosses into the runtime.
58 */
59const wrapTransformer = (raw: cf.ImageTransformer): ImageTransformer => ({
60 raw,
61 transform: (transform) => wrapTransformer(raw.transform(transform)),
62 draw: <E, R>(
63 image: Stream.Stream<Uint8Array, E, R> | ImageTransformer,
64 options?: cf.ImageDrawOptions,
65 ): Effect.Effect<ImageTransformer, never, R> => {
66 if (isTransformerClient(image)) {
67 return Effect.succeed(wrapTransformer(raw.draw(image.raw, options)));
68 }
69 return toCfReadable(image).pipe(
70 Effect.map((readable) => wrapTransformer(raw.draw(readable, options))),
71 );
72 },
73 output: (options) =>
74 tryPromise(() => raw.output(options)).pipe(Effect.map(wrapResult)),
75});
76
77/** Wrap a runtime `ImageTransformationResult` as the Effect-native result client. */
78const wrapResult = (
79 raw: cf.ImageTransformationResult,
80): ImageTransformationResult => ({
81 raw,
82 response: Effect.sync(() => raw.response()),
83 contentType: Effect.sync(() => raw.contentType()),
84 image: (options) => Effect.sync(() => raw.image(options)),
85});
86
87/**
88 * Convert an Effect `Stream<Uint8Array>` into the `cf.ReadableStream<Uint8Array>`
89 * shape the Images runtime binding expects (identical at runtime).
90 */
91const toCfReadable = <E, R>(
92 stream: Stream.Stream<Uint8Array, E, R>,
93): Effect.Effect<cf.ReadableStream<Uint8Array>, never, R> =>
94 Stream.toReadableStreamEffect(stream).pipe(
95 Effect.map((s) => s as unknown as cf.ReadableStream<Uint8Array>),
96 );
97
98const tryPromise = <T>(fn: () => Promise<T>): Effect.Effect<T, ImagesError> =>
99 Effect.tryPromise({
100 try: fn,
101 catch: (error: any) =>
102 new ImagesError({
103 message: error?.message ?? "Unknown error",
104 code: typeof error?.code === "number" ? error.code : undefined,
105 cause: error,
106 }),
107 });
108
109const isTransformerClient = (image: unknown): image is ImageTransformer =>
110 typeof image === "object" && image !== null && "raw" in image;