There are two types of long running tasks and cancelling is different for both:
1) Asynchronous tasks:
They may take a while, however they are not using the JavaScript engine, instead the engine is in idle to wait for some external data (database / files / timers whatever). In some cases (timers for example) you can easily discard that external action, also you can trigger it as an event as the engine is not blocked and can handle the cancellation. If the async action cannot be cancelled directly (database read for example) you can wait until it is done and cancel it then:
class Cancelable {
constructor() {
this.cancelled = false;
this.handlers = [];
}
onCancel(handler) { this.handlers.push(handler); }
cancel() {
this.cancelled = true;
this.handlers.forEach(handler => handler());
}
}
// inside of the request handler:
const canceller = new Cancelable;
req.setTimeout(5000, () => {
res.status(503);
res.send();
canceller.cancel(); // propagate cancellation
});
// Some long running, async cancellable task
const timer = setTimeout(function() {
res.send("done");
}, 10000 * Math.random())
// on cancellation just remove the timer
canceller.onCancel(() => clearTimeout(timer));
unCancellableAction(function callback() {
if(canceller.canceled) return; // exit early if it was cancelled
res.send("done");
});
2) Synchronous tasks:
You cannot cancel synchronous tasks directly as the engine is busy doing the task, and can't handle the cancellation. To make them cancellable you have to use polling, the task has to pause its job, check wether it should cancel, and then either continue or abort. In JS that can be done with generator functions (as they can yield their execution):
function runMax(time, action) {
const gen = action(), start = Date.now();
let done, value;
do {
({ done, value } = gen.next());
} while(!done && Date.now() < start + time)
return value;
}
// inside the request handler:
runMax(5000, function* () {
while(true) {
// ... some jobs
// yield at a safe position to allow abortion:
yield;
}
});