import { renderJakeTemplate } from '../../templateRenderer.js'; import { DOWNLOAD_CLEANUP_DELAY_MS } from '../config.js'; import { BadRequestError, ForbiddenError, NotFoundError } from '../exception/errors.js'; import { createJob, getJob, deleteJob, getJobPosition } from './jobStore.js'; import { generateContentHash, getCachedPdf } from './pdfCache.js'; import { queue, enqueueRenderJob } from './jobRunner.js'; import { recordPdfGeneration } from './usageService.js'; // A cached PDF is still a generation the user received, so it is counted too. async function createCachedJob({ uid, metadata, usageDate }, cachedPdf) { await recordPdfGeneration(uid, usageDate); return createJob({ userId: uid, status: 'completed', metadata, result: cachedPdf.buffer, fromCache: true }); } // Submit a job from ready-made HTML export async function submitHtmlJob({ uid, html, metadata, usageDate }) { if (!html) throw new BadRequestError('HTML content is required'); const cacheKey = generateContentHash(html, metadata); // RAM cache hit: instant PDF retrieval (0ms render time) const cachedPdf = getCachedPdf(cacheKey); if (cachedPdf) { const job = await createCachedJob({ uid, metadata, usageDate }, cachedPdf); console.log(`[RAM Cache Hit] Served cached PDF instantly for job ${job.id} (user: ${uid})`); return { jobId: job.id, position: 0, cached: true }; } const job = createJob({ userId: uid, status: 'queued', metadata, cacheKey, usageDate }); // If all workers are occupied, position is queue backlog (queue.size + 1) const initialPosition = queue.pending >= queue.concurrency ? (queue.size + 1) : 0; enqueueRenderJob(job, html, 'Job'); const position = initialPosition > 0 ? initialPosition : getJobPosition(job.id); return { jobId: job.id, position, queueSize: queue.size, pending: queue.pending }; } // Submit a job from resume JSON, rendered server-side with the template renderer export async function submitResumeJsonJob({ uid, data, metadata, usageDate }) { if (!data) throw new BadRequestError('Resume data is required'); const cacheKey = generateContentHash(JSON.stringify(data), metadata); const jobMetadata = { ...metadata, filename: metadata?.filename || data.personal?.fullName || 'resume' }; const cachedPdf = getCachedPdf(cacheKey); if (cachedPdf) { const job = await createCachedJob({ uid, metadata: jobMetadata, usageDate }, cachedPdf); console.log(`[RAM Cache Hit] Served cached JSON PDF instantly for job ${job.id} (user: ${uid})`); return { jobId: job.id, position: 0, cached: true }; } const html = renderJakeTemplate(data); const job = createJob({ userId: uid, status: 'queued', metadata: jobMetadata, cacheKey, usageDate }); enqueueRenderJob(job, html, 'JSON Job'); return { jobId: job.id, position: getJobPosition(job.id) }; } export function getJobStatus(jobId) { const job = getJob(jobId); if (!job) throw new NotFoundError('Job not found'); return { status: job.status, position: getJobPosition(job.id), error: job.error }; } // Returns the finished PDF for its owner and schedules the job's cleanup export function takePdfForDownload(jobId, uid) { const job = getJob(jobId); if (!job || job.status !== 'completed') throw new NotFoundError('PDF not ready or job not found'); // Ensure the authenticated requester owns this job if (job.userId && job.userId !== uid) { throw new ForbiddenError('You do not have permission to download this PDF.'); } setTimeout(() => deleteJob(job.id), DOWNLOAD_CLEANUP_DELAY_MS); return { buffer: job.result, filename: job.metadata?.filename || 'resume' }; }