alkemics/CancelablePromise

Does not work on async await chain

Closed this issue · 2 comments

I have this test which uses async/await but it does not appear to cancel the promise

  it('promiseChain partial', async () => {
    let f = 0;
    const cp = cancelable(
      (async () => {
        await new Promise((r) => setTimeout(() => r(), 100));
        f = 1;
        await new Promise((r) => setTimeout(() => r(), 100));
        f = 2;
        await new Promise((r) => setTimeout(() => r(), 100));
        f = 3;
        await new Promise((r) => setTimeout(() => r(), 100));
        f = 4;
        expect(true).toBe(false);
      })()
    );
    await new Promise((r) => setTimeout(() => r(), 250));
    cp.cancel();
    expect(f).toBe(2);
  });

Yes but your main promise is canceled:

  let f = 0;
  const cp = cancelable(
    (async () => {
      await cancelable(new Promise((r) => setTimeout(() => r(), 100)));
      f += 1;
      await cancelable(new Promise((r) => setTimeout(() => r(), 100)));
      f += 1;
      await cancelable(new Promise((r) => setTimeout(() => r(), 100)));
      f += 1;
      await cancelable(new Promise((r) => setTimeout(() => r(), 100)));
      f += 1;
    })()
  );
  (async () => {
    await cp;
    f += 1;
  })();
  cp.then(() => (f += 1));
  await delay(250);
  cp.cancel();
  await delay(250);
  expect(f).toBe(4);

Indeed the inner promises are not canceled, you need extra logic to handle your use-case if you want to. How the library can have access to these inner promises? It's not chaining such as promise.then().then(). A workaround could be:

const cp = cancelable(
  (async () => {
    await new Promise((r) => setTimeout(() => {
      if (!cp.isCanceled()) r();
    }, 100));
    f = 1;
    await new Promise((r) => setTimeout(() => {
      if (!cp.isCanceled()) r();
    }, 100));
    f = 2;
    await new Promise((r) => setTimeout(() => {
      if (!cp.isCanceled()) r();
    }, 100));
    f = 3;
    await new Promise((r) => setTimeout(() => {
      if (!cp.isCanceled()) r();
    }, 100));
    f = 4;
  })()
);

But it's not elegant... Another approach could be to use a generator, here a quick example:

async function start() {
  async function run(gen) {
    let iter = gen();
    let res;
    let val;
    while (!res?.done) {
      if (typeof gen.isCanceled === "function" && gen.isCanceled()) {
        return val;
      }
      res = iter.next(val);
      val = await res.value;
    }
    return val;
  }

  const cancelable = (gen) => {
    let isCanceled = false;
    gen.cancel = () => {
      isCanceled = true;
    };
    gen.isCanceled = () => isCanceled;
    return gen;
  };

  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  let count = 0;

  const generator = cancelable(function* () {
    count = 0;
    yield delay(100);
    count += 1;
    yield delay(100);
    count += 1;
    yield delay(100);
    count += 1;
    yield delay(100);
    count += 1;
    return count;
  });

  run(generator);
  await delay(250);
  generator.cancel();
  await delay(250);
  console.log("count:", count); // count: 2
}

start();

Live demo here: https://codesandbox.io/s/youthful-dubinsky-wf1vk?file=/src/index.js
(notice that it can be improved because here you can't re-use the generator after a first cancellation)

Thanks for the tip. I am thinking of looking into whether the babel can be tweaked to replace the Promise used with your CancelablePromise so that I can simply call cancel() on the promise