| 1 | import type * as cf from "@cloudflare/workers-types"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Namespace from "../../Namespace.ts"; |
| 4 | import type { RuntimeContext } from "../../RuntimeContext.ts"; |
| 5 | import { |
| 6 | Worker, |
| 7 | WorkerExecutionContext, |
| 8 | type CachePurgeError, |
| 9 | } from "./Worker.ts"; |
| 10 | |
| 11 | export 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 | */ |
| 28 | export 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 | */ |
| 81 | export 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 | }); |