From 624157ff40f6258c5abcbe9721accca18767ddcb Mon Sep 17 00:00:00 2001 From: Badbird5907 <50347938+Badbird5907@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:30:51 -0400 Subject: [PATCH] add state to server selector and redis --- package.json | 2 + pnpm-lock.yaml | 24 +++++++++ src/app/(authenticated)/dashboard/page.tsx | 2 +- .../dashboard/nav-server-selector.tsx | 11 ++-- src/components/dashboard/server-selector.tsx | 14 ++++-- src/components/navbar/mobile.tsx | 2 +- src/components/ui/command.tsx | 2 +- src/env.js | 8 +++ src/hooks/server-selector.ts | 14 +++--- src/lib/redis.ts | 50 +++++++++++++++++++ src/server/api/routers/guilds.ts | 13 +++-- src/server/auth.ts | 2 + src/trpc/react.tsx | 39 ++++++++------- 13 files changed, 145 insertions(+), 38 deletions(-) create mode 100644 src/lib/redis.ts diff --git a/package.json b/package.json index 75c1b70..ea73404 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "@trpc/client": "next", "@trpc/react-query": "next", "@trpc/server": "next", + "@upstash/redis": "^1.32.0", + "@vercel/functions": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3abd761..1c7b902 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,12 @@ importers: '@trpc/server': specifier: next version: 11.0.0-rc.436 + '@upstash/redis': + specifier: ^1.32.0 + version: 1.32.0 + '@vercel/functions': + specifier: ^1.0.2 + version: 1.0.2 class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -1040,6 +1046,13 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@upstash/redis@1.32.0': + resolution: {integrity: sha512-KBzCjOgF9mhNcV4HTz7U8g7QTdLVEKi1sSe/97u0E+HkzdPmDt3j9lIPIHEUJz6RrdVH6s4HW4JcPvMLC7w3ew==} + + '@vercel/functions@1.0.2': + resolution: {integrity: sha512-j3udyHOv/05Y8o3WQ/ANMWa1aYagsY5B3ouImiwgYsz5z4CBUHTY5dk74oQAXYr+bgoVDpdDlmxkpnxGzKEdLQ==} + engines: {node: '>= 16'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1232,6 +1245,9 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -3527,6 +3543,12 @@ snapshots: '@ungap/structured-clone@1.2.0': {} + '@upstash/redis@1.32.0': + dependencies: + crypto-js: 4.2.0 + + '@vercel/functions@1.0.2': {} + acorn-jsx@5.3.2(acorn@8.12.0): dependencies: acorn: 8.12.0 @@ -3749,6 +3771,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + cssesc@3.0.0: {} csstype@3.1.3: {} diff --git a/src/app/(authenticated)/dashboard/page.tsx b/src/app/(authenticated)/dashboard/page.tsx index 41bba9d..bf8cb31 100644 --- a/src/app/(authenticated)/dashboard/page.tsx +++ b/src/app/(authenticated)/dashboard/page.tsx @@ -7,7 +7,7 @@ const Page = async () => { const session = await getServerAuthSession(); return ( -

Hello, {session?.user.name}

+

Hello, {session?.user.name}

