ngxs-labs / async-storage-plugin

⏱ WIP: Async Storage Plugin
MIT License
34 stars 13 forks source link

Plugin reverts last changes on other states #67

Open eugenzor opened 5 years ago

eugenzor commented 5 years ago

Let's imagine we have 2 states: A and B

interface TestModel {
  conter: number;
}

@State<TestModel>({
  name: 'a',
  defaults: {
    counter: 0,
  },
})
export class AState {}

@State<TestModel>({
  name: 'b',
  defaults: {
    counter: 0,
  },
})
export class BState {}

The app is configured to save the BState changes via HTTP on the backend

    NgxsAsyncStoragePluginModule.forRoot(
      MyEngine,
      {
        key: 'b',
      }
    ),

Let's imagine it takes 1 second to pull data from the backend.

After the initialization we have next state tree:

{
  a: {
    counter: 0
  },
  b: {
    counter: 0
  }
}

Then we start pulling the B state which has the counter value 1 on the backend.

While B state is pulling we have updated several times the state A:

{
  a: {
    counter: 3
  },
  b: {
    counter: 0
  }
}

Then B state data received and plugin updates the state. And it reverts our changes from state A:

{
  a: {
    counter: 0
  },
  b: {
    counter: 1
  }
}

That is because of: https://github.com/ngxs-labs/async-storage-plugin/blob/39fb6ecb0720a6be05b86e4a360f3bc88d4e4dda/src/lib/async-storage.plugin.ts#L71

where 'previousState' is the state that was at the moment of pulling, not the latest one.

The expected behavior is to update the B state only:

{
  a: {
    counter: 3
  },
  b: {
    counter: 1
  }
}
SteveVanOpstal commented 5 years ago

I currently work around this issue by waiting for the init action to be fired (as this plugin hijacks the init action). This kind of makes sense as I first want to load in the, in my case indexeddb, data before I start to execute a bunch of selects/actions.

I created a Store wrapper that handles this:

import {Type} from '@angular/core';
import {Actions, InitState, ofActionCompleted, Store} from '@ngxs/store';
import {Observable} from 'rxjs';
import {first, mergeMap, shareReplay} from 'rxjs/operators';

export class AsyncStore {
  initCompleted$ = this._actions$.pipe(ofActionCompleted(InitState), first(), shareReplay(1));

  constructor(private _store: Store, private _actions$: Actions) {}

  dispatch(event: any|any[]): Observable<any> {
    const observable =
        this.initCompleted$.pipe(mergeMap(() => this._store.dispatch(event)), shareReplay(1));
    observable.subscribe();
    return observable;
  }

  selectOnce<T>(selector: (state: any, ...states: any[]) => T): Observable<T>;
  // tslint:disable-next-line: unified-signatures
  selectOnce<T = any>(selector: string|Type<any>): Observable<T>;
  selectOnce(selector: any): Observable<any> {
    return this.initCompleted$.pipe(mergeMap(() => this._store.selectOnce(selector)));
  }

  select<T>(selector: (state: any, ...states: any[]) => T): Observable<T>;
  // tslint:disable-next-line: unified-signatures
  select<T = any>(selector: string|Type<any>): Observable<T>;
  select(selector: any): Observable<any> {
    return this.initCompleted$.pipe(mergeMap(() => this._store.select(selector)));
  }

  reset(state: any): any {
    this.initCompleted$.pipe(mergeMap(() => this._store.reset(state)));
  }
}
jamajamik commented 4 years ago

Hello, I deal with memory limit of localStorage using sync storage-plugin. I tried to replace the default storage plugin with this async storage plugin and related indexedDb behind. Unfortunatelly did not pass the "regretion tests". I run in couple of issues probably caused by this bug.

I have a scenario where I fetch/hydrate multiple related entity subsets from Rest Api and use loaded status mark for each entity. After all are fetched (loaded = true for all) I prepare component view with that data.

When I use persitency per key NgxsAsyncStoragePluginModule.forRoot( { key: ['ProductPrices', 'ProductData"...]} I never get all loading status = true. Seem that subsequent fetch erases previously fetched entity from STATE.

When I use global @@STATE persistency the behaviour is different. By first page load all entity are correctly fetched from Rest API and set to loaded=true in stores. But when I reload the page, some entity are reused from indexedDB, and some are forced to fetch via Rest again.. I reconfirmed it is random behaviour and it depends in which order the result are recieved.

Do You think it is a matter of simple fix of mentioned nextState = setValue(previousState, key, val); implementation? Or could this be conceptual problem connected to async execution where the order of getItems, setItems, select execution is not guranteed. So it can break the store consistency based on funtional paradigm and sequential application of sync reducers. Is that possible?

I see the workaround. Maybe it could help in case we have only one time initial data hydration from rest api. But in case I fetch related entity subsets based on user query dynamically multiple times during the store lifecycle? Is possible to implement selectSnapShot in workaround?

BTW: Is there any simple way how to use IndexedDb with ngxs? Even with sync performance penalty? The same speed as localStorage, just to circumvent its memory limitation? Anybody tried async - await implementaion of storage interface which assures seguential storage manipulation?

nvahalik commented 3 years ago

In our situation, we are seeing @@INIT fire after the application has loaded. This has the unfortunate side-effect of wiping our data.