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)
})