diff --git a/package-lock.json b/package-lock.json index 5e58135..4c4d1bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", "@supabase/ssr": "^0.8.0", - "@supabase/supabase-js": "^2.94.1", + "@supabase/supabase-js": "^2.95.3", "@types/js-cookie": "^3.0.6", "@uiw/codemirror-theme-vscode": "^4.25.4", "@uiw/react-codemirror": "^4.25.4", @@ -3461,9 +3461,9 @@ "license": "MIT" }, "node_modules/@supabase/auth-js": { - "version": "2.94.1", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.94.1.tgz", - "integrity": "sha512-Wt/SdmAtNNiqrcBbPlzWojLcE1bQ9OYb8PTaYF6QccFX5JeXZI0sZ01MLNE+E83UK6cK0lw4YznX0D2g08UQng==", + "version": "2.95.3", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.95.3.tgz", + "integrity": "sha512-vD2YoS8E2iKIX0F7EwXTmqhUpaNsmbU6X2R0/NdFcs02oEfnHyNP/3M716f3wVJ2E5XHGiTFXki6lRckhJ0Thg==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -3473,9 +3473,9 @@ } }, "node_modules/@supabase/functions-js": { - "version": "2.94.1", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.94.1.tgz", - "integrity": "sha512-A7Bx0gnclDNZ4m8+mnO2IEEzMxtUSg7cpPEBF6Ek1LpjIQkC7vvoidiV/RuntnKX43IiVcWV1f2FsAppMagEmQ==", + "version": "2.95.3", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.95.3.tgz", + "integrity": "sha512-uTuOAKzs9R/IovW1krO0ZbUHSJnsnyJElTXIRhjJTqymIVGcHzkAYnBCJqd7468Fs/Foz1BQ7Dv6DCl05lr7ig==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -3485,9 +3485,9 @@ } }, "node_modules/@supabase/postgrest-js": { - "version": "2.94.1", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.94.1.tgz", - "integrity": "sha512-N6MTghjHnMZddT48rAj8dIFgedCU97cc1ahQM74Tc+DF4UH7y2+iEfdYV3unJsylpaiWlu92Fy8Lj14Jbrmxog==", + "version": "2.95.3", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.95.3.tgz", + "integrity": "sha512-LTrRBqU1gOovxRm1vRXPItSMPBmEFqrfTqdPTRtzOILV4jPSueFz6pES5hpb4LRlkFwCPRmv3nQJ5N625V2Xrg==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -3497,9 +3497,9 @@ } }, "node_modules/@supabase/realtime-js": { - "version": "2.94.1", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.94.1.tgz", - "integrity": "sha512-Wq8olpCAGmN4y2DH2kUdlcakdzNHRCde72BFS8zK5ub46bBeSUoE9DqrfeNFWKaF2gCE/cmK8aTUTorZD9jdtQ==", + "version": "2.95.3", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.95.3.tgz", + "integrity": "sha512-D7EAtfU3w6BEUxDACjowWNJo/ZRo7sDIuhuOGKHIm9FHieGeoJV5R6GKTLtga/5l/6fDr2u+WcW/m8I9SYmaIw==", "license": "MIT", "dependencies": { "@types/phoenix": "^1.6.6", @@ -3524,9 +3524,9 @@ } }, "node_modules/@supabase/storage-js": { - "version": "2.94.1", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.94.1.tgz", - "integrity": "sha512-/Mi18LGyrugPwtfqETfAqEGcBQotY/7IMsTGYgEFdqr8cQq280BVQWjN2wI9KibWtshPp0Ryvil5Uzd5YfM7kA==", + "version": "2.95.3", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.95.3.tgz", + "integrity": "sha512-4GxkJiXI3HHWjxpC3sDx1BVrV87O0hfX+wvJdqGv67KeCu+g44SPnII8y0LL/Wr677jB7tpjAxKdtVWf+xhc9A==", "license": "MIT", "dependencies": { "iceberg-js": "^0.8.1", @@ -3537,16 +3537,16 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.94.1", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.94.1.tgz", - "integrity": "sha512-87vOY8n3WHB3m+a/KeySj07djOQVuRA5qgX5E7db1eDkaZ1of5M+3t/tv6eYYy4BfqxuHMZuCe5uVrO/oyvoow==", + "version": "2.95.3", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.95.3.tgz", + "integrity": "sha512-Fukw1cUTQ6xdLiHDJhKKPu6svEPaCEDvThqCne3OaQyZvuq2qjhJAd91kJu3PXLG18aooCgYBaB6qQz35hhABg==", "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.94.1", - "@supabase/functions-js": "2.94.1", - "@supabase/postgrest-js": "2.94.1", - "@supabase/realtime-js": "2.94.1", - "@supabase/storage-js": "2.94.1" + "@supabase/auth-js": "2.95.3", + "@supabase/functions-js": "2.95.3", + "@supabase/postgrest-js": "2.95.3", + "@supabase/realtime-js": "2.95.3", + "@supabase/storage-js": "2.95.3" }, "engines": { "node": ">=20.0.0" diff --git a/package.json b/package.json index 66213bb..70eedf2 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", "@supabase/ssr": "^0.8.0", - "@supabase/supabase-js": "^2.94.1", + "@supabase/supabase-js": "^2.95.3", "@types/js-cookie": "^3.0.6", "@uiw/codemirror-theme-vscode": "^4.25.4", "@uiw/react-codemirror": "^4.25.4", diff --git a/src/app/api/run/route.ts b/src/app/api/run/route.ts index 4d964ee..b7449af 100644 --- a/src/app/api/run/route.ts +++ b/src/app/api/run/route.ts @@ -4,8 +4,231 @@ import { createClientCookie } from "@/lib/supabase/server"; import { getSession } from "@/modules/account/core"; import * as Core from "@/modules/live-run/core"; import { ClientMessage } from "./types"; -import { NextRequest } from "next/server"; import { redis } from "@/lib/redis"; +import { User } from "@supabase/supabase-js"; +import { UserProfile } from "@/modules/user/types"; +import { NextRequest } from "next/server"; + +export function UPGRADE(client: WebSocket, server: WebSocketServer) { + let authenticated = false; + let session: [User, UserProfile] | null = null; + let currentRun: string | undefined; + + // Heartbeat + let isAlive = true; + const heartbeat = setInterval(() => { + if (!isAlive) { + console.log("Client did not respond to ping, terminating"); + return closeClient("Heartbeat timeout"); + } + isAlive = false; + if (client.readyState === WebSocket.OPEN) client.ping(); + }, 15000); + + client.on("pong", () => { + isAlive = true; + }); + + const closeClient = (reason?: string) => { + if (reason) console.warn("Closing client:", reason); + try { + client.close(); + setTimeout(() => { + if (client.readyState !== WebSocket.CLOSED) client.terminate(); + }, 1000); + } catch {} + }; + + const cleanupRun = async () => { + if (currentRun) { + try { + await Core.removeLiveRun(currentRun); + } catch (err) { + console.error("Error cleaning up run:", err); + } + currentRun = undefined; + } + }; + + client.on("message", async (raw) => { + let requestId: string | undefined; + + try { + const msg = JSON.parse(raw.toString()); + requestId = msg.requestId; + + // AUTHENTICATION + if (!authenticated) { + const { cookies } = msg as { cookies?: Record }; + if (!cookies) return closeClient("Missing cookies"); + + const supabase = await createClientCookie( + new Map(Object.entries(cookies)), + ); + session = await getSession(supabase); + if (!session) return closeClient("Invalid session"); + + authenticated = true; + return client.send(JSON.stringify({ requestId, ok: true })); + } + + if (!session) return closeClient("Session unexpectedly null"); + + // POST-AUTH MESSAGE HANDLING + const { type, ...payload } = msg as ClientMessage; + + switch (type) { + case "create": { + const { problem, category } = payload as any; + if (!problem || !category) + return client.send( + JSON.stringify({ + requestId, + ok: false, + error: "problem and category required", + }), + ); + if (currentRun) + return client.send( + JSON.stringify({ + requestId, + ok: false, + error: + "Cannot do multiple runs, delete/submit current run first", + }), + ); + + const runId = randomUUID(); + await Core.addLiveRun({ + id: runId, + problem, + category, + username: session[1].username, + views: 0, + start: Date.now(), + runsCount: 0, + }); + + currentRun = runId; + return client.send( + JSON.stringify({ + requestId, + ok: true, + type: "create", + data: { runId }, + }), + ); + } + + case "move": { + if (!currentRun) + return client.send( + JSON.stringify({ requestId, ok: false, error: "No current run" }), + ); + + const { moves, file, language } = payload as any; + if (!moves || file === undefined || language === undefined) + return client.send( + JSON.stringify({ + requestId, + ok: false, + error: "moves, file, and language required", + }), + ); + + const liveRun = await Core.getLiveRun(currentRun); + if (!liveRun) + return client.send( + JSON.stringify({ requestId, ok: false, error: "Run not live" }), + ); + + const run = await Core.addLiveRunEvent( + liveRun.id, + file, + language, + moves, + ); + return client.send( + JSON.stringify({ requestId, ok: true, type: "move", data: run }), + ); + } + + case "submit": { + if (!currentRun) + return client.send( + JSON.stringify({ requestId, ok: false, error: "No current run" }), + ); + + const liveRun = await Core.getLiveRun(currentRun); + if (!liveRun) + return client.send( + JSON.stringify({ requestId, ok: false, error: "Run not live" }), + ); + + const run = await Core.submitRun(liveRun, session[0].id); + currentRun = undefined; + return client.send( + JSON.stringify({ requestId, ok: true, type: "submit", data: run }), + ); + } + + case "delete": { + if (!currentRun) + return client.send( + JSON.stringify({ requestId, ok: false, error: "No current run" }), + ); + + await cleanupRun(); + return client.send( + JSON.stringify({ + requestId, + ok: true, + type: "delete", + data: { id: currentRun }, + }), + ); + } + + case "getText": { + if (!currentRun) + return client.send( + JSON.stringify({ requestId, ok: false, error: "No current run" }), + ); + + const data = await redis.get(`liveRunText:${currentRun}`); + return client.send(JSON.stringify({ requestId, ok: true, data })); + } + + default: + return client.send( + JSON.stringify({ + requestId, + ok: false, + error: "Unknown message type", + }), + ); + } + } catch (err) { + console.error(err); + if (requestId) + client.send( + JSON.stringify({ requestId, ok: false, error: "Malformed message" }), + ); + closeClient("Malformed message"); + } + }); + + client.on("close", async () => { + clearInterval(heartbeat); + await cleanupRun(); + }); + + client.on("error", (err) => { + console.error("WebSocket error:", err); + clearInterval(heartbeat); + closeClient("WebSocket error"); + }); +} export function GET(req: NextRequest) { const headers = new Headers(); @@ -13,234 +236,3 @@ export function GET(req: NextRequest) { headers.set("Upgrade", "websocket"); return new Response("Upgrade Required", { status: 426, headers }); } - -export function UPGRADE(client: WebSocket, server: WebSocketServer) { - client.once("message", async (raw) => { - const { - cookies, - requestId, - }: { cookies: Record; requestId: string } = JSON.parse( - raw.toString(), - ); - - if (!cookies) { - client.send(JSON.stringify({ ok: false, error: "Missing cookies" })); - client.close(); - return; - } - - const supabase = await createClientCookie(new Map(Object.entries(cookies))); - const session = await getSession(supabase); - - if (!session) { - client.send(JSON.stringify({ ok: false, error: "Invalid session" })); - return; - } - - client.send(JSON.stringify({ requestId, ok: true })); - - console.log(`User ${session[1].username} connected to live run`); - - client.on("message", async (raw) => { - try { - const { requestId, ...msg }: ClientMessage = JSON.parse(raw.toString()); - - switch (msg.type) { - /* ---------------- CREATE ---------------- */ - case "create": { - const { problem, category } = msg; - - if (!problem || !category) { - client.send( - JSON.stringify({ - requestId, - ok: false, - error: "problem and category required", - }), - ); - return; - } - - const runId = randomUUID(); - - await Core.addLiveRun({ - id: runId, - problem, - category, - username: session[1].username, - views: 0, - start: Date.now(), - runsCount: 0, - }); - - client.send( - JSON.stringify({ - requestId, - ok: true, - type: "create", - data: { runId }, - }), - ); - break; - } - - /* ---------------- DELETE ---------------- */ - case "move": { - const { runId, moves, file, language } = msg; - - if ( - !runId || - !moves || - file === undefined || - language === undefined - ) { - client.send( - JSON.stringify({ - requestId, - ok: false, - error: "body requires runId, moves, file and language", - }), - ); - return; - } - - const liveRun = await Core.getLiveRun(runId); - - if (!liveRun) { - client.send( - JSON.stringify({ - requestId, - ok: false, - error: "Run is not live or does not exist", - }), - ); - return; - } - - const run = await Core.addLiveRunEvent( - liveRun.id, - file, - language, - moves, - ); - - client.send( - JSON.stringify({ - requestId, - ok: true, - type: "move", - data: run, - }), - ); - - break; - } - - /* ---------------- SUBMIT ---------------- */ - case "submit": { - const { runId } = msg; - - if (!runId) { - client.send( - JSON.stringify({ - requestId, - ok: false, - error: "runId required", - }), - ); - return; - } - - const liveRun = await Core.getLiveRun(runId); - - if (!liveRun) { - client.send( - JSON.stringify({ - requestId, - ok: false, - error: "Run is not live or does not exist", - }), - ); - return; - } - - const run = await Core.submitRun(supabase, liveRun, session[0].id); - - client.send( - JSON.stringify({ - requestId, - ok: true, - type: "submit", - data: run, - }), - ); - break; - } - - /* ---------------- DELETE ---------------- */ - case "delete": { - const { runId } = msg; - - if (!runId) { - client.send( - JSON.stringify({ - requestId, - ok: false, - error: "runId required", - }), - ); - return; - } - - const run = await Core.removeLiveRun(runId); - - client.send( - JSON.stringify({ - requestId, - ok: true, - type: "delete", - data: run, - }), - ); - break; - } - - case "getText": - const { runId } = msg; - const data = await redis.get(`liveRunText:${runId}`); - - client.send( - JSON.stringify({ - requestId, - ok: true, - data, - }), - ); - break; - - default: - client.send( - JSON.stringify({ - requestId, - ok: false, - error: "Unknown message type", - }), - ); - } - } catch (err) { - console.error(err); - client.send( - JSON.stringify({ - requestId, - ok: false, - error: "Malformed message", - }), - ); - } - }); - }); - - client.on("close", () => { - // optional cleanup - }); -} diff --git a/src/app/api/view-run/route.ts b/src/app/api/view-run/route.ts index 8686012..7d921b3 100644 --- a/src/app/api/view-run/route.ts +++ b/src/app/api/view-run/route.ts @@ -1,67 +1,63 @@ +// ws/liveRun.ts import { WebSocketServer, WebSocket } from "ws"; -import Redis from "ioredis"; -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest } from "next/server"; import { LiveRunEvent } from "@/modules/live-run/types"; import { updateLiveRunViews } from "@/modules/live-run/core"; -import { redis } from "@/lib/redis"; +import { redis, redisSubscriber } from "@/lib/redis"; -export function GET(req: NextRequest) { - const headers = new Headers(); - headers.set("Connection", "Upgrade"); - headers.set("Upgrade", "websocket"); - return new Response("Upgrade Required", { status: 426, headers }); -} +// Map of runId → Set of WebSockets +const runSubscribers: Map> = new Map(); -export function UPGRADE(client: WebSocket, server: WebSocketServer) { - const subscriber = new Redis(process.env.REDIS_URL!); +// Handle incoming Pub/Sub messages +redisSubscriber.on("message", (channel, message) => { + const runId = channel.split(":")[1]; + const sockets = runSubscribers.get(runId); + if (!sockets) return; + + const data: LiveRunEvent = JSON.parse(message); + + sockets.forEach((ws) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ ...data, text: null })); + } + }); +}); + +export async function UPGRADE(client: WebSocket, server: WebSocketServer) { let runId: string; + // Receive runId from client client.once("message", async (message) => { runId = message.toString(); + // Track view count await updateLiveRunViews(runId, 1); - let lastEvent: LiveRunEvent | null = null; + // Send initial text + const text = await redis.get(`liveRunText:${runId}`); + client.send( + JSON.stringify({ file: null, language: null, moves: [], text }), + ); - const sendText = async () => { - const text = await redis.get(`liveRunText:${runId}`); - - if (client.readyState === WebSocket.OPEN) { - client.send( - JSON.stringify({ - file: lastEvent?.file, - language: lastEvent?.language, - moves: [], - text, - }), - ); - } - }; - - sendText(); - - client.on("message", sendText); - - subscriber.subscribe(`liveRunEvent:${runId}`, (err, count) => { - if (err) { - console.error("Failed to subscribe", err); - client.close(1011, "Redis subscription failed"); - } - }); - - subscriber.on("message", (channel, message) => { - const data: LiveRunEvent = JSON.parse(message); - - lastEvent = data; - if (client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify({ ...data, text: null })); - } - }); + // Add WebSocket to subscribers + if (!runSubscribers.has(runId)) { + runSubscribers.set(runId, new Set()); + await redisSubscriber.subscribe(`liveRunEvent:${runId}`); + } + runSubscribers.get(runId)!.add(client); }); + // Handle client disconnect client.once("close", async () => { - await subscriber.unsubscribe(`liveRunEvent:${runId}`); - subscriber.quit(); + const set = runSubscribers.get(runId); + if (set) { + set.delete(client); + if (set.size === 0) { + await redisSubscriber.unsubscribe(`liveRunEvent:${runId}`); + runSubscribers.delete(runId); + } + } + await updateLiveRunViews(runId, -1); }); @@ -69,3 +65,10 @@ export function UPGRADE(client: WebSocket, server: WebSocketServer) { console.error("WebSocket error:", err); }); } + +export function GET(req: NextRequest) { + const headers = new Headers(); + headers.set("Connection", "Upgrade"); + headers.set("Upgrade", "websocket"); + return new Response("Upgrade Required", { status: 426, headers }); +} diff --git a/src/app/leaderboard/components/LeaderboardRow.tsx b/src/app/leaderboard/components/LeaderboardRow.tsx index 8458976..4104a8e 100644 --- a/src/app/leaderboard/components/LeaderboardRow.tsx +++ b/src/app/leaderboard/components/LeaderboardRow.tsx @@ -48,7 +48,7 @@ const LeaderboardRow = ({ - {/* Wins */} + {/* World Records */} {run.wins} diff --git a/src/app/leaderboard/page.tsx b/src/app/leaderboard/page.tsx index 3d18b83..0fff916 100644 --- a/src/app/leaderboard/page.tsx +++ b/src/app/leaderboard/page.tsx @@ -78,9 +78,9 @@ export default function Leaderboard() { {/* Runner / Username */} Runner - {/* Wins */} + {/* World Records */} - Wins + World Records {/* Podiums */} diff --git a/src/app/runs/components/LiveRunRow.tsx b/src/app/runs/components/LiveRunRow.tsx index 08faad2..05d4bec 100644 --- a/src/app/runs/components/LiveRunRow.tsx +++ b/src/app/runs/components/LiveRunRow.tsx @@ -21,7 +21,7 @@ export default function LiveRunRow({ const [problem] = useAction(() => getProblem(run.problem)); const now = new Date().getTime() + 1000; - const isTop = run.views > 5; + const isTop = run.views >= 5; return ( { + const aTrending = a.views >= 5; + const bTrending = b.views >= 5; + + if (aTrending && bTrending) { + // Both trending → sort by views descending, then timestamp descending + if (b.views !== a.views) return b.views - a.views; + return b.start - a.start; + } else if (aTrending) { + return -1; // a goes first + } else if (bTrending) { + return 1; // b goes first + } else { + // Neither trending → sort by timestamp descending + return b.start - a.start; + } + }); return (
@@ -26,7 +41,7 @@ export default function LiveRuns() {

Live Runs

- 42 live runs · Streaming in real time + {liveRuns?.length} live runs · Streaming in real time

diff --git a/src/lib/redis.ts b/src/lib/redis.ts index f433e79..33321b3 100644 --- a/src/lib/redis.ts +++ b/src/lib/redis.ts @@ -3,6 +3,12 @@ import Redis from "ioredis"; export const redis = new Redis(process.env.REDIS_URL!); +export const redisSubscriber = new Redis(process.env.REDIS_URL!); + redis.on("error", (err) => { console.error("Redis error:", err); }); + +redisSubscriber.on("error", (err) => { + console.error("Redis subscriber error:", err); +}); diff --git a/src/lib/supabase/server.ts b/src/lib/supabase/server.ts index c8fed94..dd20470 100644 --- a/src/lib/supabase/server.ts +++ b/src/lib/supabase/server.ts @@ -1,9 +1,8 @@ -"use server"; - import "server-only"; import { createServerClient, type CookieOptions } from "@supabase/ssr"; import { cookies } from "next/headers"; +import { createClient as createSimpleClient } from "@supabase/supabase-js"; export async function createClient() { const cookieStore = await cookies(); @@ -46,3 +45,8 @@ export async function createClientCookie(cookies: Map) { }, ); } + +export const supabaseApiClient = createSimpleClient( + process.env.SUPABASE_URL!, + process.env.SUPABASE_SECRET_KEY!, +); diff --git a/src/modules/account/core.ts b/src/modules/account/core.ts index d250696..920650a 100644 --- a/src/modules/account/core.ts +++ b/src/modules/account/core.ts @@ -1,7 +1,6 @@ -"use server"; +import "server-only"; import { SupabaseClient, User } from "@supabase/supabase-js"; -import "server-only"; import { UserProfile } from "../user/types"; export async function signInWithGitHub(supabase: SupabaseClient) { diff --git a/src/modules/leaderboard/core.ts b/src/modules/leaderboard/core.ts index d981b6c..a4b0634 100644 --- a/src/modules/leaderboard/core.ts +++ b/src/modules/leaderboard/core.ts @@ -1,7 +1,6 @@ -"use server"; +import "server-only"; import { SupabaseClient } from "@supabase/supabase-js"; -import "server-only"; import { LeaderboardUser } from "./types"; export async function getGlobalLeaderboard( diff --git a/src/modules/live-run/core.ts b/src/modules/live-run/core.ts index 305b9b9..396753b 100644 --- a/src/modules/live-run/core.ts +++ b/src/modules/live-run/core.ts @@ -1,11 +1,9 @@ -"use server"; - import "server-only"; + import { LiveRun, LiveRunMove } from "./types"; -import { SupabaseClient } from "@supabase/supabase-js"; import { redis } from "@/lib/redis"; -import Redis from "ioredis"; import { applyMoves } from "@/lib/move"; +import { supabaseApiClient } from "@/lib/supabase/server"; /** -------------------- Live Runs in Redis -------------------- */ @@ -46,6 +44,7 @@ export async function updateLiveRunViews( ): Promise { const run = await getLiveRun(id); if (!run) return null; + if (run.views === 0 && increment < 0) return null; run.views += increment; await redis.set(`liveRun:${run.id}`, JSON.stringify(run)); return run.views; @@ -65,7 +64,7 @@ export async function getLiveRuns(): Promise { * Remove a live run from Redis */ export async function removeLiveRun(id: string) { - await redis.del(`liveRun:${id}`, `liveRunEvent:${id}`); + await redis.del(`liveRun:${id}`, `liveRunEvent:${id}`, `liveRunText:${id}`); } /** -------------------- Live Run Moves -------------------- */ @@ -107,15 +106,11 @@ export async function addLiveRunEvent( * Submit a run to Supabase (persistent storage) * and remove it from Redis */ -export async function submitRun( - supabase: SupabaseClient, - run: LiveRun, - user_id: string, -) { +export async function submitRun(run: LiveRun, user_id: string) { const now = new Date(); const durationMs = now.getTime() - run.start; - const { data, error } = await supabase + const { data, error } = await supabaseApiClient .from("runs") .insert([ { diff --git a/src/modules/problems/core.ts b/src/modules/problems/core.ts index a0c2346..db1550e 100644 --- a/src/modules/problems/core.ts +++ b/src/modules/problems/core.ts @@ -1,6 +1,5 @@ -"use server"; - import "server-only"; + import { Problem, ProblemDefinition } from "./types"; const problems: ProblemDefinition[] = [ diff --git a/src/modules/user/core.ts b/src/modules/user/core.ts index a59f71d..3a1088a 100644 --- a/src/modules/user/core.ts +++ b/src/modules/user/core.ts @@ -1,3 +1 @@ -"use server"; - import "server-only";