1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import { DEFAULT_COMPATIBILITY_DATE } from "../internal/constants.ts";
4import * as Plugin from "../Plugin.ts";
5import type * as WorkerdConfig from "../workerd/Config.ts";
6import { HEADER_CF_BLOB } from "./CfOptions.shared.ts";
7import * as LoopbackServer from "./LoopbackServer.ts";
8
9export class Loopback extends Plugin.Service<
10 Loopback,
11 {
12 readonly route: (
13 target: string,
14 handler: LoopbackServer.RouteHandler,
15 ) => Effect.Effect<WorkerdConfig.ServiceDesignator>;
16 }
17>()("cloudflare-runtime/plugin/Loopback") {}
18
19export const LoopbackLive = Layer.effect(
20 Loopback,
21 Effect.gen(function* () {
22 const loopbackServer = yield* LoopbackServer.LoopbackServer;
23
24 return Loopback.of({
25 services: [
26 {
27 name: "loopback:server",
28 external: {
29 address: loopbackServer.address,
30 // Serialise `request.cf` into a header so it round-trips to
31 // Node-side handlers (as Miniflare does).
32 http: { cfBlobHeader: HEADER_CF_BLOB },
33 },
34 },
35 {
36 name: "loopback:fetcher",
37 worker: {
38 compatibilityDate: DEFAULT_COMPATIBILITY_DATE,
39 modules: [
40 {
41 name: "loopback/fetcher.worker.js",
42 esModule: `
43 export default {
44 async fetch(request, env, ctx) {
45 const headers = new Headers(request.headers);
46 headers.set("loopback-target", ctx.props.target);
47 headers.set("loopback-secret", env.SECRET);
48 return await env.LOOPBACK.fetch(request, { headers });
49 }
50 }
51 `,
52 },
53 ],
54 bindings: [
55 {
56 name: "LOOPBACK",
57 service: { name: "loopback:server" },
58 },
59 {
60 name: "SECRET",
61 text: loopbackServer.secret,
62 },
63 ],
64 },
65 },
66 ],
67 api: {
68 route: (target, handler) =>
69 loopbackServer.route(target, handler).pipe(
70 Effect.map(() => ({
71 name: "loopback:fetcher",
72 props: { json: JSON.stringify({ target }) },
73 })),
74 ),
75 },
76 });
77 }),
78);