1import type * as cf from "@cloudflare/workers-types";
2import * as Context from "effect/Context";
3import * as Data from "effect/Data";
4import * as Effect from "effect/Effect";
5import type { RuntimeContext } from "../../RuntimeContext.ts";
6import {
7 resolveAccessContext,
8 type WorkerExecutionContextAccess,
9} from "./WorkerAccess.ts";
10
11export const WorkerTypeId = "Cloudflare.Worker";
12export type WorkerTypeId = typeof WorkerTypeId;
13
14export class WorkerEnvironment extends Context.Service<
15 WorkerEnvironment,
16 Record<string, any>
17>()("Cloudflare.Workers.WorkerEnvironment") {}
18
19export class CachePurgeError extends Data.TaggedError("CachePurgeError")<{
20 message: string;
21 cause?: unknown;
22}> {}
23
24/**
25 * Effect-native view of the Workers Cache runtime API on the execution
26 * context (`ctx.cache`). Only available when the Worker has Workers Cache
27 * enabled (the `cache` prop or `yield* Cloudflare.cache()`).
28 */
29export interface WorkerExecutionContextCache {
30 /**
31 * Purge cached responses by `Cache-Tag`, path prefix, or everything.
32 */
33 purge(
34 options: cf.CachePurgeOptions,
35 ): Effect.Effect<cf.CachePurgeResult, CachePurgeError, RuntimeContext>;
36}
37
38export class WorkerExecutionContext extends Context.Service<
39 WorkerExecutionContext,
40 {
41 /**
42 * Run an Effect in the background without blocking the response, keeping
43 * the Worker alive until it settles. The Effect runs with the caller's
44 * full context (services, tracing), and the resulting promise is
45 * registered with workerd's `ctx.waitUntil`.
46 */
47 waitUntil<A, E, R>(
48 effect: Effect.Effect<A, E, R>,
49 ): Effect.Effect<void, never, R | RuntimeContext>;
50 /**
51 * Forward the request to the origin if the Worker throws an unhandled
52 * exception, instead of returning an error page.
53 */
54 passThroughOnException(): Effect.Effect<void, never, RuntimeContext>;
55 /**
56 * The Workers Cache runtime API (`ctx.cache`).
57 */
58 readonly cache: WorkerExecutionContextCache;
59 /**
60 * The Cloudflare Access context for the current request (`ctx.access`),
61 * or `undefined` when the request did not pass through Access. Under
62 * `alchemy dev` the Worker's `dev.access` config simulates it.
63 */
64 readonly access: Effect.Effect<
65 WorkerExecutionContextAccess | undefined,
66 never,
67 RuntimeContext
68 >;
69 /**
70 * The raw workerd ExecutionContext, for interop with async APIs.
71 */
72 readonly raw: cf.ExecutionContext;
73 }
74>()("Cloudflare.Workers.WorkerExecutionContext") {}
75
76export const fromExecutionContext = (
77 ctx: cf.ExecutionContext,
78 env?: Record<string, unknown>,
79): WorkerExecutionContext["Service"] => ({
80 raw: ctx,
81 access: Effect.sync(() => resolveAccessContext(ctx, env)),
82 waitUntil: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
83 Effect.gen(function* () {
84 const context = yield* Effect.context<R>();
85 // Register the promise with workerd un-awaited — waitUntil extends the
86 // invocation's lifetime without blocking the response.
87 yield* Effect.sync(() =>
88 ctx.waitUntil(Effect.runPromise(effect.pipe(Effect.provide(context)))),
89 );
90 }),
91 passThroughOnException: () => Effect.sync(() => ctx.passThroughOnException()),
92 cache: {
93 purge: (options) =>
94 ctx.cache
95 ? Effect.tryPromise({
96 try: () => ctx.cache!.purge(options),
97 catch: (cause) =>
98 new CachePurgeError({
99 message:
100 cause instanceof Error
101 ? cause.message
102 : "Unknown cache purge error",
103 cause,
104 }),
105 })
106 : Effect.fail(
107 new CachePurgeError({
108 message:
109 "ctx.cache is not available — enable Workers Cache on this " +
110 "Worker (the `cache` prop or `yield* Cloudflare.cache()`) " +
111 "and note it is not supported in local dev.",
112 }),
113 ),
114 },
115});
116
117/**
118 * A {@link WorkerExecutionContext} whose methods resolve the live per-event
119 * context from the calling fiber at call time. Provided during the Worker's
120 * init phase (plan and runtime module init) so the service can be yielded
121 * and closed over in the top-level closure; every method is colored with
122 * `RuntimeContext`, so it can only be *run* inside a handler, where the
123 * bridge provides the real per-event context that these methods defer to.
124 */
125export const deferredExecutionContext: WorkerExecutionContext["Service"] = {
126 get raw(): cf.ExecutionContext {
127 throw new Error(
128 "WorkerExecutionContext.raw is only available inside a request handler",
129 );
130 },
131 waitUntil: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
132 liveExecutionContext.pipe(
133 Effect.flatMap((live) => live.waitUntil(effect)),
134 ) as Effect.Effect<void, never, R | RuntimeContext>,
135 passThroughOnException: () =>
136 liveExecutionContext.pipe(
137 Effect.flatMap((live) => live.passThroughOnException()),
138 ) as Effect.Effect<void, never, RuntimeContext>,
139 cache: {
140 purge: (options) =>
141 liveExecutionContext.pipe(
142 Effect.flatMap((live) => live.cache.purge(options)),
143 ) as Effect.Effect<cf.CachePurgeResult, CachePurgeError, RuntimeContext>,
144 },
145 // A getter so this module-level literal doesn't eagerly reference
146 // `liveExecutionContext` before its declaration below.
147 get access() {
148 return liveExecutionContext.pipe(
149 Effect.flatMap((live) => live.access),
150 ) as Effect.Effect<
151 WorkerExecutionContextAccess | undefined,
152 never,
153 RuntimeContext
154 >;
155 },
156};
157
158const liveExecutionContext = WorkerExecutionContext.pipe(
159 Effect.flatMap((live) =>
160 live === deferredExecutionContext
161 ? Effect.die(
162 new Error(
163 "WorkerExecutionContext can only be used inside a request handler",
164 ),
165 )
166 : Effect.succeed(live),
167 ),
168);
169
170export type WorkerEvent = Exclude<
171 {
172 [type in keyof cf.ExportedHandler]: {
173 kind: "Cloudflare.Workers.WorkerEvent";
174 type: type;
175 input: Parameters<Exclude<cf.ExportedHandler[type], undefined>>[0];
176 env: Parameters<Exclude<cf.ExportedHandler[type], undefined>>[1];
177 context: Parameters<Exclude<cf.ExportedHandler[type], undefined>>[2];
178 };
179 }[keyof cf.ExportedHandler],
180 undefined
181>;
182
183export const isWorkerEvent = (value: any): value is WorkerEvent =>
184 value?.kind === "Cloudflare.Workers.WorkerEvent";
185
186export const ExportedHandlerMethods = [
187 "fetch",
188 "tail",
189 "trace",
190 "tailStream",
191 "scheduled",
192 "test",
193 "email",
194 "queue",
195] as const satisfies (keyof cf.ExportedHandler)[];