1import type * as cf from "@cloudflare/workers-types";
2import * as Effect from "effect/Effect";
3import * as Namespace from "../../Namespace.ts";
4import type { RuntimeContext } from "../../RuntimeContext.ts";
5import {
6 Worker,
7 WorkerExecutionContext,
8 type CachePurgeError,
9} from "./Worker.ts";
10
11export interface CacheOptions {
12 /**
13 * Whether Workers Cache checks the cache before invoking the Worker.
14 * @default true
15 */
16 enabled?: boolean;
17 /**
18 * Share cached responses across Worker versions. By default the cache is
19 * scoped to a single version, so every deploy starts cold.
20 * @default false
21 */
22 crossVersionCache?: boolean;
23}
24
25/**
26 * Runtime client for Workers Cache, returned by `yield* Cloudflare.cache()`.
27 */
28export interface CacheClient {
29 /**
30 * Purge cached responses by `Cache-Tag`, path prefix, or everything.
31 */
32 purge(
33 options: cf.CachePurgeOptions,
34 ): Effect.Effect<cf.CachePurgeResult, CachePurgeError, RuntimeContext>;
35}
36
37/**
38 * Enable [Workers Cache](https://blog.cloudflare.com/workers-cache/) on the
39 * surrounding Worker and get the runtime cache client.
40 *
41 * Yielding `cache()` in an Effect-native Worker's init phase turns the cache
42 * on at deploy time (the equivalent of setting `cache: { enabled: true }` on
43 * the Worker's props) and returns a client whose `purge` drives the runtime
44 * `ctx.cache` API from your handlers.
45 *
46 * What gets cached is controlled by standard response headers —
47 * `Cache-Control` (including `stale-while-revalidate`), `Cache-Tag` for
48 * tag-based purging, and `Vary` for content negotiation.
49 *
50 * For async (non-Effect) Workers, set the `cache` prop on the Worker
51 * instead.
52 *
53 * **Example:** Example
54 * ```typescript
55 * Effect.gen(function* () {
56 * // init: enable Workers Cache on this Worker
57 * const { purge } = yield* Cloudflare.cache();
58 *
59 * return {
60 * fetch: Effect.gen(function* () {
61 * const request = yield* HttpServerRequest;
62 * if (request.url.startsWith("/invalidate")) {
63 * yield* purge({ tags: ["products"] });
64 * return HttpServerResponse.text("purged");
65 * }
66 * return HttpServerResponse.text("hello", {
67 * headers: {
68 * "Cache-Control": "public, max-age=300",
69 * "Cache-Tag": "products",
70 * },
71 * });
72 * }),
73 * };
74 * })
75 * ```
76 *
77 * @binding
78 * @product Workers
79 * @category Workers & Compute
80 */
81export const cache = (
82 options?: CacheOptions,
83): Effect.Effect<CacheClient, never, Worker | WorkerExecutionContext> =>
84 Effect.gen(function* () {
85 const host = yield* Worker;
86 const exec = yield* WorkerExecutionContext;
87 if (!globalThis.__ALCHEMY_RUNTIME__) {
88 yield* Namespace.push(
89 host.LogicalId,
90 host.bind("Cache", {
91 cache: {
92 enabled: options?.enabled ?? true,
93 crossVersionCache: options?.crossVersionCache,
94 },
95 }),
96 );
97 }
98 return {
99 // `exec` is the init-phase deferred context; purge resolves the live
100 // per-event context from the calling handler's fiber.
101 purge: (purgeOptions) => exec.cache.purge(purgeOptions),
102 };
103 });