| 1 | import type * as cf from "@cloudflare/workers-types"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Stream from "effect/Stream"; |
| 4 | import * as Binding from "../Workers/Binding.ts"; |
| 5 | import { makeBindingLayer } from "../Workers/BindingLayer.ts"; |
| 6 | import { |
| 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). */ |
| 15 | export 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 | */ |
| 31 | export 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 | */ |
| 59 | const 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. */ |
| 78 | const 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 | */ |
| 91 | const 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 | |
| 98 | const 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 | |
| 109 | const isTransformerClient = (image: unknown): image is ImageTransformer => |
| 110 | typeof image === "object" && image !== null && "raw" in image; |