1import { loadInternalWorker } from "../internal/internal-worker.ts";
2import * as Cause from "effect/Cause";
3import * as Effect from "effect/Effect";
4import * as Fiber from "effect/Fiber";
5import * as Hash from "effect/Hash";
6import * as Layer from "effect/Layer";
7import * as HttpServerRequest from "effect/http/HttpServerRequest";
8import * as HttpServerResponse from "effect/http/HttpServerResponse";
9const ClientWorker = {
10 worker: () =>
11 loadInternalWorker(
12 "#cloudflare-runtime-core-worker/remote-bindings/workers/client.worker",
13 ),
14};
15const OutboundWorker = {
16 worker: () =>
17 loadInternalWorker(
18 "#cloudflare-runtime-core-worker/remote-bindings/workers/outbound.worker",
19 ),
20};
21import * as Loopback from "../globals/Loopback.ts";
22import { DEFAULT_COMPATIBILITY_DATE } from "../internal/constants.ts";
23import { formatInternalWorkerModules } from "../internal/internal-modules.ts";
24import * as Plugin from "../Plugin.ts";
25import * as PluginContext from "../PluginContext.ts";
26import type {
27 ApiError,
28 ConfigError,
29 SystemError,
30} from "../RuntimeError.shared.ts";
31import * as WorkerdConfig from "../workerd/Config.ts";
32import * as RemoteWorker from "./RemoteWorker.ts";
33import type {
34 RemoteBinding,
35 RemoteWorkerConfig,
36 RemoteWorkerResult,
37} from "./RemoteWorkerConfig.shared.ts";
38
39export class RemoteBindings extends Plugin.Service<
40 RemoteBindings,
41 {
42 readonly register: (
43 binding: RemoteBinding,
44 ) => Effect.Effect<WorkerdConfig.ServiceDesignator>;
45 }
46>()("cloudflare-runtime/plugin/RemoteBindings") {}
47
48export type { RemoteBinding };
49
50export const RemoteBindingsLive = Layer.effect(
51 RemoteBindings,
52 Effect.gen(function* () {
53 const loopback = yield* Loopback.Loopback;
54 const remoteWorker = yield* RemoteWorker.RemoteWorker;
55 const prefetches = new Map<
56 number,
57 Fiber.Fiber<RemoteWorkerResult, ApiError | ConfigError | SystemError>
58 >();
59 if (Effect.isEffect(loopback)) {
60 return yield* Effect.die("Expected loopback to be initialized");
61 }
62 const serviceDesignator = yield* loopback.api.route(
63 "remote-bindings",
64 Effect.gen(function* () {
65 const request = yield* HttpServerRequest.HttpServerRequest;
66 const json = (yield* request.json) as unknown as RemoteWorkerConfig;
67 const hash = Hash.structure(json);
68 const prefetched = prefetches.get(hash);
69 if (prefetched) {
70 prefetches.delete(hash);
71 }
72 const deploy = prefetched
73 ? Fiber.join(prefetched)
74 : remoteWorker.deploy(json);
75 return yield* deploy.pipe(
76 Effect.flatMap((result) =>
77 HttpServerResponse.json({ ok: true, result }),
78 ),
79 Effect.tapCause((cause) => Effect.logError(Cause.pretty(cause))),
80 Effect.catch((error) =>
81 HttpServerResponse.json({ ok: false, error }, { status: 500 }),
82 ),
83 );
84 }),
85 );
86 const build = Effect.fn(function* (
87 config: RemoteWorkerConfig,
88 ): Effect.fn.Return<Plugin.PluginConfig> {
89 if (config.bindings.length === 0) return {};
90 const fiber = yield* Effect.forkDetach(remoteWorker.deploy(config));
91 prefetches.set(Hash.structure(config), fiber);
92 const [outboundWorker, clientWorker] = yield* Effect.forEach(
93 [OutboundWorker, ClientWorker],
94 (worker) =>
95 Effect.map(
96 Effect.promise(worker.worker),
97 formatInternalWorkerModules,
98 ),
99 { concurrency: "unbounded" },
100 );
101 const outbound = {
102 name: "remote-bindings:outbound",
103 worker: {
104 compatibilityDate: DEFAULT_COMPATIBILITY_DATE,
105 modules: outboundWorker,
106 bindings: [
107 {
108 name: "PROXY",
109 durableObjectNamespace: { className: "RemoteBindingProxy" },
110 },
111 {
112 name: "LOOPBACK",
113 service: serviceDesignator,
114 },
115 {
116 name: "OPTIONS",
117 json: JSON.stringify(config),
118 },
119 ],
120 durableObjectNamespaces: [
121 {
122 className: "RemoteBindingProxy",
123 enableSql: true,
124 preventEviction: true,
125 ephemeralLocal: WorkerdConfig.kVoid,
126 },
127 ],
128 },
129 } satisfies WorkerdConfig.Service;
130 const client = {
131 name: "remote-bindings:client",
132 worker: {
133 compatibilityDate: DEFAULT_COMPATIBILITY_DATE,
134 modules: clientWorker,
135 globalOutbound: { name: outbound.name },
136 },
137 } satisfies WorkerdConfig.Service;
138 return {
139 services: [client, outbound],
140 };
141 });
142 return RemoteBindings.of(
143 Effect.gen(function* () {
144 const { worker } = yield* PluginContext.PluginContext;
145 const bindings: Array<RemoteBinding> = [];
146 const config: RemoteWorkerConfig = {
147 name: worker.name,
148 bindings,
149 };
150 return {
151 defer: Effect.suspend(() => build(config)),
152 api: {
153 register: (binding) =>
154 Effect.sync(() => {
155 bindings.push(binding);
156 return {
157 name: "remote-bindings:client",
158 props: {
159 json: JSON.stringify({ binding: binding.name }),
160 },
161 };
162 }),
163 },
164 };
165 }),
166 );
167 }),
168);
169
170export const makeRemoteBinding = (
171 binding: RemoteBinding,
172 f: (service: WorkerdConfig.ServiceDesignator) => WorkerdConfig.Worker_Binding,
173): PluginContext.BindingHook<RemoteBindings> =>
174 Effect.map(
175 Plugin.use(RemoteBindings, (remoteBindings) =>
176 remoteBindings.api.register(binding),
177 ),
178 f,
179 );