Spaces:
Runtime error
Runtime error
File size: 2,424 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | /**
* 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",
},
});
}
|