feat(upload): add realtime SSE updates

This commit is contained in:
2026-08-02 18:43:45 +07:00 Unverified
parent ccd875327a
commit 7f4a1a339c
9 changed files with 112 additions and 2 deletions
+5
View File
@@ -1,11 +1,13 @@
export type RealtimeRefreshFilter =
| { kind: "form"; formId?: string }
| { kind: "uploads" }
| { kind: "xp" }
| { kind: "vc"; channelId?: string };
export type RealtimeRefreshHandlers = {
onOpen: () => void;
onForm: (event: { formId: string }) => void;
onUpload: () => void;
onXp: () => void;
onVc: (event: { channelId: string }) => void;
};
@@ -50,6 +52,9 @@ export function startRealtimeRefresh({
onXp() {
if (filter.kind === "xp") scheduleRefresh();
},
onUpload() {
if (filter.kind === "uploads") scheduleRefresh();
},
onVc(event) {
if (
filter.kind === "vc"
+29
View File
@@ -37,3 +37,32 @@ describe("leaderboard SSE contract", () => {
)).toBeNull();
});
});
describe("upload SSE contract", () => {
test("registers an admin-only upload topic", () => {
expect(isSseTopic("uploads")).toBeTrue();
expect(sse.uploads.adminOnly).toBeTrue();
});
test("validates upload job status events", () => {
const message = JSON.stringify({
event: "update",
data: {
uploadId: "upload-1",
jobId: "job-1",
platform: "youtube",
status: "completed",
},
});
expect(sse.uploads.parseRedisMessage(message)).toEqual({
event: "update",
data: {
uploadId: "upload-1",
jobId: "job-1",
platform: "youtube",
status: "completed",
},
});
});
});
+18
View File
@@ -306,6 +306,24 @@ export const sse = createSseEndpoints({
}).strict(),
},
},
uploads: {
adminOnly: true,
events: {
update: z.object({
uploadId: z.string().min(1),
jobId: z.string().min(1).optional(),
platform: z.enum(["youtube", "tiktok"]).optional(),
status: z.enum([
"scheduled",
"processing",
"pending",
"completed",
"partial",
"failed",
]),
}).strict(),
},
},
leaderboards: {
events: {
xp: z.object({}).strict(),
+23
View File
@@ -7,6 +7,7 @@ import {
type YoutubeSettings,
} from "@/db/schema";
import { and, desc, eq } from "drizzle-orm";
import { sse } from "@/lib/realtime/sse";
const VIDEO_TYPES = new Set(["video/mp4", "video/quicktime", "video/webm"]);
@@ -338,6 +339,12 @@ export async function publishVideoJob(jobId: string) {
error: null,
})
.where(eq(videoUploadJobs.id, job.id));
await sse.uploads.pub("update", {
uploadId: upload.id,
jobId: job.id,
platform: job.platform,
status: "processing",
});
try {
const result =
job.platform === "youtube"
@@ -352,6 +359,12 @@ export async function publishVideoJob(jobId: string) {
completedAt: new Date(),
})
.where(eq(videoUploadJobs.id, job.id));
await sse.uploads.pub("update", {
uploadId: upload.id,
jobId: job.id,
platform: job.platform,
status: "completed",
});
} catch (error) {
const message =
error instanceof Error ? error.message : "Platform upload failed.";
@@ -359,6 +372,12 @@ export async function publishVideoJob(jobId: string) {
.update(videoUploadJobs)
.set({ status: "failed", error: message })
.where(eq(videoUploadJobs.id, job.id));
await sse.uploads.pub("update", {
uploadId: upload.id,
jobId: job.id,
platform: job.platform,
status: "failed",
});
throw error;
}
}
@@ -398,5 +417,9 @@ export async function processDueVideoUploads() {
.update(videoUploads)
.set({ status: nextStatus, updatedAt: new Date() })
.where(eq(videoUploads.id, upload.id));
await sse.uploads.pub("update", {
uploadId: upload.id,
status: nextStatus,
});
}
}
+9
View File
@@ -2,6 +2,7 @@ import "server-only";
import { mkdir, readdir, stat, writeFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { sse } from "@/lib/realtime/sse";
import { db } from "@/db";
import {
videoUploadJobs,
@@ -107,6 +108,10 @@ export async function saveVideoUpload(formData: FormData, discordId: string) {
{ uploadId: upload.id, platform: "tiktok" },
])
.returning({ id: videoUploadJobs.id });
await sse.uploads.pub("update", {
uploadId: upload.id,
status: scheduledAt ? "scheduled" : "processing",
});
if (!scheduledAt) {
await Promise.allSettled(jobs.map((job) => publishVideoJob(job.id)));
const remaining = await db.query.videoUploadJobs.findMany({
@@ -120,6 +125,10 @@ export async function saveVideoUpload(formData: FormData, discordId: string) {
updatedAt: new Date(),
})
.where(eq(videoUploads.id, upload.id));
await sse.uploads.pub("update", {
uploadId: upload.id,
status: remaining.length === 0 ? "completed" : "partial",
});
}
return upload.id;
}