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

77 lines
2.1 KiB
JavaScript

const express = require("express");
const { createJob, updateJob, getJob, listJobs, canStartJob, cancelJob } = require("../src/pipelineJobs");
const { runKytPipelineJob } = require("../src/runKytPipelineJob");
const { getToken } = require("../tokenStore");
const { getSource, listSources } = require("../src/business-logic/import-pipeline/sources");
const router = express.Router();
router.post("/run", async (req, res) => {
const { shop, limit = null, source = "kyt" } = req.body || {};
if (!shop) {
return res.status(400).json({ error: "Missing shop." });
}
let sourceConfig;
try {
sourceConfig = getSource(source);
} catch (error) {
return res.status(400).json({ error: error.message });
}
if (!getToken(shop)) {
return res.status(400).json({ error: "No stored Shopify token found for this shop. Complete app auth first." });
}
if (!canStartJob(shop, sourceConfig.sourceKey)) {
return res.status(409).json({ error: `An import job is already running for ${shop} and source ${sourceConfig.sourceKey}.` });
}
const job = createJob({ shop, limit, source: sourceConfig.sourceKey });
updateJob(job.id, {
status: "queued",
step: "queued",
detail: `${sourceConfig.label} import job queued`,
});
setImmediate(() => {
runKytPipelineJob(job);
});
return res.json({
jobId: job.id,
status: "queued",
shop,
source: sourceConfig.sourceKey,
limit,
});
});
router.get("/sources", (req, res) => {
return res.json({ sources: listSources() });
});
router.get("/status/:jobId", (req, res) => {
const job = getJob(req.params.jobId);
if (!job) {
return res.status(404).json({ error: "Job not found." });
}
return res.json(job);
});
router.get("/jobs", (req, res) => {
return res.json({ jobs: listJobs() });
});
router.post("/cancel/:jobId", (req, res) => {
const job = cancelJob(req.params.jobId);
if (!job) {
return res.status(404).json({ error: "Job not found or already finished." });
}
return res.json({ ok: true, jobId: job.id, status: job.status });
});
module.exports = router;