proposal-cancellation
proposal-cancellation copied to clipboard
Clarify cancelling synchronous operations as well.
I noticed that this repo talks a lot about async when it comes to cancellation. I wanted to make sure it also considered synchronous use cases.
A good cancellation primitive needs to not only be asynchronous, but also synchronous.
I say this because it would be nice if this cancellation primitive could be used to free up resources for things like EventTarget in the DOM. EventTargets can have handlers registered, triggered, and unregisted completely synchronously.
Likewise, this cancellation primitive would be useful for push-based architectures. (I'd definitely try to leverage it for RxJS), and those can be synchronous.
Consider the following example (also on codesandbox here), that deals with a design where a synchronous push stream needs to be cancellable:
/**
* Returns a function that pushes a range of numbers as fast as possible via a callback.
* @param {number} start The start of the number range
* @param {number} end The end of the number range
*/
function range(start, end) {
return (callback, cancelToken) => {
for (let n = start; n < end && !cancelToken.cancelled; n++) {
callback(n);
}
};
}
/**
* Transforms a simple push stream type (like `range` returns above)
* into a new push stream that will only emit `count` values.
* @param {number} count The number of values to take
*/
function take(count) {
return sourceFn => {
return (callback, cancelToken) => {
let i = 0;
sourceFn(value => {
if (i++ < count) {
callback(value);
} else {
cancelToken.cancel();
}
}, cancelToken);
};
};
}
/**
* A function for chaining push streams together.
*
* @param {function} source The push stream function that is the source
* @param {...any} fns transforms for the push stream functions (like `take`)
*/
function compose(source, ...fns) {
return (callback, token) =>
fns.reduce((prev, fn) => fn(prev), source)(callback, token);
}
/**
* A push stream we can test with, only
* takes 30 values from an infinite source.
*/
const source = compose(
range(0, Number.POSITIVE_INFINITY),
take(30)
);
// set up the token (again, just a simple example impl)
const token = new CancelToken();
// test out cancellation
source(n => console.log(n), token);
Yes, it's a contrived example, and there's probably more functional composition up there than is really necessary to convey the point. But the high-level point is, in my experience with cancellation, cancellation needs to happen synchronously and also needs to set a flag that can be examined synchronously.
cancellation needs to set a flag that can be examined synchronously
I could not agree more.
Whether the push-based event source is synchronous or not, I want to be sure (make it a provable invariant!) that it does not fire any cancelled callbacks immediately after I did the cancellation. To implement such a component in user land code, the only viable approach I can see is to check such a flag right before calling the callback.