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

feat(Devtools): Support for persist, lock, pause #955

Merged
merged 12 commits into from
Aug 24, 2018

Conversation

dummdidumm
Copy link
Contributor

@dummdidumm dummdidumm commented Apr 3, 2018

Closes #853:

Devtools now support:

  • persist: When devtools option is clicked, state survives full page reload
  • lock: When activated, all actions are ignored
  • pause: When activated, recording of actions is paused

Fixes part of #919:

When importing a state, there is now a one second delay to give lazy loaded reducers time to instantiate.
Think of this situation: User is on a page where a reducer was lazy loaded. He wants to use the persist feature. The page is reloaded. Now the "import state" action is fired from the devtools, but the actions are replayed too soon because the lazy loaded reducer is not instantiated yet. That means, the actions are replayed with incomplete reducers. To fix this, a small delay is added before importing a state.

@coveralls
Copy link

coveralls commented Apr 3, 2018

Coverage Status

Coverage decreased (-0.04%) to 88.579% when pulling 8b90cf2 on dummdidumm:master into d34b88a on ngrx:master.

@@ -279,7 +279,10 @@ export class StoreRouterConnectingModule {

if (this.router.url !== this.storeState[this.stateKey].state.url) {
this.navigationTriggeredByDispatch = true;
this.router.navigateByUrl(this.storeState[this.stateKey].state.url);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a separate PR. I don't think this will work for SSR apps also because you can't guarantee this will be finished before the next navigation starts.

@dummdidumm
Copy link
Contributor Author

I removed the router-store-commit from the PR and instead opened #960 .

What do you think about the other changes?

@dummdidumm
Copy link
Contributor Author

@brandonroberts any plans if/when this will be merged?

@brandonroberts
Copy link
Member

I'll take a look this week. I'd like something a little more predictable for waiting for the reducers to load when importing, but we'll see.

@dummdidumm
Copy link
Contributor Author

Thanks for the update!

Regarding the import and "wait on lazy loaded reducers": Since lazy loaded features/reducers are directly tied to lazy loaded modules - what about injecting the Router with @Optional() and then on import

  • check if the router exists
  • if yes, if it did navigate yet
  • if it did not navigate yet, it means the import is done straight after reload, so we wait for the initial router navigation to end (reducers will be loaded by then) and then import the state.
  • if it did already navigate, we can be sure that every module/reducer we know of at this point is already loaded, so we import the state straight away.
  • if no router dependency exists, we fall back to waiting for 1 second. Maybe users use a different router implementation or something like that.

I could add a commit implementing this behavior. What do you think?

Side note: This would not solve all import issues with lazy loading. What about a state history which does some navigation action and only at this point a new reducer is loaded (#919)? But I think this is far more complex to think through / implement and does not belong into this PR.

@dummdidumm
Copy link
Contributor Author

@brandonroberts any updates on this? What do you think about the more predictable "wait for reducers by using Router"-proposal?

@dummdidumm
Copy link
Contributor Author

Sorry to ask again but is there any timeline when (or if at all) this will be merged? Any feedback would be appreciated.

@brandonroberts
Copy link
Member

Apologies for getting behind on this one. Will you rebase against master?

@dummdidumm
Copy link
Contributor Author

Thanks for the update, I will do it next week.

dummdidumm added a commit to dummdidumm/platform that referenced this pull request Jul 30, 2018
dummdidumm added a commit to dummdidumm/platform that referenced this pull request Jul 30, 2018
dummdidumm added a commit to dummdidumm/platform that referenced this pull request Jul 30, 2018
@dummdidumm
Copy link
Contributor Author

dummdidumm commented Jul 30, 2018

I rebased the commits and also added the suggested import router enhancement (#955 (comment)). Please have a look.

@brandonroberts brandonroberts self-assigned this Jul 31, 2018
@dummdidumm
Copy link
Contributor Author

I resolved some new merge conflicts. The build fails but this seems to have nothing to do with the commits (bazel ran out of memory and crashed).

map(change => this.unwrapAction(change.payload)),
concatMap((action: any) => {
if (action.type === IMPORT_STATE) {
if (this.router) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the router should be integrated here. The route may need the state we are importing, but this prevents that from happening until the navigation is over.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can listen for any @ngrx/store/update-reducers actions and delay the import until none have been received within a span of time.

Copy link
Contributor Author

@dummdidumm dummdidumm Aug 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

liftedActions$ retrieves the IMPORT_STATE-action before it is passed to the reducer. So as long as IMPORT_STATE is not emitted by liftedActions$, the import will not be triggered. update-reducers etc will be fired before the route is loaded because that happens on a different stream of actions (actions$), so there is no block. So in my opinion this is fine - or maybe I don't quite understand what you mean 😃

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's not quite clear what I mean, so I'll expand on it:

Situation 1: Router is used by user. User clicks the "import"-button of the redux devtools and manually imports a state

The added logic will not matter because router.navigated will be true (the first navigation already happened) , therefore nothing to wait on, therefore the IMPORT action is fired straight away.

Situation 2: Router is used by user. User turned on the "persist"-feature and reloads.

The added logic now matters. router.navigated will be false because the router is in the process of navigating when the action comes in. What that means is that the modules connected to that route are still getting instantiated, so for example components/reducers/effects for that route are not there yet. So if we just let the IMPORT action pass through, the reducer would import the state too soon, some actions would get ignored because the reducers handling it do not exist yet. That's why we wait until the NavigationEnd-event of the router is fired. At this point we know all the initialization logic is done.

There will be no blocking of state needed for the router because initial reducers/effects/state will be loaded during navigation. These things do not come through the liftedActions$ stream, so blocking further events there until we are finished importing is not an issue. Also something like "oh you are not logged in, I will redirect you" is fine because, well, we just import the state after that and the application will get back to the correct route.

Situation 3: Router is not used by user.

The whole router logic does not matter now, we wait 1 second just to make sure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand what's going on here, but I'd still rather support this in a consistent way without the router if possible.

Idea: After the IMPORT_STATE action comes in, wait some amount of time, then switch to listening for update-actions and debounce the IMPORT_STATE action. We can inject and listen to the store actions being dispatched.

Copy link
Contributor Author

@dummdidumm dummdidumm Aug 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like this?

concatMap((action: any) => {
  if(action.type === IMPORT_STATE) {
    return actions$.pipe(
      ofType(UPDATE), // '@ngrx/store/update-reducers'
      debounceTime(1000),
      map(() => action)
    );
  }
...

This way, no router dependency + when there is no UPDATE within 1 second, it is assumed that everything is loaded.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's pretty close, we also need to account for using the import button from the extension.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot know whether the IMPORT_STATE action is fired because of the persist option turned on or because of a manual import. But that's no harm, the user just has a minimal delay (of 1 second) until the manual import starts.

Copy link
Member

@brandonroberts brandonroberts Aug 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. I had debounce in my mind and not debounceTime. What happens if we never get an update action though? The example being that they import from the extension, or possible don't have any feature states.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, a race condition / timeout in the likes of "there is no UPDATE action at all" that fires after 1 second is needed.
I will implement it and update the PR.

const extensionOptions: ReduxDevtoolsExtensionConfig = {
instanceId: instanceId,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this removed?

Copy link
Contributor Author

@dummdidumm dummdidumm Aug 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instanceId was set randomly at every start, so the devtools extension did not see any connection between the state before and after the reload (ids never matched). Removing it means the devtools extension takes care of that itself. If the id is not removed on ngrx' side, the persist feature therefore would not work.

@brandonroberts
Copy link
Member

@dummdidumm we are still working through the Bazel memory issues, but at long as the tests pass, its fine for now. I left you some feedback on the PR. Let me know what you think.

@dummdidumm
Copy link
Contributor Author

I added you some feedback, let me know what you think.

@@ -17,6 +28,8 @@ import {
sanitizeStates,
unliftState,
} from './utils';
import { UPDATE } from '../../store/src';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be @ngrx/store instead of a relative import

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

Successfully merging this pull request may close these issues.

RFC: Dev Tools support for persist, lock, pause
3 participants