From 31c139ebd48565c17e5f0f4f9981c00a277e6023 Mon Sep 17 00:00:00 2001 From: sheikh_umar Date: Tue, 12 Aug 2025 16:37:23 +0500 Subject: [PATCH] integarte sse features --- dev-test.rar | Bin 0 -> 209033 bytes package-lock.json | 131 ++++- package.json | 1 + src/app/(protected)/sse-demo/page.tsx | 476 ++++++++++++++++++ src/app/(public)/page.tsx | 19 +- src/app/(public)/signin/page.tsx | 156 ++++++ src/app/api/auth/[...nextauth]/route.ts | 2 + src/app/api/sse/route.ts | 160 ++++++ src/app/api/webhooks/sse-example/route.ts | 74 +++ src/app/providers.tsx | 73 +++ src/config/routes.ts | 12 + src/env.js | 48 +- src/features/auth/config/next-auth.ts | 76 ++- src/hooks/useSSE.ts | 275 ++++++++++ src/lib/logger.ts | 6 + src/lib/query-client.ts | 12 + src/lib/trpc/root.ts | 2 + src/server/api/routers/sse.ts | 188 +++++++ src/server/context.ts | 18 + src/server/db/client.ts | 11 + .../db/migrations/0000_lyrical_khan.sql | 21 + .../db/migrations/0001_wet_tarantula.sql | 3 + .../db/migrations/meta/0000_snapshot.json | 109 ++++ .../db/migrations/meta/0001_snapshot.json | 109 ++++ src/server/db/migrations/meta/_journal.json | 20 + src/server/db/schema.ts | 66 +++ src/server/routers/_app.ts | 39 ++ src/server/routers/channel.ts | 122 +++++ src/server/routers/post.ts | 142 ++++++ src/server/sse/ACCEPTANCE.md | 162 ++++++ src/server/sse/README.md | 411 +++++++++++++++ src/server/sse/bus.ts | 34 ++ src/server/sse/index.ts | 110 ++++ src/server/sse/manager.ts | 284 +++++++++++ src/server/sse/types.ts | 49 ++ src/server/trpc.ts | 96 ++++ 36 files changed, 3473 insertions(+), 44 deletions(-) create mode 100644 dev-test.rar create mode 100644 src/app/(protected)/sse-demo/page.tsx create mode 100644 src/app/(public)/signin/page.tsx create mode 100644 src/app/api/sse/route.ts create mode 100644 src/app/api/webhooks/sse-example/route.ts create mode 100644 src/app/providers.tsx create mode 100644 src/hooks/useSSE.ts create mode 100644 src/lib/logger.ts create mode 100644 src/lib/query-client.ts create mode 100644 src/server/api/routers/sse.ts create mode 100644 src/server/context.ts create mode 100644 src/server/db/client.ts create mode 100644 src/server/db/migrations/0000_lyrical_khan.sql create mode 100644 src/server/db/migrations/0001_wet_tarantula.sql create mode 100644 src/server/db/migrations/meta/0000_snapshot.json create mode 100644 src/server/db/migrations/meta/0001_snapshot.json create mode 100644 src/server/db/migrations/meta/_journal.json create mode 100644 src/server/db/schema.ts create mode 100644 src/server/routers/_app.ts create mode 100644 src/server/routers/channel.ts create mode 100644 src/server/routers/post.ts create mode 100644 src/server/sse/ACCEPTANCE.md create mode 100644 src/server/sse/README.md create mode 100644 src/server/sse/bus.ts create mode 100644 src/server/sse/index.ts create mode 100644 src/server/sse/manager.ts create mode 100644 src/server/sse/types.ts create mode 100644 src/server/trpc.ts diff --git a/dev-test.rar b/dev-test.rar new file mode 100644 index 0000000000000000000000000000000000000000..ca169592a723ca37a8417bd914f7fcadbfbd7580 GIT binary patch literal 209033 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%0D@9zn@reoy2f7c9@ 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 z90knd
+
+

SSE Demo Dashboard

+ + {/* Connection Status */} +
+

Connection Status

+
+
+ {connectionStatus.toUpperCase()} +
+ {connectionInfo && ( +
+ Connection ID: {connectionInfo.connId} + {connectionInfo.sessionId && ( + <> + {" "}| Session: {connectionInfo.sessionId} + + )} +
+ )} +
+
+ + {/* Statistics */} + {stats && ( +
+

SSE Statistics

+
+
+
{stats.activeConnections}
+
Active Connections
+
+
+
{stats.totalEventsSent}
+
Events Sent
+
+
+
{stats.totalErrors}
+
Errors
+
+
+
{Math.floor(stats.uptime / 1000)}s
+
Uptime
+
+
+
+ )} + + {/* Test Controls */} +
+

Test Controls

+ +
+
+ + +
+ +
+ + setTestTopic(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="Enter topic name" + /> +
+
+ +
+ + setTestMessage(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="Enter your message" + /> +
+ +
+ + + + + + + + + + + + + +
+
+ + {/* Messages */} +
+

Received Messages ({messages.length})

+
+ {messages.length === 0 ? ( +
No messages received yet
+ ) : ( +
+ {messages.map((message, index) => ( +
+
+ + {message.event} + + + {formatTimestamp(message.timestamp)} + +
+
+                      {JSON.stringify(message.data, null, 2)}
+                    
+
+ ))} +
+ )} +
+
+ + {/* My Connections */} + {getMyConnections.data && getMyConnections.data.length > 0 && ( +
+

My Connections ({getMyConnections.data.length})

+
+
+ {getMyConnections.data.map((conn: ConnectionInfo, index: number) => ( +
+
+
+
Connection {index + 1}
+
ID: {conn.connId}
+
+ Topics: {conn.topics.join(', ') || 'None'} +
+
+
+
+ Connected: {formatTimestamp(conn.connectedAt)} +
+
+ Last Activity: {formatTimestamp(conn.lastActivity)} +
+
+
+
+ ))} +
+
+
+ )} +
+
+ ); +} diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index 9726c0ed..7bcbed76 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -14,12 +14,19 @@ const LandingPage = () => ( - - {"Sign in"} - +
+ + {"Sign in"} + + +
+

🚀 SSE Testing Ready!

+

Use username login to test multiple profiles and real-time notifications

+
+
); diff --git a/src/app/(public)/signin/page.tsx b/src/app/(public)/signin/page.tsx new file mode 100644 index 00000000..004d374d --- /dev/null +++ b/src/app/(public)/signin/page.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { useState } from "react"; +import { signIn, getProviders } from "next-auth/react"; +import { useRouter } from "next/navigation"; + +export default function SignInPage() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(""); + const router = useRouter(); + + const handleUsernameLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(""); + + try { + const result = await signIn("username", { + username, + password, + redirect: false, + }); + + if (result?.error) { + setError("Invalid credentials. Use any username with password 'test123'"); + } else if (result?.ok) { + router.push("/home"); + } + } catch (error) { + setError("An error occurred during sign in"); + } finally { + setIsLoading(false); + } + }; + + const handleDiscordLogin = async () => { + setIsLoading(true); + setError(""); + + try { + await signIn("discord", { callbackUrl: "/home" }); + } catch (error) { + setError("Failed to sign in with Discord"); + setIsLoading(false); + } + }; + + return ( +
+
+
+

+ Sign in to your account +

+

+ Test with multiple profiles using username login +

+
+ +
+ {/* Username Login Form */} +
+
+
+ +
+ setUsername(e.target.value)} + className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + placeholder="Enter any username" + /> +
+
+ +
+ +
+ setPassword(e.target.value)} + className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + placeholder="Use 'test123' for any username" + /> +
+
+ + {error && ( +
{error}
+ )} + +
+ +
+
+ +
+
+
+
+
+
+ Or continue with +
+
+ +
+ +
+
+
+ + {/* Testing Instructions */} +
+

Testing Instructions

+
    +
  • • Use any username with password "test123"
  • +
  • • Open multiple Chrome profiles to test different users
  • +
  • • Each profile will have a unique session for SSE testing
  • +
  • • After testing, we'll remove this simple login system
  • +
