Skip to content

Commit 3a9ad63

Browse files
zak-cloudncbrandonroberts
authored andcommitted
feat(effects): make resubscription handler overridable (#2295)
Closes #2294 BREAKING CHANGE: `resubscribeOnError` renamed to `useEffectsErrorHandler` in `createEffect` metadata BEFORE: ```ts class MyEffects { effect$ = createEffect(() => stream$, { resubscribeOnError: true, // default }); } ``` AFTER: ```ts class MyEffects { effect$ = createEffect(() => stream$, { useEffectsErrorHandler: true, // default }); } ```
1 parent 900bf75 commit 3a9ad63

17 files changed

+290
-91
lines changed

modules/effects/spec/effect_creator.spec.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,30 +52,32 @@ describe('createEffect()', () => {
5252
a = createEffect(() => of({ type: 'a' }));
5353
b = createEffect(() => of({ type: 'b' }), { dispatch: true });
5454
c = createEffect(() => of({ type: 'c' }), { dispatch: false });
55-
d = createEffect(() => of({ type: 'd' }), { resubscribeOnError: true });
55+
d = createEffect(() => of({ type: 'd' }), {
56+
useEffectsErrorHandler: true,
57+
});
5658
e = createEffect(() => of({ type: 'd' }), {
57-
resubscribeOnError: false,
59+
useEffectsErrorHandler: false,
5860
});
5961
f = createEffect(() => of({ type: 'e' }), {
6062
dispatch: false,
61-
resubscribeOnError: false,
63+
useEffectsErrorHandler: false,
6264
});
6365
g = createEffect(() => of({ type: 'e' }), {
6466
dispatch: true,
65-
resubscribeOnError: false,
67+
useEffectsErrorHandler: false,
6668
});
6769
}
6870

6971
const mock = new Fixture();
7072

7173
expect(getCreateEffectMetadata(mock)).toEqual([
72-
{ propertyName: 'a', dispatch: true, resubscribeOnError: true },
73-
{ propertyName: 'b', dispatch: true, resubscribeOnError: true },
74-
{ propertyName: 'c', dispatch: false, resubscribeOnError: true },
75-
{ propertyName: 'd', dispatch: true, resubscribeOnError: true },
76-
{ propertyName: 'e', dispatch: true, resubscribeOnError: false },
77-
{ propertyName: 'f', dispatch: false, resubscribeOnError: false },
78-
{ propertyName: 'g', dispatch: true, resubscribeOnError: false },
74+
{ propertyName: 'a', dispatch: true, useEffectsErrorHandler: true },
75+
{ propertyName: 'b', dispatch: true, useEffectsErrorHandler: true },
76+
{ propertyName: 'c', dispatch: false, useEffectsErrorHandler: true },
77+
{ propertyName: 'd', dispatch: true, useEffectsErrorHandler: true },
78+
{ propertyName: 'e', dispatch: true, useEffectsErrorHandler: false },
79+
{ propertyName: 'f', dispatch: false, useEffectsErrorHandler: false },
80+
{ propertyName: 'g', dispatch: true, useEffectsErrorHandler: false },
7981
]);
8082
});
8183

