1import type * as cf from "@cloudflare/workers-types";
2import * as Data from "effect/Data";
3import type * as Effect from "effect/Effect";
4import type { RuntimeContext } from "../../RuntimeContext.ts";
5import * as Binding from "./Binding.ts";
6import type { RateLimitBinding } from "./RateLimitBinding.ts";
7
8const TypeId = "Cloudflare.RateLimit" as const;
9type TypeId = typeof TypeId;
10
11export type RateLimitPeriod = 10 | 60;
12
13export 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 */
22export 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
34export 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 */
111export 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
123export 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
142export const isRateLimit = (value: unknown): value is RateLimitBinding =>
143 Binding.isBinding(value) && value.kind === TypeId;