Yauheni Pozdnyakov
| 1 | args | |
|---|---|---|
| sync | value | array |
| async | promise | stream/pipe |
Async vs Sync
Just messaging or a pub-sub?
Rx
Producer
Consumer
Data pipeline
Time
Declarative
Immutable
Pure, side effect -free
Map, reduce, iterator
Functional Programming
Array as a source
source - an array or an object
from - creates an Observable-a sequence of an array-like or iterable objects
of - creates Observable-a sequence of passed arguments
const task_stream =
// Стрим из всех данных в базе
getTasks().
// выбираем задания только для текущего пользователя
filter((task) => task.user_id == user_id).
// находим те, которые еще не завершились
filter((task) => !task.completed).
// берем только название
map((task) => task.name)Observable.from(source)
.operatorOne()
.operatorTwo()
.oneMoreOperator()
.aaanddMoreOperator()
.filter(value => value)
.subscribe(console.log);Events as a source
Typeahead
return Observable.fromEvent(this.elemnt.nativeElement, 'keyup')
.map((e:any) => e.target.value)
.debounceTime(450)
.concat()
.distinctUntilChanged()
.subscribe(item => this.keywordChange.emit(item));
closed = false
Memory leak!
return Observable.fromEvent(this.elemnt.nativeElement, 'keyup')
.map((e:any) => e.target.value)
.debounceTime(450)
.concat()
.distinctUntilChanged()
.subscribe(item => this.keywordChange.emit(item));private subscription: Subscription;
onInit(){
this.subscription = this.listenAndSuggest();
}
listenAndSuggest(){
return Observable
.fromEvent(this.elemnt.nativeElement, 'keyup')
.map((e:any) => e.target.value)
.debounceTime(450)
.concat()
.distinctUntilChanged()
.subscribe(item => this.keywordChange.emit(item));
}
Forgot to unsubscribe from a Cold Pipe
Forgot to unsubscribe from a Cold Pipe
No one will Unsubscribe for you
interface Observer<T> {
closed?: boolean;
next: (value: T) => void;
error: (err: any) => void;
complete: () => void;
}
unsubscribe(): void {
let hasErrors = false;
let errors: any[];
if (this.closed) {
return;
}
let { _parent, _ parents, _unsubscribe, _subscriptions } = (<any> this);
this.closed = true;
this._parent = null;
this._parents = null;
this._subscriptions = null;
}We forgot to unsubscribe from a Cold Pipe
We want to consume a stream only while component is alive
Will be useless in case of many Observable
private subscription: Subscription;
onInit(){
this.subscription = this.listenAndSuggest();
}
listenAndSuggest(){
return Observable
.fromEvent(this.elemnt.nativeElement, 'keyup')
.map((e:any) => e.target.value)
.debounceTime(450)
.concat()
.distinctUntilChanged()
.subscribe(item => this.keywordChange.emit(item));
}
onDestroy(){
this.subscription.unsubscribe();
}Use the following:
Cause:
take ()
export function take<T> count: number): MonoTypeOperatorFunction<T> {
return (source: Observable<T>) => {
if (count === 0) {
return empty();
} else {
return source.lift(new TakeOperator(count));
}
};
}private nextOrComplete(value: T, predicateResult: boolean): void {
const destination = this.destination;
if (Boolean(predicateResult)) {
destination.next(value);
} else {
destination.complete();
}
}take ()
Looks fine
Memory leak
Observable.from([1,2,3])
.filter(value => false)
.take(0)
.subscribe(console.log)Observable.from([1,2,3])
.filter(value => false)
.take(1)
.subscribe(console.log)takeWhile ()
protected _next(value: T): void {
const destination = this.destination;
let result: boolean;
try {
result = this.predicate(value, this.index++);
} catch (err) {
destination.error(err);
return;
}
this.nextOrComplete(value, result);
}
private nextOrComplete(value: T, predicateResult: boolean): void {
const destination = this.destination;
if (Boolean(predicateResult)) {
destination.next(value);
} else {
destination.complete();
}
}takeWhile ()
in case of a situation when nothing passes through the pipe, right after isDestroyed is set to true, subscription will stay alive
takeWhile classes the pipe only in case isDestroyed= true at the moment when something passes through the pipe
isDestroyed = false;
listenAndSuggest(){
return Observable
.fromEvent(this.element.nativeElement, 'keyup')
.takeWhile(() => this.isDestroyed)
.subscribe(console.log);
}
onDestroy(){
this.isDestroyed = true;
}Утечка памяти
first()
But...
protected _complete(): void {
const destination = this.destination;
if (!this.hasCompleted && typeof this.defaultValue !== 'undefined') {
destination.next(this.defaultValue);
destination.complete();
} else if (!this.hasCompleted) {
destination.error(new EmptyError);
}
}If called with no arguments, 'first' emits the first value of the sourse Observable, then completes. If called with a 'predicate' function, 'first' emits the first value of the source that matches the specified condition.
takeUntil ()
Pretty hones method,
notifyNext(outerValue: T, innerValue: R,
outerIndex: number, innerIndex: number,
innerSub: InnerSubscriber<T, R>): void {
this.complete();
}const timerOne = Rx.Observable.timer(1000, 4000);
const timerTwo = Rx.Observable.timer(2000, 4000)
const timerThree = Rx.Observable.timer(3000, 4000)
const combined = Rx.Observable
.timer(2, 1000)
.pipe(takeUntil(this.componentDestroy))
.merge(
timerOne,
timerTwo,
timerThree,
)
.subscribe(console.log) --> 0 0 1 2 3 4 1 5 6 7 8 2 9 10 11 12 3 13 14 15 16Memory leak
merge generates a new Observable, takeUntil gonna close everything until merge
const timerOne = Rx.Observable.timer(1000, 4000);
const timerTwo = Rx.Observable.timer(2000, 4000)
const timerThree = Rx.Observable.timer(3000, 4000)
const combined = Rx.Observable
.timer(2, 1000)
.merge(
timerOne,
timerTwo,
timerThree,
).pipe(
takeUntil(this.componentDestroy)
)
.subscribe(console.log) --> 0 0 1 2 3 4 1 5 6 7 8 2 9 10 11 12 3 13 14 15 16Sounds good
merge will be also completed
import { Subject } from 'rxjs/Subject';
export function TakeUntilDestroy(constructor: any) {
const originalNgOnDestroy = constructor.prototype.ngOnDestroy;
constructor.prototype.componentDestroy = function () {
this._takeUntilDestroy$ = this._takeUntilDestroy$ || new Subject();
return this._takeUntilDestroy$.asObservable();
};
constructor.prototype.ngOnDestroy = function () {
if (this._takeUntilDestroy$) {
this._takeUntilDestroy$.next(true);
this._takeUntilDestroy$.complete();
}
if (originalNgOnDestroy && typeof originalNgOnDestroy === 'function') {
originalNgOnDestroy.apply(this, arguments);
}
};
}
18к
Angular Router Reuse Strategy
id: 1
'/ route / 1'
'/ route / 2'
id: 1
'/ route / 1'
'/ route / 2'
id: 2
Going to a place where Angular adds components to a tree
{
type: ADD_NOTE,
payload: {
content: 'This is an action object'
}
}we are not handling any logic about how the store changes
(previousState, action) => newStateTo deal with reducer complexity, we chunk them down in multiple, simpler reducers and later, we combine them with a Redux helper function called combineReducers
1. The button click handler function dispatches an action to the store with the store.dispatch() method
2. Redux passes down the dispatched action to the reducer
3. The store saves the new state returned by the reducer
4. Since we have subscribed to the store, the function we provided will be called and it will update the UI accordingly
But what to do with side effects?
That process of calling into the real world is what side-effects are. They are a way of bridging the pure Redux world with the outside world
Side-effects can happen in response to Redux actions. For example, when a user clicks “Save,” you may want to fire off an AJAX request.
Side-effects may dispatch Redux actions. Like when the save process finishes successfully, you may want to dispatch SAVE_SUCCEEDED; or when it failed, SAVE_FAILED.
They also may not dispatch anything. Some side-effects don’t need to dispatch anything: if you are doing analytics tracking based on Redux actions, you would track things in response to Redux actions, but you will not dispatch anything.
Actions
const action = createAction('[Entity] simple action');
action();const action = createAction('[Entity] simple action', props<{ name: string, age: number, }>());
action({ name: 'andrei', age: 18 });const action = createAction('action',(u: User, prefix: string) => ({ name: `${prefix}${u.name}` }) );
const u: User = { /* ... */ };
action(u, '@@@@');What is inside?
function defineType<T extends string>(
type: T,
creator: Creator
): ActionCreator<T> {
return Object.defineProperty(creator, 'type', {
value: type,
writable: false,
});
}Reducers
export interface ActionReducer<T, V extends Action = Action> {
(state: T | undefined, action: V): T;
}const REDUCERS_TOKEN = new InjectionToken('REDUCERS');
@NgModule({
imports: [
StoreModule.forRoot(REDUCERS_TOKEN)
],
providers: [
{ provide: REDUCERS_TOKEN, useValue: { foo: fooReducer } }
],
}) /* ... */StoreModule.forRoot({ foo: fooReducer, user: UserReducer })Providing reducers
How are reducers set up?
StoreModule.forRoot({ entity: entityReducer })/* ... */
{
provide: _REDUCER_FACTORY,
useValue: config.reducerFactory
? config.reducerFactory
: combineReducers,
},
{
provide: REDUCER_FACTORY,
deps: [_REDUCER_FACTORY, _RESOLVED_META_REDUCERS],
useFactory: createReducerFactory,
},
/* ... */StoreModule.forRoot will return a ModuleWithProviders object which contains, among others, these providers:
export class ReducerManager /* ... */ {
constructor(
@Inject(INITIAL_STATE) private initialState: any,
@Inject(INITIAL_REDUCERS) private reducers: ActionReducerMap<any, any>,
@Inject(REDUCER_FACTORY)
private reducerFactory: ActionReducerFactory<any, any>
) {
super(reducerFactory(reducers, initialState));
}
/* ... */
}The REDUCER_FACTORY token will only be injected in ReducerManager class:
Why a pure function?
export function combineReducers(
reducers: any,
initialState: any = {}
): ActionReducer<any, Action> {
const reducerKeys = Object.keys(reducers);
const finalReducers: any = {};
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key];
}
}
/*
Remember from the previous snippet: `const reducer = reducerFactory(reducers)`
Now, the `reducer` will be the below function.
*/
return function combination(state, action) {
state = state === undefined ? initialState : state;
let hasChanged = false;
const nextState: any = {};
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i];
const reducer: any = finalReducers[key];
const previousStateForKey = state[key];
const nextStateForKey = reducer(previousStateForKey, action);
nextState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
return hasChanged ? nextState : state;
};
}If a reducer returned the same reference of an object, but with a property changed, this would not be reflected into the UI as nextStateForKey !== previousStateForKey would fail
Reducer
const increment = createAction('increment');
const decrement = createAction('decrement');
const reset = createAction('reset');
const _counterReducer = createReducer(initialState,
on(increment, state => state + 1 /* reducer#1 */),
on(decrement, state => state - 1 /* reducer#2 */),
on(reset, state => 0 /* reducer#3 */),
);
export function counterReducer(state, action) {
return _counterReducer(state, action);
}export function on<C1 extends ActionCreator, S>(
creator1: C1,
reducer: OnReducer<S, [C1]>
): On<S>;
/* ... Overloads ... */
export function on(
...args: (ActionCreator | Function)[]
): { reducer: Function; types: string[] } {
const reducer = args.pop() as Function;
const types = args.reduce(
// `creator.type` is a property directly attached to the function so that
// it can be easily accessed(`createAction` is responsible for that)
(result, creator) => [...result, (creator as ActionCreator).type],
[] as string[]
);
return { reducer, types };
}The on functions are an alternative for using the switch statement.
The on function can bind a reducer to multiple actions. Then, in the reducer, with the help of discriminated unions, we can perform the appropriate state change depending on action.
const a1 = createAction('a1', props<{ name: string }>());
const a2 = createAction('a2', props<{ age: number }>());
const initialState = /* ... */;
const reducer = createReducer(
initialState,
on(a1, a2, (state, action) => {
if (action.type === 'a1') {
action.name
} else {
action.age
}
}),
)export function on<C1 extends ActionCreator, C2 extends ActionCreator, S>(
creator1: C1,
creator2: C2,
reducer: OnReducer<S, [C1, C2]>
): On<S>;
// `C[number]` will result in a union
export interface OnReducer<S, C extends ActionCreator[]> {
(state: S, action: ActionType<C[number]>): S;
}export function createReducer<S, A extends Action = Action>(
initialState: S,
...ons: On<S>[]
): ActionReducer<S, A> { /* ... */ }Using factory, not on
import { Action, ActionReducer } from '@ngrx/store';
export interface ActionReducers<S> { [action: string]: ((p: any, s: S) => (S | ((s: S) => S))); }export interface GenericAction extends Action {
payload?: any;
}export const wrappingFunction = (maybeFunction: Function | any, ...args) => {
return typeof maybeFunction === 'function'
? maybeFunction(...args)
: maybeFunction;
}export const reducingFunction = <S>(
actionReducers: ActionReducers<S>,
state: S,
action: GenericAction,
): S => {
const reducingFunc = actionReducers[action.type];
return reducingFunc
? wrappingFunction(reducingFunc(action.payload, state), state)
: state;
}Using factory, not on
export const initialState: State = {
someValue: null,
};
export const actionReducers: ActionReducers<FormState> = {
[FEATURE.AREA.ACTION]: () => initialFormState,
[FEATURE.AREA.ACTION]: ({
value,
}) =>
assoc('someValue', value),
};
export const sampleReducer = (state = initialState, action) => {
return reducingFunction<State>(actionReducers, state, action);
}The store
export class Store<T> extends Observable<T> implements Observer<Action> {
constructor(
state$: StateObservable,
private actionsObserver: ActionsSubject,
private reducerManager: ReducerManager
) {
super();
this.source = state$;
}
/* ... */
}const s = new Subject();
class Custom extends Observable<any> {
constructor () {
super();
// By doing this, every time you do `customInstance.subscribe(subscriber)`,
// the subscriber wll be part of the subject's subscribers list
this.source = s;
}
}
const obs$ = new Custom();
// The subject has no subscribers at this point
s.next('no');
// The subject has one subscriber now
obs$.subscribe(console.log);
// `s.next()` -> sending values to the active subscribers
timer(1000)
.subscribe(() => s.next('john'));
timer(2000)
.subscribe(() => s.next('doe'));dispatch<V extends Action = Action>(
action: V /* ... type check here - skipped */
) {
this.actionsObserver.next(action);
}EffectsModule.forRoot([effectClass]), EffectsModule.forFeature([effectClass]) or the USER_PROVIDED_EFFECTS multi token
{
return {
ngModule: EffectsRootModule,
providers: [
{
// Make sure the `forRoot` static method is called only once
provide: _ROOT_EFFECTS_GUARD,
useFactory: _provideForRootGuard,
deps: [[EffectsRunner, new Optional(), new SkipSelf()]],
},
EffectsRunner,
EffectSources,
Actions,
rootEffects, // The array of effects
{
// Dependency for `ROOT_EFFECTS`
provide: _ROOT_EFFECTS,
// Providing it as an array because of how `createEffects` is implemented
useValue: [rootEffects],
},
{
// This token would be provided by the user in its separate module
provide: USER_PROVIDED_EFFECTS,
multi: true,
useValue: [], // [UserProvidedEffectsClass]
},
{
provide: ROOT_EFFECTS,
useFactory: createEffects,
deps: [Injector, _ROOT_EFFECTS, USER_PROVIDED_EFFECTS],
},
],
};
}
export const = createEffects => (
injector: Injector,
effectGroups: Type<any>[][],
userProvidedEffectGroups: Type<any>[][]
): any[] {
const mergedEffects: Type<any>[] = [];
for (let effectGroup of effectGroups) {
mergedEffects.push(...effectGroup);
}
for (let userProvidedEffectGroup of userProvidedEffectGroups) {
mergedEffects.push(...userProvidedEffectGroup);
}
return createEffectInstances(injector, mergedEffects);
}
// Here the instances are created
export const createEffectInstances = (/* ... */): any[] =>
effects.map(effect => injector.get(effect);
@NgModule({})
export class EffectsRootModule {
constructor(
private sources: EffectSources,
runner: EffectsRunner,
store: Store<any>,
@Inject(ROOT_EFFECTS) rootEffects: any[],
/* ... */
) {
// Subscribe to the `effects stream`
// The `observer` is the Store entity
runner.start();
rootEffects.forEach(effectSourceInstance =>
// Push values into the stream
sources.addEffects(effectSourceInstance)
);
store.dispatch({ type: ROOT_EFFECTS_INIT });
}
addEffects(effectSourceInstance: any) {
this.sources.addEffects(effectSourceInstance);
}
}EffectsRootModule will inject ROOT_EFFECTS which will contain the needed instances and will push them into the effects stream
createReducer(
initialState,
on(rootEffectsInit, (s, a) = {/* ... */})
)perform specific state changes when the root effects are initialized by registering the rootEffectsInit action in a reduce
@NgModule({})
export class EffectsFeatureModule {
constructor(
// Make sure the essential services(EffectsRunner, EffectSources)
// are initialized first
root: EffectsRootModule,
@Inject(FEATURE_EFFECTS) effectSourceGroups: any[][],
/* ... */
) {
effectSourceGroups.forEach(group =>
group.forEach(effectSourceInstance =>
root.addEffects(effectSourceInstance)
)
);
}
}the EffectsRootModule does a bit more than just instantiating the effects
@NgModule({})
export class EffectsRootModule {
constructor(
private sources: EffectSources,
runner: EffectsRunner,
store: Store<any>,
@Inject(ROOT_EFFECTS) rootEffects: any[],
@Optional() storeRootModule: StoreRootModule,
@Optional() storeFeatureModule: StoreFeatureModule,
@Optional()
@Inject(_ROOT_EFFECTS_GUARD)
guard: any
) {
runner.start(); // Creating the stream
rootEffects.forEach(effectSourceInstance =>
sources.addEffects(effectSourceInstance)
);
store.dispatch({ type: ROOT_EFFECTS_INIT });
}
addEffects(effectSourceInstance: any) {
// Pushing values into the stream
this.sources.addEffects(effectSourceInstance);
}
}@Optional() storeRootModule: StoreRootModule and @Optional() storeFeatureModule: StoreFeatureModule will make sure that effects are initialized after the ngrx/store entities (the results of StoreModule.forRoot() and eventually StoreModule.forFeature()) have been initialized.
This beforehand initialization includes:
// EffectsRunner
start() {
if (!this.effectsSubscription) {
this.effectsSubscription = this.effectSources
.toActions()
.subscribe(this.store);
}
}export class Store<T = object> extends Observable<T>
implements Observer<Action> {
next(action: Action) {
this.actionsObserver.next(action);
}
}e - effect
M - Observable of merged effects
I - Observable that calls ngrxOnInitEffects
A - Resulted merged actions
type DispatchType<T> = T extends { dispatch: infer U } ? U : true;
type ObservableType<T, OriginalType> = T extends false ? OriginalType : Action;
export function createEffect<
C extends EffectConfig,
DT extends DispatchType<C>,
OT extends ObservableType<DT, OT>,
R extends Observable<OT> | ((...args: any[]) => Observable<OT>)
>(source: () => R, config?: Partial<C>): R & CreateEffectMetadata {
const effect = source();
const value: EffectConfig = {
...DEFAULT_EFFECT_CONFIG,
...config, // Overrides any defaults if values are provided
};
Object.defineProperty(effect, CREATE_EFFECT_METADATA_KEY, {
value,
});
return effect as typeof effect & CreateEffectMetadata;
}createEffect() will return an observable with a property CREATE_EFFECT_METADATA_KEY attached to it which will hold the configuration object for that particular effect
const observable$: Observable<any> =
typeof sourceInstance[propertyName] === 'function'
? sourceInstance[propertyName]()
: sourceInstance[propertyName];
const effectAction$ = useEffectsErrorHandler
? effectsErrorHandler(observable$, globalErrorHandler)
: observable$;
if (dispatch === false) {
return effectAction$.pipe(ignoreElements());
}The ignoreElements operator will ignore everything, except error or completenotifications.
addUser$ = createEffect(
() => this.actions$.pipe(
ofType(UserAction.add),
exhaustMap(u => this.userService.add(u)),
map(/* Map to action */)
),
)If an error occurs due to calling userService.add() and it is not handled anywhere, like
// `this.userService.add(u)` is a cold observable
exhaustMap(
u => this.userService.add(u).pipe(catchError(err => /* Action */))
),
addUser$ will unsubscribe from the actions$ stream. defaultEffectsErrorHandler will simply re-subscribe to actions$, but there's another thing that's worth mentioning: the actions$ stream is actually a Subject so we know for sure that when re-subscribed, we won't receive any of the previously emitted values, only the newer ones
{
provide: EFFECTS_ERROR_HANDLER,
useValue: customErrHandler,
},
function customErrHandler (obs$, handler) {
return obs$.pipe(
catchError((err, caught$) => {
console.log('caught!')
// Only re-subscribe once
// return obs$;
// Re-subscribe every time an error occurs
return caught$;
}),
)
}addUser$ = createEffect(
() => this.actions$.pipe(
ofType(UserAction.add),
exhaustMap(u => this.userService.add(u)),
map(/* Map to action */)
),
)If an error occurs due to calling userService.add() and it is not handled anywhere, like
// `this.userService.add(u)` is a cold observable
exhaustMap(
u => this.userService.add(u).pipe(catchError(err => /* Action */))
),
addUser$ will unsubscribe from the actions$ stream. defaultEffectsErrorHandler will simply re-subscribe to actions$, but there's another thing that's worth mentioning: the actions$ stream is actually a Subject so we know for sure that when re-subscribed, we won't receive any of the previously emitted values, only the newer ones
{
provide: EFFECTS_ERROR_HANDLER,
useValue: customErrHandler,
},
function customErrHandler (obs$, handler) {
return obs$.pipe(
catchError((err, caught$) => {
console.log('caught!')
// Only re-subscribe once
// return obs$;
// Re-subscribe every time an error occurs
return caught$;
}),
)
}Meta-reducers are functions that receive a reducer and return a reducer
export class StoreModule {
static forRoot(
reducers,
config: RootStoreConfig<any, any> = {}
): ModuleWithProviders<StoreRootModule> {
return {
ngModule: StoreRootModule,
providers: [
/* ... */
{
provide: USER_PROVIDED_META_REDUCERS,
useValue: config.metaReducers ? config.metaReducers : [],
},
{
provide: _RESOLVED_META_REDUCERS,
deps: [META_REDUCERS, USER_PROVIDED_META_REDUCERS],
useFactory: _concatMetaReducers,
},
{
provide: REDUCER_FACTORY,
deps: [_REDUCER_FACTORY, _RESOLVED_META_REDUCERS],
useFactory: createReducerFactory,
},
/* ... */
]
}
}
}RESOLVED_META_REDUCERS when injected in createReducerFactory, it will be an array resulted from merging the built-in meta-reducers with the custom ones
Meta-reducers are functions that receive a reducer and return a reducer
export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
return function(state, action) {
console.log('state', state);
console.log('action', action);
return reducer(state, action);
};
}
export const metaReducers: MetaReducer<any>[] = [debug];
this.store.select('pizzas');this.store.select(state => state.pizzas);export const getProductsState = createFeatureSelector('products');createFeatureSelector allows us to get a top-level feature state property of the state tree simply by calling it out by its feature name:
export interface ProductsState {
pizzas: fromPizzas.PizzaState;
toppings: fromToppings.ToppingsState;
}
export const getProductsState = createFeatureSelector<ProductsState>('products');
export const getPizzaState = createSelector(
getProductsState,
(state: ProductsState) => state.pizzas
);state = {
// ProductState
products: {
// PizzaState
pizzas: {
entities: {},
loaded: false,
loading: true,
},
// ToppingsState
toppings: {
entities: {},
loaded: false,
loading: true,
},
},
};state -> products -> pizzas -> entities// src/products/store/reducers/index.ts
export interface ProductsState {
pizzas: fromPizzas.PizzaState;
toppings: fromToppings.ToppingsState;
}
export const getProductsState = createFeatureSelector<ProductsState>('products');
export const getPizzaState = createSelector(
getProductsState,
(state: ProductsState) => state.pizzas
);export const getPizzaState = createSelector(
getProductsState,
(state: ProductsState) => state.pizzas
);
export const getPizzasEntities = createSelector(
getPizzaState,
fromPizzas.getPizzasEntities
);export const getPizzasEntities = createSelector(
getPizzaState,
fromPizzas.getPizzasEntities
);
export const getAllPizzas = createSelector(getPizzasEntities, entities => {
return Object.keys(entities).map(id => entities[id]);
});export const getPizzaState = createSelector(
getProductsState,
(state: ProductsState) => state.pizzas
);
export const getPizzasEntities = createSelector(
getPizzaState,
fromPizzas.getPizzasEntities
);export const getPizzasEntities = createSelector(
getPizzaState,
fromPizzas.getPizzasEntities
);
export const getAllPizzas = createSelector(getPizzasEntities, entities => {
return Object.keys(entities).map(id => entities[id]);
});@Injectable()
export class AsyncResolver implements Resolve<any> {
constructor(private store: Store<AppState>, private router: Router) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> {
const currentDate = Date.now();
return combineLatest(
this.store.select(getAllCampaignsStatus),
this.store.select(getLastChecked),
this.store.select(getAsyncUpdateStatus),
of(currentDate),
).pipe(
tap(([campaignsStatus]) => {
if (!campaignsStatus.loaded && !campaignsStatus.loading) {
this.store.dispatch(new LoadAllCampaigns());
}
}),
filter(([campaignsStatus]) => campaignsStatus.loaded),
tap(([campaignsStatus, lastChecked, status, now]) => {
// isChecked is required to not accidentally emit excessive event
const isChecked = isNotNil(lastChecked) && lastChecked >= now;
if (!status.loading && !isChecked) {
this.store.dispatch(new CheckAsyncStatus());
}
}),
filter(isNotNil(lastChecked)),
take(1)
);
}
}