diff --git a/src/components/dashboard/nav-server-selector.tsx b/src/components/dashboard/nav-server-selector.tsx index dab2952..76fe0a5 100644 --- a/src/components/dashboard/nav-server-selector.tsx +++ b/src/components/dashboard/nav-server-selector.tsx @@ -20,11 +20,12 @@ import { CommandList } from "cmdk"; import { api } from "@/trpc/react"; import { type Guild } from "@/types/guild"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { useServerSelector } from "@/hooks/server-selector"; const NavServerSelector = () => { const [open, setOpen] = React.useState(false) - const [value, setValue] = React.useState("") const servers = api.guilds.getGuilds.useQuery(); + const serverSelector = useServerSelector(); return ( @@ -34,8 +35,8 @@ const NavServerSelector = () => { aria-expanded={open} className="w-[200px] justify-between" > - {value && servers.data - ? truncate(servers.data?.[value]?.name ?? value, 20) + {serverSelector.value && servers.data + ? truncate(servers.data?.[serverSelector.value]?.name ?? serverSelector.value, 20) : "Select server..."} @@ -56,7 +57,7 @@ const NavServerSelector = () => { key={server.id} value={server.id} onSelect={(currentValue) => { - setValue(currentValue === value ? "" : currentValue) + serverSelector.setSelectedServer(currentValue === serverSelector.value ? "" : currentValue) setOpen(false) }} className="hover:cursor-pointer truncate" @@ -64,7 +65,7 @@ const NavServerSelector = () => { {truncate(server.name, 20)} diff --git a/src/components/dashboard/server-selector.tsx b/src/components/dashboard/server-selector.tsx index 3cf88af..e5674f4 100644 --- a/src/components/dashboard/server-selector.tsx +++ b/src/components/dashboard/server-selector.tsx @@ -1,14 +1,18 @@ "use client"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { useServerSelector } from "@/hooks/server-selector"; import { api } from "@/trpc/react"; import type { Guild } from "@/types/guild"; import Image from "next/image"; -const Server = ({ guild }: { guild: Guild }) => { +const Server = ({ guild, onClick }: { guild: Guild; onClick: (id: string) => void }) => { const img = `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}`; return ( - + onClick(guild.id)} + > {guild.name} @@ -31,11 +35,13 @@ const Server = ({ guild }: { guild: Guild }) => { const ServerSelector = () => { const servers = api.guilds.getGuilds.useQuery(); - console.log(servers.data) + const serverSelector = useServerSelector(); return (
{servers.data && Object.values(servers.data).map(guild => ( - + { + serverSelector.setSelectedServer(id); + }} /> ))}
); diff --git a/src/components/navbar/mobile.tsx b/src/components/navbar/mobile.tsx index ab7015d..c5314bd 100644 --- a/src/components/navbar/mobile.tsx +++ b/src/components/navbar/mobile.tsx @@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; -import { NavConfig, siteConfig } from "@/lib/site-config"; +import { type NavConfig, siteConfig } from "@/lib/site-config"; import { cn } from "@/lib/utils"; import Link, { type LinkProps } from "next/link"; import { useRouter } from "next/navigation"; diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index 007798a..68a0591 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -23,7 +23,7 @@ const Command = React.forwardRef< )) Command.displayName = CommandPrimitive.displayName -interface CommandDialogProps extends DialogProps {} +type CommandDialogProps = DialogProps const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( diff --git a/src/env.js b/src/env.js index 42cc126..ece53c2 100644 --- a/src/env.js +++ b/src/env.js @@ -23,6 +23,10 @@ export const env = createEnv({ ), DISCORD_CLIENT_ID: z.string(), DISCORD_CLIENT_SECRET: z.string(), + BACKEND_TOKEN: z.string(), + REDIS_URI: z.string(), + REDIS_TOKEN: z.string(), + DISABLE_CACHING: z.boolean().default(false), }, /** @@ -44,6 +48,10 @@ export const env = createEnv({ NEXTAUTH_URL: process.env.NEXTAUTH_URL, DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET, + BACKEND_TOKEN: process.env.BACKEND_TOKEN, + REDIS_URI: process.env.REDIS_URI, + REDIS_TOKEN: process.env.REDIS_TOKEN, + DISABLE_CACHING: process.env.DISABLE_CACHING, // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, }, /** diff --git a/src/hooks/server-selector.ts b/src/hooks/server-selector.ts index 8ebd591..beb7c83 100644 --- a/src/hooks/server-selector.ts +++ b/src/hooks/server-selector.ts @@ -1,13 +1,15 @@ import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; -const useServerSelector = create( +type ServerSelectorState = { + value: string | null; + setSelectedServer: (server: string | null) => void; +} +export const useServerSelector = create()( persist( - (set, get) => ({ - selectedServer: null, - setSelectedServer: (server: string) => { - set({ selectedServer: server }); - }, + (set) => ({ + value: null, + setSelectedServer: (server) => set({ value: server }), }), { name: "serverSelector", diff --git a/src/lib/redis.ts b/src/lib/redis.ts new file mode 100644 index 0000000..87a21e6 --- /dev/null +++ b/src/lib/redis.ts @@ -0,0 +1,50 @@ +"use server"; + +import { Redis } from "@upstash/redis"; + +import { env } from "@/env"; + +const redis = new Redis({ + url: env.REDIS_URI, + token: env.REDIS_TOKEN, +}); + +export interface getOptions { + force?: boolean; +} +export async function get( + key: string, + options?: getOptions, +): Promise { + if (env.DISABLE_CACHING === true && !options?.force) return null; + const value = await redis.get(key); + if (!value) return null; + return value as T; +} + +export interface setOptions { + ttl?: number; // in seconds + force?: boolean; +} + +export async function set( + key: string, + value: T, + options?: setOptions, +): Promise { + if (env.DISABLE_CACHING === true && !options?.force) return; + await redis.set( + key, + JSON.stringify(value), + options?.ttl ? { ex: options?.ttl } : { ex: 300 }, + ); +} + +export interface delOptions { + force?: boolean; +} + +export async function del(key: string, options?: delOptions): Promise { + if (env.DISABLE_CACHING === true && !options?.force) return; + await redis.del(key); +} diff --git a/src/server/api/routers/guilds.ts b/src/server/api/routers/guilds.ts index 8ec4395..07f571e 100644 --- a/src/server/api/routers/guilds.ts +++ b/src/server/api/routers/guilds.ts @@ -1,15 +1,21 @@ +import { get, set } from "@/lib/redis"; import { createTRPCRouter, protectedProcedure, } from "@/server/api/trpc"; import type { Guild } from "@/types/guild"; import { TRPCError } from "@trpc/server"; +import { waitUntil } from "@vercel/functions"; export const guildsRouter = createTRPCRouter({ getGuilds: protectedProcedure.query(async ({ ctx, input }) => { // TODO: cache this! - const { accessToken, refreshToken } = ctx.session.user.discord; - console.log(ctx.session); + const { accessToken, id } = ctx.session.user.discord; + type GuildRecord = Record; + const cached = await get(`guilds:${id}`); + if (cached) { + return cached; + } const resp = await fetch("https://discord.com/api/users/@me/guilds", { headers: { Authorization: `Bearer ${accessToken}`, @@ -37,10 +43,11 @@ export const guildsRouter = createTRPCRouter({ if (b.owner) return 1; return a.name.localeCompare(b.name); }); - const map: Record = {}; + const map: GuildRecord = {}; hasPermission.forEach(guild => { map[guild.id] = guild; }); + waitUntil(set(`guilds:${id}`, map, { ttl: 128 })) return map; }), }); \ No newline at end of file diff --git a/src/server/auth.ts b/src/server/auth.ts index 2668624..8d26681 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -20,6 +20,7 @@ declare module "next-auth" { discord: { accessToken: string; refreshToken: string; + id: string; } // ...other properties // role: UserRole; @@ -58,6 +59,7 @@ export const authOptions: NextAuthOptions = { token.discord = { accessToken: account.access_token, refreshToken: account.refresh_token, + id: account.userId }; } return token; diff --git a/src/trpc/react.tsx b/src/trpc/react.tsx index 1336bde..4ea9b62 100644 --- a/src/trpc/react.tsx +++ b/src/trpc/react.tsx @@ -1,7 +1,7 @@ "use client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client"; +import { createTRPCClient, loggerLink, unstable_httpBatchStreamLink } from "@trpc/client"; import { createTRPCReact } from "@trpc/react-query"; import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; import { useState } from "react"; @@ -11,6 +11,23 @@ import { type AppRouter } from "@/server/api/root"; const createQueryClient = () => new QueryClient(); +const links = [ + loggerLink({ + enabled: (op) => + process.env.NODE_ENV === "development" || + (op.direction === "down" && op.result instanceof Error), + }), + unstable_httpBatchStreamLink({ + transformer: SuperJSON, + url: getBaseUrl() + "/api/trpc", + headers: () => { + const headers = new Headers(); + headers.set("x-trpc-source", "nextjs-react"); + return headers; + }, + }), +]; + let clientQueryClientSingleton: QueryClient | undefined = undefined; const getQueryClient = () => { if (typeof window === "undefined") { @@ -22,6 +39,9 @@ const getQueryClient = () => { }; export const api = createTRPCReact(); +export const apiQueryClient = createTRPCClient({ + links, +}) /** * Inference helper for inputs. @@ -42,22 +62,7 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) { const [trpcClient] = useState(() => api.createClient({ - links: [ - loggerLink({ - enabled: (op) => - process.env.NODE_ENV === "development" || - (op.direction === "down" && op.result instanceof Error), - }), - unstable_httpBatchStreamLink({ - transformer: SuperJSON, - url: getBaseUrl() + "/api/trpc", - headers: () => { - const headers = new Headers(); - headers.set("x-trpc-source", "nextjs-react"); - return headers; - }, - }), - ], + links, }) );