diff --git a/components/dashboard-nav.tsx b/components/dashboard-nav.tsx index b54d7eb..906765d 100644 --- a/components/dashboard-nav.tsx +++ b/components/dashboard-nav.tsx @@ -38,6 +38,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" export function DashboardNav() { const pathname = usePathname() + const currentPath = pathname ?? "" const router = useRouter() const { toast } = useToast() const { setTheme, theme } = useTheme() @@ -69,10 +70,10 @@ export function DashboardNav() { ] const isActive = (path: string) => { - if (path === "/dashboard" && pathname === "/dashboard") { + if (path === "/dashboard" && currentPath === "/dashboard") { return true } - return path !== "/dashboard" && pathname.startsWith(path) + return path !== "/dashboard" && currentPath.startsWith(path) } const handleLogout = async () => { diff --git a/components/dashboard-shell.tsx b/components/dashboard-shell.tsx index f26ec39..61c065d 100644 --- a/components/dashboard-shell.tsx +++ b/components/dashboard-shell.tsx @@ -37,6 +37,7 @@ interface DashboardShellProps { export function DashboardShell({ children }: DashboardShellProps) { const router = useRouter() const pathname = usePathname() + const currentPath = pathname ?? "" const [isMobile, setIsMobile] = useState(false) const [isOpen, setIsOpen] = useState(false) @@ -58,10 +59,10 @@ export function DashboardShell({ children }: DashboardShellProps) { // Check if the menu item should be active const isActive = (path: string) => { - if (path === "/dashboard" && pathname === "/dashboard") { + if (path === "/dashboard" && currentPath === "/dashboard") { return true } - return path !== "/dashboard" && pathname.startsWith(path) + return path !== "/dashboard" && currentPath.startsWith(path) } const handleLogout = async () => { diff --git a/components/providers.tsx b/components/providers.tsx index 4e0d273..5c38314 100644 --- a/components/providers.tsx +++ b/components/providers.tsx @@ -7,6 +7,7 @@ import { ThemeProvider } from "@/components/theme-provider" import { PageViewTracker } from "@/components/page-view-tracker" import { CookieBanner } from "@/components/cookie-banner" import { Logger } from "@/components/logger" +import { RealtimeProvider } from "@/components/realtime-provider" import { Toaster } from "@/components/ui/toaster" interface ProvidersProps { @@ -19,7 +20,7 @@ export function Providers({ children }: ProvidersProps) { - {children} + {children} diff --git a/components/realtime-provider.tsx b/components/realtime-provider.tsx new file mode 100644 index 0000000..1413485 --- /dev/null +++ b/components/realtime-provider.tsx @@ -0,0 +1,50 @@ +"use client" + +import type { ReactNode } from "react" +import { useEffect } from "react" +import { useRouter } from "next/navigation" +import { getRealtimeClient } from "@/lib/realtime-client" + +interface RealtimeProviderProps { + children: ReactNode +} + +export function RealtimeProvider({ children }: RealtimeProviderProps) { + const router = useRouter() + + useEffect(() => { + const controller = new AbortController() + + fetch("/api/socket/io", { signal: controller.signal }).catch(() => { + // The socket route will respond immediately once the server is ready. + }) + + const socket = getRealtimeClient() + + if (!socket) { + controller.abort() + return + } + + const handleEvent = () => { + router.refresh() + } + + const handleConnect = () => { + router.refresh() + } + + socket.on("connect", handleConnect) + + socket.on("realtime:event", handleEvent) + + return () => { + controller.abort() + socket.off("realtime:event", handleEvent) + socket.off("connect", handleConnect) + } + }, [router]) + + return <>{children} +} + diff --git a/components/unsubscribe-client.tsx b/components/unsubscribe-client.tsx index 0879ad4..95a4ef2 100644 --- a/components/unsubscribe-client.tsx +++ b/components/unsubscribe-client.tsx @@ -17,7 +17,7 @@ import { export default function UnsubscribeClient() { const searchParams = useSearchParams() - const token = searchParams.get("token") + const token = searchParams?.get("token") ?? null const [status, setStatus] = useState< "verifying" | "ready" | "processing" | "success" | "error" | "preferences" >(token ? "verifying" : "error") diff --git a/lib/db.ts b/lib/db.ts index c45f426..d9b9787 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -1,5 +1,7 @@ import type { PrismaClient as PrismaClientType } from "@prisma/client" +import { registerPrismaRealtime } from "@/lib/realtime-events" + type PrismaClientConstructor = new (...args: unknown[]) => PrismaClientType const globalForPrisma = globalThis as unknown as { prisma?: PrismaClientType } @@ -108,6 +110,10 @@ const prisma = }) : createStubClient()) +if (PrismaClientCtor) { + registerPrismaRealtime(prisma as PrismaClientType) +} + if (process.env.NODE_ENV !== "production") { globalForPrisma.prisma = prisma } diff --git a/lib/realtime-client.ts b/lib/realtime-client.ts new file mode 100644 index 0000000..7df9c10 --- /dev/null +++ b/lib/realtime-client.ts @@ -0,0 +1,24 @@ +"use client" + +import { io, type Socket } from "socket.io-client" + +let socketInstance: Socket | null = null + +export function getRealtimeClient() { + if (typeof window === "undefined") { + return null + } + + if (!socketInstance) { + socketInstance = io({ + path: "/api/socket/io", + transports: ["websocket", "polling"], + autoConnect: true, + reconnection: true, + reconnectionDelay: 1000, + reconnectionDelayMax: 5000, + }) + } + + return socketInstance +} diff --git a/lib/realtime-events.ts b/lib/realtime-events.ts new file mode 100644 index 0000000..fa820e6 --- /dev/null +++ b/lib/realtime-events.ts @@ -0,0 +1,78 @@ +import type { PrismaClient } from "@prisma/client" + +import { enqueueRealtimeEvent } from "@/lib/realtime-queue" + +type PrismaMiddleware = Parameters[0] +type PrismaMiddlewareParams = Parameters[0] +type PrismaMiddlewareNext = Parameters[1] + +const MUTATION_ACTIONS = new Set([ + "create", + "createMany", + "update", + "updateMany", + "upsert", + "delete", + "deleteMany", +]) + +const globalForRealtime = globalThis as unknown as { + prismaRealtimeRegistered?: boolean +} + +export function registerPrismaRealtime(prisma: PrismaClient) { + if (globalForRealtime.prismaRealtimeRegistered) { + return + } + + const middlewareCapablePrisma = prisma as PrismaClient & { + $use?: PrismaClient["$use"] + } + + if (typeof middlewareCapablePrisma.$use !== "function") { + console.warn("Prisma client does not support middleware; skipping realtime registration") + globalForRealtime.prismaRealtimeRegistered = true + return + } + + const realtimeMiddleware: PrismaMiddleware = async ( + params: PrismaMiddlewareParams, + next: PrismaMiddlewareNext, + ) => { + const result = await next(params) + + if (MUTATION_ACTIONS.has(params.action)) { + const model = params.model ?? "unknown" + + try { + const enqueuePromise = enqueueRealtimeEvent(`prisma:${model}:${params.action}`, { + model, + action: params.action, + args: params.args, + result, + timestamp: Date.now(), + }) + + void enqueuePromise.catch(error => { + console.error("Failed to enqueue realtime Prisma event", error) + }) + } catch (error) { + console.error("Failed to enqueue realtime Prisma event", error) + } + } + + return result + } + + middlewareCapablePrisma.$use(realtimeMiddleware) + + globalForRealtime.prismaRealtimeRegistered = true +} + +export async function broadcastRealtimeEvent(event: string, payload: unknown) { + try { + await enqueueRealtimeEvent(event, payload) + } catch (error) { + console.error("Failed to enqueue realtime event", error) + } +} diff --git a/lib/realtime-queue.ts b/lib/realtime-queue.ts new file mode 100644 index 0000000..c925308 --- /dev/null +++ b/lib/realtime-queue.ts @@ -0,0 +1,89 @@ +import { Queue, Worker } from "bullmq" +import type { JobsOptions } from "bullmq" + +import { createRedisConnection, getRedisUrl } from "@/lib/redis" +import { getRealtimeIO } from "@/lib/realtime" + +const queueName = "realtime-events" + +const globalForQueue = globalThis as unknown as { + realtimeQueue?: Queue + realtimeWorker?: Worker + realtimeQueueConnection?: ReturnType + realtimeWorkerConnection?: ReturnType +} + +function getQueueConnection() { + if (!globalForQueue.realtimeQueueConnection) { + globalForQueue.realtimeQueueConnection = createRedisConnection() + } + + return globalForQueue.realtimeQueueConnection +} + +function getWorkerConnection() { + if (!globalForQueue.realtimeWorkerConnection) { + globalForQueue.realtimeWorkerConnection = createRedisConnection() + } + + return globalForQueue.realtimeWorkerConnection +} + +export function getRealtimeQueue() { + const connection = getQueueConnection() + + if (!globalForQueue.realtimeQueue) { + globalForQueue.realtimeQueue = new Queue(queueName, { + connection, + }) + } + + return globalForQueue.realtimeQueue +} + +function ensureWorker() { + const connection = getWorkerConnection() + + if (!globalForQueue.realtimeWorker) { + globalForQueue.realtimeWorker = new Worker( + queueName, + async job => { + const io = getRealtimeIO() + + if (io) { + io.emit("realtime:event", { + event: job.name, + payload: job.data, + jobId: job.id, + timestamp: Date.now(), + }) + + io.emit(job.name, job.data) + } + }, + { connection }, + ) + + globalForQueue.realtimeWorker.on("error", error => { + console.error("Realtime worker error", error) + }) + } +} + +export async function enqueueRealtimeEvent(event: string, payload: unknown, options?: JobsOptions) { + const queue = getRealtimeQueue() + ensureWorker() + + await queue.add(event, payload, { + removeOnComplete: { age: 60, count: 1000 }, + removeOnFail: { age: 60 * 60, count: 100 }, + ...options, + }) +} + +export function describeRealtimeQueue() { + return { + name: queueName, + redis: getRedisUrl(), + } +} diff --git a/lib/realtime.ts b/lib/realtime.ts new file mode 100644 index 0000000..0d17537 --- /dev/null +++ b/lib/realtime.ts @@ -0,0 +1,14 @@ +import type { Server as IOServer } from "socket.io" + +const globalForRealtime = globalThis as unknown as { + realtimeIO?: IOServer +} + +export function getRealtimeIO() { + return globalForRealtime.realtimeIO +} + +export function setRealtimeIO(io: IOServer) { + globalForRealtime.realtimeIO = io + return io +} diff --git a/lib/redis.ts b/lib/redis.ts new file mode 100644 index 0000000..2bf5cc0 --- /dev/null +++ b/lib/redis.ts @@ -0,0 +1,28 @@ +import IORedis from "ioredis" + +const redisUrl = process.env.REDIS_URL || "redis://127.0.0.1:6379" + +const baseOptions = { + maxRetriesPerRequest: null as number | null, + enableReadyCheck: true, +} + +const globalForRedis = globalThis as unknown as { + redisConnection?: IORedis +} + +export function createRedisConnection() { + return new IORedis(redisUrl, baseOptions) +} + +export function getRedisConnection() { + if (!globalForRedis.redisConnection) { + globalForRedis.redisConnection = createRedisConnection() + } + + return globalForRedis.redisConnection +} + +export function getRedisUrl() { + return redisUrl +} diff --git a/next-env.d.ts b/next-env.d.ts index 1b3be08..3cd7048 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index fd8d22f..f847077 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@react-three/fiber": "^9.0.0", "autoprefixer": "^10.4.20", "bcryptjs": "3.0.2", + "bullmq": "^5.63.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.0.4", @@ -67,6 +68,7 @@ "framer-motion": "^11.11.7", "immer": "10.1.3", "input-otp": "1.4.1", + "ioredis": "^5.8.2", "lucide-react": "^0.454.0", "next": "15.4.7", "next-auth": "4.24.13", @@ -80,6 +82,8 @@ "react-resizable-panels": "^2.1.7", "react-syntax-highlighter": "^16.1.0", "recharts": "2.15.0", + "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1", "sonner": "^1.7.1", "swr": "^2.3.3", "tailwind-merge": "^2.5.5", diff --git a/pages/api/socket/io.ts b/pages/api/socket/io.ts new file mode 100644 index 0000000..a9c4ebb --- /dev/null +++ b/pages/api/socket/io.ts @@ -0,0 +1,48 @@ +import type { NextApiRequest } from "next" +import { Server as IOServer } from "socket.io" + +import type { NextApiResponseServerIO } from "@/types/next" +import { getRealtimeIO, setRealtimeIO } from "@/lib/realtime" + +export const config = { + api: { + bodyParser: false, + }, +} + +export default function handler(req: NextApiRequest, res: NextApiResponseServerIO) { + const existingServer = res.socket.server as typeof res.socket.server & { io?: IOServer } + + if (!existingServer.io) { + const io = new IOServer(res.socket.server, { + path: "/api/socket/io", + cors: { + origin: process.env.NEXT_PUBLIC_SITE_URL || "*", + }, + }) + + setRealtimeIO(io) + existingServer.io = io + + io.on("connection", socket => { + socket.emit("realtime:event", { + event: "socket:connected", + socketId: socket.id, + timestamp: Date.now(), + }) + + socket.on("disconnect", reason => { + socket.broadcast.emit("realtime:event", { + event: "socket:disconnected", + socketId: socket.id, + reason, + timestamp: Date.now(), + }) + }) + }) + } else if (!getRealtimeIO()) { + setRealtimeIO(existingServer.io) + } + + res.end() +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aca571..9e8091e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,6 +119,9 @@ importers: bcryptjs: specifier: 3.0.2 version: 3.0.2 + bullmq: + specifier: ^5.63.0 + version: 5.63.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -143,6 +146,9 @@ importers: input-otp: specifier: 1.4.1 version: 1.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + ioredis: + specifier: ^5.8.2 + version: 5.8.2 lucide-react: specifier: ^0.454.0 version: 0.454.0(react@18.3.1) @@ -182,6 +188,12 @@ importers: recharts: specifier: 2.15.0 version: 2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + socket.io: + specifier: ^4.8.1 + version: 4.8.1 + socket.io-client: + specifier: ^4.8.1 + version: 4.8.1 sonner: specifier: ^1.7.1 version: 1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -710,6 +722,9 @@ packages: cpu: [x64] os: [win32] + '@ioredis/commands@1.4.0': + resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -738,6 +753,36 @@ packages: peerDependencies: three: '>= 0.159.0' + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + '@next/env@15.4.7': resolution: {integrity: sha512-PrBIpO8oljZGTOe9HH0miix1w5MUiGJ/q83Jge03mHEE0E3pyqzAy2+l5G6aJDbXoobmxPJTVhbCuwlLtjSHwg==} @@ -1714,6 +1759,9 @@ packages: '@so-ric/colorspace@1.1.6': resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -1726,6 +1774,9 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -1889,6 +1940,10 @@ packages: '@webgpu/types@0.1.66': resolution: {integrity: sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1951,6 +2006,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + baseline-browser-mapping@2.8.18: resolution: {integrity: sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==} hasBin: true @@ -1987,6 +2046,9 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bullmq@5.63.0: + resolution: {integrity: sha512-HT1iM3Jt4bZeg3Ru/MxrOy2iIItxcl1Pz5Ync1Vrot70jBpVguMxFEiSaDU57BwYwR4iwnObDnzct2lirKkX5A==} + c12@3.1.0: resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} peerDependencies: @@ -2048,6 +2110,10 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + cmdk@1.0.4: resolution: {integrity: sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg==} peerDependencies: @@ -2101,6 +2167,14 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} @@ -2168,6 +2242,15 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2193,6 +2276,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2261,6 +2348,17 @@ packages: enabled@2.0.0: resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + engine.io-client@6.6.3: + resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.4: + resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==} + engines: {node: '>=10.2.0'} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2538,6 +2636,10 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + ioredis@5.8.2: + resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} + engines: {node: '>=12.22.0'} + is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -2659,6 +2761,12 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -2691,6 +2799,10 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + maath@0.10.8: resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} peerDependencies: @@ -2713,6 +2825,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2733,6 +2853,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.5: + resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -2744,6 +2871,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + next-auth@4.24.13: resolution: {integrity: sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==} peerDependencies: @@ -2785,9 +2916,16 @@ packages: sass: optional: true + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + node-releases@2.0.26: resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==} @@ -3155,6 +3293,14 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + refractor@5.0.0: resolution: {integrity: sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==} @@ -3216,6 +3362,21 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + + socket.io-client@4.8.1: + resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.1: + resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} + engines: {node: '>=10.2.0'} + sonner@1.7.4: resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==} peerDependencies: @@ -3232,6 +3393,9 @@ packages: stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + stats-gl@2.4.2: resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==} peerDependencies: @@ -3442,10 +3606,18 @@ packages: resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} engines: {node: '>= 4'} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vaul@0.9.9: resolution: {integrity: sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ==} peerDependencies: @@ -3486,6 +3658,22 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -4214,6 +4402,8 @@ snapshots: '@img/sharp-win32-x64@0.34.4': optional: true + '@ioredis/commands@1.4.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -4249,6 +4439,24 @@ snapshots: promise-worker-transferable: 1.0.4 three: 0.170.0 + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + '@next/env@15.4.7': {} '@next/eslint-plugin-next@15.4.7': @@ -5382,6 +5590,8 @@ snapshots: color: 5.0.2 text-hex: 1.0.0 + '@socket.io/component-emitter@3.1.2': {} + '@standard-schema/spec@1.0.0': {} '@swc/helpers@0.5.15': @@ -5392,6 +5602,10 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/cors@2.8.19': + dependencies: + '@types/node': 22.18.12 + '@types/d3-array@3.2.2': {} '@types/d3-color@3.1.3': {} @@ -5585,6 +5799,11 @@ snapshots: '@webgpu/types@0.1.66': {} + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -5639,6 +5858,8 @@ snapshots: base64-js@1.5.1: {} + base64id@2.0.0: {} + baseline-browser-mapping@2.8.18: {} bcryptjs@3.0.2: {} @@ -5677,6 +5898,18 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bullmq@5.63.0: + dependencies: + cron-parser: 4.9.0 + ioredis: 5.8.2 + msgpackr: 1.11.5 + node-abort-controller: 3.1.1 + semver: 7.7.3 + tslib: 2.8.1 + uuid: 11.1.0 + transitivePeerDependencies: + - supports-color + c12@3.1.0: dependencies: chokidar: 4.0.3 @@ -5743,6 +5976,8 @@ snapshots: clsx@2.1.1: {} + cluster-key-slot@1.1.2: {} + cmdk@1.0.4(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -5790,6 +6025,15 @@ snapshots: cookie@0.7.2: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 + cross-env@7.0.3: dependencies: cross-spawn: 7.0.6 @@ -5846,6 +6090,10 @@ snapshots: date-fns@4.1.0: {} + debug@4.3.7: + dependencies: + ms: 2.1.3 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -5862,6 +6110,8 @@ snapshots: defu@6.1.4: {} + denque@2.1.0: {} + dequal@2.0.3: {} destr@2.0.5: {} @@ -5917,6 +6167,36 @@ snapshots: enabled@2.0.0: {} + engine.io-client@6.6.3: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + engine.io@6.6.4: + dependencies: + '@types/cors': 2.8.19 + '@types/node': 22.18.12 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -6197,6 +6477,20 @@ snapshots: internmap@2.0.3: {} + ioredis@5.8.2: + dependencies: + '@ioredis/commands': 1.4.0 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -6294,6 +6588,10 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + lodash.merge@4.6.2: {} lodash@4.17.21: {} @@ -6330,6 +6628,8 @@ snapshots: dependencies: react: 18.3.1 + luxon@3.7.2: {} + maath@0.10.8(@types/three@0.181.0)(three@0.170.0): dependencies: '@types/three': 0.181.0 @@ -6348,6 +6648,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -6366,6 +6672,22 @@ snapshots: ms@2.1.3: {} + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.5: + optionalDependencies: + msgpackr-extract: 3.0.3 + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -6376,6 +6698,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@0.6.3: {} + next-auth@4.24.13(@auth/core@0.34.3(nodemailer@7.0.7))(next@15.4.7(@babel/core@7.28.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@7.0.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.28.4 @@ -6422,8 +6746,15 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-abort-controller@3.1.1: {} + node-fetch-native@1.6.7: {} + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + node-releases@2.0.26: {} nodemailer@7.0.7: {} @@ -6768,6 +7099,12 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + refractor@5.0.0: dependencies: '@types/hast': 3.0.4 @@ -6843,6 +7180,47 @@ snapshots: signal-exit@4.1.0: {} + socket.io-adapter@2.5.5: + dependencies: + debug: 4.3.7 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-client@4.8.1: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-client: 6.6.3 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.1: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.7 + engine.io: 6.6.4 + socket.io-adapter: 2.5.5 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + sonner@1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -6854,6 +7232,8 @@ snapshots: stack-trace@0.0.10: {} + standard-as-callback@2.1.0: {} + stats-gl@2.4.2(@types/three@0.181.0)(three@0.170.0): dependencies: '@types/three': 0.181.0 @@ -7074,8 +7454,12 @@ snapshots: utility-types@3.11.0: {} + uuid@11.1.0: {} + uuid@8.3.2: {} + vary@1.1.2: {} + vaul@0.9.9(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -7144,6 +7528,10 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.2 + ws@8.17.1: {} + + xmlhttprequest-ssl@2.1.2: {} + yallist@3.1.1: {} yallist@4.0.0: {} diff --git a/types/next.d.ts b/types/next.d.ts new file mode 100644 index 0000000..038318e --- /dev/null +++ b/types/next.d.ts @@ -0,0 +1,11 @@ +import type { Server as HttpServer } from "http" +import type { Server as IOServer } from "socket.io" +import type { NextApiResponse } from "next" + +export type NextApiResponseServerIO = NextApiResponse & { + socket: NextApiResponse["socket"] & { + server: HttpServer & { + io?: IOServer + } + } +}