import { DBOS } from "@dbos-inc/dbos-sdk"; import { eq } from "drizzle-orm"; import Elysia, { t } from "elysia"; import { betterAuthElysia } from "../auth"; import { db } from "../db"; import { party } from "../db/schema"; import { getMemberRecord, getPartyStatus } from "../party-data"; import type { QuizState } from "../party-types"; import { QuizWorkflow, quizQueue } from "../workflows/quiz"; import { pubsub } from "./party-socket"; function broadcastStatusToMembers( status: Awaited>, ) { if (!status) return; const payload = JSON.stringify({ type: "party_status", party: status.party, members: status.members, }); pubsub.publish(`party:${status.party.id}`, payload); for (const member of status.members) { pubsub.publish(`user:${member.userId}`, payload); } } const quizWf = new QuizWorkflow(); export const quizRoutes = new Elysia() .use(betterAuthElysia) .group("/party/:partyId/quiz", (app) => app .post( "/start", async ({ user, set, params }) => { const membership = await getMemberRecord(db, user.id); if (!membership || membership.partyId !== params.partyId) { set.status = 403; return { error: "Not a member of this party" }; } const existingQuiz = await db .select({ status: party.status }) .from(party) .where(eq(party.id, params.partyId)) .limit(1) .then((rows) => rows[0]); if (existingQuiz?.status === "started") { set.status = 409; return { error: "Quiz already running" }; } const handle = await DBOS.startWorkflow(quizWf.startQuiz, { queueName: quizQueue.name, enqueueOptions: { queuePartitionKey: params.partyId }, })(params.partyId); await db .update(party) .set({ status: "started", data: null, lastUpdated: new Date(), }) .where(eq(party.id, params.partyId)); const status = await getPartyStatus(params.partyId); broadcastStatusToMembers(status); return { message: "Quiz started", workflowId: handle.workflowID, }; }, { auth: true }, ) .post( "/restart", async ({ user, set, params }) => { const membership = await getMemberRecord(db, user.id); if (!membership || membership.partyId !== params.partyId) { set.status = 403; return { error: "Not a member of this party" }; } const currentParty = await db.query.party.findFirst({ where: { id: params.partyId, }, }); if (!currentParty) { set.status = 404; return { error: "Party not found" }; } const quizData = currentParty.data as QuizState | null; if (!quizData || quizData.status !== "results") { set.status = 409; return { error: "Quiz is not finished yet" }; } await db .update(party) .set({ status: "started", data: null, lastUpdated: new Date(), }) .where(eq(party.id, params.partyId)); const handle = await DBOS.startWorkflow(quizWf.restartQuiz, { queueName: quizQueue.name, enqueueOptions: { queuePartitionKey: params.partyId }, })(params.partyId); const status = await getPartyStatus(params.partyId); broadcastStatusToMembers(status); return { message: "Quiz restarted", workflowId: handle.workflowID, }; }, { auth: true }, ) .post( "/response", async ({ user, body, params, set }) => { const party = await db.query.party.findFirst({ where: { id: params.partyId, }, }); if (!party) { set.status = 404; return { error: "Party not found" }; } const quizData = party.data as QuizState | null; if (!quizData || quizData.status !== "running") { set.status = 400; return { error: "Quiz not running" }; } if (!quizData.workflowId) { set.status = 500; return { error: "Workflow ID not found" }; } await DBOS.send( quizData.workflowId, { playerId: user.id, selected: body.selected }, "quiz_responses", ); return { message: "Response recorded" }; }, { auth: true, body: t.Object({ selected: t.Integer(), }), }, ), );