Saga misses multiple actions dispatched within the same event queue
See original GitHub issueConsider 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:
- Created 8 years ago
- Comments:25 (9 by maintainers)
Top 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 >Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start FreeTop Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Top GitHub Comments
@emirotin
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.
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
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)
So I’d have to comeup with clear semantics and API; (and also find the time 😃 to do it ).
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.
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 butyield 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.