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>
129 lines
2.6 KiB
JavaScript
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,
|
|
};
|