Skip to content

Commit 7a0fd29

Browse files
matt-aitkenericallam
authored andcommitted
WIP using the redis package instead
1 parent 306df7e commit 7a0fd29

File tree

2 files changed

+74
-24
lines changed

2 files changed

+74
-24
lines changed

apps/webapp/app/routes/api.v1.events.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { generateErrorMessage } from "zod-error";
55
import { authenticateApiRequest } from "~/services/apiAuth.server";
66
import { IngestSendEvent } from "~/services/events/ingestSendEvent.server";
77
import { eventRecordToApiJson } from "~/api.server";
8+
import { standardRateLimitter } from "~/services/apiRateLimit.server";
89

9-
export async function action({ request }: ActionFunctionArgs) {
10+
export const action = standardRateLimitter.action("sendEvent", async ({ request }) => {
1011
// Ensure this is a POST request
1112
if (request.method.toUpperCase() !== "POST") {
1213
return { status: 405, body: "Method Not Allowed" };
@@ -39,4 +40,4 @@ export async function action({ request }: ActionFunctionArgs) {
3940
}
4041

4142
return json(eventRecordToApiJson(event));
42-
}
43+
});
Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,86 @@
1-
import Redis, { RedisOptions } from "ioredis";
1+
import { RedisClientOptions, createClient } from "redis";
22
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";
49
import { env } from "~/env.server";
510

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);
1515

1616
return {
1717
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);
1919
},
2020
eval: <TArgs extends unknown[], TData = unknown>(
2121
...args: [script: string, keys: string[], args: TArgs]
2222
): 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>;
2427
},
2528
};
2629
}
2730

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"];
3734
};
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

Comments
 (0)