Comments (18)
If hot-reloading of the saga only affects the future I'll be fine with that.
I agree. Just pausing existing sagas and restarting hot reloaded ones would cover most cases where you’d care about hot reloading them, in my opinion. Sure, it’s not perfect, but it’s better than nothing, isn’t it?
from redux-saga.
I'm more concerned about state that the sagas have. I mean if you have a long-lived saga and it uses local variables, then when we time-travel it's not easy to update this state accordingly, and worse the saga also has some kind of "progress state" (I mean at which execution point the Saga is currently).
Generators seems to allow an user-friendly syntax but also seems to introduce some kind of implicit state and I don't really see how to deal with that.
from redux-saga.
Here is my try to get HMR working with sagas. Time traveling doesn't work. Feedback is greatly appreciated :)
https://gist.github.com/hoschi/6538249ad079116840825e20c48f1690
from redux-saga.
I was thinking more about this problem and it occurred to me that there is already a very similar mechanism to reloading: cancellation. The simplest thing would be to just cancel any reloaded saga and run it again, but as discussed above this has the downside of reseting state.
We can look to the bare usage of HMR for a solution to this problem - any module with state that needs to be transferred to the newly loaded module can do so using dispose. We can introduce a similar mechanism for sagas:
function* reloadableSaga(hot) {
let someState = hot.someState || defaultValue;
while (true) {
try {
someState = yield call(doSomethingWith, someState);
} finally {
if (yield reloaded()) { // potentially reloaded can be rolled in with cancelled
yield reloadWith({ someState });
}
}
}
}
where the data passed to reloadWith
is passed to the new version of reloadableSaga
.
Unfortunately unlike react components sagas don't already nicely isolate their state, so users would have to do it semi-manually, but by implementing reloading support in many saga combinators (takeEvery
, takeLatest
, etc.) a lot of simple sagas will gain reloading support automatically.
EDIT: I think with additional libraries that feature saga helpers like takeEvery
and takeLatest
which make writing declarative sagas easier we could cover even more sagas automatically.
For instance an always
combinator could look like this:
function* always(effect) {
while (true) {
try {
yield effect;
} finally {
if (yield reloaded()) { // this takes care of reloading the always saga itself, not the parent
yield reloadWith(effect);
}
}
}
}
EDIT2: Thinking about it more, we can take care of the parent saga reloading by having the reloaded
effect return us our new arguments from the parent, and the saga can then choose to override some of those with reloadWith
or even just continue on with the new state.
from redux-saga.
Generators seems to allow an user-friendly syntax but also seems to introduce some kind of implicit state and I don't really see how to deal with that.
IMO it's not only about Generators. Although I agree that the flexibility offered by Generators makes it a bit harder to achieve. I think it's more related to the complex nature of long-running flows, I mean flows which span across multiple actions.
Even if we implement a saga as a state machine which reacts to a sequence of actions like we can do actually with redux-thunk (or even in a more restrictive way like in Cerebral). It doesn't mean we can time travel - correctly - that code. Time travel is driven by the event-log/actions (at least in its actual conception). Give me an action, the previous state. I'll give you an output and the next state. But imagine a long-running authorization flow
SIGN_IN -> authorization -> SIGN_OUT
In theory, you can time travel the code managing the above flow - and probably it`s easier with a state machine like approach -. But imagine you change requirements of the above flow into something like
SIGN_IN -> authorization -> (refresh with timeout of 5 mins) -> SIGN_OUT
You change your code and hot reload it, how to replay actions with the new state machine code ? the whole flow has changed, it means the state machine will probably produce a new sequence of actions: the recorded event log is no longer valid, so it wont make sens to reply it.
from redux-saga.
Time travelling Sagas, IMO, is not the real challenge. If we record the states at the different breakpoints (i.e. yields) of a saga. We can travel to any point in the past and see the app/state at that point. We can even time travel different Sagas independently, as if there is a local time for each saga (vs the global time defined in the current devtools)
what's really challenging is hot reloading saga code. And this is a conceptual issue, because modifying an existing saga code can lead to an action path totally different from the recorded one. Hot reloading Sagas can change the past. At the point of time a hot reloaded Saga dispatches an action different from the one dispatched in the old saga code, the remaining actions in the log no longer make sens, because we can now have a future totally different from the recorded one.
A possible way to hot reload control flow, IMO, is to replay the control flow from the beginning. We have to exclude any 'saga triggered action' from the log and take 'the user triggered actions' as a single source of truth: something similar to what @slorber called 'command sourcing' (#8 (comment)). Then we have to replay the Saga from the beginning by re-dispatching the recorded user actions to the store. So we'll effectively recompute a new action log. A side issue of this approach is that replaying the Saga will re-triggers all api calls inside the Saga (or possibly will trigger new api calls with the modified saga code). But at least we don't have to repeat the UI actions manually to test the new Saga code.
Those UI triggered actions seems more like Cerebral signals. A user triggered action can lead to different cascaded actions (like REQUEST -> SUCCESS) fired by the Saga. So instead of a flat view of the action log (like the actual devtools view), we'd have a structured view : user actions/program actions
this approach would also enable devtools to work with any other async middleware, as long the devtools can
1- make a distinction between user actions/program actions
2- link program actions to their parent user actions
from redux-saga.
Debugging/time-travel is a "hard" problem to do with timing-dependent code. It is not easy with any of the similar middlewares (that I know of), but Saga introduces an API that could in principle make it possible to debug in a reasonable way - see #5 for some ideas in this department.
from redux-saga.
In my experience debugging sagas, there were a few different behaviors that would have made sense at different times, and that was in the span of a very short time. The general takeaway that I had was that trying to debug these effects without the redux dev tools visible and able to "turn off" some of the effects from a previous-reload saga, it was awkward to debug stuff. I'd change some code to change the behavior of a saga, which would hot-load things, prevent the previous saga from completing, and I'd have to undo the actions the saga performed (leaving it in a now-incorrect state), so that I could trigger it again. This was fine in my case, because of the simplicity involved. Using the reset/revert/sweep/commit buttons would make this approachable for more complex interactions.
All this is to say that this does complicate the dream of "just keep iterating, and don't touch the app much", though I don't see a way of dealing with this unless we build sagas into a dev tool. In such a dev tool, we could indicate what kind of behavior we want, such as "I am interested in THIS point in the saga" - if we add first-class support for timeouts in the sagas API, we can have the saga timeline appear with a "scrubber" that moves in response to user action & the flow of time, but we can also "peg" it somewhere. Every time a reload happens, we play back the actions and the saga flow until we get to that point in the saga.
from redux-saga.
A side issue of this approach is that replaying the Saga will re-triggers all api calls inside the Saga
This seems like an undesired effect (pun intended), especially if you have API calls creating resources on the server.
This is more of a brain dump; I'm not sure if this makes sense or is possible to implement in a maintainable way:
With "pure effect/operations" it seems like you could safely replay these to get the saga back into the proper state without actually executing the effects (would involve capturing the effect description + result); The challenge is what to do with impure operations (and how to detect them).
Once you hit a point where a saga starts to branch from the previous timeline any future actions or operations (in redux devtools / all sagas) are no longer valid since they could depend on values which were only valid in the "alternate universe".
Git almost has a similar issue with merge conflicts where with manual user intervention you can get to the desired end result; I could see a devtool possibly having a similar interface which would give you the choice to keep the previous result or execute the effect again when we detect a possible branch point; If the effect had the same result it would be safe to continue replaying future actions + operations, otherwise we would have to stop at the point it branched.
from redux-saga.
Technically it is possible to externalize the state of an iterator. See ramdajs.unfold for example. Perhaps some sort of babel transform might make it possible to capture the state of a generator.
However you will run into the issue that not all state is serializable in a meaningful way, a websocket once closed is closed, a completed promise once completed wont complete again unless restarted. You would probably need some sort of hook that can be called when generator state is being serialized and deserialized to allow this state to restore the state or reset it as needed.
A bit like suspending a laptop, internet connections will be lost and it may or may not be possible to restore them when the laptop resumes... but its a feature I use everyday and have all but forgotten how much time I wasted shutting down and booting up my laptop and with a few hooks from the os to inform the process that it is being suspended and resumed it works quite well.
I don't think it will be easy to do with generators, but it can be done with externalized state and a few hooks.
from redux-saga.
Just to wonder, anybody have a real usecase where he would like to alter the past with a redux-saga?
I think it's quite complicated to manage. If hot-reloading of the saga only affects the future I'll be fine with that.
from redux-saga.
I've been exploring various strategies centered around redux for handling side effects, and this is one of the key implementation details that has kept me from adopting any solution: side effects break time travel (by their very nature). However, we might be able to skirt around this issue with regards to sagas.
As @slorber has mentioned, the real source of truth for an event sourced application is not a collection of states, but rather a log of events. If we capture and replay this log, we should be able to time travel by starting with a fresh application and replaying events 0...N
. The trick here I believe is to morph sagas while events are being replayed. I say "morph" here because I don't think "disable" is the right word.
Consider "disabling" a saga:
In the simplest case, if you have a saga that waits for event_a
then eventually performs task_a_1 ... task_a_n
, you must disable the saga middleware because you don't want to duplicate task_a_1 ... task_a_n
when replaying event_a
. You (more than likely) already have the results of those tasks (along with any data they fetched) stored inside the event log, so as far as the non-middleware part of the application is concerned, the saga did actually trigger and the resulting side effects were pumped into the system.
In the non-simple case, you are part-way through a saga's side effects when the time travel begins. Consider a saga at step 2
out of 4 steps. If we start with a fresh application and replay events 0...N
with sagas disabled, our reset saga will never receive the necessary events to move from step 0
back to step 2
where it belongs, and thus future events that would have triggered step 3
and step 4
are dropped on the floor.
My initial solution in this case is that we need to have sagas enabled, but somehow prevent them from performing their side effects... just let them capture incoming events to move into their appropriate positions. I can't think of a generic way to do this as it would, at the very least, require disabling any xhr during replay, and more than likely require other hacks of a similar fashion. But even then, disabling xhr that ultimately returns to emit another event means our saga will get stuck in an abandoned state.
Another possible solution is (at least for the purposes of debugging) to only replay events in acceptable "chunks" corresponding to sagas. If your saga is 3 steps, but the event log only has 1/3 steps completed, only replay 0...N-1
to where the saga has not yet started. I don't know how this would hold up to scrutiny.
from redux-saga.
@aft-luke instead of disabling state, the processor that executes them can simply memoize the effects. Instead of reexecuting the effect, it will simply yield its memoized return directly.
For example if you have a statement like:
const {stop, tick} = yield race({
stop : take('STOP'),
tick : call(wait, ONE_SECOND);
})
During real execution, the race could return from one of the 2 effects.
During replay, if the result of the race is memoized, there's no wait/take to call at all and we simply return the value that has already been returned during real execution. It is easy to reconstruct saga state by using the memoized effects, the complicated part is to stop at a given point in time as it means tracking at which event/action we are and not executing effects after this action/effect.
Also when time-travelling it's probably not a good idea to emit any effect in the past and go to the future: it could produce weird things. However it makes sense to rollback to a past state and restart real app execution from this point, and sagas should be able to be set in an appropriate state during that rollback.
from redux-saga.
Does that require a memoization wrapper around everything exported by io
that extracts a value, such as race/take
, or are we talking about a higher level memoization of the saga itself (as you say, by the executor)? I can't see how a higher level memoization of the executor would allow you to invoke the sagas to "put them back where they should be" when replaying events... so it must be the first case?
I like this idea of memoization, but it gets more complex if your side effects aren't idempotent. Given event_a
, move saga to step 1
if state a
- or move saga to step 2
if state b
. I'm digging through a few github projects to find the comments/issues stream where others were talking about this issue; how can we eliminate getState()
entirely from either a thunk or a saga, so it's simply handed exactly what it needs to know and remains idempotent.
from redux-saga.
After some more thought, I wonder if we can get away with ignoring this issue entirely and accept a "good enough" solution. What are the chances that you want to debug something not related to a saga, but takes place during the lifetime of a saga? Seems like a very edge case to me. With that in mind, If we just accept the hard truth that "when writing/debugging sagas, we are going to have to f5", simple solutions become more plausible.
Just disable sagas when replaying events... don't worry about trying to memoize their results or capture their partially-completed state or "guarantee" that they are restored to the proper state during a replay. The caveat here is that if you want to step back in time, wait until your saga has completed (returned xhr, timeout, whatever) so the event log has everything it needs and no saga is left in a partial state.
Does that sound reasonable? Can someone think of use cases where it becomes such an inconvenience to not have time-travel in sagas that we have to have a complicated solution implementing state serialization/deserialization, generator forwarding, side effect memoization, and whatever else might be required?
from redux-saga.
Catching up on everything redux and came across #5.
Assuming there already is a log of all past effects (as per above issue), appropriately interleaved with other events, there shouldn't be any need for explicit memoization in order to "dry run" effects. When replaying, could not a call() be turned into a take() on the event that represents the already known result? Likewise, puts would be ignored as they "have already happened".
This would cause problems when the logic / control flow of the saga changed as per comments made already, but with enough information stored about the effects, this could be made quite clear, i.e. you could flag that "last time, the saga was waiting for something else at this point".
from redux-saga.
just pausing existing sagas and restarting hot reloaded ones would cover most cases where you’d care about hot reloading them
the issue is most of the time you have only one root saga that is visible to the external world; the rest of Sagas are started 'internally' using fork/call. W'll have to determine where the modified Saga is actually in the execution tree (there maybe many instances)? and more importantly we need to determine the impact of hot reloading this Saga on other running Sagas. The most predictable way I can think of is to replay the top level Saga withe the event log (see below)
@hoschi(thanks for sharing this)
So in your solution you restart all Sagas. In the case of simple watchers (watch-and-fork) it's sufficient. But note in the case of a more complex Saga (say an authorization saga in the middle of a login/logout) restarting the Saga will put it at the beginning of the flow.
There is also the issue of the already running tasks, normally if you have only fork
ed sagas (non spawn
ed) it should be sufficient because cancelling the root Saga will cancel all forked tasks on the execution tree. But in the case of non attached forks (via spawn
Effects) it won't work.
The solution I had in mind is to replay the Sagas with the past event log: But then we have to distinguish between 2 classes of actions
- External Actions (User, Websockets, ...): I'll call those Events
- Internal Actions (dispatched by the app in reaction to Events) I'll call them just Actions
So if we hot-reload the Saga code we replay the Sagas only with past Events, which may trigger a different internal Action log (e.g. Saga changed from dispatching ACTION_1
to dispatching ACTION_2
in reaction to an event).
There is still the issue of the api calls: we can memoize api calls to avoid hitting the server, but in the case the changed Saga code trigger Api calls with different arguments then in this case w're forced to make a real API call (or throw a message to the user/developer)
from redux-saga.
@yelouafi thanks for the warning that problems occure with spawn
effects 👍 I add a note to my code repo for my team members.
Your idea with replaying an event log sounds interresting, but has this approach the same problem with serializing things discussed already? This ist just something which cames to my mind when thinking about it, I have no deep knowledge about saga/effects, yet.
from redux-saga.
Related Issues (20)
- TypeError: Cannot assign to read only property 'cont' of object '#<Object>' HOT 1
- yield and takeLatest not working with jest HOT 8
- Waiting for an action with takeMaybe / take after END is dispatched for SSR HOT 7
- Is it possible to selectively cancel tasks in an actionChannel? Ie cancel the 3rd task out of 5 running ones. HOT 5
- Is it possible for a saga to "trace" the effect "chain"? HOT 4
- Delay inside of while loop may never fire with React Native 0.71.6 HOT 2
- UI freezes when chrome devtools is open HOT 4
- Redux 4.0 - Unable to access updated data using useSelector HOT 2
- could we add leading/trailing edge options for debounce? HOT 3
- Workflow has flaw
- Why not use the await and async instead of the generator and yield? HOT 1
- TS2345 error while putting thunk actions
- React native Redux Saga with Redux Tollkit
- Module '"redux-saga/effects"' has no exported member 'call'. HOT 4
- Is there a standard way to break while true loops with call effect when END is dispatched? HOT 1
- Can put type improvements be released downstream? HOT 2
- Sending very large files, tasks in parallel are using a lot of memory
- How to use package that use redux-saga as dependency when its in webpack externals? HOT 7
- Help me connect redux-saga with Nextjs 13.5 using app router HOT 2
- Update peer dependencies to include `redux@5` (currently beta) HOT 14
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from redux-saga.