origamitower / folktale

[not actively maintained!] A standard library for functional programming in JavaScript
https://folktale.origamitower.com/
MIT License
2.05k stars 102 forks source link

Can't handle `fetch` cancelation #230

Open phwb opened 3 years ago

phwb commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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: https://github.com/origamitower/folktale/blob/3e2f007907323069467529cbc9ba574df19dfabd/packages/base/source/concurrency/future/_deferred.js#L33

phwb commented 3 years ago

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 commented 2 years ago
//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