question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Saga misses multiple actions dispatched within the same event queue

See original GitHub issue

Consider this example:

function* fnA() {
  while(true) {
    let {payload} = yield take('a')
    yield fork(someAction, payload)
  }
}

function* fnB() {
  yield put({type: 'a', payload: 1})
  yield put({type: 'a', payload: 2})
  yield put({type: 'a', payload: 3})
}

function* someAction(payload) {
  console.log(payload)
}

export default function* root() {
  yield [
    fork(fnA),
    fork(fnB)
  ]
}

The output I was expecting was 1, 2, 3, however the actual output is 1, 3 this is because the yield fork doesn’t continue on the while loop until a subsequent tick. This can be fixed by changing fnA to:

function* fnA() {
  let forked = false
  while (!forked) {
    let {payload} = yield take('a')
    forked = yield fork(fnA)
    yield fork(someAction, payload)
  }
}

However this makes logging turn into a pyramid and feels like the wrong approach. Am I thinking about this incorrectly or could we somehow force the next tick after a fork call?

Issue Analytics

  • State:closed
  • Created 8 years ago
  • Comments:25 (9 by maintainers)

github_iconTop GitHub Comments

4reactions
yelouaficommented, Jan 25, 2016

@emirotin

Sorry if I sound irritated here. It wasn’t meant to criticize the project by any means. I really value how you listen to your users and look open to adapting the library behavior to real use-cases

No worry 😃. i’m also thankful to all people who are trying this lib in their apps although it’s still on its early stages.

I think the actual problem here is the mental dissonance. Sagas are middleware and other middlewares are guaranteed to be called for every action dispatched. When one reads the saga code it looks like it will be the case as well, with just the code being suspended between the events. So one (me 😃) imagines the unhandled actions queued until the saga unshifts them and executes.

Agree 100%. The pull approach suggests that the saga is actually iterating over an event queue. so the user expects to not miss any event.

And I’m conscious that library users shouldn’t concern themselves with underlying implementation details (promises, microtasks, …) in order to use the library, but only with its communicated semantics. Because it’ll add another mental overhead while the library has already a steep learning curve (generators, event handlng paradigm shift, processes …)

When implementing the take effect. There were 2 choices for its semantics

1- Either queue all incoming actions for the Saga. So it wont miss any action; even if it’s blocked on an api call when the action arrives

2- discard the incoming action if the Saga isn’t waiting for it

The 1st behavior is more close to how CSP channels work. While the 2nd looks more close to the Actor model.

Both approaches has pros and cons. Depending on the use case, the developer will sometimes want the incoming actions to be queued (buffered) while in other use cases it’s not necessary to buffer the actions.

From a conceptual view, event buffering is more close to how iterators works: when i’m iterating over some iterable, i don’t expect to miss some value. But the iterator can’t be shared between multiple consumers, each consumer has to create its own iterator from a given iterable (array, generator, event queue).

So in a order to share the store channel between many Sagas, w’ll have to create an event iterator (or event queue/bufffer) for each Saga. However, buffering all actions can lead to more or less serious memory issues: A generic action queue can’t determine in advance which action to keep or discard (i.e. will be taken later by the saga); so it’l have to buffer all incoming actions even if those actions will never be taken by a Saga.

So this is how somewhat I ended up with the actual behavior.

I was of course thinking in adding dedicated channel support which could support the iterator semantics (buffering). Could be something like

function* saga() {
  // buffered channel, wont miss any API_CALL action
  const apiChannel = yield channel('API_CALL')

  while(true) {
    const apiAction = yield take(apiChannel)
    ....
  }
}

Or could be something generic created outside of sagas, and called with a simple call. This has the advantage of being usable also with other event sources (Observables, websocket messages, even raw DOM events)

const apiCallChannel= createChannel(fromStoreActions(store, 'API_CALL'))

function* saga() {
 while(true) {
    const apiAction = yield call(apiCallChannel.nextEvent)
    ....
  }
}

So I’d have to comeup with clear semantics and API; (and also find the time 😃 to do it ).

This will create a normal middleware that picks every CALL_API action and forks the callApi method. Such kind of notation will also make the actual sagas code shorter as I always find myself writing

yeah. that also crossed my mind, but w’d also have to define some way to coordinate those forked daemons. Otherwise, the solution won’t have a real advantage above normal callback based solutions (like thunks) when managing complex flow for all forked daemons. We could provide some saga coordinator to the daemon function; But then we shift away from the simple synchronous-like mental model of generators to a more complex async/push/callback model.

2reactions
yelouaficommented, Jan 30, 2016

After examining all issues. It become clear that reliance on promise isn’t going to play nice with synchronous actions. Channels may solve issues for saga inter-communications and are certainly useful. but there are other issues which can’t be solved like Sagas taking some time to start (cc @tgriesser like use yield [fork(a), fork(b)] works but yield fork(a); fork(b) not).

I think as long as the core relies on promises, there will be always some issue reported kind of this thing started before this thing. This is inavoidable because of the nature of promise, we ont have control of order of things (which sounds a bit ironic for a lib whose purpose is to allow people to manage order of things).

So to tackle that problem I made a more radical choice, rewrite the redux-saga core without relying on promise to drive the generators but only callbacks (this is only an internal impl. detail and doesnt affect the external API). The advantage its that now w’ve total control of order of things, and effects like fork are truly non blocking, even call effects which return non promise results are non blocking.

It a was bit (well more than a bit) trickier to achieve (cancellation propagation, had to reinvent race/parallel …) But finally it works, and now all tests cases pass, even with sync actions. And no more requestAnimationFrame are necessary now.

The code is the no-promise branch. I’ll merge it with the master after adding some more edge-case tests.

Read more comments on GitHub >

github_iconTop Results From Across the Web

redux-saga some of the rapid fired same actions missed
In other words, I expected that whenever the ITEMS_UPDATE_START action gets dispatched, it forks a new updateItemDbCrud and proceeds to do some ...
Read more >
Troubleshooting | Redux-Saga
As illustrated above, when a Saga is blocked on a blocking call then it will miss all the actions dispatched in-between. To avoid...
Read more >
Ryan Florence on Twitter: "@dan_abramov I've only seen it be ...
Saga misses multiple actions dispatched within the same event queue · Issue #50 · redux-saga/redu... Consider this example: function* fnA() { while(true) ...
Read more >
The event loop - JavaScript - MDN Web Docs - Mozilla
JavaScript has a runtime model based on an event loop, which is responsible for executing the code, collecting and processing events, ...
Read more >
Read Me | redux-saga
After the delay, the Saga dispatches an INCREMENT_COUNTER action using the put(action) function. ... Sagas Generators can yield Effects in multiple forms.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found