+
+
+
+
+ ); +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 789f7d22..11132817 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,3 +1,5 @@ import { handlers } from "@/features/auth"; +// src/app/api/auth/[...nextauth]/route.ts + export const { GET, POST } = handlers; diff --git a/src/app/api/sse/route.ts b/src/app/api/sse/route.ts new file mode 100644 index 00000000..b5d83df2 --- /dev/null +++ b/src/app/api/sse/route.ts @@ -0,0 +1,160 @@ +import { NextRequest } from "next/server"; +import { auth as getServerSession } from "@/features/auth/handlers"; +import { sse as manager } from "@/server/sse"; +import { randomUUID } from "crypto"; +import { log } from "@/lib/logger"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +function sseLine(evt: { event?: string; data?: any; id?: string; retry?: number }) { + let out = ""; + if (evt.id) out += `id: ${evt.id}\n`; + if (evt.retry) out += `retry: ${evt.retry}\n`; + if (evt.event) out += `event: ${evt.event}\n`; + if (evt.data !== undefined) out += `data: ${typeof evt.data === "string" ? evt.data : JSON.stringify(evt.data)}\n`; + return out + "\n"; +} + +export async function GET(req: NextRequest) { + try { + const session = await getServerSession(); + if (!session?.user?.id) { + log.warn("Unauthorized SSE connection attempt"); + return new Response("Unauthorized", { status: 401 }); + } + + const { searchParams } = new URL(req.url); + const topics = (searchParams.get("topic") ?? "") + .split(",").map(s => s.trim()).filter(Boolean); + + const connId = randomUUID(); + const sessionId = (session as any).sessionToken as string | undefined; + const userAgent = req.headers.get("user-agent") || undefined; + const ip = req.headers.get("x-forwarded-for") || req.headers.get("x-real-ip") || "unknown"; + + log.info("SSE connection request", { + connId, + userId: session.user.id, + sessionId, + topics, + userAgent, + ip + }); + + const stream = new ReadableStream({ + start: async (controller) => { + const enc = new TextEncoder(); + const write = (s: string) => { + try { + controller.enqueue(enc.encode(s)); + } catch (error) { + log.error("Failed to write to SSE stream", { connId, error }); + } + }; + + // Send initial connection setup + write(sseLine({ retry: 5000 })); + + try { + const unregister = await manager.register({ + connId, + userId: session.user.id, + sessionId, + topics, + userAgent, + ip, + send: (evt) => { + try { + write(sseLine({ + event: evt.event, + data: evt.data, + id: evt.id + })); + } catch (error) { + log.error("Failed to send SSE event to client", { + connId, + event: evt.event, + error + }); + } + }, + }); + + // Send welcome message + write(sseLine({ + event: "welcome", + data: { + connId, + userId: session.user.id, + sessionId, + timestamp: Date.now() + } + })); + + // Set up heartbeat + const heartbeatInterval = setInterval(() => { + try { + write(`: heartbeat ${Date.now()}\n\n`); + } catch (error) { + log.error("Failed to send heartbeat", { connId, error }); + } + }, 30000); + + // Handle connection cleanup + const cleanup = async () => { + clearInterval(heartbeatInterval); + await unregister(); + try { + controller.close(); + } catch (error) { + log.error("Error closing SSE stream", { connId, error }); + } + }; + + // Listen for abort signal + req.signal.addEventListener("abort", cleanup); + + // Handle stream errors + req.signal.addEventListener("error", (error) => { + log.error("SSE stream error", { connId, error }); + cleanup(); + }); + + } catch (error) { + log.error("Failed to register SSE connection", { connId, error }); + write(sseLine({ + event: "error", + data: { + message: "Failed to establish connection", + code: "CONNECTION_ERROR" + } + })); + controller.close(); + } + }, + cancel: async () => { + try { + await manager.unregister(connId); + log.info("SSE connection cancelled", { connId }); + } catch (error) { + log.error("Error during SSE connection cancellation", { connId, error }); + } + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache, no-transform", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Cache-Control", + }, + }); + } catch (error) { + log.error("SSE route error", { error }); + return new Response("Internal Server Error", { status: 500 }); + } +} diff --git a/src/app/api/webhooks/sse-example/route.ts b/src/app/api/webhooks/sse-example/route.ts new file mode 100644 index 00000000..51eaaccd --- /dev/null +++ b/src/app/api/webhooks/sse-example/route.ts @@ -0,0 +1,74 @@ +import { NextRequest } from "next/server"; +import { notifyUser, notifyTopic, broadcast } from "@/server/sse"; +import { log } from "@/lib/logger"; + +export async function POST(req: NextRequest) { + try { + const payload = await req.json(); + + log.info("SSE Example webhook received", { + type: payload.type, + userId: payload.userId + }); + + // Example: Process different webhook types + switch (payload.type) { + case "user_notification": + // Send notification to specific user + if (payload.userId) { + await notifyUser(payload.userId, "webhook_notification", { + type: payload.type, + message: payload.message, + timestamp: Date.now(), + source: "webhook" + }); + } + break; + + case "system_alert": + // Broadcast system alert to all users + await broadcast("system_alert", { + level: payload.level || "info", + message: payload.message, + timestamp: Date.now(), + source: "webhook" + }); + break; + + case "topic_update": + // Send update to topic subscribers + if (payload.topic) { + await notifyTopic(payload.topic, "topic_update", { + data: payload.data, + timestamp: Date.now(), + source: "webhook" + }); + } + break; + + case "live_data": + // Send live data updates + await notifyTopic("live-data", "data_update", { + data: payload.data, + timestamp: Date.now(), + source: "webhook" + }); + break; + + default: + log.warn("Unknown webhook type", { type: payload.type }); + } + + return Response.json({ + success: true, + message: "Webhook processed and SSE notifications sent" + }); + + } catch (error) { + log.error("Error processing SSE example webhook", { error }); + return Response.json( + { success: false, error: "Failed to process webhook" }, + { status: 500 } + ); + } +} diff --git a/src/app/providers.tsx b/src/app/providers.tsx new file mode 100644 index 00000000..92e4d6b0 --- /dev/null +++ b/src/app/providers.tsx @@ -0,0 +1,73 @@ +'use client'; + +import type { QueryClient } from '@tanstack/react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { + httpBatchStreamLink, + httpSubscriptionLink, + loggerLink, + splitLink, +} from '@trpc/client'; +import { createQueryClient } from '..//lib/query-client'; +import { trpc } from '../lib/trpc'; +import { useState } from 'react'; +import SuperJSON from 'superjson'; + +let clientQueryClientSingleton: QueryClient | undefined = undefined; +const getQueryClient = () => { + if (typeof window === 'undefined') { + // Server: always make a new query client + return createQueryClient(); + } else { + // Browser: use singleton pattern to keep the same query client + return (clientQueryClientSingleton ??= createQueryClient()); + } +}; + +const getUrl = () => { + const base = (() => { + if (typeof window !== 'undefined') return window.location.origin; + if (process.env.APP_URL) return process.env.APP_URL; + return `http://localhost:${process.env.PORT ?? 3000}`; + })(); + + return `${base}/api/trpc`; +}; + +export function TRPCProviders(props: Readonly<{ children: React.ReactNode }>) { + const queryClient = getQueryClient(); + const [trpcClient] = useState(() => + trpc.createClient({ + links: [ + // adds pretty logs to your console in development and logs errors in production + loggerLink(), + splitLink({ + condition: (op) => op.type === 'subscription', + true: httpSubscriptionLink({ + url: getUrl(), + /** + * @see https://trpc.io/docs/v11/data-transformers + */ + transformer: SuperJSON, + }), + false: httpBatchStreamLink({ + url: getUrl(), + /** + * @see https://trpc.io/docs/v11/data-transformers + */ + transformer: SuperJSON, + }), + }), + ], + }), + ); + return ( + + + {props.children} + + + + ); +} diff --git a/src/config/routes.ts b/src/config/routes.ts index 8908c756..347dee64 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -29,8 +29,10 @@ import type { RouteData } from "@/types/routes"; // Ensure every route in the app has an entry here export const paths = { landingPage: "/", + signinPage: "/signin", homePage: "/home", reelsUploadPage: "/reels/upload", + sseDemoPage: "/sse-demo", } as const; // ⚠️ DEFINE METADATA FOR NEW ROUTES HERE ⚠️ @@ -41,6 +43,11 @@ export const routes: Record = { path: paths.landingPage, accessType: "public", }, + signinPage: { + name: "Sign In Page", + path: paths.signinPage, + accessType: "public", + }, homePage: { name: "Home Page", path: paths.homePage, @@ -51,4 +58,9 @@ export const routes: Record = { path: paths.reelsUploadPage, accessType: "protected", }, + sseDemoPage: { + name: "SSE Demo Page", + path: paths.sseDemoPage, + accessType: "protected", + }, }; diff --git a/src/env.js b/src/env.js index 4564a09f..ddb6ea63 100644 --- a/src/env.js +++ b/src/env.js @@ -11,31 +11,43 @@ export const env = createEnv({ process.env.NODE_ENV === "production" ? z.string() : z.string().optional(), - AUTH_DISCORD_ID: z.string(), - AUTH_DISCORD_SECRET: z.string(), + // Align with Discord provider used in auth config. Optional in dev. + AUTH_DISCORD_ID: + process.env.NODE_ENV === "production" ? z.string() : z.string().optional(), + AUTH_DISCORD_SECRET: + process.env.NODE_ENV === "production" ? z.string() : z.string().optional(), DATABASE_URL: z.string().url(), NODE_ENV: z .enum(["development", "test", "production"]) .default("development"), - // Mux environment variables - MUX_TOKEN_ID: z.string().uuid(), - MUX_TOKEN_SECRET: z.string().length(75), - MUX_SIGNING_KEY_ID: z.string().length(45), - MUX_SIGNING_KEY_SECRET: z - .string() - .min(200) - .refine((val) => val.endsWith("==")), - MUX_VIDEO_QUALITY: z.enum(["basic", "plus", "premium"]), - MUX_WEBHOOK_SECRET: z.string().min(32), + // Mux environment variables (optional in dev) + MUX_TOKEN_ID: + process.env.NODE_ENV === "production" ? z.string().uuid() : z.string().uuid().optional(), + MUX_TOKEN_SECRET: + process.env.NODE_ENV === "production" ? z.string().length(75) : z.string().length(75).optional(), + MUX_SIGNING_KEY_ID: + process.env.NODE_ENV === "production" ? z.string().length(45) : z.string().length(45).optional(), + MUX_SIGNING_KEY_SECRET: + process.env.NODE_ENV === "production" + ? z.string().min(200).refine((val) => val.endsWith("==")) + : z.string().min(200).refine((val) => val.endsWith("==")).optional(), + MUX_VIDEO_QUALITY: + process.env.NODE_ENV === "production" ? z.enum(["basic", "plus", "premium"]) : z.enum(["basic", "plus", "premium"]).optional(), + MUX_WEBHOOK_SECRET: + process.env.NODE_ENV === "production" ? z.string().min(32) : z.string().min(32).optional(), - // Meilisearch environment variables - MEILISEARCH_HOST: z.string().url(), - MEILISEARCH_API_KEY: z.string(), + // Meilisearch environment variables (optional in dev) + MEILISEARCH_HOST: + process.env.NODE_ENV === "production" ? z.string().url() : z.string().url().optional(), + MEILISEARCH_API_KEY: + process.env.NODE_ENV === "production" ? z.string() : z.string().optional(), - // Resend environment variables - RESEND_API_KEY: z.string().length(36), - RESEND_TO_DEV_ADDRESS: z.string().email(), + // Resend environment variables (optional in dev) + RESEND_API_KEY: + process.env.NODE_ENV === "production" ? z.string().length(36) : z.string().length(36).optional(), + RESEND_TO_DEV_ADDRESS: + process.env.NODE_ENV === "production" ? z.string().email() : z.string().email().optional(), RESEND_FROM_EMAIL: z.string().refine((val) => { // The "from" email address can be in the format "Name " or just "my@email.com". // We need to extract the email part and validate it. diff --git a/src/features/auth/config/next-auth.ts b/src/features/auth/config/next-auth.ts index aaf11523..90dc57f2 100644 --- a/src/features/auth/config/next-auth.ts +++ b/src/features/auth/config/next-auth.ts @@ -1,6 +1,8 @@ import { PrismaAdapter } from "@auth/prisma-adapter"; import { type DefaultSession, type NextAuthConfig } from "next-auth"; import DiscordProvider from "next-auth/providers/discord"; +import CredentialsProvider from "next-auth/providers/credentials"; +import { z } from "zod"; import { db } from "@/lib/db"; @@ -30,19 +32,69 @@ declare module "next-auth" { * * @see https://next-auth.js.org/configuration/options */ +const providers: NextAuthConfig["providers"] = []; + +// Always include simple username login for testing multiple profiles +providers.push( + CredentialsProvider({ + id: "username", + name: "Username Login", + credentials: { + username: { label: "Username", type: "text", placeholder: "Enter username" }, + password: { label: "Password", type: "password", placeholder: "Enter password" } + }, + async authorize(input) { + const creds = z + .object({ + username: z.string().min(1), + password: z.string().min(1) + }) + .safeParse(input); + if (!creds.success) return null; + + const { username, password } = creds.data; + + // Simple test credentials - in production, you'd hash and check against database + // For now, accept any username with password "test123" + if (password === "test123") { + return { + id: `user-${username}`, + name: username, + email: `${username}@test.local`, + image: `https://api.dicebear.com/7.x/initials/svg?seed=${username}` + }; + } + + return null; + }, + }) +); + +// Include Discord if env vars are set +const hasDiscordEnv = Boolean(process.env.AUTH_DISCORD_ID && process.env.AUTH_DISCORD_SECRET); +if (hasDiscordEnv) { + providers.push(DiscordProvider); +} else { + // Fallback dev credentials when Discord not configured + providers.push( + CredentialsProvider({ + id: "dev", + name: "Dev Login", + credentials: { name: { label: "Name", type: "text" } }, + async authorize(input) { + const creds = z + .object({ name: z.string().min(1) }) + .safeParse(input); + if (!creds.success) return null; + const name = creds.data.name; + return { id: name, name, email: `${name}@example.test` }; + }, + }) + ); +} + export const nextAuthConfig = { - providers: [ - DiscordProvider, - /** - * ...add more providers here. - * - * Most other providers require a bit more work than the Discord provider. For example, the - * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account - * model. Refer to the NextAuth.js docs for the provider you want to use. Example: - * - * @see https://next-auth.js.org/providers/github - */ - ], + providers, adapter: PrismaAdapter(db), callbacks: { session: ({ session, user }) => ({ diff --git a/src/hooks/useSSE.ts b/src/hooks/useSSE.ts new file mode 100644 index 00000000..8b0d24f3 --- /dev/null +++ b/src/hooks/useSSE.ts @@ -0,0 +1,275 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; + +interface SSEOptions { + topics?: string[]; + onMessage?: (event: string, data: any) => void; + onConnect?: () => void; + onDisconnect?: () => void; + onError?: (error: Event) => void; + autoReconnect?: boolean; + reconnectInterval?: number; + maxReconnectAttempts?: number; +} + +interface SSEState { + status: 'connecting' | 'connected' | 'disconnected' | 'error'; + connectionInfo: any; + error: string | null; + reconnectAttempts: number; +} + +export function useSSE(options: SSEOptions = {}) { + const { + topics = [], + onMessage, + onConnect, + onDisconnect, + onError, + autoReconnect = true, + reconnectInterval = 5000, + maxReconnectAttempts = 5 + } = options; + + const [state, setState] = useState({ + status: 'disconnected', + connectionInfo: null, + error: null, + reconnectAttempts: 0 + }); + + const eventSourceRef = useRef(null); + const reconnectTimeoutRef = useRef(null); + const reconnectAttemptsRef = useRef(0); + + const connect = useCallback(() => { + if (eventSourceRef.current) { + eventSourceRef.current.close(); + } + + setState(prev => ({ ...prev, status: 'connecting', error: null })); + + try { + const topicParam = topics.length > 0 ? `?topic=${topics.join(',')}` : ''; + const eventSource = new EventSource(`/api/sse${topicParam}`, { + withCredentials: true + }); + + eventSource.addEventListener('open', () => { + setState(prev => ({ + ...prev, + status: 'connected', + error: null, + reconnectAttempts: 0 + })); + reconnectAttemptsRef.current = 0; + onConnect?.(); + }); + + eventSource.addEventListener('message', (e) => { + try { + const data = JSON.parse(e.data); + onMessage?.('message', data); + } catch (error) { + onMessage?.('message', e.data); + } + }); + + eventSource.addEventListener('welcome', (e) => { + try { + const data = JSON.parse(e.data); + setState(prev => ({ ...prev, connectionInfo: data })); + onMessage?.('welcome', data); + } catch (error) { + console.error('Failed to parse welcome message:', error); + } + }); + + // Listen for custom events + const customEvents = [ + 'test_message', 'topic_message', 'broadcast_message', + 'new_message', 'system_update', 'live_update', + 'webhook_notification', 'system_alert', 'topic_update', + 'data_update' + ]; + + customEvents.forEach(eventType => { + eventSource.addEventListener(eventType, (e) => { + try { + const data = JSON.parse(e.data); + onMessage?.(eventType, data); + } catch (error) { + onMessage?.(eventType, e.data); + } + }); + }); + + eventSource.addEventListener('error', (e) => { + setState(prev => ({ + ...prev, + status: 'error', + error: 'Connection error occurred' + })); + onError?.(e); + }); + + eventSource.addEventListener('close', () => { + setState(prev => ({ ...prev, status: 'disconnected' })); + onDisconnect?.(); + }); + + eventSourceRef.current = eventSource; + + } catch (error) { + setState(prev => ({ + ...prev, + status: 'error', + error: 'Failed to create connection' + })); + onError?.(error as Event); + } + }, [topics, onMessage, onConnect, onDisconnect, onError]); + + const disconnect = useCallback(() => { + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + reconnectTimeoutRef.current = null; + } + + if (eventSourceRef.current) { + eventSourceRef.current.close(); + eventSourceRef.current = null; + } + + setState(prev => ({ + ...prev, + status: 'disconnected', + error: null, + reconnectAttempts: 0 + })); + reconnectAttemptsRef.current = 0; + }, []); + + const reconnect = useCallback(() => { + if (!autoReconnect || reconnectAttemptsRef.current >= maxReconnectAttempts) { + setState(prev => ({ + ...prev, + status: 'error', + error: 'Max reconnection attempts reached' + })); + return; + } + + reconnectAttemptsRef.current++; + setState(prev => ({ + ...prev, + reconnectAttempts: reconnectAttemptsRef.current + })); + + const delay = reconnectInterval * Math.pow(2, reconnectAttemptsRef.current - 1); + + reconnectTimeoutRef.current = setTimeout(() => { + connect(); + }, delay); + }, [autoReconnect, maxReconnectAttempts, reconnectInterval, connect]); + + // Auto-reconnect on error + useEffect(() => { + if (state.status === 'error' && autoReconnect) { + reconnect(); + } + }, [state.status, autoReconnect, reconnect]); + + // Initial connection + useEffect(() => { + connect(); + + return () => { + disconnect(); + }; + }, [connect, disconnect]); + + // Cleanup on unmount + useEffect(() => { + return () => { + disconnect(); + }; + }, [disconnect]); + + return { + status: state.status, + connectionInfo: state.connectionInfo, + error: state.error, + reconnectAttempts: state.reconnectAttempts, + connect, + disconnect, + reconnect + }; +} + +// Hook for listening to specific events +export function useSSEEvent(eventType: string, callback: (data: any) => void) { + const [lastEvent, setLastEvent] = useState(null); + + const handleMessage = useCallback((event: string, data: any) => { + if (event === eventType) { + setLastEvent(data); + callback(data); + } + }, [eventType, callback]); + + const sse = useSSE({ onMessage: handleMessage }); + + return { + ...sse, + lastEvent + }; +} + +// Hook for user-specific notifications +export function useUserNotifications(userId: string) { + const [notifications, setNotifications] = useState([]); + + const handleMessage = useCallback((event: string, data: any) => { + if (event === 'new_message' || event === 'webhook_notification') { + setNotifications(prev => [data, ...prev.slice(0, 49)]); // Keep last 50 + } + }, []); + + const sse = useSSE({ onMessage: handleMessage }); + + const clearNotifications = useCallback(() => { + setNotifications([]); + }, []); + + return { + ...sse, + notifications, + clearNotifications + }; +} + +// Hook for topic subscriptions +export function useTopicSubscription(topic: string) { + const [updates, setUpdates] = useState([]); + + const handleMessage = useCallback((event: string, data: any) => { + if (event === 'topic_update' || event === 'live_update' || event === 'data_update') { + setUpdates(prev => [data, ...prev.slice(0, 49)]); // Keep last 50 + } + }, []); + + const sse = useSSE({ + topics: [topic], + onMessage: handleMessage + }); + + const clearUpdates = useCallback(() => { + setUpdates([]); + }, []); + + return { + ...sse, + updates, + clearUpdates + }; +} diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 00000000..97d0156c --- /dev/null +++ b/src/lib/logger.ts @@ -0,0 +1,6 @@ + +export const log = { + info: console.log, + error: console.error, + warn: console.warn, + }; \ No newline at end of file diff --git a/src/lib/query-client.ts b/src/lib/query-client.ts new file mode 100644 index 00000000..58333eb2 --- /dev/null +++ b/src/lib/query-client.ts @@ -0,0 +1,12 @@ +import { QueryClient } from '@tanstack/react-query'; + +export const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 30 * 1000, + }, + }, + }); diff --git a/src/lib/trpc/root.ts b/src/lib/trpc/root.ts index 129dcabc..a9f27414 100644 --- a/src/lib/trpc/root.ts +++ b/src/lib/trpc/root.ts @@ -1,5 +1,6 @@ import { createCallerFactory, createTRPCRouter } from "@/lib/trpc"; import { searchRouter } from "@/features/search"; +import { sseRouter } from "@/server/api/routers/sse"; /** * This is the primary router for your server. @@ -8,6 +9,7 @@ import { searchRouter } from "@/features/search"; */ export const appRouter = createTRPCRouter({ search: searchRouter, + sse: sseRouter, }); // export type definition of API diff --git a/src/server/api/routers/sse.ts b/src/server/api/routers/sse.ts new file mode 100644 index 00000000..33bea193 --- /dev/null +++ b/src/server/api/routers/sse.ts @@ -0,0 +1,188 @@ +import { createTRPCRouter, protectedProcedure } from "@/lib/trpc"; +import { z } from "zod"; +import { + notifyUser, + broadcast, + notifyTopic, + notifySession, + notifyConnection, + notifyNewMessage, + notifySystemUpdate, + notifyUserStatus, + notifyLiveUpdate, + getStats, + getConnectionInfo, + getUserConnections, + getTopicConnections, + healthCheck +} from "@/server/sse"; + +export const sseRouter = createTRPCRouter({ + // Basic test endpoints + debugSend: protectedProcedure.mutation(async ({ ctx }) => { + await notifyUser(ctx.session.user.id, "test", { ok: true, at: Date.now() }); + await broadcast("test", { msg: "Hello all" }); + return { ok: true }; + }), + + // Send test message to current user + sendTestToUser: protectedProcedure + .input(z.object({ + message: z.string().optional(), + event: z.string().optional() + })) + .mutation(async ({ ctx, input }) => { + const event = input.event || "test_message"; + const message = input.message || "Hello from SSE!"; + + await notifyUser(ctx.session.user.id, event, { + message, + timestamp: Date.now(), + userId: ctx.session.user.id + }); + + return { success: true, event, message }; + }), + + // Send test message to a topic + sendTestToTopic: protectedProcedure + .input(z.object({ + topic: z.string(), + message: z.string().optional(), + event: z.string().optional() + })) + .mutation(async ({ input }) => { + const event = input.event || "topic_message"; + const message = input.message || "Hello from topic!"; + + await notifyTopic(input.topic, event, { + message, + timestamp: Date.now(), + topic: input.topic + }); + + return { success: true, event, message, topic: input.topic }; + }), + + // Send test broadcast + sendTestBroadcast: protectedProcedure + .input(z.object({ + message: z.string().optional(), + event: z.string().optional() + })) + .mutation(async ({ input }) => { + const event = input.event || "broadcast_message"; + const message = input.message || "Hello everyone!"; + + await broadcast(event, { + message, + timestamp: Date.now() + }); + + return { success: true, event, message }; + }), + + // Send test message to specific session + sendTestToSession: protectedProcedure + .input(z.object({ + sessionId: z.string(), + message: z.string().optional(), + event: z.string().optional() + })) + .mutation(async ({ input }) => { + const event = input.event || "session_message"; + const message = input.message || "Hello from session!"; + + await notifySession(input.sessionId, event, { + message, + timestamp: Date.now(), + sessionId: input.sessionId + }); + + return { success: true, event, message, sessionId: input.sessionId }; + }), + + // Utility notification endpoints + sendNewMessage: protectedProcedure + .input(z.object({ + userId: z.string(), + message: z.object({ + id: z.string(), + content: z.string(), + sender: z.string(), + roomId: z.string().optional() + }) + })) + .mutation(async ({ input }) => { + await notifyNewMessage(input.userId, input.message); + return { success: true }; + }), + + sendSystemUpdate: protectedProcedure + .input(z.object({ + event: z.string(), + data: z.any() + })) + .mutation(async ({ input }) => { + await notifySystemUpdate(input.event, input.data); + return { success: true }; + }), + + sendUserStatus: protectedProcedure + .input(z.object({ + userId: z.string(), + status: z.string() + })) + .mutation(async ({ input }) => { + await notifyUserStatus(input.userId, input.status); + return { success: true }; + }), + + sendLiveUpdate: protectedProcedure + .input(z.object({ + topic: z.string(), + data: z.any() + })) + .mutation(async ({ input }) => { + await notifyLiveUpdate(input.topic, input.data); + return { success: true }; + }), + + // Monitoring and management endpoints + getStats: protectedProcedure.query(() => { + return getStats(); + }), + + getHealth: protectedProcedure.query(() => { + return healthCheck(); + }), + + getConnectionInfo: protectedProcedure + .input(z.object({ + connId: z.string() + })) + .query(({ input }) => { + return getConnectionInfo(input.connId); + }), + + getUserConnections: protectedProcedure + .input(z.object({ + userId: z.string() + })) + .query(({ input }) => { + return getUserConnections(input.userId); + }), + + getTopicConnections: protectedProcedure + .input(z.object({ + topic: z.string() + })) + .query(({ input }) => { + return getTopicConnections(input.topic); + }), + + // Get current user's connections + getMyConnections: protectedProcedure.query(({ ctx }) => { + return getUserConnections(ctx.session.user.id); + }), +}); diff --git a/src/server/context.ts b/src/server/context.ts new file mode 100644 index 00000000..3a753441 --- /dev/null +++ b/src/server/context.ts @@ -0,0 +1,18 @@ +import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'; +import { auth } from './auth'; + +/** + * Creates context for an incoming request + * @see https://trpc.io/docs/v11/context + */ +export const createContext = async (opts: FetchCreateContextFnOptions) => { + const session = await auth(); + + console.log('createContext for', session?.user?.name ?? 'unknown user'); + + return { + session, + }; +}; + +export type Context = Awaited>; diff --git a/src/server/db/client.ts b/src/server/db/client.ts new file mode 100644 index 00000000..3f6b05e5 --- /dev/null +++ b/src/server/db/client.ts @@ -0,0 +1,11 @@ +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import * as schema from './schema'; + +const DB_URL = process.env.DATABASE_URL ?? process.env.POSTGRES_URL; +if (!DB_URL) { + throw new Error('Missing POSTGRES_URL or DATABASE_URL environment variable'); +} + +const queryClient = postgres(DB_URL); +export const db = drizzle(queryClient, { schema }); diff --git a/src/server/db/migrations/0000_lyrical_khan.sql b/src/server/db/migrations/0000_lyrical_khan.sql new file mode 100644 index 00000000..20ff7883 --- /dev/null +++ b/src/server/db/migrations/0000_lyrical_khan.sql @@ -0,0 +1,21 @@ +CREATE TABLE IF NOT EXISTS "sse-chat_channel" ( + "id" text PRIMARY KEY NOT NULL, + "name" text, + "created_at" timestamp (3) with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp (3) with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "sse-chat_post" ( + "id" text PRIMARY KEY NOT NULL, + "channel_id" text, + "name" text, + "text" text, + "created_at" timestamp (3) with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp (3) with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "sse-chat_post" ADD CONSTRAINT "sse-chat_post_channel_id_sse-chat_channel_id_fk" FOREIGN KEY ("channel_id") REFERENCES "public"."sse-chat_channel"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/src/server/db/migrations/0001_wet_tarantula.sql b/src/server/db/migrations/0001_wet_tarantula.sql new file mode 100644 index 00000000..e6737f46 --- /dev/null +++ b/src/server/db/migrations/0001_wet_tarantula.sql @@ -0,0 +1,3 @@ +ALTER TABLE "sse-chat_post" ALTER COLUMN "channel_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "sse-chat_post" ALTER COLUMN "name" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "sse-chat_post" ALTER COLUMN "text" SET NOT NULL; \ No newline at end of file diff --git a/src/server/db/migrations/meta/0000_snapshot.json b/src/server/db/migrations/meta/0000_snapshot.json new file mode 100644 index 00000000..2bcb25e7 --- /dev/null +++ b/src/server/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,109 @@ +{ + "id": "726a54a4-0cb9-44fa-bce5-aee86c5ae30f", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.sse-chat_channel": { + "name": "sse-chat_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.sse-chat_post": { + "name": "sse-chat_post", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sse-chat_post_channel_id_sse-chat_channel_id_fk": { + "name": "sse-chat_post_channel_id_sse-chat_channel_id_fk", + "tableFrom": "sse-chat_post", + "tableTo": "sse-chat_channel", + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/src/server/db/migrations/meta/0001_snapshot.json b/src/server/db/migrations/meta/0001_snapshot.json new file mode 100644 index 00000000..b29cb554 --- /dev/null +++ b/src/server/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,109 @@ +{ + "id": "720da587-6309-4c5a-83ed-230a080c0b88", + "prevId": "726a54a4-0cb9-44fa-bce5-aee86c5ae30f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.sse-chat_channel": { + "name": "sse-chat_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.sse-chat_post": { + "name": "sse-chat_post", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sse-chat_post_channel_id_sse-chat_channel_id_fk": { + "name": "sse-chat_post_channel_id_sse-chat_channel_id_fk", + "tableFrom": "sse-chat_post", + "tableTo": "sse-chat_channel", + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/src/server/db/migrations/meta/_journal.json b/src/server/db/migrations/meta/_journal.json new file mode 100644 index 00000000..3dc62870 --- /dev/null +++ b/src/server/db/migrations/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1718001586910, + "tag": "0000_lyrical_khan", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1721401000711, + "tag": "0001_wet_tarantula", + "breakpoints": true + } + ] +} diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts new file mode 100644 index 00000000..f96b75a7 --- /dev/null +++ b/src/server/db/schema.ts @@ -0,0 +1,66 @@ +import type { InferSelectModel } from 'drizzle-orm'; +import { relations } from 'drizzle-orm'; +import { pgTableCreator, text, timestamp } from 'drizzle-orm/pg-core'; + +const pgTable = pgTableCreator((name) => `sse-chat_${name}`); + +export const Post = pgTable('post', { + id: text('id') + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + channelId: text('channel_id') + .notNull() + .references(() => Channel.id), + + name: text('name').notNull(), + text: text('text').notNull(), + + createdAt: timestamp('created_at', { + mode: 'date', + precision: 3, + withTimezone: true, + }) + .notNull() + .defaultNow(), + updatedAt: timestamp('updated_at', { + mode: 'date', + precision: 3, + withTimezone: true, + }) + .notNull() + .defaultNow() + .$onUpdateFn(() => new Date()), +}); +export type PostType = InferSelectModel; + +export const Channel = pgTable('channel', { + id: text('id') + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + name: text('name'), + + createdAt: timestamp('created_at', { + mode: 'date', + precision: 3, + withTimezone: true, + }) + .notNull() + .defaultNow(), + updatedAt: timestamp('updated_at', { + mode: 'date', + precision: 3, + withTimezone: true, + }) + .notNull() + .defaultNow() + .$onUpdateFn(() => new Date()), +}); +export type ChannelType = InferSelectModel; + +export const PostRelations = relations(Post, ({ one }) => ({ + channel: one(Channel, { fields: [Post.channelId], references: [Channel.id] }), +})); + +export const ChannelRelations = relations(Channel, ({ many }) => ({ + posts: many(Post), +})); diff --git a/src/server/routers/_app.ts b/src/server/routers/_app.ts new file mode 100644 index 00000000..e72c85c1 --- /dev/null +++ b/src/server/routers/_app.ts @@ -0,0 +1,39 @@ +/** + * This file contains the root router of your tRPC-backend + */ +import { observable } from '@trpc/server/observable'; +import { createCallerFactory } from '@trpc/server/unstable-core-do-not-import'; +import { cache } from 'react'; +import { auth } from '../auth'; +import type { Context } from '../context'; +import { publicProcedure, router } from '../trpc'; +import { channelRouter } from './channel'; +import { postRouter } from './post'; + +export const appRouter = router({ + healthcheck: publicProcedure.query(() => 'yay!'), + + channel: channelRouter, + post: postRouter, + + randomNumber: publicProcedure.subscription(() => { + return observable((emit) => { + const int = setInterval(() => { + emit.next(Math.random()); + }, 500); + return () => { + clearInterval(int); + }; + }); + }), +}); + +export type AppRouter = typeof appRouter; + +const createCallerContext = cache( + async (): Promise => ({ + session: await auth(), + }), +); + +export const caller = createCallerFactory()(appRouter)(createCallerContext); diff --git a/src/server/routers/channel.ts b/src/server/routers/channel.ts new file mode 100644 index 00000000..93a517b1 --- /dev/null +++ b/src/server/routers/channel.ts @@ -0,0 +1,122 @@ +import EventEmitter, { on } from 'node:events'; +import type { TRPCRouterRecord } from '@trpc/server'; +import { db } from '~/server/db/client'; +import type { PostType } from '~/server/db/schema'; +import { Channel } from '~/server/db/schema'; +import { authedProcedure, publicProcedure } from '~/server/trpc'; +import { z } from 'zod'; + +export type WhoIsTyping = Record; + +type EventMap = Record; +class IterableEventEmitter> extends EventEmitter { + toIterable( + eventName: TEventName, + opts?: NonNullable[2]>, + ): AsyncIterable { + return on(this as any, eventName, opts) as any; + } +} + +export interface MyEvents { + add: [channelId: string, data: PostType]; + isTypingUpdate: [channelId: string, who: WhoIsTyping]; +} + +// In a real app, you'd probably use Redis or something +export const ee = new IterableEventEmitter(); + +// who is currently typing for each channel, key is `name` +export const currentlyTyping: Record = Object.create(null); + +// every 1s, clear old "isTyping" +setInterval(() => { + const updatedChannels = new Set(); + const now = Date.now(); + for (const [channelId, typers] of Object.entries(currentlyTyping)) { + for (const [key, value] of Object.entries(typers ?? {})) { + if (now - value.lastTyped.getTime() > 3e3) { + delete typers[key]; + updatedChannels.add(channelId); + } + } + } + updatedChannels.forEach((channelId) => { + ee.emit('isTypingUpdate', channelId, currentlyTyping[channelId] ?? {}); + }); +}, 3e3).unref(); + +export const channelRouter = { + list: publicProcedure.query(() => { + return db.query.Channel.findMany(); + }), + + create: authedProcedure + .input(z.object({ name: z.string().trim().min(2) })) + .mutation(async ({ ctx, input }) => { + const [channel] = await db + .insert(Channel) + .values({ + name: input.name, + }) + .returning(); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return channel!.id; + }), + + isTyping: authedProcedure + .input(z.object({ channelId: z.string().uuid(), typing: z.boolean() })) + .mutation(async (opts) => { + const { name } = opts.ctx.user; + const { channelId } = opts.input; + + currentlyTyping[channelId] ??= {}; + + if (!opts.input.typing) { + delete currentlyTyping[channelId][name]; + } else { + currentlyTyping[channelId][name] = { + lastTyped: new Date(), + }; + } + ee.emit('isTypingUpdate', channelId, currentlyTyping[channelId]); + }), + + whoIsTyping: publicProcedure + .input( + z.object({ + channelId: z.string().uuid(), + }), + ) + .subscription(async function* (opts) { + const { channelId } = opts.input; + + let lastIsTyping = ''; + + /** + * yield who is typing if it has changed + * won't yield if it's the same as last time + */ + function* maybeYield(who: WhoIsTyping) { + const idx = Object.keys(who).toSorted().toString(); + if (idx === lastIsTyping) { + return; + } + yield Object.keys(who); + + lastIsTyping = idx; + } + + // emit who is currently typing + yield* maybeYield(currentlyTyping[channelId] ?? {}); + + for await (const [channelId, who] of ee.toIterable('isTypingUpdate', { + signal: opts.signal, + })) { + if (channelId === opts.input.channelId) { + yield* maybeYield(who); + } + } + }), +} satisfies TRPCRouterRecord; diff --git a/src/server/routers/post.ts b/src/server/routers/post.ts new file mode 100644 index 00000000..732f2912 --- /dev/null +++ b/src/server/routers/post.ts @@ -0,0 +1,142 @@ +import { tracked } from '@trpc/server'; +import { db } from '~/server/db/client'; +import { Post, type PostType } from '~/server/db/schema'; +import { z } from 'zod'; +import { authedProcedure, publicProcedure, router } from '../trpc'; +import { currentlyTyping, ee } from './channel'; + +export const postRouter = router({ + add: authedProcedure + .input( + z.object({ + id: z.string().uuid().optional(), + channelId: z.string().uuid(), + text: z.string().trim().min(1), + }), + ) + .mutation(async (opts) => { + const { channelId } = opts.input; + + const [post] = await db + .insert(Post) + .values({ + id: opts.input.id, + text: opts.input.text, + name: opts.ctx.user.name, + channelId, + }) + .returning(); + + const channelTyping = currentlyTyping[channelId]; + if (channelTyping) { + delete channelTyping[opts.ctx.user.name]; + ee.emit('isTypingUpdate', channelId, channelTyping); + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const defPost = post!; + ee.emit('add', channelId, defPost); + + return post; + }), + + infinite: publicProcedure + .input( + z.object({ + channelId: z.string().uuid(), + cursor: z.date().nullish(), + take: z.number().min(1).max(50).nullish(), + }), + ) + .query(async (opts) => { + const take = opts.input.take ?? 20; + const cursor = opts.input.cursor; + + const page = await db.query.Post.findMany({ + orderBy: (fields, ops) => ops.desc(fields.createdAt), + where: (fields, ops) => + ops.and( + ops.eq(fields.channelId, opts.input.channelId), + cursor ? ops.lte(fields.createdAt, cursor) : undefined, + ), + limit: take + 1, + }); + + const items = page.reverse(); + let nextCursor: typeof cursor | null = null; + if (items.length > take) { + const prev = items.shift(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + nextCursor = prev!.createdAt; + } + return { + items, + nextCursor, + }; + }), + + onAdd: publicProcedure + .input( + z.object({ + channelId: z.string().uuid(), + // lastEventId is the last event id that the client has received + // On the first call, it will be whatever was passed in the initial setup + // If the client reconnects, it will be the last event id that the client received + lastEventId: z.string().nullish(), + }), + ) + .subscription(async function* (opts) { + // We start by subscribing to the event emitter so that we don't miss any new events while fetching + const iterable = ee.toIterable('add', { + signal: opts.signal, + }); + + // Fetch the last message createdAt based on the last event id + let lastMessageCreatedAt = await (async () => { + const lastEventId = opts.input.lastEventId; + if (!lastEventId) return null; + + const itemById = await db.query.Post.findFirst({ + where: (fields, ops) => ops.eq(fields.id, lastEventId), + }); + return itemById?.createdAt ?? null; + })(); + + const newPostsSinceLastMessage = await db.query.Post.findMany({ + where: (fields, ops) => + ops.and( + ops.eq(fields.channelId, opts.input.channelId), + lastMessageCreatedAt + ? ops.gt(fields.createdAt, lastMessageCreatedAt) + : undefined, + ), + orderBy: (fields, ops) => ops.asc(fields.createdAt), + }); + + function* maybeYield(post: PostType) { + if (post.channelId !== opts.input.channelId) { + // ignore posts from other channels - the event emitter can emit from other channels + return; + } + if (lastMessageCreatedAt && post.createdAt <= lastMessageCreatedAt) { + // ignore posts that we've already sent - happens if there is a race condition between the query and the event emitter + return; + } + + yield tracked(post.id, post); + + // update the cursor so that we don't send this post again + lastMessageCreatedAt = post.createdAt; + } + + // yield the posts we fetched from the db + for (const post of newPostsSinceLastMessage) { + yield* maybeYield(post); + } + + // yield any new posts from the event emitter + for await (const [channelId, post] of iterable) { + yield* maybeYield(post); + } + }), +}); diff --git a/src/server/sse/ACCEPTANCE.md b/src/server/sse/ACCEPTANCE.md new file mode 100644 index 00000000..a837c75f --- /dev/null +++ b/src/server/sse/ACCEPTANCE.md @@ -0,0 +1,162 @@ +# SSE Acceptance Guide + +This document captures the acceptance criteria, flow, verification steps, and edge cases for the SSE feature: "SSE endpoint implemented to accept client connections and maintain open streams." + +## Scope + +- Server supports establishing Server-Sent Events (SSE) connections from authenticated clients. +- Connection remains open and continuously streams events until the client disconnects or an error occurs. +- Heartbeats are sent to keep the connection active through proxies. +- Proper cleanup happens on disconnect or error. + +## Acceptance Criteria (Verified) + +1. SSE endpoint implemented to accept client connections and maintain open streams + - Evidence: + - Endpoint implemented at `src/app/api/sse/route.ts` using a `ReadableStream` and returning appropriate SSE headers (`text/event-stream`, `keep-alive`, `no-cache`). + +2. Clients can subscribe and receive events pushed from the server + - Evidence: + - Demo client in `src/app/(protected)/sse-demo/page.tsx` establishes `EventSource` to `/api/sse`, listens for `message`, `welcome`, and named events. + - Reusable hook in `src/hooks/useSSE.ts` supports topics and event handlers. + +3. Server code can send arbitrary named events with JSON payloads to individual or multiple clients + - Evidence: + - API helpers in `src/server/sse/index.ts`: `notifyUser`, `notifySession`, `notifyTopic`, `broadcast`, `notifyConnection` accept an event name and arbitrary data. + - tRPC router in `src/server/api/routers/sse.ts` exposes test mutations allowing custom `event` names and payloads. + +4. Heartbeat/ping mechanism in place to keep connections alive + - Evidence: + - Route-level heartbeat comment lines every 30s in `src/app/api/sse/route.ts`. + - Manager-level broadcast heartbeat every 30s in `src/server/sse/manager.ts`. + +5. Proper handling of client disconnects with cleanup of server resources + - Evidence: + - Abort handling and cleanup in `src/app/api/sse/route.ts` unregisters connections and closes the stream. + - Manager `unregister` disposes of bus subscriptions and removes connection (`src/server/sse/manager.ts`). + +6. Error handling and logging included + - Evidence: + - Logging via `src/lib/logger.ts` in route and manager for connection attempts, sends, failures, cleanup, and errors. + - Stream write and event send operations wrapped with try/catch and error logs. + +7. Well-documented usage for backend integration + - Evidence: + - Developer documentation in `src/server/sse/README.md` (Quick Start, API Reference, Integration Examples). + - This `ACCEPTANCE.md` provides criteria, flow, verification, and edge cases. + +## Components Involved + +- Endpoint: `GET /api/sse` in `src/app/api/sse/route.ts` +- Manager: `ConnectionManager` in `src/server/sse/manager.ts` +- API helpers: `notifyUser`, `notifyTopic`, `broadcast`, etc. in `src/server/sse/index.ts` +- Demo Client: `src/app/(protected)/sse-demo/page.tsx` + +## Functional Acceptance Criteria + +1. Authentication + - If the user is not authenticated, the endpoint returns `401 Unauthorized` and does not create a stream. + - If the user is authenticated, the server proceeds to establish an SSE stream. + +2. SSE Response + - Response headers include: + - `Content-Type: text/event-stream` + - `Cache-Control: no-cache, no-transform` + - `Connection: keep-alive` + - `X-Accel-Buffering: no` + - The response body is a `ReadableStream` that remains open. + +3. Connection Lifecycle + - On connect, server sends an initial `retry` line and a `welcome` event with connection metadata. + - Server registers the connection with `ConnectionManager`, subscribing to broadcast, user, session, and optional topic channels. + - Heartbeats are sent at a fixed interval to keep intermediaries from closing the stream. + - On client disconnect or error, the server unregisters the connection and closes the stream. + +4. Event Delivery + - Events published via `notifyUser`, `notifySession`, `notifyTopic`, or `broadcast` are delivered to matching open connections. + - Event data is serialized as JSON and emitted using SSE format (`event`, `data`, optional `id`, and blank line terminator). + +5. Observability + - Connection registrations/unregistrations and send failures are logged via `src/lib/logger.ts`. + - Statistics are available via tRPC router (`src/server/api/routers/sse.ts` → `getStats`, `getMyConnections`, etc.). + +## Non-Functional Acceptance Criteria + +- Heartbeat interval keeps streams active across proxies/CDNs. +- Resource cleanup on disconnect prevents connection leaks. +- Basic CORS headers are present. For cross-origin with credentials, explicit origin and `Access-Control-Allow-Credentials: true` should be used (not `*`). + +## End-to-End Flow + +1. Client (browser) creates an `EventSource` to `/api/sse`, optionally with `?topic=a,b,c`. +2. Server authenticates the session. If valid, it creates a `ReadableStream` and registers the connection in `ConnectionManager`. +3. Server immediately sends `retry` metadata and a `welcome` event containing `connId`, `userId`, `sessionId`. +4. `ConnectionManager` subscribes the connection to channels: `broadcast`, `user:{userId}`, `session:{sessionId}`, and `topic:{topic}` for each requested topic. +5. Events published through the SSE API helpers are routed through the bus to subscribers and written to the stream. +6. Heartbeats are sent periodically. On abort/error, cleanup is performed and the stream is closed. + +## How to Verify (Manual) + +Precondition: You are signed in to the app. + +1. Navigate to the demo page `/(protected)/sse-demo`. + - Expected: Connection status transitions to `CONNECTED`. + - Expected: A `welcome` event appears with a `connId` and `sessionId` (if available). + +2. Send test events using the demo controls: + - "To User": emits your selected event type to your user; verify it appears in the messages list. + - "To Topic": choose a topic (e.g., `test-topic`); verify receipt when subscribed to that topic. + - "Broadcast": verify all open demo tabs receive the event. + +3. Disconnect behavior: + - Close the tab or navigate away; the server should log unregistration, and stats should show fewer active connections. + +4. Unauthorized check: + - Open the SSE endpoint in an incognito window without logging in (e.g., navigate to `/api/sse`). + - Expected: `401 Unauthorized` response. + +## Minimal Code Snippets (Reference) + +Client connection example (browser console): + +```javascript +const es = new EventSource('/api/sse?topic=test-topic'); +es.addEventListener('open', () => console.log('open')); +es.addEventListener('welcome', (e) => console.log('welcome', JSON.parse(e.data))); +es.addEventListener('message', (e) => console.log('message', e.data)); +es.addEventListener('error', (e) => console.error('error', e)); +``` + +Publishing events (server-side): + +```ts +import { notifyUser, notifyTopic, broadcast } from '@/server/sse'; + +await notifyUser('user-id', 'test_message', { message: 'hello' }); +await notifyTopic('test-topic', 'topic_message', { message: 'hello topic' }); +await broadcast('broadcast_message', { message: 'hello everyone' }); +``` + +## Edge Cases + +- Multiple tabs per user: all tabs receive user-targeted events; session-targeted events go only to the matching tab. +- Empty topic list: stream still opens; only broadcast/user/session channels are subscribed. +- Network interruptions: browser will auto-retry; server supports reconnect via `retry` line. If you need resume with `Last-Event-ID`, add event `id`s in the stream. + +## Definition of Done + +- Authenticated clients can connect to `/api/sse` and receive a `welcome` event. +- Response uses `text/event-stream` and remains open with periodic heartbeats. +- Events published through the API helpers are delivered to connected clients as expected. +- Disconnections trigger cleanup and stats reflect accurate counts. +- Unauthorized requests return `401` without opening a stream. + +## Related Files + +- `src/app/api/sse/route.ts` +- `src/server/sse/manager.ts` +- `src/server/sse/index.ts` +- `src/server/api/routers/sse.ts` +- `src/app/(protected)/sse-demo/page.tsx` + + diff --git a/src/server/sse/README.md b/src/server/sse/README.md new file mode 100644 index 00000000..0da95381 --- /dev/null +++ b/src/server/sse/README.md @@ -0,0 +1,411 @@ +# Server-Sent Events (SSE) Implementation + +This directory contains a comprehensive Server-Sent Events (SSE) implementation for real-time, server-to-client notifications across the application. + +## Overview + +The SSE layer provides: +- **Centralized connection management** - Track active client connections per user/session +- **Flexible event routing** - Send events to specific users, sessions, topics, or broadcast to all +- **Automatic heartbeat** - Keep connections alive with periodic ping messages +- **Comprehensive error handling** - Graceful handling of disconnections and errors +- **Real-time monitoring** - Statistics and connection tracking +- **Clean API** - Easy integration for backend modules + +## Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Client (UI) │ │ SSE Manager │ │ Event Bus │ +│ │ │ │ │ │ +│ EventSource │◄──►│ ConnectionMgr │◄──►│ InMemoryBus │ +│ │ │ │ │ │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ tRPC API │ + │ │ + │ SSE Endpoints │ + └─────────────────┘ +``` + +## Core Components + +### 1. Connection Manager (`manager.ts`) +- Manages active SSE connections +- Handles connection lifecycle (connect, disconnect, errors) +- Provides statistics and monitoring +- Implements automatic heartbeat mechanism + +### 2. Event Bus (`bus.ts`) +- Abstract event publishing/subscribing system +- In-memory implementation for single-server deployments +- Redis implementation available for multi-server scaling + +### 3. Types (`types.ts`) +- Comprehensive type definitions +- Connection information interfaces +- Error handling types +- Statistics interfaces + +### 4. API Layer (`index.ts`) +- High-level API functions for sending events +- Utility functions for common notification patterns +- Management and monitoring functions + +## Quick Start + +### 1. Connect from Client + +```typescript +// Basic SSE connection +const eventSource = new EventSource("/api/sse?topic=notifications,updates", { + withCredentials: true +}); + +// Listen for events +eventSource.addEventListener("message", (e) => { + console.log("Received:", e.data); +}); + +eventSource.addEventListener("welcome", (e) => { + const data = JSON.parse(e.data); + console.log("Connected with ID:", data.connId); +}); + +// Handle errors +eventSource.addEventListener("error", (e) => { + console.error("SSE error:", e); +}); +``` + +### 2. Send Events from Server + +```typescript +import { notifyUser, broadcast, notifyTopic } from "@/server/sse"; + +// Send to specific user +await notifyUser("user123", "new_message", { + message: "Hello!", + timestamp: Date.now() +}); + +// Broadcast to all connected clients +await broadcast("system_update", { + message: "Server maintenance in 5 minutes", + type: "warning" +}); + +// Send to topic subscribers +await notifyTopic("live-scores", "score_update", { + team: "Home Team", + score: 2 +}); +``` + +## API Reference + +### Core Functions + +#### `notifyUser(userId: string, event: string, data: any)` +Send an event to all connections of a specific user. + +#### `notifySession(sessionId: string, event: string, data: any)` +Send an event to a specific browser session/tab. + +#### `notifyTopic(topic: string, event: string, data: any)` +Send an event to all clients subscribed to a topic. + +#### `broadcast(event: string, data: any)` +Send an event to all connected clients. + +#### `notifyConnection(connId: string, event: string, data: any)` +Send an event to a specific connection. + +### Utility Functions + +#### `notifyNewMessage(userId: string, message: any)` +Send a new message notification to a user. + +#### `notifySystemUpdate(event: string, data: any)` +Broadcast a system update to all clients. + +#### `notifyUserStatus(userId: string, status: string)` +Notify about user status changes. + +#### `notifyLiveUpdate(topic: string, data: any)` +Send live data updates to topic subscribers. + +### Management Functions + +#### `getStats(): SseStats` +Get comprehensive SSE statistics. + +#### `getConnectionInfo(connId: string): ConnectionInfo | null` +Get information about a specific connection. + +#### `getUserConnections(userId: string): ConnectionInfo[]` +Get all connections for a user. + +#### `getTopicConnections(topic: string): ConnectionInfo[]` +Get all connections for a topic. + +#### `cleanup(): Promise` +Clean up all connections (useful for graceful shutdown). + +#### `healthCheck()` +Get health status of the SSE system. + +## Integration Examples + +### Webhook Handler Integration + +```typescript +// In a webhook handler +export async function POST(req: Request) { + const payload = await req.json(); + + // Process webhook + const result = await processWebhook(payload); + + // Notify relevant users + if (result.userId) { + await notifyUser(result.userId, "webhook_processed", { + type: payload.type, + status: "success", + timestamp: Date.now() + }); + } + + return Response.json({ success: true }); +} +``` + +### Background Job Integration + +```typescript +// In a background job processor +export async function processJob(jobData: any) { + try { + const result = await performJob(jobData); + + // Notify user about job completion + await notifyUser(jobData.userId, "job_completed", { + jobId: jobData.id, + result, + timestamp: Date.now() + }); + + } catch (error) { + // Notify about job failure + await notifyUser(jobData.userId, "job_failed", { + jobId: jobData.id, + error: error.message, + timestamp: Date.now() + }); + } +} +``` + +### Real-time Chat Integration + +```typescript +// In a chat message handler +export async function sendMessage(message: ChatMessage) { + // Save message to database + const savedMessage = await saveMessage(message); + + // Notify all users in the room + await notifyTopic(`chat:${message.roomId}`, "new_message", { + message: savedMessage, + timestamp: Date.now() + }); + + // Notify sender about delivery + await notifyUser(message.senderId, "message_sent", { + messageId: savedMessage.id, + timestamp: Date.now() + }); +} +``` + +## Monitoring and Debugging + +### SSE Statistics + +The system provides comprehensive statistics: + +```typescript +const stats = getStats(); +console.log({ + activeConnections: stats.activeConnections, + totalEventsSent: stats.totalEventsSent, + totalErrors: stats.totalErrors, + uptime: stats.uptime, + connectionsByUser: stats.connectionsByUser, + connectionsByTopic: stats.connectionsByTopic +}); +``` + +### Connection Monitoring + +```typescript +// Get all connections for a user +const userConnections = getUserConnections("user123"); +console.log(`User has ${userConnections.length} active connections`); + +// Get all connections for a topic +const topicConnections = getTopicConnections("live-scores"); +console.log(`Topic has ${topicConnections.length} subscribers`); +``` + +### Health Check + +```typescript +const health = healthCheck(); +console.log({ + status: health.status, + activeConnections: health.activeConnections, + uptime: health.uptime +}); +``` + +## Error Handling + +The SSE system includes comprehensive error handling: + +### Connection Errors +- Automatic reconnection with exponential backoff +- Error logging and monitoring +- Graceful cleanup of failed connections + +### Event Delivery Errors +- Failed event delivery is logged +- Connection status tracking +- Automatic retry mechanisms + +### Server Errors +- Graceful degradation +- Error reporting to monitoring systems +- Automatic recovery + +## Scaling Considerations + +### Single Server (Current Implementation) +- Uses in-memory event bus +- Suitable for small to medium applications +- All connections handled by single process + +### Multi-Server Scaling +- Switch to Redis event bus for horizontal scaling +- Multiple server instances can share connections +- Load balancing across SSE endpoints + +```typescript +// For production scaling, replace InMemoryBus with RedisBus +import { RedisBus } from "./bus"; +import Redis from "ioredis"; + +const pub = new Redis(process.env.REDIS_URL); +const sub = new Redis(process.env.REDIS_URL); +const bus = new RedisBus(sub, pub); +export const sse = new ConnectionManager(bus); +``` + +## Security Considerations + +### Authentication +- SSE connections require valid session +- User-specific event routing +- Session-based access control + +### Rate Limiting +- Implement rate limiting on SSE endpoints +- Prevent abuse and resource exhaustion +- Monitor connection patterns + +### Data Validation +- Validate all event data before sending +- Sanitize user inputs +- Implement proper error boundaries + +## Testing + +### Demo Interface +Visit `/sse-demo` to test all SSE functionality: +- Connection status monitoring +- Event sending and receiving +- Statistics and metrics +- Different event types and routing + +### Automated Testing +```typescript +// Example test +describe("SSE", () => { + it("should send events to connected clients", async () => { + // Setup SSE connection + const eventSource = new EventSource("/api/sse"); + + // Send test event + await notifyUser("test-user", "test", { message: "Hello" }); + + // Verify event received + // ... test assertions + }); +}); +``` + +## Best Practices + +1. **Use appropriate event types** - Choose the right routing method for your use case +2. **Handle reconnections** - Implement proper reconnection logic in clients +3. **Monitor performance** - Track connection counts and event volumes +4. **Clean up resources** - Properly close connections when components unmount +5. **Error boundaries** - Implement error handling for SSE failures +6. **Rate limiting** - Prevent abuse of SSE endpoints +7. **Logging** - Log important events for debugging and monitoring + +## Troubleshooting + +### Common Issues + +1. **Connections not receiving events** + - Check user authentication + - Verify topic subscriptions + - Check connection status + +2. **High memory usage** + - Monitor connection counts + - Implement connection limits + - Check for connection leaks + +3. **Events not being sent** + - Verify SSE manager is initialized + - Check event bus configuration + - Review error logs + +### Debug Commands + +```typescript +// Check connection status +const stats = getStats(); +console.log("Active connections:", stats.activeConnections); + +// Check specific user connections +const userConnections = getUserConnections("user123"); +console.log("User connections:", userConnections); + +// Test event sending +await notifyUser("user123", "test", { message: "Debug test" }); +``` + +## Performance Optimization + +1. **Connection pooling** - Reuse connections when possible +2. **Event batching** - Batch multiple events when appropriate +3. **Selective subscriptions** - Only subscribe to necessary topics +4. **Connection limits** - Implement per-user connection limits +5. **Memory monitoring** - Track memory usage and connection counts + +This SSE implementation provides a robust foundation for real-time features in your application. The modular design allows for easy extension and customization based on your specific requirements. diff --git a/src/server/sse/bus.ts b/src/server/sse/bus.ts new file mode 100644 index 00000000..0b5a7dda --- /dev/null +++ b/src/server/sse/bus.ts @@ -0,0 +1,34 @@ +export interface EventBus { + publish(channel: string, msg: any): Promise; + subscribe(channel: string, handler: (msg: any) => void): Promise<() => Promise>; + } + + // Dev/local adapter + import { EventEmitter } from "events"; + export class InMemoryBus implements EventBus { + private ee = new EventEmitter(); + async publish(ch: string, msg: any) { this.ee.emit(ch, msg); } + async subscribe(ch: string, handler: (msg: any) => void) { + const h = (m: unknown) => { try { handler(m); } catch {} }; + this.ee.on(ch, h); + return async () => { this.ee.off(ch, h); }; + } + } + + // Redis adapter (wire later if you run multiple instances) + export class RedisBus implements EventBus { + constructor(private sub: any, private pub: any) {} + async publish(ch: string, msg: any) { + await this.pub.publish(ch, JSON.stringify(msg)); + } + async subscribe(ch: string, handler: (msg: any) => void) { + const onMessage = (channel: string, payload: string) => { + if (channel !== ch) return; + try { handler(JSON.parse(payload)); } catch {} + }; + await this.sub.subscribe(ch); + this.sub.on("message", onMessage); + return async () => { this.sub.off("message", onMessage); await this.sub.unsubscribe(ch); }; + } + } + \ No newline at end of file diff --git a/src/server/sse/index.ts b/src/server/sse/index.ts new file mode 100644 index 00000000..8f8496ca --- /dev/null +++ b/src/server/sse/index.ts @@ -0,0 +1,110 @@ +import { InMemoryBus /*, RedisBus */ } from "./bus"; +import { ConnectionManager } from "./manager"; +import type { SseStats, ConnectionInfo } from "./types"; +import { log } from "@/lib/logger"; + +// For prod fan-out: create ioredis pub/sub clients and pass into RedisBus. +const bus = new InMemoryBus(); +export const sse = new ConnectionManager(bus); + +/** + * Core SSE API functions for sending events to clients + */ + +// Send to specific user (all their connections) +export const notifyUser = (userId: string, event: string, data: any) => + sse.send({ type: "user", userId }, event, data); + +// Send to specific session (single browser tab) +export const notifySession = (sessionId: string, event: string, data: any) => + sse.send({ type: "session", sessionId }, event, data); + +// Send to all clients subscribed to a topic +export const notifyTopic = (topic: string, event: string, data: any) => + sse.send({ type: "topic", topic }, event, data); + +// Broadcast to all connected clients +export const broadcast = (event: string, data: any) => + sse.send({ type: "broadcast" }, event, data); + +// Send to a specific connection +export const notifyConnection = (connId: string, event: string, data: any) => + sse.sendToConnection(connId, event, data); + +/** + * Utility functions for common notification patterns + */ + +// Notify user about a new message +export const notifyNewMessage = (userId: string, message: any) => { + return notifyUser(userId, "new_message", { + message, + timestamp: Date.now() + }); +}; + +// Notify about system updates +export const notifySystemUpdate = (event: string, data: any) => { + return broadcast("system_update", { + event, + data, + timestamp: Date.now() + }); +}; + +// Notify about user status changes +export const notifyUserStatus = (userId: string, status: string) => { + return notifyUser(userId, "status_change", { + status, + timestamp: Date.now() + }); +}; + +// Notify about real-time updates (e.g., live data) +export const notifyLiveUpdate = (topic: string, data: any) => { + return notifyTopic(topic, "live_update", { + data, + timestamp: Date.now() + }); +}; + +/** + * Management and monitoring functions + */ + +// Get connection statistics +export const getStats = (): SseStats => sse.getStats(); + +// Get information about a specific connection +export const getConnectionInfo = (connId: string): ConnectionInfo | null => + sse.getConnectionInfo(connId); + +// Get all connections for a user +export const getUserConnections = (userId: string): ConnectionInfo[] => + sse.getConnectionsByUser(userId); + +// Get all connections for a topic +export const getTopicConnections = (topic: string): ConnectionInfo[] => + sse.getConnectionsByTopic(topic); + +// Clean up all connections (useful for graceful shutdown) +export const cleanup = async () => { + log.info("Starting SSE cleanup"); + await sse.cleanup(); + log.info("SSE cleanup completed"); +}; + +/** + * Health check function + */ +export const healthCheck = () => { + const stats = getStats(); + return { + status: "healthy", + uptime: stats.uptime, + activeConnections: stats.activeConnections, + totalConnections: stats.totalConnections, + totalEventsSent: stats.totalEventsSent, + totalErrors: stats.totalErrors + }; +}; diff --git a/src/server/sse/manager.ts b/src/server/sse/manager.ts new file mode 100644 index 00000000..717df895 --- /dev/null +++ b/src/server/sse/manager.ts @@ -0,0 +1,284 @@ +import type { EventBus } from "./bus"; +import type { Target, SseEventEnvelope, ConnectionInfo, SseError, ConnectionStatus, SseStats } from "./types"; +import { log } from "@/lib/logger"; + +type ConnId = string; + +interface Connection { + info: ConnectionInfo; + status: ConnectionStatus; + send: (evt: SseEventEnvelope) => void; + dispose: () => Promise; + error?: SseError; +} + +export class ConnectionManager { + private connections = new Map(); + private startTime = Date.now(); + private totalEventsSent = 0; + private totalErrors = 0; + private heartbeatInterval?: NodeJS.Timeout; + + constructor(private bus: EventBus) { + this.startHeartbeat(); + } + + private startHeartbeat() { + // Send heartbeat every 30 seconds to keep connections alive + this.heartbeatInterval = setInterval(() => { + this.broadcastHeartbeat(); + }, 30000); + } + + private async broadcastHeartbeat() { + try { + await this.send({ type: "broadcast" }, "heartbeat", { timestamp: Date.now() }); + } catch (error) { + log.error("Failed to send heartbeat", { error }); + } + } + + async register(opts: { + connId: ConnId; + userId?: string; + sessionId?: string; + topics?: string[]; + send: (evt: SseEventEnvelope) => void; + userAgent?: string; + ip?: string; + }): Promise<() => Promise> { + try { + const subs: Array<() => Promise> = []; + const sub = async (ch: string) => { + const unsubscribe = await this.bus.subscribe(ch, (evt) => { + try { + opts.send(evt); + this.totalEventsSent++; + this.updateConnectionActivity(opts.connId); + } catch (error) { + log.error("Failed to send event to connection", { + connId: opts.connId, + error, + event: evt + }); + this.handleConnectionError(opts.connId, { + code: "SEND_ERROR", + message: "Failed to send event to client", + details: error, + timestamp: Date.now() + }); + } + }); + subs.push(unsubscribe); + }; + + // Subscribe to channels + await sub("broadcast"); + if (opts.userId) await sub(`user:${opts.userId}`); + if (opts.sessionId) await sub(`session:${opts.sessionId}`); + for (const topic of (opts.topics ?? [])) { + await sub(`topic:${topic}`); + } + + const connectionInfo: ConnectionInfo = { + connId: opts.connId, + userId: opts.userId, + sessionId: opts.sessionId, + topics: opts.topics ?? [], + connectedAt: Date.now(), + lastActivity: Date.now(), + userAgent: opts.userAgent, + ip: opts.ip, + }; + + const connection: Connection = { + info: connectionInfo, + status: 'connected', + send: opts.send, + dispose: async () => { + await Promise.all(subs.map(s => s())); + } + }; + + this.connections.set(opts.connId, connection); + + log.info("SSE connection registered", { + connId: opts.connId, + userId: opts.userId, + sessionId: opts.sessionId, + topics: opts.topics, + totalConnections: this.connections.size + }); + + // Send welcome message + await this.sendToConnection(opts.connId, "connected", { + connId: opts.connId, + timestamp: Date.now() + }); + + return async () => this.unregister(opts.connId); + } catch (error) { + log.error("Failed to register SSE connection", { + connId: opts.connId, + error + }); + throw error; + } + } + + async unregister(connId: ConnId) { + const connection = this.connections.get(connId); + if (!connection) { + log.warn("Attempted to unregister non-existent connection", { connId }); + return; + } + + try { + await connection.dispose(); + this.connections.delete(connId); + + log.info("SSE connection unregistered", { + connId, + userId: connection.info.userId, + duration: Date.now() - connection.info.connectedAt, + totalConnections: this.connections.size + }); + } catch (error) { + log.error("Error during connection cleanup", { connId, error }); + } + } + + private updateConnectionActivity(connId: ConnId) { + const connection = this.connections.get(connId); + if (connection) { + connection.info.lastActivity = Date.now(); + } + } + + private handleConnectionError(connId: ConnId, error: SseError) { + const connection = this.connections.get(connId); + if (connection) { + connection.status = 'error'; + connection.error = error; + this.totalErrors++; + + log.error("SSE connection error", { + connId, + error: error.message, + code: error.code + }); + } + } + + async send(target: Target, event: string, data: any) { + try { + const envelope: SseEventEnvelope = { + event, + data, + timestamp: Date.now() + }; + + const channel = + target.type === "broadcast" ? "broadcast" : + target.type === "user" ? `user:${target.userId}` : + target.type === "session" ? `session:${target.sessionId}` : + `topic:${target.topic}`; + + await this.bus.publish(channel, envelope); + + log.info("SSE event sent", { + target, + event, + channel, + dataSize: JSON.stringify(data).length + }); + } catch (error) { + log.error("Failed to send SSE event", { target, event, error }); + throw error; + } + } + + async sendToConnection(connId: ConnId, event: string, data: any) { + const connection = this.connections.get(connId); + if (!connection) { + throw new Error(`Connection ${connId} not found`); + } + + try { + const envelope: SseEventEnvelope = { + event, + data, + timestamp: Date.now() + }; + + connection.send(envelope); + this.totalEventsSent++; + this.updateConnectionActivity(connId); + } catch (error) { + this.handleConnectionError(connId, { + code: "SEND_ERROR", + message: "Failed to send event to specific connection", + details: error, + timestamp: Date.now() + }); + throw error; + } + } + + getConnectionInfo(connId: ConnId): ConnectionInfo | null { + const connection = this.connections.get(connId); + return connection ? connection.info : null; + } + + getConnectionsByUser(userId: string): ConnectionInfo[] { + return Array.from(this.connections.values()) + .filter(conn => conn.info.userId === userId) + .map(conn => conn.info); + } + + getConnectionsByTopic(topic: string): ConnectionInfo[] { + return Array.from(this.connections.values()) + .filter(conn => conn.info.topics.includes(topic)) + .map(conn => conn.info); + } + + getStats(): SseStats { + const connectionsByUser: Record = {}; + const connectionsByTopic: Record = {}; + + for (const connection of this.connections.values()) { + if (connection.info.userId) { + connectionsByUser[connection.info.userId] = + (connectionsByUser[connection.info.userId] || 0) + 1; + } + + for (const topic of connection.info.topics) { + connectionsByTopic[topic] = + (connectionsByTopic[topic] || 0) + 1; + } + } + + return { + totalConnections: this.connections.size, + activeConnections: Array.from(this.connections.values()) + .filter(conn => conn.status === 'connected').length, + totalEventsSent: this.totalEventsSent, + totalErrors: this.totalErrors, + uptime: Date.now() - this.startTime, + connectionsByUser, + connectionsByTopic + }; + } + + async cleanup() { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + } + + // Clean up all connections + const connIds = Array.from(this.connections.keys()); + await Promise.all(connIds.map(connId => this.unregister(connId))); + + log.info("SSE manager cleaned up", { totalConnections: connIds.length }); + } +} diff --git a/src/server/sse/types.ts b/src/server/sse/types.ts new file mode 100644 index 00000000..df52c1cd --- /dev/null +++ b/src/server/sse/types.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; + +export const SseEventEnvelope = z.object({ + event: z.string().min(1), + data: z.any(), + id: z.string().optional(), + userId: z.string().optional(), + sessionId: z.string().optional(), + topic: z.string().optional(), + timestamp: z.number().optional(), + retry: z.number().optional(), +}); +export type SseEventEnvelope = z.infer; + +export type Target = + | { type: "broadcast" } + | { type: "user"; userId: string } + | { type: "session"; sessionId: string } + | { type: "topic"; topic: string }; + +export interface ConnectionInfo { + connId: string; + userId?: string; + sessionId?: string; + topics: string[]; + connectedAt: number; + lastActivity: number; + userAgent?: string; + ip?: string; +} + +export interface SseError { + code: string; + message: string; + details?: any; + timestamp: number; +} + +export type ConnectionStatus = 'connected' | 'disconnected' | 'error'; + +export interface SseStats { + totalConnections: number; + activeConnections: number; + totalEventsSent: number; + totalErrors: number; + uptime: number; + connectionsByUser: Record; + connectionsByTopic: Record; +} diff --git a/src/server/trpc.ts b/src/server/trpc.ts new file mode 100644 index 00000000..a00afe5e --- /dev/null +++ b/src/server/trpc.ts @@ -0,0 +1,96 @@ +/** + * This is your entry point to setup the root configuration for tRPC on the server. + * - `initTRPC` should only be used once per app. + * - We export only the functionality that we use so we can enforce which base procedures should be used + * + * Learn how to create protected base procedures and other things below: + * @see https://trpc.io/docs/v11/router + * @see https://trpc.io/docs/v11/procedures + */ + +import { initTRPC, TRPCError } from '@trpc/server'; +import superjson from 'superjson'; +import type { Context } from './context'; + +const t = initTRPC.context().create({ + /** + * @see https://trpc.io/docs/v11/data-transformers + */ + transformer: superjson, + /** + * @see https://trpc.io/docs/v11/error-formatting + */ + errorFormatter({ shape }) { + return shape; + }, + sse: { + maxDurationMs: 5 * 60 * 1_000, // 5 minutes + ping: { + enabled: true, + intervalMs: 3_000, + }, + client: { + reconnectAfterInactivityMs: 5_000, + }, + }, +}); + +export const createCallerFactory = t.createCallerFactory; + +/** + * Create a router + * @see https://trpc.io/docs/v11/router + */ +export const router = t.router; + +/** + * Create an unprotected procedure + * @see https://trpc.io/docs/v11/procedures + **/ +export const publicProcedure = t.procedure.use( + async function artificialDelayInDevelopment(opts) { + const res = opts.next(opts); + + if (process.env.NODE_ENV === 'development') { + const randomNumber = (min: number, max: number) => + Math.floor(Math.random() * (max - min + 1)) + min; + + const delay = randomNumber(300, 1_000); + console.debug( + 'ℹ️ doing artificial delay of', + delay, + 'ms before returning', + opts.path, + ); + + await new Promise((resolve) => setTimeout(resolve, delay)); + } + + return res; + }, +); + +/** + * @see https://trpc.io/docs/v11/merging-routers + */ +export const mergeRouters = t.mergeRouters; + +/** + * Protected base procedure + */ +export const authedProcedure = publicProcedure.use(function isAuthed(opts) { + const user = opts.ctx.session?.user; + + if (!user?.name) { + throw new TRPCError({ code: 'UNAUTHORIZED' }); + } + + return opts.next({ + ctx: { + user: { + ...user, + name: user.name, + }, + }, + }); +});

