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",
    },
  });
}