diff --git a/apps/content/docs/helpers/ratelimit.md b/apps/content/docs/helpers/ratelimit.md index 0257b0675..40deb9d88 100644 --- a/apps/content/docs/helpers/ratelimit.md +++ b/apps/content/docs/helpers/ratelimit.md @@ -72,7 +72,7 @@ const limiter = new RedisRatelimiter({ You can use any Redis client that supports Lua script evaluation by providing an `eval` function. ::: -### Upstash Adapter +### Upstash Ratelimit Adapter Adapter for [@upstash/ratelimit](https://www.npmjs.com/package/@upstash/ratelimit), optimized for serverless environments like Vercel Edge and Cloudflare Workers. @@ -103,6 +103,22 @@ const limiter = new UpstashRatelimiter(ratelimit, { ::: +### Cloudflare Ratelimit Adapter + +Adapter for [Cloudflare Workers Ratelimit](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/). + +```ts +import { CloudflareRatelimiter } from '@orpc/experimental-ratelimit/cloudflare-ratelimit' + +export default { + async fetch(request, env) { + const limiter = new CloudflareRatelimiter(env.MY_RATE_LIMITER) + + return new Response(`Hello World!`) + } +} +``` + ## Blocking Mode Some adapters support blocking mode, which waits for the rate limit to reset instead of immediately rejecting requests. diff --git a/packages/ratelimit/package.json b/packages/ratelimit/package.json index e53ecfe03..63d478fa2 100644 --- a/packages/ratelimit/package.json +++ b/packages/ratelimit/package.json @@ -34,6 +34,11 @@ "types": "./dist/adapters/upstash-ratelimit.d.mts", "import": "./dist/adapters/upstash-ratelimit.mjs", "default": "./dist/adapters/upstash-ratelimit.mjs" + }, + "./cloudflare-ratelimit": { + "types": "./dist/adapters/cloudflare-ratelimit.d.mts", + "import": "./dist/adapters/cloudflare-ratelimit.mjs", + "default": "./dist/adapters/cloudflare-ratelimit.mjs" } } }, @@ -41,7 +46,8 @@ ".": "./src/index.ts", "./memory": "./src/adapters/memory.ts", "./redis": "./src/adapters/redis.ts", - "./upstash-ratelimit": "./src/adapters/upstash-ratelimit.ts" + "./upstash-ratelimit": "./src/adapters/upstash-ratelimit.ts", + "./cloudflare-ratelimit": "./src/adapters/cloudflare-ratelimit.ts" }, "files": [ "dist" diff --git a/packages/ratelimit/src/adapters/cloudflare-ratelimit.test.ts b/packages/ratelimit/src/adapters/cloudflare-ratelimit.test.ts new file mode 100644 index 000000000..3d99781dd --- /dev/null +++ b/packages/ratelimit/src/adapters/cloudflare-ratelimit.test.ts @@ -0,0 +1,17 @@ +import { CloudflareRatelimiter } from './cloudflare-ratelimit' + +it('cloudflareRatelimiter', async () => { + const ratelimit = { + limit: vi.fn(() => Promise.resolve({ success: true })), + } + + const ratelimiter = new CloudflareRatelimiter(ratelimit) + + const result = ratelimiter.limit('test-key') + + expect(ratelimit.limit).toHaveBeenCalledTimes(1) + expect(ratelimit.limit).toHaveBeenCalledWith({ key: 'test-key' }) + expect(result).toBe(ratelimit.limit.mock.results[0]!.value) + + expect(await result).toEqual({ success: true }) +}) diff --git a/packages/ratelimit/src/adapters/cloudflare-ratelimit.ts b/packages/ratelimit/src/adapters/cloudflare-ratelimit.ts new file mode 100644 index 000000000..a94fa368f --- /dev/null +++ b/packages/ratelimit/src/adapters/cloudflare-ratelimit.ts @@ -0,0 +1,13 @@ +import type { Ratelimiter, RatelimiterLimitResult } from '../types' + +export class CloudflareRatelimiter implements Ratelimiter { + constructor( + private readonly ratelimit: { + limit: (options: { key: string }) => Promise + }, + ) {} + + limit(key: string): Promise { + return this.ratelimit.limit({ key }) + } +}