127 lines
4.3 KiB
TypeScript
127 lines
4.3 KiB
TypeScript
import { createLogger } from '@/lib/logger';
|
|
import { generateClassroom, type GenerateClassroomInput } from '@/lib/server/classroom-generation';
|
|
import {
|
|
markClassroomGenerationJobCancelled,
|
|
markClassroomGenerationJobFailed,
|
|
markClassroomGenerationJobRunning,
|
|
markClassroomGenerationJobSucceeded,
|
|
readClassroomGenerationJob,
|
|
updateClassroomGenerationJobProgress,
|
|
updateClassroomGenerationJob,
|
|
} from '@/lib/server/classroom-job-store';
|
|
|
|
const log = createLogger('ClassroomJob');
|
|
const runningJobs = new Map<string, Promise<void>>();
|
|
const runningControllers = new Map<string, AbortController>();
|
|
|
|
function isAbortError(error: unknown): boolean {
|
|
return (
|
|
(error instanceof DOMException && error.name === 'AbortError') ||
|
|
(error instanceof Error && error.name === 'AbortError')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Request cancellation of a running job. Cooperative: the generation stops at
|
|
* the next scene boundary (see `generateClassroom`'s signal checks) and the
|
|
* job is then marked `cancelled`. No-op for jobs that are not running.
|
|
*/
|
|
export function cancelClassroomGenerationJob(jobId: string): boolean {
|
|
const controller = runningControllers.get(jobId);
|
|
if (!controller) return false;
|
|
controller.abort();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Resume a cancelled/failed job from its persisted input: reset it to
|
|
* `queued` and re-run. The generation restarts from the beginning — server-side
|
|
* generation commits atomically, so there is no partial state to continue from.
|
|
*/
|
|
export async function resumeClassroomGenerationJob(
|
|
jobId: string,
|
|
baseUrl: string,
|
|
): Promise<boolean> {
|
|
const job = await readClassroomGenerationJob(jobId);
|
|
if (!job) return false;
|
|
if (job.status !== 'failed' && job.status !== 'cancelled') return false;
|
|
if (!job.input) return false;
|
|
|
|
await updateClassroomGenerationJob(jobId, {
|
|
status: 'queued',
|
|
step: 'queued',
|
|
progress: 0,
|
|
message: 'Classroom generation job queued (resumed)',
|
|
completedAt: undefined,
|
|
error: undefined,
|
|
result: undefined,
|
|
});
|
|
|
|
runClassroomGenerationJob(jobId, job.input, baseUrl);
|
|
return true;
|
|
}
|
|
|
|
export function runClassroomGenerationJob(
|
|
jobId: string,
|
|
input: GenerateClassroomInput,
|
|
baseUrl: string,
|
|
): Promise<void> {
|
|
const existing = runningJobs.get(jobId);
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
runningControllers.set(jobId, controller);
|
|
|
|
const jobPromise = (async () => {
|
|
try {
|
|
const runningJob = await markClassroomGenerationJobRunning(jobId);
|
|
|
|
const result = await generateClassroom(input, {
|
|
baseUrl,
|
|
signal: controller.signal,
|
|
ownership: {
|
|
...(runningJob.ownerPrincipalId ? { ownerPrincipalId: runningJob.ownerPrincipalId } : {}),
|
|
...(runningJob.guestPrincipalId ? { guestPrincipalId: runningJob.guestPrincipalId } : {}),
|
|
...(runningJob.ownershipBoundAt ? { ownershipBoundAt: runningJob.ownershipBoundAt } : {}),
|
|
...(runningJob.ownershipMigratedAt
|
|
? { ownershipMigratedAt: runningJob.ownershipMigratedAt }
|
|
: {}),
|
|
},
|
|
onProgress: async (progress) => {
|
|
// Do not keep reporting progress after a cancellation request —
|
|
// the generate loop may still be finishing its current scene.
|
|
if (controller.signal.aborted) return;
|
|
await updateClassroomGenerationJobProgress(jobId, progress);
|
|
},
|
|
});
|
|
|
|
await markClassroomGenerationJobSucceeded(jobId, result);
|
|
} catch (error) {
|
|
if (isAbortError(error)) {
|
|
log.info(`Classroom generation job ${jobId} cancelled`);
|
|
try {
|
|
await markClassroomGenerationJobCancelled(jobId);
|
|
} catch (markCancelledError) {
|
|
log.error(`Failed to persist cancelled status for job ${jobId}:`, markCancelledError);
|
|
}
|
|
return;
|
|
}
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
log.error(`Classroom generation job ${jobId} failed:`, error);
|
|
try {
|
|
await markClassroomGenerationJobFailed(jobId, message);
|
|
} catch (markFailedError) {
|
|
log.error(`Failed to persist failed status for job ${jobId}:`, markFailedError);
|
|
}
|
|
} finally {
|
|
runningJobs.delete(jobId);
|
|
runningControllers.delete(jobId);
|
|
}
|
|
})();
|
|
|
|
runningJobs.set(jobId, jobPromise);
|
|
return jobPromise;
|
|
}
|