modules/effects/spec/effect_decorator.spec.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,26 @@ describe('@Effect()', () => {
99
b: any;
1010
@Effect({ dispatch: false })
1111
c: any;
12-
@Effect({ resubscribeOnError: true })
12+
@Effect({ useEffectsErrorHandler: true })
1313
d: any;
14-
@Effect({ resubscribeOnError: false })
14+
@Effect({ useEffectsErrorHandler: false })
1515
e: any;
16-
@Effect({ dispatch: false, resubscribeOnError: false })
16+
@Effect({ dispatch: false, useEffectsErrorHandler: false })
1717
f: any;
18-
@Effect({ dispatch: true, resubscribeOnError: false })
18+
@Effect({ dispatch: true, useEffectsErrorHandler: false })
1919
g: any;
2020
}
2121

2222
const mock = new Fixture();
2323

2424
expect(getEffectDecoratorMetadata(mock)).toEqual([
25-
{ propertyName: 'a', dispatch: true, resubscribeOnError: true },
26-
{ propertyName: 'b', dispatch: true, resubscribeOnError: true },
27-
{ propertyName: 'c', dispatch: false, resubscribeOnError: true },
28-
{ propertyName: 'd', dispatch: true, resubscribeOnError: true },
29-
{ propertyName: 'e', dispatch: true, resubscribeOnError: false },
30-
{ propertyName: 'f', dispatch: false, resubscribeOnError: false },
31-
{ propertyName: 'g', dispatch: true, resubscribeOnError: false },
25+
{ propertyName: 'a', dispatch: true, useEffectsErrorHandler: true },
26+
{ propertyName: 'b', dispatch: true, useEffectsErrorHandler: true },
27+
{ propertyName: 'c', dispatch: false, useEffectsErrorHandler: true },
28+
{ propertyName: 'd', dispatch: true, useEffectsErrorHandler: true },
29+
{ propertyName: 'e', dispatch: true, useEffectsErrorHandler: false },
30+
{ propertyName: 'f', dispatch: false, useEffectsErrorHandler: false },
31+
{ propertyName: 'g', dispatch: true, useEffectsErrorHandler: false },
3232
]);
3333
});
3434

modules/effects/spec/effect_sources.spec.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,27 @@ import {
1818
OnIdentifyEffects,
1919
OnInitEffects,
2020
createEffect,
21+
EFFECTS_ERROR_HANDLER,
22+
EffectsErrorHandler,
2123
Actions,
2224
} from '../';
25+
import { defaultEffectsErrorHandler } from '../src/effects_error_handler';
2326
import { EffectsRunner } from '../src/effects_runner';
2427
import { Store } from '@ngrx/store';
2528
import { ofType } from '../src';
2629

