- Add migration 0004: partial unique index on user_points (user_id, action, recording_id) for RECORDING_CREATE and RECORDING_FEATURED to prevent earn-on-republish farming - Add revokePoints() to gamification lib; awardPoints() now uses onConflictDoNothing - Add gamificationQueue (BullMQ) with 3-attempt exponential backoff - Add gamification worker handling awardPoints, revokePoints, checkAchievements jobs - Move all inline gamification calls in recordings + comments resolvers to queue - Revoke RECORDING_CREATE points when a recording is unpublished (published → draft) - Register gamification worker at server startup alongside mail worker Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
50 lines
1.8 KiB
TypeScript
50 lines
1.8 KiB
TypeScript
import { Worker } from "bullmq";
|
|
import { redisConnectionOpts } from "../connection.js";
|
|
import { awardPoints, revokePoints, checkAchievements } from "../../lib/gamification.js";
|
|
import { db } from "../../db/connection.js";
|
|
import { logger } from "../../lib/logger.js";
|
|
import type { POINT_VALUES } from "../../lib/gamification.js";
|
|
|
|
const log = logger.child({ component: "gamification-worker" });
|
|
|
|
export type GamificationJobData =
|
|
| { job: "awardPoints"; userId: string; action: keyof typeof POINT_VALUES; recordingId?: string }
|
|
| { job: "revokePoints"; userId: string; action: keyof typeof POINT_VALUES; recordingId: string }
|
|
| { job: "checkAchievements"; userId: string; category?: string };
|
|
|
|
export function startGamificationWorker(): Worker {
|
|
const worker = new Worker(
|
|
"gamification",
|
|
async (bullJob) => {
|
|
const data = bullJob.data as GamificationJobData;
|
|
log.info({ jobId: bullJob.id, job: data.job, userId: data.userId }, "Processing gamification job");
|
|
|
|
switch (data.job) {
|
|
case "awardPoints":
|
|
await awardPoints(db, data.userId, data.action, data.recordingId);
|
|
break;
|
|
case "revokePoints":
|
|
await revokePoints(db, data.userId, data.action, data.recordingId);
|
|
break;
|
|
case "checkAchievements":
|
|
await checkAchievements(db, data.userId, data.category);
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown gamification job: ${(data as GamificationJobData).job}`);
|
|
}
|
|
|
|
log.info({ jobId: bullJob.id, job: data.job }, "Gamification job completed");
|
|
},
|
|
{ connection: redisConnectionOpts },
|
|
);
|
|
|
|
worker.on("failed", (bullJob, err) => {
|
|
log.error(
|
|
{ jobId: bullJob?.id, job: (bullJob?.data as GamificationJobData)?.job, err: err.message },
|
|
"Gamification job failed",
|
|
);
|
|
});
|
|
|
|
return worker;
|
|
}
|