Skip to content

Commit

Permalink
perf: ⚡️ added ratelimit - create workspaces, forms
Browse files Browse the repository at this point in the history
Closes: #199
  • Loading branch information
growupanand committed Mar 1, 2024
1 parent 6be1374 commit aa70d75
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Loader2, PenLine, Plus, Sparkles } from "lucide-react";
import { z } from "zod";

import { montserrat } from "@/app/fonts";
import { isRateLimitError } from "@/lib/errorHandlers";
import { cn } from "@/lib/utils";
import { createFormSchema } from "@/lib/validations/form";
import { api } from "@/trpc/react";
Expand Down Expand Up @@ -50,9 +51,12 @@ export default function CreateFormButton({ workspace }: Readonly<Props>) {
});
router.push(`/forms/${newForm.id}`);
},
onError: () => {
onError: (error) => {
toast({
title: "Unable to create form",
duration: 2000,
variant: "destructive",
description: isRateLimitError(error) ? error.message : undefined,
});
},
});
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/components/mainPage/navigationCardContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { toast } from "@convoform/ui/components/ui/use-toast";
import { useQueryClient } from "@tanstack/react-query";
import { Loader2, Plus } from "lucide-react";

import { isRateLimitError } from "@/lib/errorHandlers";
import { NavigationConfig } from "@/lib/types/navigation";
import { api } from "@/trpc/react";
import BrandName from "../common/brandName";
Expand Down Expand Up @@ -36,11 +37,12 @@ export function NavigationCardContent({ orgId }: Readonly<Props>) {
});
router.push(`/workspaces/${newWorkspace.id}/`);
},
onError: () =>
onError: (error) =>
toast({
title: "Unable to create workspace",
duration: 1500,
duration: 2000,
variant: "destructive",
description: isRateLimitError(error) ? error.message : undefined,
}),
});
const isCreatingWorkspace = createWorkspace.isPending;
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/lib/errorHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ export const sendErrorResponse = (error: any) => {
{ status: error.cause?.statusCode || 500 },
);
};

export const isRateLimitError = (error: any) => {
return error.data?.code === "TOO_MANY_REQUESTS";
};
18 changes: 17 additions & 1 deletion apps/web/src/trpc/react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@

import { useState } from "react";
import type { AppRouter } from "@convoform/api";
import { toast } from "@convoform/ui/components/ui/use-toast";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
import {
loggerLink,
TRPCClientError,
unstable_httpBatchStreamLink,
} from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import SuperJSON from "superjson";

import { isRateLimitError } from "@/lib/errorHandlers";
import { getTRPCUrl } from "@/lib/url";

export const api = createTRPCReact<AppRouter>();
Expand All @@ -21,6 +27,16 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) {
refetchInterval: false,
retry: false,
},
mutations: {
onError: (err) => {
if (err instanceof TRPCClientError && isRateLimitError(err)) {
toast({
title: err.message ?? "Too many requests",
duration: 1500,
});
}
},
},
},
}),
);
Expand Down
30 changes: 23 additions & 7 deletions packages/api/src/lib/rateLimit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,41 @@ if (!isRateLimiterAvailable) {

const redis = isRateLimiterAvailable ? Redis.fromEnv() : undefined;

type LimitType = "common" | "core:create";
type RateLimit = Record<LimitType, any>;

export const ratelimit = redis
? {
core: new Ratelimit({
? ({
common: new Ratelimit({
redis,
analytics: true,
prefix: "ratelimit",
limiter: Ratelimit.fixedWindow(200, "60s"),
}),
["core:create"]: new Ratelimit({
redis,
analytics: true,
prefix: "ratelimit:core",
prefix: "ratelimit:api",
limiter: Ratelimit.fixedWindow(2, "10s"),
}),
}
} as RateLimit)
: undefined;

/**
* Check for rate limit and throw an error if the limit is exceeded.
* @returns `Promise<void>`
*/
export const checkRateLimit = async ({
identifier,
message,
rateLimitType,
identifier,
}: {
/** A unique string value to identify user */
identifier: string;
/** Custom message to send in response */
message?: string;
rateLimitType?: keyof typeof ratelimit;
/** Limit type E.g. `core`, `AI` etc */
rateLimitType?: LimitType;
}) => {
if (
!redis ||
Expand All @@ -45,7 +61,7 @@ export const checkRateLimit = async ({
success,
/** Unix timestamp in milliseconds when the limits are reset. */
reset: resetTimeStamp,
} = await ratelimit[rateLimitType ?? "core"].limit(identifier);
} = await ratelimit[rateLimitType ?? "common"].limit(identifier);

if (!success) {
const remainingSeconds = getRemainingSeconds(resetTimeStamp);
Expand Down
5 changes: 5 additions & 0 deletions packages/api/src/router/form.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { and, count, eq, form, formField } from "@convoform/db";
import { z } from "zod";

import { checkRateLimit } from "../lib/rateLimit";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import {
formCreateSchema,
Expand All @@ -17,6 +18,10 @@ export const formRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
await checkRateLimit({
identifier: ctx.userId,
rateLimitType: "core:create",
});
const [newForm] = await ctx.db
.insert(form)
.values({
Expand Down
5 changes: 5 additions & 0 deletions packages/api/src/router/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { eq, insertWorkspaceSchema, workspace } from "@convoform/db";
import { z } from "zod";

import { checkRateLimit } from "../lib/rateLimit";
import { createTRPCRouter, protectedProcedure } from "../trpc";

export const workspaceRouter = createTRPCRouter({
create: protectedProcedure
.input(insertWorkspaceSchema)
.mutation(async ({ input, ctx }) => {
await checkRateLimit({
identifier: ctx.userId,
rateLimitType: "core:create",
});
const [result] = await ctx.db
.insert(workspace)
.values({
Expand Down

0 comments on commit aa70d75

Please sign in to comment.