| 1 | import type * as cf from "@cloudflare/workers-types"; |
| 2 | import * as Data from "effect/Data"; |
| 3 | import type * as Effect from "effect/Effect"; |
| 4 | import type { RuntimeContext } from "../../RuntimeContext.ts"; |
| 5 | import * as Binding from "./Binding.ts"; |
| 6 | import type { RateLimitBinding } from "./RateLimitBinding.ts"; |
| 7 | |
| 8 | const TypeId = "Cloudflare.RateLimit" as const; |
| 9 | type TypeId = typeof TypeId; |
| 10 | |
| 11 | export type RateLimitPeriod = 10 | 60; |
| 12 | |
| 13 | export class RateLimitError extends Data.TaggedError("RateLimitError")<{ |
| 14 | message: string; |
| 15 | cause: unknown; |
| 16 | }> {} |
| 17 | |
| 18 | /** |
| 19 | * Effect-native client for a Cloudflare Rate Limit binding. Wraps the runtime |
| 20 | * `cf.RateLimit` so `limit` returns an `Effect` tagged with {@link RateLimitError}. |
| 21 | */ |
| 22 | export interface RateLimitClient { |
| 23 | /** Effect resolving to the raw Cloudflare runtime binding. */ |
| 24 | raw: Effect.Effect<cf.RateLimit, never, RuntimeContext>; |
| 25 | limit( |
| 26 | options: Parameters<cf.RateLimit["limit"]>[0], |
| 27 | ): Effect.Effect< |
| 28 | Awaited<ReturnType<cf.RateLimit["limit"]>>, |
| 29 | RateLimitError, |
| 30 | RuntimeContext |
| 31 | >; |
| 32 | } |
| 33 | |
| 34 | export type RateLimitProps = { |
| 35 | /** |
| 36 | * Positive integer or string that uniquely identifies this rate limit |
| 37 | * configuration. |
| 38 | */ |
| 39 | namespaceId: number | string; |
| 40 | /** Simple rate limiting configuration. */ |
| 41 | simple: { |
| 42 | /** The number of requests allowed within the period. */ |
| 43 | limit: number; |
| 44 | /** The period, in seconds, over which requests are counted. */ |
| 45 | period: RateLimitPeriod; |
| 46 | }; |
| 47 | }; |
| 48 | |
| 49 | /** |
| 50 | * A Cloudflare Rate Limit binding for counting arbitrary keys inside Workers — a |
| 51 | * Worker-only binding with no backing cloud resource. |
| 52 | * |
| 53 | * `RateLimit` is a single value that is at once the `Binding.Service` tag, the |
| 54 | * callable that produces a {@link RateLimitBinding}, and the type. Declare it on |
| 55 | * a Worker's `env` (it flows through `InferEnv` → the native `cf.RateLimit`) or |
| 56 | * `yield*` it inside an Effect-native Worker to attach the binding and obtain |
| 57 | * the {@link RateLimitClient}. |
| 58 | * |
| 59 | * ### Declaring on a Worker's env |
| 60 | * **Example:** Async (non-Effect) Worker |
| 61 | * ```typescript |
| 62 | * export const Worker = Cloudflare.Worker("Worker", { |
| 63 | * main: "./src/worker.ts", |
| 64 | * env: { |
| 65 | * THROTTLE: Cloudflare.RateLimit("THROTTLE", { |
| 66 | * namespaceId: 1001, |
| 67 | * simple: { limit: 10, period: 60 }, |
| 68 | * }), |
| 69 | * }, |
| 70 | * }); |
| 71 | * |
| 72 | * export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>; |
| 73 | * // { THROTTLE: RateLimit } — the native Cloudflare binding |
| 74 | * |
| 75 | * // worker.ts |
| 76 | * export default { |
| 77 | * fetch: async (req: Request, env: WorkerEnv) => { |
| 78 | * const { success } = await env.THROTTLE.limit({ key: "ip" }); |
| 79 | * return new Response(success ? "ok" : "rate limited"); |
| 80 | * }, |
| 81 | * }; |
| 82 | * ``` |
| 83 | * |
| 84 | * ### Binding inside an Effect-native Worker |
| 85 | * **Example:** yield* RateLimit does the binding |
| 86 | * ```typescript |
| 87 | * Cloudflare.Worker("Worker", { main: "./src/worker.ts" }, |
| 88 | * Effect.gen(function* () { |
| 89 | * // Attaches the binding to this Worker AND returns the runtime client. |
| 90 | * const throttle = yield* Cloudflare.RateLimit("THROTTLE", { |
| 91 | * namespaceId: 1001, |
| 92 | * simple: { limit: 10, period: 60 }, |
| 93 | * }); |
| 94 | * |
| 95 | * return { |
| 96 | * fetch: Effect.gen(function* () { |
| 97 | * const { success } = yield* throttle.limit({ key: "ip" }); |
| 98 | * return HttpServerResponse.text(success ? "ok" : "rate limited"); |
| 99 | * }), |
| 100 | * }; |
| 101 | * }).pipe(Effect.provide(Cloudflare.Workers.RateLimitBinding)), |
| 102 | * ); |
| 103 | * ``` |
| 104 | * |
| 105 | * @see https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/ |
| 106 | * |
| 107 | * @binding |
| 108 | * @product Rate Limiting |
| 109 | * @category Application Security |
| 110 | */ |
| 111 | export interface RateLimit extends Binding.Service< |
| 112 | RateLimit, |
| 113 | TypeId, |
| 114 | RateLimitClient |
| 115 | > { |
| 116 | /** |
| 117 | * @param name Binding name (logical id) — the `env` key it resolves to. |
| 118 | * @param props Rate limit namespace + simple config. |
| 119 | */ |
| 120 | (name: string, props: RateLimitProps): RateLimitBinding; |
| 121 | } |
| 122 | |
| 123 | export const RateLimit = Binding.Service< |
| 124 | RateLimit, |
| 125 | { namespaceId: string; simple: { limit: number; period: RateLimitPeriod } } |
| 126 | >({ |
| 127 | id: TypeId, |
| 128 | defaultName: "RATE_LIMIT", |
| 129 | parse: (name: string, props: RateLimitProps) => ({ |
| 130 | name, |
| 131 | namespaceId: String(props.namespaceId), |
| 132 | simple: props.simple, |
| 133 | }), |
| 134 | toWorkerBinding: (binding) => ({ |
| 135 | type: "ratelimit", |
| 136 | name: binding.name, |
| 137 | namespaceId: binding.namespaceId, |
| 138 | simple: binding.simple, |
| 139 | }), |
| 140 | }); |
| 141 | |
| 142 | export const isRateLimit = (value: unknown): value is RateLimitBinding => |
| 143 | Binding.isBinding(value) && value.kind === TypeId; |