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

Can't handle fetch cancelation #230

Open
phwb opened this issue Mar 10, 2021 · 4 comments
Open

Can't handle fetch cancelation #230

phwb opened this issue Mar 10, 2021 · 4 comments

Comments

@phwb
Copy link

phwb commented Mar 10, 2021

Hi, I can't handle fetch cancelation. See code below:

Steps to reproduce

const fetchTask = (url) => task(resolver => {
  const controller = new AbortController()
  const init = {
    signal: controller.signal,
    method: 'post',
  }
  fetch(url, init)
    .then(resolver.resolve)
    .catch(resolver.reject)
  resolver.onCancelled(() => {
    controller.abort()
  })
})
fetchTask('/some/path').run().cancel() 
// after cancel() i'll got this error:
// Uncaught (in promise) Error: Only pending deferreds can be rejected, this deferred is already rejected.

Expected behaviour

Task rejected with error

I understand why does it happen but how I can handle cancelation error? Or may be I do it wrong?

@robotlolita
Copy link
Member

Ah, sadly you'll need to keep your own state to handle the race condition here. Something like the following will work:

const fetchTask = (url) => task(resolver => {
  const controller = new AbortController()
  const init = {
    signal: controller.signal,
    method: 'post',
  }
  let resolved = false;
  let transition = (f) => (x) => { if (!resolved) { resolved = true; f(x); } };
  fetch(url, init)
    .then(transition(resolver.resolve))
    .catch(transition(resolver.reject))
  resolver.onCancelled(() => {
    controller.abort()
  })

I've thought a lot about whether requiring people to explicitly handle these race conditions or having the task resolver just implicitly ignore these calls after a task has moved away from the pending state and I'm still not quite sure what the answer should be---on one hand making it implicit makes things easier, but OTOH it also tends to hide some errors that then become very hard to debug :/

@phwb
Copy link
Author

phwb commented Mar 17, 2021

Sorry for the long silence... But you suggest is the same. The error occurs due to task can't pass from canceled state to rejected. You can do transition only from Pending state:

if (!Pending.hasInstance(deferred._state)) {

@phwb
Copy link
Author

phwb commented Mar 17, 2021

the problem is i can't handle cancelled task:

const execution = asyncTask() // long execution
  .mapRejected((e) => {
    console.dir(e) // never get here
  })
  .run()
setTimeout(() => {
  execution.cancel() // cancel after 100 ms
}, 100)

how can i handle, that task was cancelled?

@andelkocvjetkovic
Copy link

andelkocvjetkovic commented May 15, 2022

//ApiActionGetWorkshopCancelable :: string -> Task
export const ApiActionGetWorkshopCancelable = workshopId =>
  task(resolver => {
    const abortController = new AbortController();
    API(abortController)
      .get(`/workshops/${workshopId}`)
      .then(resolver.resolve)
      .catch(e => !resolver.isCancelled && resolver.rejected(e));
    resolver.onCancelled(() => abortController.abort());
  });

Docs:
The onCancelled function takes a callback that'll be invoked when the task's execution is cancelled. The resolver also has an isCancelled boolean field that can be queried at any time to determine whether the task has been cancelled or not at that point in time.

This works for me

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

No branches or pull requests

3 participants