2730
describe('EffectSources', () => {
2831
let mockErrorReporter: ErrorHandler;
2932
let effectSources: EffectSources;
33+
let effectsErrorHandler: EffectsErrorHandler;
3034

3135
beforeEach(() => {
3236
TestBed.configureTestingModule({
3337
providers: [
38+
{
39+
provide: EFFECTS_ERROR_HANDLER,
40+
useValue: defaultEffectsErrorHandler,
41+
},
3442
EffectSources,
3543
EffectsRunner,
3644
{
@@ -47,6 +55,7 @@ describe('EffectSources', () => {
4755

4856
mockErrorReporter = TestBed.get(ErrorHandler);
4957
effectSources = TestBed.get(EffectSources);
58+
effectsErrorHandler = TestBed.get(EFFECTS_ERROR_HANDLER);
5059

5160
spyOn(mockErrorReporter, 'handleError');
5261
});
@@ -144,6 +153,12 @@ describe('EffectSources', () => {
144153
});
145154

146155
describe('toActions() Operator', () => {
156+
function toActions(source: any): Observable<any> {
157+
source['errorHandler'] = mockErrorReporter;
158+
source['effectsErrorHandler'] = effectsErrorHandler;
159+
return (effectSources as any)['toActions'].call(source);
160+
}
161+
147162
describe('with @Effect()', () => {
148163
const a = { type: 'From Source A' };
149164
const b = { type: 'From Source B' };
@@ -346,9 +361,9 @@ describe('EffectSources', () => {
346361
expect(toActions(sources$)).toBeObservable(expected);
347362
});
348363

349-
it('should not resubscribe on error when resubscribeOnError is false', () => {
364+
it('should not resubscribe on error when useEffectsErrorHandler is false', () => {
350365
class Eff {
351-
@Effect({ resubscribeOnError: false })
366+
@Effect({ useEffectsErrorHandler: false })
352367
b$ = hot('a--b--c--d').pipe(
353368
map(v => {
354369
if (v == 'b') throw new Error('An Error');
@@ -387,11 +402,6 @@ describe('EffectSources', () => {
387402

388403
expect(output).toBeObservable(expected);
389404
});
390-
391-
function toActions(source: any): Observable<any> {
392-
source['errorHandler'] = mockErrorReporter;
393-
return (effectSources as any)['toActions'].call(source);
394-
}
395405
});
396406

397407
describe('with createEffect()', () => {
@@ -635,7 +645,7 @@ describe('EffectSources', () => {
635645
expect(toActions(sources$)).toBeObservable(expected);
636646
});
637647

638-
it('should not resubscribe on error when resubscribeOnError is false', () => {
648+
it('should not resubscribe on error when useEffectsErrorHandler is false', () => {
639649
const sources$ = of(
640650
new class {
641651
b$ = createEffect(
@@ -646,7 +656,7 @@ describe('EffectSources', () => {
646656
return v;
647657
})
648658
),
649-
{ dispatch: false, resubscribeOnError: false }
659+
{ dispatch: false, useEffectsErrorHandler: false }
650660
);
651661
}()
652662
);
@@ -678,11 +688,6 @@ describe('EffectSources', () => {
678688

679689
expect(output).toBeObservable(expected);
680690
});
681-
682-
function toActions(source: any): Observable<any> {
683-
source['errorHandler'] = mockErrorReporter;
684-
return (effectSources as any)['toActions'].call(source);
685-
}
686691
});
687692
});
688693

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { ErrorHandler, Provider } from '@angular/core';
2+
import { TestBed } from '@angular/core/testing';
3+
import { Action, Store } from '@ngrx/store';
4+
import { Observable, of } from 'rxjs';
5+
import { catchError } from 'rxjs/operators';
6+
import { createEffect, EFFECTS_ERROR_HANDLER, EffectsModule } from '..';
7+
8+
describe('Effects Error Handler', () => {
9+
let subscriptionCount: number;
10+
let globalErrorHandler: jasmine.Spy;
11+
let storeNext: jasmine.Spy;
12+
13+
function makeEffectTestBed(...providers: Provider[]) {
14+
subscriptionCount = 0;
15+
16+
TestBed.configureTestingModule({
17+
imports: [EffectsModule.forRoot([ErrorEffect])],
18+
providers: [
19+
{
20+
provide: Store,
21+
useValue: {
22+
next: jasmine.createSpy('storeNext'),
23+
dispatch: jasmine.createSpy('dispatch'),
24+
},
25+
},
26+
{
27+
provide: ErrorHandler,
28+
useValue: {
29+
handleError: jasmine.createSpy('globalErrorHandler'),
30+
},
31+
},
32+
...providers,
33+
],
34+
});
35+
36+
globalErrorHandler = TestBed.get(ErrorHandler).handleError;
37+
const store = TestBed.get(Store);
38+
storeNext = store.next;
39+
}
40+
41+
it('should retry and notify error handler when effect error handler is not provided', () => {
42+
makeEffectTestBed();
43+
44+
// two subscriptions expected:
45+
// 1. Initial subscription to the effect (this will error)
46+
// 2. Resubscription to the effect after error (this will not error)
47+
expect(subscriptionCount).toBe(2);
48+
expect(globalErrorHandler).toHaveBeenCalledWith(new Error('effectError'));
49+
});
50+
51+
it('should use custom error behavior when EFFECTS_ERROR_HANDLER is provided', () => {
52+
const effectsErrorHandlerSpy = jasmine
53+
.createSpy()
54+
.and.callFake((effect$: Observable<any>, errorHandler: ErrorHandler) => {
55+
return effect$.pipe(
56+
catchError(err => {
57+
errorHandler.handleError(
58+
new Error('inside custom handler: ' + err.message)
59+
);
60+
return of({ type: 'custom action' });
61+
})
62+
);
63+
});
64+
65+
makeEffectTestBed({
66+
provide: EFFECTS_ERROR_HANDLER,
67+
useValue: effectsErrorHandlerSpy,
68+
});
69+
70+
expect(effectsErrorHandlerSpy).toHaveBeenCalledWith(
71+
jasmine.any(Observable),
72+
TestBed.get(ErrorHandler)
73+
);
74+
expect(globalErrorHandler).toHaveBeenCalledWith(
75+
new Error('inside custom handler: effectError')
76+
);
77+
expect(subscriptionCount).toBe(1);
78+
expect(storeNext).toHaveBeenCalledWith({ type: 'custom action' });
79+
});
80+
81+
class ErrorEffect {
82+
effect$ = createEffect(errorFirstSubscriber, {
83+
useEffectsErrorHandler: true,
84+
});
85+
}
86+
87+
/**
88+
* This observable factory returns an observable that will never emit, but the first subscriber will get an immediate
89+
* error. All subsequent subscribers will just get an observable that does not emit.
90+
*/
91+
function errorFirstSubscriber(): Observable<Action> {
92+
return new Observable(observer => {
93+
subscriptionCount++;
94+
95+
if (subscriptionCount === 1) {
96+
observer.error(new Error('effectError'));
97+
}
98+
});
99+
}
100+
});

modules/effects/spec/effects_metadata.spec.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@ describe('Effects metadata', () => {
1212
@Effect({ dispatch: false })
1313
c: any;
1414
d = createEffect(() => of({ type: 'a' }), { dispatch: false });
15-
@Effect({ dispatch: false, resubscribeOnError: false })
15+
@Effect({ dispatch: false, useEffectsErrorHandler: false })
1616
e: any;
1717
z: any;
1818
}
1919

2020
const mock = new Fixture();
2121
const expected: EffectMetadata<Fixture>[] = [
22-
{ propertyName: 'a', dispatch: true, resubscribeOnError: true },
23-
{ propertyName: 'c', dispatch: false, resubscribeOnError: true },
24-
{ propertyName: 'b', dispatch: true, resubscribeOnError: true },
25-
{ propertyName: 'd', dispatch: false, resubscribeOnError: true },
26-
{ propertyName: 'e', dispatch: false, resubscribeOnError: false },
22+
{ propertyName: 'a', dispatch: true, useEffectsErrorHandler: true },
23+
{ propertyName: 'c', dispatch: false, useEffectsErrorHandler: true },
24+
{ propertyName: 'b', dispatch: true, useEffectsErrorHandler: true },
25+
{ propertyName: 'd', dispatch: false, useEffectsErrorHandler: true },
26+
{ propertyName: 'e', dispatch: false, useEffectsErrorHandler: false },
2727
];
2828

2929
expect(getSourceMetadata(mock)).toEqual(
@@ -45,20 +45,20 @@ describe('Effects metadata', () => {
4545
e: any;
4646
f = createEffect(() => of({ type: 'f' }), { dispatch: false });
4747
g = createEffect(() => of({ type: 'g' }), {
48-
resubscribeOnError: false,
48+
useEffectsErrorHandler: false,
4949
});
5050
}
5151

5252
const mock = new Fixture();
5353

5454
expect(getEffectsMetadata(mock)).toEqual({
55-
a: { dispatch: true, resubscribeOnError: true },
56-
c: { dispatch: true, resubscribeOnError: true },
57-
e: { dispatch: false, resubscribeOnError: true },
58-
b: { dispatch: true, resubscribeOnError: true },
59-
d: { dispatch: true, resubscribeOnError: true },
60-
f: { dispatch: false, resubscribeOnError: true },
61-
g: { dispatch: true, resubscribeOnError: false },
55+
a: { dispatch: true, useEffectsErrorHandler: true },
56+
c: { dispatch: true, useEffectsErrorHandler: true },
57+
e: { dispatch: false, useEffectsErrorHandler: true },
58+
b: { dispatch: true, useEffectsErrorHandler: true },
59+
d: { dispatch: true, useEffectsErrorHandler: true },
60+
f: { dispatch: false, useEffectsErrorHandler: true },
61+
g: { dispatch: true, useEffectsErrorHandler: false },
6262
});
6363
});
6464

modules/effects/src/effect_creator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type ObservableType<T, OriginalType> = T extends false ? OriginalType : Action;
1515
* Creates an effect from an `Observable` and an `EffectConfig`.
1616
*
1717
* @param source A function which returns an `Observable`.
18-
* @param config A `Partial<EffectConfig>` to configure the effect. By default, `dispatch` is true and `resubscribeOnError` is true.
18+
* @param config A `Partial<EffectConfig>` to configure the effect. By default, `dispatch` is true and `useEffectsErrorHandler` is true.
1919
* @returns If `EffectConfig`#`dispatch` is true, returns `Observable<Action>`. Else, returns `Observable<unknown>`.
2020
*
2121
* @usageNotes

0 commit comments

Comments
 (0)