MOHAN 08f21d9bc9 Add job cancellation — backend pipeline + cancel API route
pipelineJobs.js:
- cancelJob(jobId): marks job as cancelled=true, status=cancelling
- isJobCancelled(jobId): checked by the pipeline between stages

runSourcePipeline.js:
- PipelineCancelledError class
- checkCancelled() called before each of the 6 pipeline stages
- Accepts options.isCancelled() callback from the job runner

runKytPipelineJob.js:
- Passes isCancelled: () => isJobCancelled(job.id) into pipeline
- Catches PipelineCancelledError separately, sets status=cancelled

routes/pipeline.js:
- POST /pipeline/cancel/:jobId — marks job for cancellation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 16:57:35 +05:30

129 lines
2.6 KiB
JavaScript

const jobs = {};
const MAX_LOG_LINES = 120;
function listJobs() {
return Object.values(jobs)
.sort((a, b) => String(b.startedAt || "").localeCompare(String(a.startedAt || "")));
}
function getJob(jobId) {
return jobs[jobId] || null;
}
function getJobIdForPayload(payload = {}) {
const shop = String(payload.shop || "").trim();
const source = String(payload.source || "kyt").trim().toLowerCase();
if (!shop) {
return "";
}
return source === "kyt" ? shop : `${shop}::${source}`;
}
function canStartJob(shop, source = "kyt") {
if (!shop) {
return false;
}
const existing = jobs[getJobIdForPayload({ shop, source })];
return !existing || existing.status === "done" || existing.status === "error";
}
function createJob(payload = {}) {
const id = getJobIdForPayload(payload);
if (!id) {
throw new Error("Shop is required to create a job.");
}
jobs[id] = {
id,
status: "queued",
step: "queued",
stepIndex: 0,
totalSteps: 6,
detail: null,
summary: null,
error: null,
logs: [],
liveStats: {},
payload,
startedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
return jobs[id];
}
function updateJob(jobId, patch) {
const current = jobs[jobId];
if (!current) {
return null;
}
jobs[jobId] = {
...current,
...patch,
liveStats: {
...(current.liveStats || {}),
...(patch.liveStats || {}),
},
updatedAt: new Date().toISOString(),
};
return jobs[jobId];
}
function appendJobLog(jobId, line, extraPatch = {}) {
const current = jobs[jobId];
if (!current) {
return null;
}
const nextLogs = [...(current.logs || []), {
at: new Date().toISOString(),
line,
}].slice(-MAX_LOG_LINES);
jobs[jobId] = {
...current,
...extraPatch,
liveStats: {
...(current.liveStats || {}),
...(extraPatch.liveStats || {}),
},
logs: nextLogs,
updatedAt: new Date().toISOString(),
};
return jobs[jobId];
}
function cancelJob(jobId) {
const job = jobs[jobId];
if (!job) return null;
if (job.status === "done" || job.status === "error" || job.status === "cancelled") return job;
jobs[jobId] = {
...job,
cancelled: true,
status: "cancelling",
detail: "Cancellation requested — stopping after current step...",
updatedAt: new Date().toISOString(),
};
return jobs[jobId];
}
function isJobCancelled(jobId) {
return Boolean(jobs[jobId]?.cancelled);
}
module.exports = {
listJobs,
getJob,
canStartJob,
createJob,
updateJob,
appendJobLog,
getJobIdForPayload,
cancelJob,
isJobCancelled,
};