1import type * as runtime from "@cloudflare/workers-types";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
5import * as Result from "effect/Result";
6import * as Stream from "effect/Stream";
7import * as HttpClientError from "effect/http/HttpClientError";
8import * as HttpClientRequest from "effect/http/HttpClientRequest";
9import * as HttpClientResponse from "effect/http/HttpClientResponse";
10import * as Url from "effect/http/Url";
11import * as Binding from "../../Binding.ts";
12import type { RuntimeContext } from "../../RuntimeContext.ts";
13import { isWorker, type Worker, WorkerEnvironment } from "./Worker.ts";
14
15/**
16 * @binding
17 * @product Workers
18 * @category Workers & Compute
19 */
20export interface Fetch extends Binding.Service<
21 Fetch,
22 "Cloudflare.Workers.Fetch",
23 (
24 worker: Worker,
25 ) => Effect.Effect<
26 (
27 request: HttpClientRequest.HttpClientRequest,
28 ) => Effect.Effect<
29 HttpClientResponse.HttpClientResponse,
30 HttpClientError.RequestError,
31 RuntimeContext
32 >
33 >
34> {}
35
36export const Fetch = Binding.Service<Fetch>("Cloudflare.Workers.Fetch");
37
38export const FetchBinding = Layer.effect(
39 Fetch,
40 Effect.gen(function* () {
41 const env = yield* WorkerEnvironment;
42
43 return Effect.fn(function* (worker: Worker) {
44 if (!globalThis.__ALCHEMY_RUNTIME__) {
45 // Deploy-time only: register the service binding for the *target*
46 // worker on the host Worker.
47 const host = yield* Binding.Host;
48 if (isWorker(host)) {
49 yield* host.bind`${worker}`({
50 bindings: [
51 {
52 type: "service",
53 name: worker.LogicalId,
54 service: worker.workerName,
55 },
56 ],
57 });
58 }
59 }
60 // Lazy — the `WorkerEnvironment` bindings are only populated at exec
61 // phase, so the fetcher must be resolved per call, not at bind time.
62 const fetcher = Effect.sync(
63 () => (env as Record<string, runtime.Fetcher>)[worker.LogicalId]!,
64 ) as Effect.Effect<runtime.Fetcher, never, RuntimeContext>;
65
66 return (request: HttpClientRequest.HttpClientRequest) =>
67 Effect.flatMap(fetcher, (f) => doFetch(f, request));
68 });
69 }),
70);
71
72const doFetch = (
73 fetcher: runtime.Fetcher,
74 request: HttpClientRequest.HttpClientRequest,
75): Effect.Effect<
76 HttpClientResponse.HttpClientResponse,
77 HttpClientError.RequestError
78> => {
79 const urlResult = Url.make(
80 request.url,
81 request.urlParams,
82 request.hash.pipe(Option.getOrUndefined),
83 );
84 if (Result.isFailure(urlResult)) {
85 return Effect.fail(
86 new HttpClientError.InvalidUrlError({
87 request,
88 cause: urlResult.failure,
89 description: "Failed to construct URL",
90 }),
91 );
92 }
93 const url = urlResult.success;
94
95 const send = (body: BodyInit | undefined) =>
96 Effect.mapError(
97 Effect.map(
98 Effect.tryPromise({
99 try: () =>
100 fetcher.fetch(
101 url.toString() as runtime.RequestInfo,
102 {
103 method: request.method,
104 headers: request.headers as unknown as runtime.HeadersInit,
105 body,
106 duplex: request.body._tag === "Stream" ? "half" : undefined,
107 } as runtime.RequestInit,
108 ) as unknown as Promise<Response>,
109 catch: (cause) => cause,
110 }),
111 (response) => HttpClientResponse.fromWeb(request, response),
112 ),
113 (cause) =>
114 new HttpClientError.TransportError({
115 request,
116 cause,
117 description: "Service binding fetch failed",
118 }),
119 );
120
121 switch (request.body._tag) {
122 case "Raw":
123 case "Uint8Array":
124 return send(request.body.body as BodyInit);
125 case "FormData":
126 return send(request.body.formData);
127 case "Stream":
128 return Effect.flatMap(
129 Effect.mapError(
130 Stream.toReadableStreamEffect(request.body.stream),
131 (cause) =>
132 new HttpClientError.EncodeError({
133 request,
134 cause,
135 description: "Failed to encode stream body",
136 }),
137 ),
138 send,
139 );
140 default:
141 return send(undefined);
142 }
143};