Compare commits
No commits in common. "55c44934599874ba96b055fe1bfcf1690cd7e67d" and "642a034e08b68cc997b45b3a8b51fc7104fd8349" have entirely different histories.
55c4493459
...
642a034e08
9 changed files with 82 additions and 331 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.4.12/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.4.11/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
|
|
|
||||||
|
|
@ -35,20 +35,6 @@ export type Question = {
|
||||||
points: number;
|
points: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QuizResponse = {
|
|
||||||
playerId: string;
|
|
||||||
selected: number;
|
|
||||||
correct: boolean;
|
|
||||||
answeredAt: number;
|
|
||||||
pointsGained: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QuizRound = {
|
|
||||||
questionIndex: number;
|
|
||||||
question: Question;
|
|
||||||
responses: QuizResponse[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QuizState = {
|
export type QuizState = {
|
||||||
status: "running" | "results";
|
status: "running" | "results";
|
||||||
workflowId: string | null;
|
workflowId: string | null;
|
||||||
|
|
@ -56,16 +42,9 @@ export type QuizState = {
|
||||||
currentQuestion: Question | null;
|
currentQuestion: Question | null;
|
||||||
answers: Record<
|
answers: Record<
|
||||||
string,
|
string,
|
||||||
{
|
{ playerId: string; selected: number; correct: boolean }
|
||||||
playerId: string;
|
|
||||||
selected: number;
|
|
||||||
correct: boolean;
|
|
||||||
pointsGained: number;
|
|
||||||
answeredAt: number;
|
|
||||||
}
|
|
||||||
>;
|
>;
|
||||||
scores: Record<string, number>;
|
scores: Record<string, number>;
|
||||||
history: QuizRound[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PartySocketEvent =
|
export type PartySocketEvent =
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import type { Question } from "../party-types";
|
|
||||||
import {
|
|
||||||
buildQuestionWindow,
|
|
||||||
getTopArtistName,
|
|
||||||
getTopGenreName,
|
|
||||||
getTopTrackName,
|
|
||||||
type PartyAnalytics,
|
|
||||||
} from "./question-utils";
|
|
||||||
|
|
||||||
export function buildAudioMetadataQuestion(
|
|
||||||
analytics: PartyAnalytics,
|
|
||||||
index: number,
|
|
||||||
): Question {
|
|
||||||
const questions: Array<Omit<Question, "startTimestamp" | "endTimestamp">> = [
|
|
||||||
{
|
|
||||||
text: "Which genre appears most in the party analytics?",
|
|
||||||
options: [getTopGenreName(analytics), "Electronic", "Rock", "Jazz"],
|
|
||||||
correct: 0,
|
|
||||||
points: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Which artist shows up most often in the shared audio data?",
|
|
||||||
options: [
|
|
||||||
getTopArtistName(analytics),
|
|
||||||
"Artist B",
|
|
||||||
"Artist C",
|
|
||||||
"Artist D",
|
|
||||||
],
|
|
||||||
correct: 0,
|
|
||||||
points: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Which track looks most shared across the party?",
|
|
||||||
options: [getTopTrackName(analytics), "Track B", "Track C", "Track D"],
|
|
||||||
correct: 0,
|
|
||||||
points: 10,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const question = questions[index % questions.length];
|
|
||||||
if (!question) throw new Error("Question not found");
|
|
||||||
return buildQuestionWindow(question);
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import type { db } from "../db";
|
|
||||||
import type { Question, QuizState } from "../party-types";
|
|
||||||
import { buildAudioMetadataQuestion } from "./audio-question-generator";
|
|
||||||
import type { PartyAnalytics } from "./question-utils";
|
|
||||||
import { fetchPartyMembers } from "./question-utils";
|
|
||||||
import { buildSocialQuestion } from "./social-question-generator";
|
|
||||||
|
|
||||||
export type PartyQuestionType = "audio-metadata" | "social";
|
|
||||||
|
|
||||||
type GenerateQuestionInput = {
|
|
||||||
db: typeof db;
|
|
||||||
partyId: string;
|
|
||||||
quizState: QuizState;
|
|
||||||
analytics: PartyAnalytics;
|
|
||||||
index: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function generatePartyQuestion({
|
|
||||||
db: dbClient,
|
|
||||||
partyId,
|
|
||||||
quizState,
|
|
||||||
analytics,
|
|
||||||
index,
|
|
||||||
}: GenerateQuestionInput): Promise<Question> {
|
|
||||||
const members = await fetchPartyMembers(dbClient, partyId);
|
|
||||||
const type: PartyQuestionType = index % 2 === 0 ? "audio-metadata" : "social";
|
|
||||||
return type === "audio-metadata"
|
|
||||||
? buildAudioMetadataQuestion(analytics, index)
|
|
||||||
: buildSocialQuestion(quizState, analytics, members, index);
|
|
||||||
}
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
import type { db as Db } from "../db";
|
|
||||||
|
|
||||||
export type PartyQuestionMember = {
|
|
||||||
userId: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PartyAnalytics = {
|
|
||||||
groupSummary?: {
|
|
||||||
mostSharedGenres?: { name: string }[];
|
|
||||||
};
|
|
||||||
storyClusters?: {
|
|
||||||
tracks?: { name: string }[];
|
|
||||||
artists?: { name: string }[];
|
|
||||||
}[];
|
|
||||||
memberProfiles?: { userId: string }[];
|
|
||||||
pairwise?: { userIdA: string; userIdB: string }[];
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
export const QUESTION_DURATION_MS = 60_000;
|
|
||||||
export const MIN_PARTY_SIZE = 2;
|
|
||||||
export const MAX_PARTY_SIZE = 4;
|
|
||||||
|
|
||||||
export async function fetchPartyMembers(
|
|
||||||
db: typeof Db,
|
|
||||||
partyId: string,
|
|
||||||
): Promise<PartyQuestionMember[]> {
|
|
||||||
const members = await db.query.partyMember.findMany({
|
|
||||||
where: {
|
|
||||||
partyId,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
user: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return members.map((member) => ({
|
|
||||||
userId: member.userId,
|
|
||||||
name: member.user?.name ?? member.userId,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPartySize(memberCount: number): number {
|
|
||||||
return Math.max(MIN_PARTY_SIZE, Math.min(MAX_PARTY_SIZE, memberCount));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildQuestionWindow<T extends object>(
|
|
||||||
question: T,
|
|
||||||
timestamp = Date.now(),
|
|
||||||
): T & { startTimestamp: number; endTimestamp: number } {
|
|
||||||
return {
|
|
||||||
...question,
|
|
||||||
startTimestamp: timestamp,
|
|
||||||
endTimestamp: timestamp + QUESTION_DURATION_MS,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTopGenreName(analytics: PartyAnalytics): string {
|
|
||||||
return analytics?.groupSummary?.mostSharedGenres?.[0]?.name ?? "Hip-Hop";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTopArtistName(analytics: PartyAnalytics): string {
|
|
||||||
return analytics?.storyClusters?.[0]?.artists?.[0]?.name ?? "Artist A";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTopTrackName(analytics: PartyAnalytics): string {
|
|
||||||
return analytics?.storyClusters?.[0]?.tracks?.[0]?.name ?? "Track A";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCurrentLeader(
|
|
||||||
quizState: { scores: Record<string, number> },
|
|
||||||
members: PartyQuestionMember[],
|
|
||||||
): PartyQuestionMember {
|
|
||||||
const leaderId = Object.entries(quizState.scores)
|
|
||||||
.sort(([, a], [, b]) => b - a)
|
|
||||||
.at(0)?.[0];
|
|
||||||
|
|
||||||
return (
|
|
||||||
members.find((member) => member.userId === leaderId) ??
|
|
||||||
members[0] ?? { userId: "", name: "Player A" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMostDiverseMember(
|
|
||||||
analytics: PartyAnalytics,
|
|
||||||
members: PartyQuestionMember[],
|
|
||||||
): PartyQuestionMember {
|
|
||||||
const userId = analytics?.memberProfiles?.[0]?.userId;
|
|
||||||
return (
|
|
||||||
members.find((member) => member.userId === userId) ??
|
|
||||||
members[1] ??
|
|
||||||
members[0] ?? { userId: "", name: "Player B" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMostAlignedMember(
|
|
||||||
analytics: PartyAnalytics,
|
|
||||||
members: PartyQuestionMember[],
|
|
||||||
): PartyQuestionMember {
|
|
||||||
const userId = analytics?.pairwise?.[0]?.userIdA;
|
|
||||||
return (
|
|
||||||
members.find((member) => member.userId === userId) ??
|
|
||||||
members[2] ??
|
|
||||||
members[0] ?? { userId: "", name: "Player C" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildMemberOptions(
|
|
||||||
correctMember: PartyQuestionMember,
|
|
||||||
members: PartyQuestionMember[],
|
|
||||||
): string[] {
|
|
||||||
const desiredCount = getPartySize(members.length);
|
|
||||||
const options = uniqueStrings([
|
|
||||||
correctMember.name,
|
|
||||||
...members.map((member) => member.name),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (options.length < desiredCount) {
|
|
||||||
for (const fallback of fallbackPlayerNames(desiredCount)) {
|
|
||||||
if (options.length >= desiredCount) break;
|
|
||||||
if (!options.includes(fallback)) options.push(fallback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ordered = [
|
|
||||||
correctMember.name,
|
|
||||||
...options.filter((name) => name !== correctMember.name),
|
|
||||||
];
|
|
||||||
return ordered.slice(0, desiredCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
function uniqueStrings(values: string[]): string[] {
|
|
||||||
return values.filter((value, index, list) => list.indexOf(value) === index);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fallbackPlayerNames(count: number): string[] {
|
|
||||||
return Array.from(
|
|
||||||
{ length: count },
|
|
||||||
(_, index) => `Player ${String.fromCharCode(65 + index)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import type { Question, QuizState } from "../party-types";
|
|
||||||
import {
|
|
||||||
buildMemberOptions,
|
|
||||||
buildQuestionWindow,
|
|
||||||
getCurrentLeader,
|
|
||||||
getMostAlignedMember,
|
|
||||||
getMostDiverseMember,
|
|
||||||
type PartyAnalytics,
|
|
||||||
type PartyQuestionMember,
|
|
||||||
} from "./question-utils";
|
|
||||||
|
|
||||||
export function buildSocialQuestion(
|
|
||||||
quizState: QuizState,
|
|
||||||
analytics: PartyAnalytics,
|
|
||||||
members: PartyQuestionMember[],
|
|
||||||
index: number,
|
|
||||||
): Question {
|
|
||||||
const leader = getCurrentLeader(quizState, members);
|
|
||||||
const diverse = getMostDiverseMember(analytics, members);
|
|
||||||
const aligned = getMostAlignedMember(analytics, members);
|
|
||||||
|
|
||||||
const questions: Array<Omit<Question, "startTimestamp" | "endTimestamp">> = [
|
|
||||||
{
|
|
||||||
text: "Who is leading the quiz right now?",
|
|
||||||
options: buildMemberOptions(leader, members),
|
|
||||||
correct: 0,
|
|
||||||
points: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Who looks like the most diverse listener in the party?",
|
|
||||||
options: buildMemberOptions(diverse, members),
|
|
||||||
correct: 0,
|
|
||||||
points: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Which member seems most aligned with the rest of the party?",
|
|
||||||
options: buildMemberOptions(aligned, members),
|
|
||||||
correct: 0,
|
|
||||||
points: 10,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const question = questions[index % questions.length];
|
|
||||||
if (!question) throw new Error("Question not found");
|
|
||||||
return buildQuestionWindow(question);
|
|
||||||
}
|
|
||||||
|
|
@ -114,5 +114,30 @@ export const quizRoutes = new Elysia()
|
||||||
selected: t.Integer(),
|
selected: t.Integer(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
"/status",
|
||||||
|
async ({ params, set }) => {
|
||||||
|
const existingQuiz = await db
|
||||||
|
.select({ data: party.data })
|
||||||
|
.from(party)
|
||||||
|
.where(eq(party.id, params.partyId))
|
||||||
|
.limit(1)
|
||||||
|
.then((rows) => rows[0]);
|
||||||
|
|
||||||
|
if (!existingQuiz) {
|
||||||
|
set.status = 404;
|
||||||
|
return { error: "Party not found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const quizData = (
|
||||||
|
(existingQuiz.data ?? {}) as Record<string, unknown>
|
||||||
|
).quiz as QuizState | undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
quiz: quizData,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ auth: true },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -358,8 +358,8 @@ export async function seedPartyWithThreeDiverseUsers(): Promise<{
|
||||||
|
|
||||||
// Give each user their unique track
|
// Give each user their unique track
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
await addTopTrack(userIds[i]!, tracks[i]!.id, 1);
|
await addTopTrack(userIds[i]!, tracks[i]?.id, 1);
|
||||||
await addTopArtist(userIds[i]!, artists[i]!.id, 1);
|
await addTopArtist(userIds[i]!, artists[i]?.id, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,8 @@ import { ConfiguredInstance, DBOS, WorkflowQueue } from "@dbos-inc/dbos-sdk";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import { partyMember } from "../db/schema";
|
import { partyMember } from "../db/schema";
|
||||||
import { generatePartyQuestion } from "../party/question-generator";
|
|
||||||
import type { PartyAnalytics } from "../party/question-utils";
|
|
||||||
import { updatePartyData } from "../party/state";
|
import { updatePartyData } from "../party/state";
|
||||||
import type { QuizResponse, QuizRound, QuizState } from "../party-types";
|
import type { QuizState } from "../party-types";
|
||||||
|
|
||||||
const TOTAL_QUESTIONS = 5;
|
const TOTAL_QUESTIONS = 5;
|
||||||
|
|
||||||
|
|
@ -33,7 +31,6 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
currentQuestion: null,
|
currentQuestion: null,
|
||||||
answers: {},
|
answers: {},
|
||||||
scores: {},
|
scores: {},
|
||||||
history: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize quiz state
|
// Initialize quiz state
|
||||||
|
|
@ -48,19 +45,9 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
for (let i = 0; i < TOTAL_QUESTIONS; i++) {
|
for (let i = 0; i < TOTAL_QUESTIONS; i++) {
|
||||||
quizState.questionIndex = i;
|
quizState.questionIndex = i;
|
||||||
|
|
||||||
const question = await QuizWorkflow.generateQuestion(
|
const question = await QuizWorkflow.generateQuestion(i);
|
||||||
partyId,
|
|
||||||
quizState,
|
|
||||||
i,
|
|
||||||
);
|
|
||||||
quizState.currentQuestion = question;
|
quizState.currentQuestion = question;
|
||||||
quizState.answers = {};
|
quizState.answers = {};
|
||||||
const round: QuizRound = {
|
|
||||||
questionIndex: i,
|
|
||||||
question,
|
|
||||||
responses: [],
|
|
||||||
};
|
|
||||||
quizState.history.push(round);
|
|
||||||
|
|
||||||
await QuizWorkflow.updatePartyData(partyId, quizState);
|
await QuizWorkflow.updatePartyData(partyId, quizState);
|
||||||
// Wait for all responses with timeout
|
// Wait for all responses with timeout
|
||||||
|
|
@ -74,19 +61,14 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
|
|
||||||
if (response === null) {
|
if (response === null) {
|
||||||
// Timeout - fill in missing players with no answer
|
// Timeout - fill in missing players with no answer
|
||||||
const now = Date.now();
|
|
||||||
for (const memberId of memberIds) {
|
for (const memberId of memberIds) {
|
||||||
if (!receivedPlayers.has(memberId)) {
|
if (!receivedPlayers.has(memberId)) {
|
||||||
receivedPlayers.add(memberId);
|
receivedPlayers.add(memberId);
|
||||||
const noAnswer: QuizResponse = {
|
quizState.answers[memberId] = {
|
||||||
playerId: memberId,
|
playerId: memberId,
|
||||||
selected: -1,
|
selected: -1,
|
||||||
correct: false,
|
correct: false,
|
||||||
answeredAt: now,
|
|
||||||
pointsGained: 0,
|
|
||||||
};
|
};
|
||||||
quizState.answers[memberId] = noAnswer;
|
|
||||||
round.responses.push(noAnswer);
|
|
||||||
await QuizWorkflow.updatePartyData(partyId, quizState);
|
await QuizWorkflow.updatePartyData(partyId, quizState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -94,21 +76,15 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
receivedPlayers.add(response.playerId);
|
receivedPlayers.add(response.playerId);
|
||||||
const answeredAt = Date.now();
|
|
||||||
const isCorrect = response.selected === question.correct;
|
const isCorrect = response.selected === question.correct;
|
||||||
const pointsGained = isCorrect ? question.points : 0;
|
quizState.answers[response.playerId] = {
|
||||||
const quizResponse: QuizResponse = {
|
|
||||||
...response,
|
...response,
|
||||||
correct: isCorrect,
|
correct: isCorrect,
|
||||||
answeredAt,
|
|
||||||
pointsGained,
|
|
||||||
};
|
};
|
||||||
quizState.answers[response.playerId] = quizResponse;
|
|
||||||
round.responses.push(quizResponse);
|
|
||||||
|
|
||||||
if (isCorrect) {
|
if (isCorrect) {
|
||||||
quizState.scores[response.playerId] =
|
quizState.scores[response.playerId] =
|
||||||
(quizState.scores[response.playerId] ?? 0) + pointsGained;
|
(quizState.scores[response.playerId] ?? 0) + question.points;
|
||||||
}
|
}
|
||||||
|
|
||||||
await QuizWorkflow.updatePartyData(partyId, quizState);
|
await QuizWorkflow.updatePartyData(partyId, quizState);
|
||||||
|
|
@ -130,11 +106,7 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
@DBOS.step()
|
@DBOS.step()
|
||||||
static async generateQuestion(
|
static async generateQuestion(index: number): Promise<{
|
||||||
partyId: string,
|
|
||||||
quizState: QuizState,
|
|
||||||
index: number,
|
|
||||||
): Promise<{
|
|
||||||
text: string;
|
text: string;
|
||||||
options: string[];
|
options: string[];
|
||||||
correct: number;
|
correct: number;
|
||||||
|
|
@ -142,19 +114,54 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
endTimestamp: number;
|
endTimestamp: number;
|
||||||
points: number;
|
points: number;
|
||||||
}> {
|
}> {
|
||||||
const partyRecord = await db.query.party.findFirst({
|
// Placeholder - returns same question for now, question generation comes later
|
||||||
where: {
|
const questions: {
|
||||||
id: partyId,
|
text: string;
|
||||||
|
options: string[];
|
||||||
|
correct: number;
|
||||||
|
points: number;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
text: "What is the most common genre in your party's shared taste?",
|
||||||
|
options: ["Hip-Hop", "Rock", "Electronic", "Jazz"],
|
||||||
|
correct: 0,
|
||||||
|
points: 10,
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
const analytics = (partyRecord?.analysisData ?? null) as PartyAnalytics;
|
text: "Which artist do most party members follow?",
|
||||||
return generatePartyQuestion({
|
options: ["Artist A", "Artist B", "Artist C", "Artist D"],
|
||||||
db,
|
correct: 1,
|
||||||
partyId,
|
points: 10,
|
||||||
quizState,
|
},
|
||||||
analytics,
|
{
|
||||||
index,
|
text: "What percentage of the party shares at least 1 album?",
|
||||||
});
|
options: ["0-25%", "25-50%", "50-75%", "75-100%"],
|
||||||
|
correct: 2,
|
||||||
|
points: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Who has the most diverse taste in the party?",
|
||||||
|
options: ["Player A", "Player B", "Player C", "Player D"],
|
||||||
|
correct: 0,
|
||||||
|
points: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Which track appears most in everyone's top 50?",
|
||||||
|
options: ["Track A", "Track B", "Track C", "Track D"],
|
||||||
|
correct: 3,
|
||||||
|
points: 10,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const question = questions[index % questions.length];
|
||||||
|
if (!question) {
|
||||||
|
throw new Error("Question not found");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...question,
|
||||||
|
startTimestamp: Date.now(),
|
||||||
|
endTimestamp: Date.now() + 60_000,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@DBOS.step()
|
@DBOS.step()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue