briebug / ngrx-auto-entity

NgRx Auto-Entity: Simplifying Reactive State
https://briebug.gitbook.io/ngrx-auto-entity/
Other
66 stars 12 forks source link

Question: Roadmap & date #187

Closed Inexad closed 2 years ago

Inexad commented 3 years ago

Thanks for a great package !

I wonder is there anywhere i can see timeframe / dates for upcoming updates & roadmap ?

Regards.

jrista commented 3 years ago

Hi @Inexad. We are actively working on some internal improvements, that won't really change how the library is used, but should improve how it works (lower memory footprint, only generate code that is actually used, etc.) Some of the internal reorganization is to help with long term maintenance.

We have just recently been working on angular 12 support, and have achieved that. That said, the next major hurdle will be angular 13, which will be killing off View Engine entirely, which will affect our goals of maintaining compatibility with several versions of angular (currently, we support back to 9).

There is one developer-facing feature we have planned before a 1.0 release. That is a set of RxJs operators that will support auto-correlated effects. Instead of manually dispatching say a createMyEntity action, you would use a createEntity RxJs operator, which would allow you to synchronize handling the result (success or failure) within a single effect. Once those are added, and the rest of the recent changes verified, we plan to release 1.0.

We don't have an exact timeframe, however within the next couple of months is the goal.

Inexad commented 3 years ago

Thanks so much for quick answer!

Is it possible to get some kind of callback functionality for actions? Currently i override all actions and save the response in array, which i then can access using correlation id.. This works good but doesn't seem optimal.

jrista commented 3 years ago

Can you explain more about what you are trying to do?

One thing I do have in our issues list is a batch actions feature. This would be kind of like a client-side corollary for transactions. Batches would allow you to start a batch (which in turn, may allow you to set up a distributed transaction, or start a transaction on the server, etc.), then reference the batch whenever you dispatch an auto-entity action, allowing that action to automatically take part of the batch. Batches would support automatic correlation, so that when a load or any CRUD result occurs (success or failure), those results, and each of those actions correlation ids, are tracked in the batch.

This is not a feature I plan on implementing before 1.0 at this point, but it is something I've been playing around with. The exact implementation has not been set, but the general idea is to make it easier to handle multi-entity behaviors that need to be correlated together, and possibly linked to transactions on the server. Batches could be "committed" or "canceled/aborted/rolledback", which would also support committing or rolling back server-side or distributed transactions as well.

If your needs are more complex, that may be the feature you are waiting for. ATM we are pushing to get an Auto-Entity 1.0 out the door, with our current feature set, plus the new operators to support auto-correlated entity operations. The operators themselves, in fact, may actually be split off into a simple addon library, just to help keep bundle size more manageable (as auto-entity has grown a bit in terms of bundle size...technically, since we generate at runtime so much code for you, use of Auto-Entity SHOULD in fact mean the overall bundle size of your entire application should be lower, thanks to all the reuse, and the more entity states you create with Auto-Entity, the greater the savings you should have...but still, people tend to look at package bundle size, and if its large enough that tends to kick off warning bells. ;) So we are considering a multi-bundle approach to help end developers control just how much of auto-entity they use.)

Anyway, that's most of what we have planned right now. The batch stuff may come right after 1.0, it may not arrive immediately though, as we need to actually design a solution that could work with server-side transactions, potentially even existing distributed transaction systems, etc. But the goal is to get it designed, implemented, and released as soon as we can.

I guess the only other thing we've mulled over adding, probably as another separate bundle, is a pre-made general-purpose reusable entity service that can work with REST APIs that are designed in certain ways. We have implemented a number of such reusable entity services for our clients with our own in-house use of Auto-Entity, and we can take that knowledge to build out a rather rich and configurable reusable entity service for the community as well. We haven't really planned out exactly when that will happen, but again, post 1.0.

Inexad commented 3 years ago

Ok !

158 what i mean is related to this topic.

I've achieved this by doing:

