Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clarify cancelling synchronous operations as well. #26

Open
benlesh opened this issue Oct 15, 2019 · 1 comment
Open

Clarify cancelling synchronous operations as well. #26

benlesh opened this issue Oct 15, 2019 · 1 comment

Comments

@benlesh
Copy link

benlesh commented Oct 15, 2019

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.

@bergus
Copy link

bergus commented Oct 15, 2019

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants