1

I've been messing with the idea of canceling a promise is a transparent way using a higher order function. And I came up with this:

export const fnGetter = state => fn => (...args) => {
  if (!state.canceled) return fn(...args)
  return Promise.resolve()
}

export const cancelable = (promise, state = {}) => {
  const getFn = fnGetter(state)

  return {
    then: fn => cancelable(promise.then(getFn(fn)), state),
    catch: fn => cancelable(promise.catch(getFn(fn)), state),
    cancel: () => {
      state.canceled = true
    }
  }
}

export const withCancel = promiseReturningFn => (...args) =>
  cancelable(promiseReturningFn(...args))

And here are some unit tests where I'm verifying the behavior I want.

const delay = withCancel(ms => new Promise(run => setTimeout(run, ms)))

test('works like normal promise when not canceled', async () => {
  const first = jest.fn()
  const second = jest.fn()

  await delay(1000).then(first).then(second)

  expect(first).toHaveBeenCalledTimes(1)
  expect(second).toHaveBeenCalledTimes(1)
})

test('when ignored, does not call callbacks', async () => {
  const first = jest.fn()
  const second = jest.fn()

  const promise = delay(1000).then(first).then(second)
  promise.cancel()

  await promise

  expect(first).not.toHaveBeenCalled()
  expect(second).not.toHaveBeenCalled()
})

I can't figure out why the first test passes, but calling .cancel() in the second unit test makes it time out.

Edit

I think it has something to do with the way that await handles the then method under the hood. I just need help at this point. I'd like it to be compatible with async await. Here's a passing version that doesn't rely on await.

test('when ignored, does not call callbacks', async () => {
  const first = jest.fn()
  const second = jest.fn()

  const promise = delay(1000).then(first).then(second)
  promise.cancel()

  setTimeout(() => {
    expect(first).not.toHaveBeenCalled()
    expect(second).not.toHaveBeenCalled()
  }, 2000)
})
Euroclydon37
  • 667
  • 7
  • 20

2 Answers2

1

One problem is that then should take two parameters to deal with rejections.

As for why it times out: you are using await promise, but await does use then, and your promise is cancelled so it never calls its callbacks. A cancelled promise should call the onreject callback, and your fnGetter should then ignore that cancellation error only for those callbacks that were actually expecting your cancellation.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
0

I think I know what’s happening. From reading, it looks like await just takes subsequent code, wraps it into a function, and then passes that function into the then method of whatever is being awaited. If that’s the case, then my code is working and the expect statements never run because my promise (the one being awaited) is canceled. That’s why running the test with setTimeout works.

Here’s a basic example of the functionality I’m referring to.

const func = async () => {
  await { then: () => console.log("test") }
  console.log("after")
}

The above code prints "test" and never prints "after" because console.log("after") gets wrapped in a function and passed to the object’s then method, which never calls it.

Euroclydon37
  • 667
  • 7
  • 20