effect.ts

  loadAllCustom$ = createEffect(
    () => this.actions$.pipe(
      ofType(CheckoutActions.loadAllCustom),
      map(({ correlationId, criteria }) => new LoadAll(Checkout, criteria, correlationId)),
    ),
  );

  loadAllSuccess$ = createEffect(
    () => combineLatest([
      this.actions$.pipe(ofType(CheckoutActions.loadAllCustom)),
      this.actions$.pipe(ofEntityType(Checkout, EntityActionTypes.LoadAllSuccess)),
    ]).pipe(
      filter(([{ correlationId }, success]) => !!success && success.correlationId === correlationId),
      map(([success]) => {
        const updatedValue = [...this.checkouts.effected$.value, { checkout: undefined, correlationId: success.correlationId }];
        this.checkouts.effected$.next(updatedValue);
        return success;
      }),
    ), { dispatch: false },
  `);

Then in the facade.ts i can access "effected" array.

  findByCorrelationId(correlationId:string, removeAfter = false): Observable<Checkout | undefined> {
    return this.effected$.asObservable().pipe(
      map((x) => x.find((y) => y.correlationId === correlationId)),
      filter((x) => x !== undefined),
      first(), // Required to fire "finalize()"
      switchMap((x) => of(x?.checkout)),
      finalize(() => {
        if (removeAfter) { this.removeByCorrelationId(correlationId); }
      }),
    );
  }

In component.ts:


const correlationId = this.checkouts.loadAll();
this.checkouts.findByCorrelationId(correlationId).subscribe()...

This works.. But it requires me to override every effect and make a custom of it.

Regards.

jrista commented 3 years ago

Sorry about this. I thought I posted this, but I think the call timed out and it never went. Here it is again.

So, I think you may be able to solve this problem better with a reducer. You are using a subject to store information, basically. An accumulation of data over time. Now, I am going to assume your checkout is not actually undefined. Solving this by accumulating each checkout mapped to their correlation id in state, then writing a selector to retrieve from state by a "current" correlationId, would be much easier.

If you use the latest version of Auto Entity, you can generate unique actions per entity, so you no longer have to use ofEntityType, and you can use auto-entity actions very easily in reducers:

export interface CheckoutState extends IEntityState<Checkout> {
  lastCorrelationId?: string;
  effected: { [correlationId: string]: Checkout }
}

const {
  initialState: checkoutInitialState,
  actions: {
    loadAll: loadAllCheckouts,
    loadAllSuccess: allCheckoutsLoadedSuccessfully,
  }
} = buildState(Checkout, {
  effected: {}
} as CheckoutState);

export const trackLastCorrelationId = (state: CheckoutState, { correlationId }) => ({
  ...state,
  lastCorrelationId: 

export const mapCorrelationIdToCheckout = (state: CheckoutState, { entity, correlationId }) => ({
  ...state,
  effected: {
    ...state.effected,
    [correlationId]: entity // Map correlationId of this successful load to checkout entity that was loaded
  }
});

const reduce = createReducer(
  checkoutInitialState,
  // ... other cases ...
  on(loadAllCheckouts, 
  on(allCheckoutsLoadedSuccessfully, mapCorrelationIdToCheckout),
  // ... other cases ...
)

export function checkoutReducer(state = checkoutInitialState, action: Action): CheckoutState {
  return reduce(state, action);
}

export const getCheckoutState = state => state.checkout;
export const getEffected = state => state.effected;

export const effected = createSelector(getCheckoutState, getEffected);

This will track the effected information in state, rather than force you to write more complex effects. You then just have a selector for your effected. Parameterized selectors are going away, so you could either still have your facade method:

  findByCorrelationId(correlationId:string, removeAfter = false): Observable<Checkout | undefined> {
    return this.store.select(effected).pipe(
      map((x) => x.find((y) => y.correlationId === correlationId)),
      filter((x) => x !== undefined),
      first(), // Required to fire "finalize()"
      map((x) => x?.checkout),
      finalize(() => {
        if (removeAfter) { this.removeByCorrelationId(correlationId); }
      }),
    );
  }

If it supports your use case, instead of writing a facade method like this, you could also add a "selectedCorrelationId" field to your state, and create another selector to grab the effected checkout based on that:

export const getSelectedCorrelationId = state => state.selectedCorrelationId;

export const selectedCorrelationId = createSelector(getCheckoutState, getSelectedCorrelationId);

export const findCheckoutByCorrelationId = (checkouts, correlationId) =>
  checkouts?.find(checkout => checkout.correlationId === correlationId) ?? null;

export const selectedCheckout = createSelector(effected, selectedCorrelationId, findCheckoutByCorrelationId);

Anyway, there are a few ways to solve the problem. I do think, however, that you should be using state/reducers for some of what you are currently doing in your effect.

Inexad commented 3 years ago

Thanks so much for example ! I will for sure look into using state/reducers for this.

Is it possible to get this kind of functionality built into auto-entity? Would save so much time.

Regards.

jrista commented 3 years ago

I have to be careful about how much I build into auto-entity. It serves a particular purpose, to take care of creating entity boilerplate for you. But, there are countless ways to use that state, and I can't encapsulate all of them.

I think what you are doing here is a specific use case, so it should probably be left up to the developer to manage this kind of thing.

Inexad commented 3 years ago

I understand that.

Isn't a callback functionality for any of the actions a good feature that are useable by most developers ? I'm thinking in case of loading spinners, redirecting, other actions that need to happen right after a auto-entity action is done.

Regards.

On Thu, 19 Aug 2021, 17:31 Jon, @.***> wrote:

I have to be careful about how much I build into auto-entity. It serves a particular purpose, to take care of creating entity boilerplate for you. But, there are countless ways to use that state, and I can't encapsulate all of them.

I think what you are doing here is a specific use case, so it should probably be left up to the developer to manage this kind of thing.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/briebug/ngrx-auto-entity/issues/187#issuecomment-902012976, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACBW7TDTFCDP4MFMZCSOAM3T5UPUVANCNFSM5BMO5VZA . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&utm_campaign=notification-email .

jrista commented 3 years ago

So, a lot of that can already be done. A loading spinner, for example, can be displayed so long as one (or more, if you write a custom selector) isLoading selectors from Auto-Entity emit true. That was why the isLoading states were included, for spinners or in-page loading messages.

If you actually do need to correlate the result of a specific auto-entity action, that is what the correlationIds are for. But, usually, you should be doing that in effects, not anywhere else. A well-written NgRx app will do most work in effects, and very little anywhere else. If you need to do additional work or dispatch additional actions after a particular auto-entity action has resulted in success, or failure, or both, there is a specific correlation pattern in effects that you should be using:

  // This effect will kick off the loading of all checkouts for the specified criteria based on other events (i.e. a page being initialized (which can also be done with @ngrx/router-store), or a refresh button being clicked
  loadAllCheckouts$ = createEffect(
    () => this.actions$.pipe(
      ofType(checkoutsPageInitialized, checkoutsRefreshed),
      map(({criteria}) => loadAllCheckouts({criteria})),
      switchMap(action => [correlateLoadAllCheckouts({ correlationId: action.correlationId }), action])
    ),
  );

  doSomethingWhenCheckoutsAreLoaded$ = createEffect(
    () => combineLatest([
      this.actions$.pipe(ofType(allCheckoutsSuccessfullyLoaded),
      this.actions$.pipe(ofType(correlateLoadAllCheckouts),
    ]).pipe(
      filter(([{ correlationId }, { correlationId: expectedCorrelationId }]) => 
        correlationId === expectedCorrelationId
      ),
      // TODO: Handle the successful loading of all checkouts...find specific checkouts, dispatch another action, etc. 
    )
  );

Correlation can be done purely with standard NgRx features. This pattern can be extremely powerful to allow you to correlate any "result" action (success or failure) for any "initiating" action (loadAll, loadMany, create, update, delete, etc.). The correlationIds are created automatically, although if you want to pass them in so you have more control over them, you can do that as well (just include them in the actions that need to initiate loading, or CUD, etc.)

So you shouldn't really need "callback" functionality. Technically, its already there, through the existing correlation system...whenever a "result" occurs for a given "initiation", the correlationIds will match. You just have to capture the correlationId in another action, so you can "hop" to the next effect (or, for that matter, many effects...if you need to correlate many things as a result of loading data, you can have more than one effect work with the result actions and that "correlateWhatever" action, so you can perform many tasks when loading, or CUD, etc. succeed or fail.)

Now, there is a feature in the works that should simplify this correlation process. We've defined a set of custom RxJs operators that will allow you to kick off correlated auto-entity behaviors and handle their results all in a single bit of code. Basically it wraps the above pattern, into an operator, so that all you have to worry about is passing in any required input (i.e. criteria, data, etc.) for loading, creating, etc. then just pass in handlers for success and failure, and the actual process of correlating those is taken care of for you. Those should arrive in 0.8.x.

Inexad commented 2 years ago

Thanks ! This works well :).