From b553ce7dfb6114e8bf1c84971447c48cb20c603f Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Tue, 4 Dec 2018 14:19:15 +0100 Subject: [PATCH] feat(effects): add OnIdentifyEffects interface to register multiple effect instances (#1448) * feat(Effects): add OnIdentifyEffects interface * refactor(Effects): move OnRunEffects to lifecycle_hooks --- modules/effects/spec/effect_sources.spec.ts | 69 ++++++++++++++++-- modules/effects/src/effect_sources.ts | 43 ++++++++++- modules/effects/src/effects_resolver.ts | 11 --- modules/effects/src/index.ts | 2 +- modules/effects/src/lifecycle_hooks.ts | 81 +++++++++++++++++++++ modules/effects/src/on_run_effects.ts | 24 ------ 6 files changed, 185 insertions(+), 45 deletions(-) create mode 100644 modules/effects/src/lifecycle_hooks.ts delete mode 100644 modules/effects/src/on_run_effects.ts diff --git a/modules/effects/spec/effect_sources.spec.ts b/modules/effects/spec/effect_sources.spec.ts index ca606ab066..7437565bd9 100644 --- a/modules/effects/spec/effect_sources.spec.ts +++ b/modules/effects/spec/effect_sources.spec.ts @@ -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; @@ -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; @@ -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 }); @@ -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$); diff --git a/modules/effects/src/effect_sources.ts b/modules/effects/src/effect_sources.ts index e831d07656..331f17cf88 100644 --- a/modules/effects/src/effect_sources.ts +++ b/modules/effects/src/effect_sources.ts @@ -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 { @@ -28,7 +35,8 @@ export class EffectSources extends Subject { */ toActions(): Observable { return this.pipe( - groupBy(source => source), + groupBy(getSourceForInstance), + mergeMap(source$ => source$.pipe(groupBy(effectsInstance))), mergeMap(source$ => source$.pipe( exhaustMap(resolveEffectSource), @@ -47,3 +55,34 @@ export class EffectSources extends Subject { ); } } + +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' + ); +} diff --git a/modules/effects/src/effects_resolver.ts b/modules/effects/src/effects_resolver.ts index 5825369801..405aecf529 100644 --- a/modules/effects/src/effects_resolver.ts +++ b/modules/effects/src/effects_resolver.ts @@ -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 @@ -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$; -} diff --git a/modules/effects/src/index.ts b/modules/effects/src/index.ts index da9f9e4180..ddc0431c0a 100644 --- a/modules/effects/src/index.ts +++ b/modules/effects/src/index.ts @@ -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'; diff --git a/modules/effects/src/lifecycle_hooks.ts b/modules/effects/src/lifecycle_hooks.ts new file mode 100644 index 0000000000..7ca45a248d --- /dev/null +++ b/modules/effects/src/lifecycle_hooks.ts @@ -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 +) => Observable; + +/** + * @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) { + * 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'; diff --git a/modules/effects/src/on_run_effects.ts b/modules/effects/src/on_run_effects.ts deleted file mode 100644 index 3bb6a9e486..0000000000 --- a/modules/effects/src/on_run_effects.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Observable } from 'rxjs'; - -import { EffectNotification } from './effect_notification'; -import { getSourceForInstance } from './effects_metadata'; - -export type onRunEffectsFn = ( - resolvedEffects$: Observable -) => Observable; - -export interface OnRunEffects { - ngrxOnRunEffects: onRunEffectsFn; -} - -export const onRunEffectsKey: keyof OnRunEffects = 'ngrxOnRunEffects'; - -export function isOnRunEffects(sourceInstance: { - [onRunEffectsKey]?: onRunEffectsFn; -}): sourceInstance is OnRunEffects { - const source = getSourceForInstance(sourceInstance); - - return ( - onRunEffectsKey in source && typeof source[onRunEffectsKey] === 'function' - ); -}