// jobStore.js — in-memory job tracker for product import jobs const { v4: uuid } = require('uuid'); const jobs = {}; const MAX_LOGS = 120; const MAX_ERRORS = 30; const MAX_RESULTS = 100; function nowIso() { return new Date().toISOString(); } function elapsedSeconds(startedAt) { if (!startedAt) return 0; return Number(((Date.now() - new Date(startedAt).getTime()) / 1000).toFixed(1)); } function createJob({ shop, brandId, brandName, totalSelected = 0 }) { const id = uuid(); const job = { id, shop, brandId: String(brandId || ''), brandName: brandName || '', status: 'started', step: 'started', detail: 'Initialising import job...', currentProduct: null, liveStats: { total: totalSelected, processed: 0, created: 0, skipped: 0, failed: 0, remaining: totalSelected, successRate: 0, label: `0/${totalSelected}`, }, errors: [], logs: [], results: [], startedAt: nowIso(), updatedAt: nowIso(), finishedAt: null, durationSeconds: null, cancelled: false, }; jobs[id] = job; return job; } function updateJob(jobId, patch) { const job = jobs[jobId]; if (!job) return null; Object.assign(job, patch, { updatedAt: nowIso() }); // Recompute derived stats whenever liveStats is patched if (patch.liveStats) { const s = job.liveStats; s.remaining = Math.max(0, s.total - s.processed); s.successRate = s.processed > 0 ? Number((((s.processed - s.failed) / s.processed) * 100).toFixed(1)) : 0; s.label = `${s.processed}/${s.total}`; } return job; } function appendJobLog(jobId, line, extraPatch = {}) { const job = jobs[jobId]; if (!job) return null; job.logs.push({ at: nowIso(), line }); if (job.logs.length > MAX_LOGS) job.logs.shift(); Object.assign(job, extraPatch, { updatedAt: nowIso() }); return job; } function recordProductResult(jobId, result) { const job = jobs[jobId]; if (!job) return null; job.results.push(result); if (job.results.length > MAX_RESULTS) job.results.shift(); job.updatedAt = nowIso(); return job; } function recordProductError(jobId, errorEntry) { const job = jobs[jobId]; if (!job) return null; job.errors.push(errorEntry); if (job.errors.length > MAX_ERRORS) job.errors.shift(); job.updatedAt = nowIso(); return job; } function finishJob(jobId, status = 'done') { const job = jobs[jobId]; if (!job) return null; const finishedAt = nowIso(); const durationSeconds = elapsedSeconds(job.startedAt); const s = job.liveStats; Object.assign(job, { status, step: status === 'done' ? 'completed' : status, finishedAt, durationSeconds, updatedAt: finishedAt, }); s.remaining = 0; s.successRate = s.processed > 0 ? Number((((s.processed - s.failed) / s.processed) * 100).toFixed(1)) : 0; return job; } function cancelJob(jobId) { const job = jobs[jobId]; if (!job) return null; job.cancelled = true; job.status = 'cancelling'; job.updatedAt = nowIso(); return job; } function isJobCancelled(jobId) { return !!(jobs[jobId]?.cancelled); } function getJob(jobId) { return jobs[jobId] || null; } function listJobs(shop = null) { const all = Object.values(jobs); const filtered = shop ? all.filter(j => j.shop === shop) : all; return filtered.sort((a, b) => new Date(b.startedAt) - new Date(a.startedAt)); } function getLatestJobForShop(shop) { const shopJobs = listJobs(shop); return shopJobs[0] || null; } module.exports = { createJob, updateJob, appendJobLog, recordProductResult, recordProductError, finishJob, cancelJob, isJobCancelled, getJob, listJobs, getLatestJobForShop, };