fiemwl / src /app /api /gamification /stream /route.ts
automindy's picture
Upload 1980 files
6111b2b verified
Raw
History Blame Contribute Delete
2.51 kB
/**
* SSE Leaderboard Stream β€” /api/gamification/stream
*
* Pushes live leaderboard updates to connected clients via Server-Sent Events.
* Supports all leaderboard scopes (global, weekly, monthly, tokens_shared, contributions).
*/
import { NextRequest } from "next/server";
import { type LeaderboardScope, getTopN } from "@/lib/gamification/leaderboard";
import { CORS_HEADERS } from "@/shared/utils/cors";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
const VALID_SCOPES: ReadonlySet<string> = new Set([
"global",
"weekly",
"monthly",
"tokens_shared",
"contributions",
]);
/**
* GET /api/gamification/stream β€” SSE leaderboard updates
*
* Query params:
* scope β€” one of: global, weekly, monthly, tokens_shared, contributions (default: global)
*/
export async function GET(request: NextRequest) {
const authErr = await requireManagementAuth(request);
if (authErr) return authErr;
const url = new URL(request.url);
const rawScope = url.searchParams.get("scope") || "global";
const scope: LeaderboardScope = VALID_SCOPES.has(rawScope)
? (rawScope as LeaderboardScope)
: "global";
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
const sendUpdate = async () => {
try {
const entries = await getTopN(scope, 50);
const data = JSON.stringify({ type: "leaderboard", scope, entries });
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
controller.enqueue(
encoder.encode(`event: error\ndata: ${JSON.stringify({ error: msg })}\n\n`)
);
}
};
// Send initial state
sendUpdate();
// Heartbeat every 15s
const heartbeat = setInterval(() => {
controller.enqueue(encoder.encode(`: heartbeat ${Date.now()}\n\n`));
}, 15_000);
// Update every 5s
const updater = setInterval(sendUpdate, 5_000);
request.signal.addEventListener("abort", () => {
clearInterval(heartbeat);
clearInterval(updater);
controller.close();
});
},
});
return new Response(stream, {
headers: {
...CORS_HEADERS,
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}