OCrXYDh#mw6)0nDLz`AshHIqd7~}C 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 literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json index 46a5af0e..c0e0dbe7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@react-email/components": "^0.0.42", "@t3-oss/env-nextjs": "^0.12.0", "@tanstack/react-query": "^5.69.0", + "@tanstack/react-query-devtools": "^5.80.3", "@trpc/client": "^11.0.0", "@trpc/react-query": "^11.0.0", "@trpc/server": "^11.0.0", @@ -28,6 +29,7 @@ "clsx": "^2.1.1", "i18next": "^25.2.1", "i18next-resources-to-backend": "^1.2.1", + "ioredis": "^5.7.0", "lucide-react": "^0.514.0", "meilisearch": "^0.51.0", "next": "15.3.4", @@ -1650,6 +1652,12 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@ioredis/commands": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz", + "integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==", + "license": "MIT" + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -3128,9 +3136,19 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.81.2", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.81.2.tgz", - "integrity": "sha512-QLYkPdrudoMATDFa3MiLEwRhNnAlzHWDf0LKaXUqJd0/+QxN8uTPi7bahRlxoAyH0UbLMBdeDbYzWALj7THOtw==", + "version": "5.83.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.1.tgz", + "integrity": "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.84.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.84.0.tgz", + "integrity": "sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==", "license": "MIT", "funding": { "type": "github", @@ -3138,12 +3156,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.81.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.81.2.tgz", - "integrity": "sha512-pe8kFlTrL2zFLlcAj2kZk9UaYYHDk9/1hg9EBaoO3cxDhOZf1FRGJeziSXKrVZyxIfs7b3aoOj/bw7Lie0mDUg==", + "version": "5.84.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.84.2.tgz", + "integrity": "sha512-cZadySzROlD2+o8zIfbD978p0IphuQzRWiiH3I2ugnTmz4jbjc0+TdibpwqxlzynEen8OulgAg+rzdNF37s7XQ==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.81.2" + "@tanstack/query-core": "5.83.1" }, "funding": { "type": "github", @@ -3153,6 +3171,23 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.84.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.84.2.tgz", + "integrity": "sha512-ojJ66QoW9noqK35Lsmfqpfucj6wuOxLL2TYwEwpvU+iUQ5R/7TKpapWvpy9kZyNSl0mxv5mpS+ImfR8aL8/x3g==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.84.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.84.2", + "react": "^18 || ^19" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -4874,6 +4909,15 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -5114,7 +5158,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5206,6 +5249,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -6839,6 +6891,30 @@ "node": ">= 0.4" } }, + "node_modules/ioredis": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", + "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.3.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "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" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -7858,6 +7934,18 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9234,6 +9322,27 @@ "node": ">=8" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -9848,6 +9957,12 @@ "dev": true, "license": "MIT" }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", diff --git a/package.json b/package.json index 0c65b9d8..36a2b610 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "clsx": "^2.1.1", "i18next": "^25.2.1", "i18next-resources-to-backend": "^1.2.1", + "ioredis": "^5.7.0", "lucide-react": "^0.514.0", "meilisearch": "^0.51.0", "next": "15.3.4", diff --git a/src/app/(protected)/sse-demo/page.tsx b/src/app/(protected)/sse-demo/page.tsx new file mode 100644 index 00000000..9804aaec --- /dev/null +++ b/src/app/(protected)/sse-demo/page.tsx @@ -0,0 +1,476 @@ +"use client"; +import { useEffect, useRef, useState } from "react"; +import { api } from "@/trpc/react"; + +interface SseMessage { + event: string; + data: any; + timestamp: number; +} + +interface ConnectionInfo { + connId: string; + userId?: string; + sessionId?: string; + topics: string[]; + connectedAt: number; + lastActivity: number; + userAgent?: string; + ip?: string; +} + +export default function SseDemo() { + const [messages, setMessages] = useState([]); + const [connectionStatus, setConnectionStatus] = useState<'connecting' | 'connected' | 'disconnected' | 'error'>('disconnected'); + const [connectionInfo, setConnectionInfo] = useState(null); + const [stats, setStats] = useState(null); + const [testMessage, setTestMessage] = useState(''); + const [testTopic, setTestTopic] = useState('test-topic'); + const [selectedEvent, setSelectedEvent] = useState('test_message'); + const esRef = useRef(null); + + // SSE connection management + useEffect(() => { + const connectSSE = () => { + setConnectionStatus('connecting'); + const es = new EventSource("/api/sse?topic=test-topic,notifications,updates", { + withCredentials: true + }); + + es.addEventListener("open", () => { + setConnectionStatus('connected'); + console.log("SSE connection opened"); + }); + + es.addEventListener("message", (e) => { + const message: SseMessage = { + event: 'message', + data: e.data, + timestamp: Date.now() + }; + setMessages(prev => [message, ...prev.slice(0, 49)]); // Keep last 50 messages + }); + + es.addEventListener("welcome", (e) => { + const data = JSON.parse(e.data); + setConnectionInfo(data); + const message: SseMessage = { + event: 'welcome', + data, + timestamp: Date.now() + }; + setMessages(prev => [message, ...prev.slice(0, 49)]); + }); + + es.addEventListener("test_message", (e) => { + const data = JSON.parse(e.data); + const message: SseMessage = { + event: 'test_message', + data, + timestamp: Date.now() + }; + setMessages(prev => [message, ...prev.slice(0, 49)]); + }); + + es.addEventListener("topic_message", (e) => { + const data = JSON.parse(e.data); + const message: SseMessage = { + event: 'topic_message', + data, + timestamp: Date.now() + }; + setMessages(prev => [message, ...prev.slice(0, 49)]); + }); + + es.addEventListener("broadcast_message", (e) => { + const data = JSON.parse(e.data); + const message: SseMessage = { + event: 'broadcast_message', + data, + timestamp: Date.now() + }; + setMessages(prev => [message, ...prev.slice(0, 49)]); + }); + + es.addEventListener("new_message", (e) => { + const data = JSON.parse(e.data); + const message: SseMessage = { + event: 'new_message', + data, + timestamp: Date.now() + }; + setMessages(prev => [message, ...prev.slice(0, 49)]); + }); + + es.addEventListener("system_update", (e) => { + const data = JSON.parse(e.data); + const message: SseMessage = { + event: 'system_update', + data, + timestamp: Date.now() + }; + setMessages(prev => [message, ...prev.slice(0, 49)]); + }); + + es.addEventListener("live_update", (e) => { + const data = JSON.parse(e.data); + const message: SseMessage = { + event: 'live_update', + data, + timestamp: Date.now() + }; + setMessages(prev => [message, ...prev.slice(0, 49)]); + }); + + es.addEventListener("error", (e) => { + setConnectionStatus('error'); + console.error("SSE connection error:", e); + const message: SseMessage = { + event: 'error', + data: { message: 'Connection error occurred' }, + timestamp: Date.now() + }; + setMessages(prev => [message, ...prev.slice(0, 49)]); + }); + + es.addEventListener("close", () => { + setConnectionStatus('disconnected'); + console.log("SSE connection closed"); + }); + + esRef.current = es; + }; + + connectSSE(); + + return () => { + if (esRef.current) { + esRef.current.close(); + } + }; + }, []); + + // tRPC mutations + const sendTestToUser = api.sse.sendTestToUser.useMutation(); + const sendTestToTopic = api.sse.sendTestToTopic.useMutation(); + const sendTestBroadcast = api.sse.sendTestBroadcast.useMutation(); + const sendTestToSession = api.sse.sendTestToSession.useMutation(); + const sendNewMessage = api.sse.sendNewMessage.useMutation(); + const sendSystemUpdate = api.sse.sendSystemUpdate.useMutation(); + const sendLiveUpdate = api.sse.sendLiveUpdate.useMutation(); + const getStats = api.sse.getStats.useQuery(undefined, { + refetchInterval: 5000, // Refresh every 5 seconds + }); + const getMyConnections = api.sse.getMyConnections.useQuery(undefined, { + refetchInterval: 3000, // Refresh every 3 seconds + }); + + const handleSendTestToUser = () => { + sendTestToUser.mutate({ + message: testMessage || "Hello from user test!", + event: selectedEvent + }); + }; + + const handleSendTestToSession = () => { + const sessionId = + connectionInfo?.sessionId || + (getMyConnections.data && (getMyConnections.data as any[])[0]?.sessionId) || + undefined; + + if (!sessionId) { + alert("No sessionId available. Open the SSE connection first."); + return; + } + + sendTestToSession.mutate({ + sessionId, + message: testMessage || "Hello from session test!", + event: selectedEvent, + }); + }; + + const handleSendTestToTopic = () => { + sendTestToTopic.mutate({ + topic: testTopic, + message: testMessage || "Hello from topic test!", + event: selectedEvent + }); + }; + + const handleSendBroadcast = () => { + sendTestBroadcast.mutate({ + message: testMessage || "Hello everyone!", + event: selectedEvent + }); + }; + + const handleSendNewMessage = () => { + sendNewMessage.mutate({ + userId: connectionInfo?.userId || "test-user", + message: { + id: `msg-${Date.now()}`, + content: testMessage || "This is a new message!", + sender: "Demo User", + roomId: "demo-room" + } + }); + }; + + const handleSendSystemUpdate = () => { + sendSystemUpdate.mutate({ + event: "demo_update", + data: { + message: testMessage || "System update from demo", + version: "1.0.0", + timestamp: Date.now() + } + }); + }; + + const handleSendLiveUpdate = () => { + sendLiveUpdate.mutate({ + topic: testTopic, + data: { + message: testMessage || "Live update from demo", + value: Math.random() * 100, + timestamp: Date.now() + } + }); + }; + + const formatTimestamp = (timestamp: number) => { + return new Date(timestamp).toLocaleTimeString(); + }; + + return ( +