|
1 |
| -import Redis, { RedisOptions } from "ioredis"; |
| 1 | +import { RedisClientOptions, createClient } from "redis"; |
2 | 2 | import { Ratelimit } from "@upstash/ratelimit";
|
3 |
| -import { LoaderFunction, LoaderFunctionArgs } from "@remix-run/server-runtime"; |
| 3 | +import { |
| 4 | + ActionFunction, |
| 5 | + ActionFunctionArgs, |
| 6 | + LoaderFunction, |
| 7 | + LoaderFunctionArgs, |
| 8 | +} from "@remix-run/server-runtime"; |
4 | 9 | import { env } from "~/env.server";
|
5 | 10 |
|
6 |
| -export function createRedisRateLimitClient(): ConstructorParameters<typeof Ratelimit>[0]["redis"] { |
7 |
| - const redis = new Redis({ |
8 |
| - port: env.REDIS_PORT, |
9 |
| - host: env.REDIS_HOST, |
10 |
| - username: env.REDIS_USERNAME, |
11 |
| - password: env.REDIS_PASSWORD, |
12 |
| - enableAutoPipelining: true, |
13 |
| - ...(env.REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }), |
14 |
| - }); |
| 11 | +function createRedisRateLimitClient( |
| 12 | + redisOptions: RedisClientOptions |
| 13 | +): ConstructorParameters<typeof Ratelimit>[0]["redis"] { |
| 14 | + const redis = createClient(redisOptions); |
15 | 15 |
|
16 | 16 | return {
|
17 | 17 | sadd: async <TData>(key: string, ...members: TData[]): Promise<number> => {
|
18 |
| - return redis.sadd(key, members as (string | Buffer | number)[]); |
| 18 | + return redis.sAdd(key as string, members as any); |
19 | 19 | },
|
20 | 20 | eval: <TArgs extends unknown[], TData = unknown>(
|
21 | 21 | ...args: [script: string, keys: string[], args: TArgs]
|
22 | 22 | ): Promise<TData> => {
|
23 |
| - return redis.eval(...args) as TData; |
| 23 | + return redis.eval(args[0], { |
| 24 | + keys: args[1], |
| 25 | + arguments: args[2] as string[], |
| 26 | + }) as Promise<TData>; |
24 | 27 | },
|
25 | 28 | };
|
26 | 29 | }
|
27 | 30 |
|
28 |
| -const ratelimitter = new Ratelimit({ |
29 |
| - redis: createRedisRateLimitClient(), |
30 |
| - limiter: Ratelimit.cachedFixedWindow(10, "10s"), |
31 |
| - ephemeralCache: new Map(), |
32 |
| - analytics: true, |
33 |
| -}); |
34 |
| - |
35 |
| -export const rateLimit = { |
36 |
| - loader: (args: LoaderFunctionArgs): ReturnType<LoaderFunction> => {}, |
| 31 | +type Options = { |
| 32 | + redis: RedisClientOptions; |
| 33 | + limiter: ConstructorParameters<typeof Ratelimit>[0]["limiter"]; |
37 | 34 | };
|
| 35 | + |
| 36 | +class RateLimitter { |
| 37 | + #rateLimitter: Ratelimit; |
| 38 | + |
| 39 | + constructor({ redis, limiter }: Options) { |
| 40 | + this.#rateLimitter = new Ratelimit({ |
| 41 | + redis: createRedisRateLimitClient(redis), |
| 42 | + limiter: limiter, |
| 43 | + ephemeralCache: new Map(), |
| 44 | + analytics: true, |
| 45 | + }); |
| 46 | + } |
| 47 | + |
| 48 | + async loader(key: string, fn: LoaderFunction): Promise<ReturnType<LoaderFunction>> { |
| 49 | + const { success, pending, limit, reset, remaining } = await this.#rateLimitter.limit( |
| 50 | + `ratelimit:${key}` |
| 51 | + ); |
| 52 | + |
| 53 | + if (success) { |
| 54 | + return fn; |
| 55 | + } |
| 56 | + |
| 57 | + const response = new Response("Rate limit exceeded", { status: 429 }); |
| 58 | + response.headers.set("X-RateLimit-Limit", limit.toString()); |
| 59 | + response.headers.set("X-RateLimit-Remaining", remaining.toString()); |
| 60 | + response.headers.set("X-RateLimit-Reset", reset.toString()); |
| 61 | + return response; |
| 62 | + } |
| 63 | + |
| 64 | + async action(key: string, fn: (args: ActionFunctionArgs) => Promise<ReturnType<ActionFunction>>) { |
| 65 | + const { success, pending, limit, reset, remaining } = await this.#rateLimitter.limit( |
| 66 | + `ratelimit:${key}` |
| 67 | + ); |
| 68 | + |
| 69 | + if (success) { |
| 70 | + return fn; |
| 71 | + } |
| 72 | + |
| 73 | + const response = new Response("Rate limit exceeded", { status: 429 }); |
| 74 | + response.headers.set("X-RateLimit-Limit", limit.toString()); |
| 75 | + response.headers.set("X-RateLimit-Remaining", remaining.toString()); |
| 76 | + response.headers.set("X-RateLimit-Reset", reset.toString()); |
| 77 | + return response; |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +export const standardRateLimitter = new RateLimitter({ |
| 82 | + redis: { |
| 83 | + url: `redis://${env.REDIS_USERNAME}:${env.REDIS_PASSWORD}@${env.REDIS_HOST}:${env.REDIS_PORT}`, |
| 84 | + }, |
| 85 | + limiter: Ratelimit.slidingWindow(1, "60 s"), |
| 86 | +}); |
0 commit comments