Skip to content

Commit b553ce7

Browse files
timdeschryverbrandonroberts
authored andcommitted
feat(effects): add OnIdentifyEffects interface to register multiple effect instances (#1448)
* feat(Effects): add OnIdentifyEffects interface * refactor(Effects): move OnRunEffects to lifecycle_hooks
1 parent 6a754aa commit b553ce7

File tree

6 files changed

+185
-45
lines changed

6 files changed

+185
-45
lines changed

modules/effects/spec/effect_sources.spec.ts

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { ErrorHandler } from '@angular/core';
22
import { TestBed } from '@angular/core/testing';
33
import { cold, getTestScheduler } from 'jasmine-marbles';
4-
import { concat, empty, NEVER, Observable, of, throwError, timer } from 'rxjs';
4+
import { concat, NEVER, Observable, of, throwError, timer } from 'rxjs';
55
import { map } from 'rxjs/operators';
66

7-
import { Effect, EffectSources } from '../';
7+
import { Effect, EffectSources, OnIdentifyEffects } from '../';
88

99
describe('EffectSources', () => {
1010
let mockErrorReporter: ErrorHandler;
@@ -37,6 +37,8 @@ describe('EffectSources', () => {
3737
const d = { not: 'a valid action' };
3838
const e = undefined;
3939
const f = null;
40+
const i = { type: 'From Source Identifier' };
41+
const i2 = { type: 'From Source Identifier 2' };
4042

4143
let circularRef = {} as any;
4244
circularRef.circularRef = circularRef;
@@ -82,6 +84,32 @@ describe('EffectSources', () => {
8284
never = timer(50, getTestScheduler() as any).pipe(map(() => 'update'));
8385
}
8486

87+
class SourceWithIdentifier implements OnIdentifyEffects {
88+
effectIdentifier: string;
89+
@Effect() i$ = alwaysOf(i);
90+
91+
ngrxOnIdentifyEffects() {
92+
return this.effectIdentifier;
93+
}
94+
95+
constructor(identifier: string) {
96+
this.effectIdentifier = identifier;
97+
}
98+
}
99+
100+
class SourceWithIdentifier2 implements OnIdentifyEffects {
101+
effectIdentifier: string;
102+
@Effect() i2$ = alwaysOf(i2);
103+
104+
ngrxOnIdentifyEffects() {
105+
return this.effectIdentifier;
106+
}
107+
108+
constructor(identifier: string) {
109+
this.effectIdentifier = identifier;
110+
}
111+
}
112+
85113
it('should resolve effects from instances', () => {
86114
const sources$ = cold('--a--', { a: new SourceA() });
87115
const expected = cold('--a--', { a });
@@ -102,13 +130,40 @@ describe('EffectSources', () => {
102130
expect(output).toBeObservable(expected);
103131
});
104132

105-
it('should resolve effects from same class but different instances', () => {
133+
it('should resolve effects with different identifiers', () => {
106134
const sources$ = cold('--a--b--c--', {
107-
a: new SourceA(),
108-
b: new SourceA(),
109-
c: new SourceA(),
135+
a: new SourceWithIdentifier('a'),
136+
b: new SourceWithIdentifier('b'),
137+
c: new SourceWithIdentifier('c'),
138+
});
139+
const expected = cold('--i--i--i--', { i });
140+
141+
const output = toActions(sources$);
142+
143+
expect(output).toBeObservable(expected);
144+
});
145+
146+
it('should ignore effects with the same identifier', () => {
147+
const sources$ = cold('--a--b--c--', {
148+
a: new SourceWithIdentifier('a'),
149+
b: new SourceWithIdentifier('a'),
150+
c: new SourceWithIdentifier('a'),
151+
});
152+
const expected = cold('--i--------', { i });
153+
154+
const output = toActions(sources$);
155+
156+
expect(output).toBeObservable(expected);
157+
});
158+
159+
it('should resolve effects with same identifiers but different classes', () => {
160+
const sources$ = cold('--a--b--c--d--', {
161+
a: new SourceWithIdentifier('a'),
162+
b: new SourceWithIdentifier2('a'),
163+
c: new SourceWithIdentifier('b'),
164+
d: new SourceWithIdentifier2('b'),
110165
});
111-
const expected = cold('--a--a--a--', { a });
166+
const expected = cold('--a--b--a--b--', { a: i, b: i2 });
112167

113168
const output = toActions(sources$);
114169

modules/effects/src/effect_sources.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ import {
1111
} from 'rxjs/operators';
1212

1313
import { verifyOutput } from './effect_notification';
14-
import { resolveEffectSource } from './effects_resolver';
14+
import { mergeEffects } from './effects_resolver';
15+
import { getSourceForInstance } from './effects_metadata';
16+
import {
17+
onIdentifyEffectsKey,
18+
onRunEffectsKey,
19+
onRunEffectsFn,
20+
OnRunEffects,
21+
} from './lifecycle_hooks';
1522

1623
@Injectable()
1724
export class EffectSources extends Subject<any> {
@@ -28,7 +35,8 @@ export class EffectSources extends Subject<any> {
2835
*/
2936
toActions(): Observable<Action> {
3037
return this.pipe(
31-
groupBy(source => source),
38+
groupBy(getSourceForInstance),
39+
mergeMap(source$ => source$.pipe(groupBy(effectsInstance))),
3240
mergeMap(source$ =>
3341
source$.pipe(
3442
exhaustMap(resolveEffectSource),
@@ -47,3 +55,34 @@ export class EffectSources extends Subject<any> {
4755
);
4856
}
4957
}
58+
59+
function effectsInstance(sourceInstance: any) {
60+
if (
61+
onIdentifyEffectsKey in sourceInstance &&
62+
typeof sourceInstance[onIdentifyEffectsKey] === 'function'
63+
) {
64+
return sourceInstance[onIdentifyEffectsKey]();
65+
}
66+
67+
return '';
68+
}
69+
70+
function resolveEffectSource(sourceInstance: any) {
71+
const mergedEffects$ = mergeEffects(sourceInstance);
72+
73+
if (isOnRunEffects(sourceInstance)) {
74+
return sourceInstance.ngrxOnRunEffects(mergedEffects$);
75+
}
76+
77+
return mergedEffects$;
78+
}
79+
80+
function isOnRunEffects(sourceInstance: {
81+
[onRunEffectsKey]?: onRunEffectsFn;
82+
}): sourceInstance is OnRunEffects {
83+
const source = getSourceForInstance(sourceInstance);
84+
85+
return (
86+
onRunEffectsKey in source && typeof source[onRunEffectsKey] === 'function'
87+
);
88+
}

modules/effects/src/effects_resolver.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { ignoreElements, map, materialize } from 'rxjs/operators';
44

55
import { EffectNotification } from './effect_notification';
66
import { getSourceForInstance, getSourceMetadata } from './effects_metadata';
7-
import { isOnRunEffects } from './on_run_effects';
87

98
export function mergeEffects(
109
sourceInstance: any
@@ -40,13 +39,3 @@ export function mergeEffects(
4039

4140
return merge(...observables);
4241
}
43-
44-
export function resolveEffectSource(sourceInstance: any) {
45-
const mergedEffects$ = mergeEffects(sourceInstance);
46-
47-
if (isOnRunEffects(sourceInstance)) {
48-
return sourceInstance.ngrxOnRunEffects(mergedEffects$);
49-
}
50-
51-
return mergedEffects$;
52-
}

modules/effects/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export { mergeEffects } from './effects_resolver';
77
export { Actions, ofType } from './actions';
88
export { EffectsModule } from './effects_module';
99
export { EffectSources } from './effect_sources';
10-
export { OnRunEffects } from './on_run_effects';
1110
export { EffectNotification } from './effect_notification';
1211
export { ROOT_EFFECTS_INIT } from './effects_root_module';
1312
export { UPDATE_EFFECTS, UpdateEffects } from './effects_feature_module';
13+
export { OnIdentifyEffects, OnRunEffects } from './lifecycle_hooks';
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Observable } from 'rxjs';
2+
import { EffectNotification } from '.';
3+
4+
/**
5+
* @description
6+
* Interface to set an identifier for effect instances.
7+
*
8+
* By default, each Effects class is registered
9+
* once regardless of how many times the Effect class
10+
* is loaded. By implementing this interface, you define
11+
* a unique identifier to register an Effects class instance
12+
* multiple times.
13+
*
14+
* @usageNotes
15+
*
16+
* ### Set an identifier for an Effects class
17+
*
18+
* ```ts
19+
* class EffectWithIdentifier implements OnIdentifyEffects {
20+
* private effectIdentifier: string;
21+
*
22+
* ngrxOnIdentifyEffects () {
23+
* return this.effectIdentifier;
24+
* }
25+
*
26+
* constructor(identifier: string) {
27+
* this.effectIdentifier = identifier;
28+
* }
29+
* ```
30+
*/
31+
export interface OnIdentifyEffects {
32+
/**
33+
* @description
34+
* String identifier to differentiate effect instances.
35+
*/
36+
ngrxOnIdentifyEffects: () => string;
37+
}
38+
39+
export const onIdentifyEffectsKey: keyof OnIdentifyEffects =
40+
'ngrxOnIdentifyEffects';
41+
42+
export type onRunEffectsFn = (
43+
resolvedEffects$: Observable<EffectNotification>
44+
) => Observable<EffectNotification>;
45+
46+
/**
47+
* @description
48+
* Interface to control the lifecycle of effects.
49+
*
50+
* By default, effects are merged and subscribed to the store. Implement the OnRunEffects interface to control the lifecycle of the resolved effects.
51+
*
52+
* @usageNotes
53+
*
54+
* ### Implement the OnRunEffects interface on an Effects class
55+
*
56+
* ```ts
57+
* export class UserEffects implements OnRunEffects {
58+
* constructor(private actions$: Actions) {}
59+
*
60+
* ngrxOnRunEffects(resolvedEffects$: Observable<EffectNotification>) {
61+
* return this.actions$.pipe(
62+
* ofType('LOGGED_IN'),
63+
* exhaustMap(() =>
64+
* resolvedEffects$.pipe(
65+
* takeUntil(this.actions$.pipe(ofType('LOGGED_OUT')))
66+
* )
67+
* )
68+
* );
69+
* }
70+
* }
71+
* ```
72+
*/
73+
export interface OnRunEffects {
74+
/**
75+
* @description
76+
* Method to control the lifecycle of effects.
77+
*/
78+
ngrxOnRunEffects: onRunEffectsFn;
79+
}
80+
81+
export const onRunEffectsKey: keyof OnRunEffects = 'ngrxOnRunEffects';

modules/effects/src/on_run_effects.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)