Skip to content

Commit

Permalink
feat(effects): add OnIdentifyEffects interface to register multiple e…
Browse files Browse the repository at this point in the history
…ffect instances (#1448)

* feat(Effects): add OnIdentifyEffects interface
* refactor(Effects): move OnRunEffects to lifecycle_hooks
  • Loading branch information
timdeschryver authored and brandonroberts committed Dec 4, 2018
1 parent 6a754aa commit b553ce7
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 45 deletions.
69 changes: 62 additions & 7 deletions modules/effects/spec/effect_sources.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ErrorHandler } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { cold, getTestScheduler } from 'jasmine-marbles';
import { concat, empty, NEVER, Observable, of, throwError, timer } from 'rxjs';
import { concat, NEVER, Observable, of, throwError, timer } from 'rxjs';
import { map } from 'rxjs/operators';

import { Effect, EffectSources } from '../';
import { Effect, EffectSources, OnIdentifyEffects } from '../';

describe('EffectSources', () => {
let mockErrorReporter: ErrorHandler;
Expand Down Expand Up @@ -37,6 +37,8 @@ describe('EffectSources', () => {
const d = { not: 'a valid action' };
const e = undefined;
const f = null;
const i = { type: 'From Source Identifier' };
const i2 = { type: 'From Source Identifier 2' };

let circularRef = {} as any;
circularRef.circularRef = circularRef;
Expand Down Expand Up @@ -82,6 +84,32 @@ describe('EffectSources', () => {
never = timer(50, getTestScheduler() as any).pipe(map(() => 'update'));
}

class SourceWithIdentifier implements OnIdentifyEffects {
effectIdentifier: string;
@Effect() i$ = alwaysOf(i);

ngrxOnIdentifyEffects() {
return this.effectIdentifier;
}

constructor(identifier: string) {
this.effectIdentifier = identifier;
}
}

class SourceWithIdentifier2 implements OnIdentifyEffects {
effectIdentifier: string;
@Effect() i2$ = alwaysOf(i2);

ngrxOnIdentifyEffects() {
return this.effectIdentifier;
}

constructor(identifier: string) {
this.effectIdentifier = identifier;
}
}

it('should resolve effects from instances', () => {
const sources$ = cold('--a--', { a: new SourceA() });
const expected = cold('--a--', { a });
Expand All @@ -102,13 +130,40 @@ describe('EffectSources', () => {
expect(output).toBeObservable(expected);
});

it('should resolve effects from same class but different instances', () => {
it('should resolve effects with different identifiers', () => {
const sources$ = cold('--a--b--c--', {
a: new SourceA(),
b: new SourceA(),
c: new SourceA(),
a: new SourceWithIdentifier('a'),
b: new SourceWithIdentifier('b'),
c: new SourceWithIdentifier('c'),
});
const expected = cold('--i--i--i--', { i });

const output = toActions(sources$);

expect(output).toBeObservable(expected);
});

it('should ignore effects with the same identifier', () => {
const sources$ = cold('--a--b--c--', {
a: new SourceWithIdentifier('a'),
b: new SourceWithIdentifier('a'),
c: new SourceWithIdentifier('a'),
});
const expected = cold('--i--------', { i });

const output = toActions(sources$);

expect(output).toBeObservable(expected);
});

it('should resolve effects with same identifiers but different classes', () => {
const sources$ = cold('--a--b--c--d--', {
a: new SourceWithIdentifier('a'),
b: new SourceWithIdentifier2('a'),
c: new SourceWithIdentifier('b'),
d: new SourceWithIdentifier2('b'),
});
const expected = cold('--a--a--a--', { a });
const expected = cold('--a--b--a--b--', { a: i, b: i2 });

const output = toActions(sources$);

Expand Down
43 changes: 41 additions & 2 deletions modules/effects/src/effect_sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ import {
} from 'rxjs/operators';

import { verifyOutput } from './effect_notification';
import { resolveEffectSource } from './effects_resolver';
import { mergeEffects } from './effects_resolver';
import { getSourceForInstance } from './effects_metadata';
import {
onIdentifyEffectsKey,
onRunEffectsKey,
onRunEffectsFn,
OnRunEffects,
} from './lifecycle_hooks';

@Injectable()
export class EffectSources extends Subject<any> {
Expand All @@ -28,7 +35,8 @@ export class EffectSources extends Subject<any> {
*/
toActions(): Observable<Action> {
return this.pipe(
groupBy(source => source),
groupBy(getSourceForInstance),
mergeMap(source$ => source$.pipe(groupBy(effectsInstance))),
mergeMap(source$ =>
source$.pipe(
exhaustMap(resolveEffectSource),
Expand All @@ -47,3 +55,34 @@ export class EffectSources extends Subject<any> {
);
}
}

function effectsInstance(sourceInstance: any) {
if (
onIdentifyEffectsKey in sourceInstance &&
typeof sourceInstance[onIdentifyEffectsKey] === 'function'
) {
return sourceInstance[onIdentifyEffectsKey]();
}

return '';
}

function resolveEffectSource(sourceInstance: any) {
const mergedEffects$ = mergeEffects(sourceInstance);

if (isOnRunEffects(sourceInstance)) {
return sourceInstance.ngrxOnRunEffects(mergedEffects$);
}

return mergedEffects$;
}

function isOnRunEffects(sourceInstance: {
[onRunEffectsKey]?: onRunEffectsFn;
}): sourceInstance is OnRunEffects {
const source = getSourceForInstance(sourceInstance);

return (
onRunEffectsKey in source && typeof source[onRunEffectsKey] === 'function'
);
}
11 changes: 0 additions & 11 deletions modules/effects/src/effects_resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { ignoreElements, map, materialize } from 'rxjs/operators';

import { EffectNotification } from './effect_notification';
import { getSourceForInstance, getSourceMetadata } from './effects_metadata';
import { isOnRunEffects } from './on_run_effects';

export function mergeEffects(
sourceInstance: any
Expand Down Expand Up @@ -40,13 +39,3 @@ export function mergeEffects(

return merge(...observables);
}

export function resolveEffectSource(sourceInstance: any) {
const mergedEffects$ = mergeEffects(sourceInstance);

if (isOnRunEffects(sourceInstance)) {
return sourceInstance.ngrxOnRunEffects(mergedEffects$);
}

return mergedEffects$;
}
2 changes: 1 addition & 1 deletion modules/effects/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export { mergeEffects } from './effects_resolver';
export { Actions, ofType } from './actions';
export { EffectsModule } from './effects_module';
export { EffectSources } from './effect_sources';
export { OnRunEffects } from './on_run_effects';
export { EffectNotification } from './effect_notification';
export { ROOT_EFFECTS_INIT } from './effects_root_module';
export { UPDATE_EFFECTS, UpdateEffects } from './effects_feature_module';
export { OnIdentifyEffects, OnRunEffects } from './lifecycle_hooks';
81 changes: 81 additions & 0 deletions modules/effects/src/lifecycle_hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Observable } from 'rxjs';
import { EffectNotification } from '.';

/**
* @description
* Interface to set an identifier for effect instances.
*
* By default, each Effects class is registered
* once regardless of how many times the Effect class
* is loaded. By implementing this interface, you define
* a unique identifier to register an Effects class instance
* multiple times.
*
* @usageNotes
*
* ### Set an identifier for an Effects class
*
* ```ts
* class EffectWithIdentifier implements OnIdentifyEffects {
* private effectIdentifier: string;
*
* ngrxOnIdentifyEffects () {
* return this.effectIdentifier;
* }
*
* constructor(identifier: string) {
* this.effectIdentifier = identifier;
* }
* ```
*/
export interface OnIdentifyEffects {
/**
* @description
* String identifier to differentiate effect instances.
*/
ngrxOnIdentifyEffects: () => string;
}

export const onIdentifyEffectsKey: keyof OnIdentifyEffects =
'ngrxOnIdentifyEffects';

export type onRunEffectsFn = (
resolvedEffects$: Observable<EffectNotification>
) => Observable<EffectNotification>;

/**
* @description
* Interface to control the lifecycle of effects.
*
* By default, effects are merged and subscribed to the store. Implement the OnRunEffects interface to control the lifecycle of the resolved effects.
*
* @usageNotes
*
* ### Implement the OnRunEffects interface on an Effects class
*
* ```ts
* export class UserEffects implements OnRunEffects {
* constructor(private actions$: Actions) {}
*
* ngrxOnRunEffects(resolvedEffects$: Observable<EffectNotification>) {
* return this.actions$.pipe(
* ofType('LOGGED_IN'),
* exhaustMap(() =>
* resolvedEffects$.pipe(
* takeUntil(this.actions$.pipe(ofType('LOGGED_OUT')))
* )
* )
* );
* }
* }
* ```
*/
export interface OnRunEffects {
/**
* @description
* Method to control the lifecycle of effects.
*/
ngrxOnRunEffects: onRunEffectsFn;
}

export const onRunEffectsKey: keyof OnRunEffects = 'ngrxOnRunEffects';
24 changes: 0 additions & 24 deletions modules/effects/src/on_run_effects.ts

This file was deleted.

0 comments on commit b553ce7

Please sign in to comment.