diff --git a/modules/effects/spec/effect_creator.spec.ts b/modules/effects/spec/effect_creator.spec.ts index 70264ed88e..f56236d055 100644 --- a/modules/effects/spec/effect_creator.spec.ts +++ b/modules/effects/spec/effect_creator.spec.ts @@ -52,30 +52,32 @@ describe('createEffect()', () => { a = createEffect(() => of({ type: 'a' })); b = createEffect(() => of({ type: 'b' }), { dispatch: true }); c = createEffect(() => of({ type: 'c' }), { dispatch: false }); - d = createEffect(() => of({ type: 'd' }), { resubscribeOnError: true }); + d = createEffect(() => of({ type: 'd' }), { + useEffectsErrorHandler: true, + }); e = createEffect(() => of({ type: 'd' }), { - resubscribeOnError: false, + useEffectsErrorHandler: false, }); f = createEffect(() => of({ type: 'e' }), { dispatch: false, - resubscribeOnError: false, + useEffectsErrorHandler: false, }); g = createEffect(() => of({ type: 'e' }), { dispatch: true, - resubscribeOnError: false, + useEffectsErrorHandler: false, }); } const mock = new Fixture(); expect(getCreateEffectMetadata(mock)).toEqual([ - { propertyName: 'a', dispatch: true, resubscribeOnError: true }, - { propertyName: 'b', dispatch: true, resubscribeOnError: true }, - { propertyName: 'c', dispatch: false, resubscribeOnError: true }, - { propertyName: 'd', dispatch: true, resubscribeOnError: true }, - { propertyName: 'e', dispatch: true, resubscribeOnError: false }, - { propertyName: 'f', dispatch: false, resubscribeOnError: false }, - { propertyName: 'g', dispatch: true, resubscribeOnError: false }, + { propertyName: 'a', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'b', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'c', dispatch: false, useEffectsErrorHandler: true }, + { propertyName: 'd', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'e', dispatch: true, useEffectsErrorHandler: false }, + { propertyName: 'f', dispatch: false, useEffectsErrorHandler: false }, + { propertyName: 'g', dispatch: true, useEffectsErrorHandler: false }, ]); }); diff --git a/modules/effects/spec/effect_decorator.spec.ts b/modules/effects/spec/effect_decorator.spec.ts index 7b91245a98..80cf15bdde 100644 --- a/modules/effects/spec/effect_decorator.spec.ts +++ b/modules/effects/spec/effect_decorator.spec.ts @@ -9,26 +9,26 @@ describe('@Effect()', () => { b: any; @Effect({ dispatch: false }) c: any; - @Effect({ resubscribeOnError: true }) + @Effect({ useEffectsErrorHandler: true }) d: any; - @Effect({ resubscribeOnError: false }) + @Effect({ useEffectsErrorHandler: false }) e: any; - @Effect({ dispatch: false, resubscribeOnError: false }) + @Effect({ dispatch: false, useEffectsErrorHandler: false }) f: any; - @Effect({ dispatch: true, resubscribeOnError: false }) + @Effect({ dispatch: true, useEffectsErrorHandler: false }) g: any; } const mock = new Fixture(); expect(getEffectDecoratorMetadata(mock)).toEqual([ - { propertyName: 'a', dispatch: true, resubscribeOnError: true }, - { propertyName: 'b', dispatch: true, resubscribeOnError: true }, - { propertyName: 'c', dispatch: false, resubscribeOnError: true }, - { propertyName: 'd', dispatch: true, resubscribeOnError: true }, - { propertyName: 'e', dispatch: true, resubscribeOnError: false }, - { propertyName: 'f', dispatch: false, resubscribeOnError: false }, - { propertyName: 'g', dispatch: true, resubscribeOnError: false }, + { propertyName: 'a', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'b', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'c', dispatch: false, useEffectsErrorHandler: true }, + { propertyName: 'd', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'e', dispatch: true, useEffectsErrorHandler: false }, + { propertyName: 'f', dispatch: false, useEffectsErrorHandler: false }, + { propertyName: 'g', dispatch: true, useEffectsErrorHandler: false }, ]); }); diff --git a/modules/effects/spec/effect_sources.spec.ts b/modules/effects/spec/effect_sources.spec.ts index b7411ddde0..a5354a0f2e 100644 --- a/modules/effects/spec/effect_sources.spec.ts +++ b/modules/effects/spec/effect_sources.spec.ts @@ -18,8 +18,11 @@ import { OnIdentifyEffects, OnInitEffects, createEffect, + EFFECTS_ERROR_HANDLER, + EffectsErrorHandler, Actions, } from '../'; +import { defaultEffectsErrorHandler } from '../src/effects_error_handler'; import { EffectsRunner } from '../src/effects_runner'; import { Store } from '@ngrx/store'; import { ofType } from '../src'; @@ -27,10 +30,15 @@ import { ofType } from '../src'; describe('EffectSources', () => { let mockErrorReporter: ErrorHandler; let effectSources: EffectSources; + let effectsErrorHandler: EffectsErrorHandler; beforeEach(() => { TestBed.configureTestingModule({ providers: [ + { + provide: EFFECTS_ERROR_HANDLER, + useValue: defaultEffectsErrorHandler, + }, EffectSources, EffectsRunner, { @@ -47,6 +55,7 @@ describe('EffectSources', () => { mockErrorReporter = TestBed.get(ErrorHandler); effectSources = TestBed.get(EffectSources); + effectsErrorHandler = TestBed.get(EFFECTS_ERROR_HANDLER); spyOn(mockErrorReporter, 'handleError'); }); @@ -144,6 +153,12 @@ describe('EffectSources', () => { }); describe('toActions() Operator', () => { + function toActions(source: any): Observable { + source['errorHandler'] = mockErrorReporter; + source['effectsErrorHandler'] = effectsErrorHandler; + return (effectSources as any)['toActions'].call(source); + } + describe('with @Effect()', () => { const a = { type: 'From Source A' }; const b = { type: 'From Source B' }; @@ -346,9 +361,9 @@ describe('EffectSources', () => { expect(toActions(sources$)).toBeObservable(expected); }); - it('should not resubscribe on error when resubscribeOnError is false', () => { + it('should not resubscribe on error when useEffectsErrorHandler is false', () => { class Eff { - @Effect({ resubscribeOnError: false }) + @Effect({ useEffectsErrorHandler: false }) b$ = hot('a--b--c--d').pipe( map(v => { if (v == 'b') throw new Error('An Error'); @@ -387,11 +402,6 @@ describe('EffectSources', () => { expect(output).toBeObservable(expected); }); - - function toActions(source: any): Observable { - source['errorHandler'] = mockErrorReporter; - return (effectSources as any)['toActions'].call(source); - } }); describe('with createEffect()', () => { @@ -635,7 +645,7 @@ describe('EffectSources', () => { expect(toActions(sources$)).toBeObservable(expected); }); - it('should not resubscribe on error when resubscribeOnError is false', () => { + it('should not resubscribe on error when useEffectsErrorHandler is false', () => { const sources$ = of( new class { b$ = createEffect( @@ -646,7 +656,7 @@ describe('EffectSources', () => { return v; }) ), - { dispatch: false, resubscribeOnError: false } + { dispatch: false, useEffectsErrorHandler: false } ); }() ); @@ -678,11 +688,6 @@ describe('EffectSources', () => { expect(output).toBeObservable(expected); }); - - function toActions(source: any): Observable { - source['errorHandler'] = mockErrorReporter; - return (effectSources as any)['toActions'].call(source); - } }); }); diff --git a/modules/effects/spec/effects_error_handler.spec.ts b/modules/effects/spec/effects_error_handler.spec.ts new file mode 100644 index 0000000000..2d768a7cb1 --- /dev/null +++ b/modules/effects/spec/effects_error_handler.spec.ts @@ -0,0 +1,100 @@ +import { ErrorHandler, Provider } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { Action, Store } from '@ngrx/store'; +import { Observable, of } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { createEffect, EFFECTS_ERROR_HANDLER, EffectsModule } from '..'; + +describe('Effects Error Handler', () => { + let subscriptionCount: number; + let globalErrorHandler: jasmine.Spy; + let storeNext: jasmine.Spy; + + function makeEffectTestBed(...providers: Provider[]) { + subscriptionCount = 0; + + TestBed.configureTestingModule({ + imports: [EffectsModule.forRoot([ErrorEffect])], + providers: [ + { + provide: Store, + useValue: { + next: jasmine.createSpy('storeNext'), + dispatch: jasmine.createSpy('dispatch'), + }, + }, + { + provide: ErrorHandler, + useValue: { + handleError: jasmine.createSpy('globalErrorHandler'), + }, + }, + ...providers, + ], + }); + + globalErrorHandler = TestBed.get(ErrorHandler).handleError; + const store = TestBed.get(Store); + storeNext = store.next; + } + + it('should retry and notify error handler when effect error handler is not provided', () => { + makeEffectTestBed(); + + // two subscriptions expected: + // 1. Initial subscription to the effect (this will error) + // 2. Resubscription to the effect after error (this will not error) + expect(subscriptionCount).toBe(2); + expect(globalErrorHandler).toHaveBeenCalledWith(new Error('effectError')); + }); + + it('should use custom error behavior when EFFECTS_ERROR_HANDLER is provided', () => { + const effectsErrorHandlerSpy = jasmine + .createSpy() + .and.callFake((effect$: Observable, errorHandler: ErrorHandler) => { + return effect$.pipe( + catchError(err => { + errorHandler.handleError( + new Error('inside custom handler: ' + err.message) + ); + return of({ type: 'custom action' }); + }) + ); + }); + + makeEffectTestBed({ + provide: EFFECTS_ERROR_HANDLER, + useValue: effectsErrorHandlerSpy, + }); + + expect(effectsErrorHandlerSpy).toHaveBeenCalledWith( + jasmine.any(Observable), + TestBed.get(ErrorHandler) + ); + expect(globalErrorHandler).toHaveBeenCalledWith( + new Error('inside custom handler: effectError') + ); + expect(subscriptionCount).toBe(1); + expect(storeNext).toHaveBeenCalledWith({ type: 'custom action' }); + }); + + class ErrorEffect { + effect$ = createEffect(errorFirstSubscriber, { + useEffectsErrorHandler: true, + }); + } + + /** + * This observable factory returns an observable that will never emit, but the first subscriber will get an immediate + * error. All subsequent subscribers will just get an observable that does not emit. + */ + function errorFirstSubscriber(): Observable { + return new Observable(observer => { + subscriptionCount++; + + if (subscriptionCount === 1) { + observer.error(new Error('effectError')); + } + }); + } +}); diff --git a/modules/effects/spec/effects_metadata.spec.ts b/modules/effects/spec/effects_metadata.spec.ts index 5cc2e141e0..af5afb7783 100644 --- a/modules/effects/spec/effects_metadata.spec.ts +++ b/modules/effects/spec/effects_metadata.spec.ts @@ -12,18 +12,18 @@ describe('Effects metadata', () => { @Effect({ dispatch: false }) c: any; d = createEffect(() => of({ type: 'a' }), { dispatch: false }); - @Effect({ dispatch: false, resubscribeOnError: false }) + @Effect({ dispatch: false, useEffectsErrorHandler: false }) e: any; z: any; } const mock = new Fixture(); const expected: EffectMetadata[] = [ - { propertyName: 'a', dispatch: true, resubscribeOnError: true }, - { propertyName: 'c', dispatch: false, resubscribeOnError: true }, - { propertyName: 'b', dispatch: true, resubscribeOnError: true }, - { propertyName: 'd', dispatch: false, resubscribeOnError: true }, - { propertyName: 'e', dispatch: false, resubscribeOnError: false }, + { propertyName: 'a', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'c', dispatch: false, useEffectsErrorHandler: true }, + { propertyName: 'b', dispatch: true, useEffectsErrorHandler: true }, + { propertyName: 'd', dispatch: false, useEffectsErrorHandler: true }, + { propertyName: 'e', dispatch: false, useEffectsErrorHandler: false }, ]; expect(getSourceMetadata(mock)).toEqual( @@ -45,20 +45,20 @@ describe('Effects metadata', () => { e: any; f = createEffect(() => of({ type: 'f' }), { dispatch: false }); g = createEffect(() => of({ type: 'g' }), { - resubscribeOnError: false, + useEffectsErrorHandler: false, }); } const mock = new Fixture(); expect(getEffectsMetadata(mock)).toEqual({ - a: { dispatch: true, resubscribeOnError: true }, - c: { dispatch: true, resubscribeOnError: true }, - e: { dispatch: false, resubscribeOnError: true }, - b: { dispatch: true, resubscribeOnError: true }, - d: { dispatch: true, resubscribeOnError: true }, - f: { dispatch: false, resubscribeOnError: true }, - g: { dispatch: true, resubscribeOnError: false }, + a: { dispatch: true, useEffectsErrorHandler: true }, + c: { dispatch: true, useEffectsErrorHandler: true }, + e: { dispatch: false, useEffectsErrorHandler: true }, + b: { dispatch: true, useEffectsErrorHandler: true }, + d: { dispatch: true, useEffectsErrorHandler: true }, + f: { dispatch: false, useEffectsErrorHandler: true }, + g: { dispatch: true, useEffectsErrorHandler: false }, }); }); diff --git a/modules/effects/src/effect_creator.ts b/modules/effects/src/effect_creator.ts index 15cea4a320..7d972d41cb 100644 --- a/modules/effects/src/effect_creator.ts +++ b/modules/effects/src/effect_creator.ts @@ -15,7 +15,7 @@ type ObservableType = T extends false ? OriginalType : Action; * Creates an effect from an `Observable` and an `EffectConfig`. * * @param source A function which returns an `Observable`. - * @param config A `Partial` to configure the effect. By default, `dispatch` is true and `resubscribeOnError` is true. + * @param config A `Partial` to configure the effect. By default, `dispatch` is true and `useEffectsErrorHandler` is true. * @returns If `EffectConfig`#`dispatch` is true, returns `Observable`. Else, returns `Observable`. * * @usageNotes diff --git a/modules/effects/src/effect_sources.ts b/modules/effects/src/effect_sources.ts index db7099cec0..53989ee7ef 100644 --- a/modules/effects/src/effect_sources.ts +++ b/modules/effects/src/effect_sources.ts @@ -1,4 +1,4 @@ -import { ErrorHandler, Injectable } from '@angular/core'; +import { ErrorHandler, Inject, Injectable } from '@angular/core'; import { Action, Store } from '@ngrx/store'; import { Notification, Observable, Subject } from 'rxjs'; import { @@ -15,6 +15,7 @@ import { reportInvalidActions, EffectNotification, } from './effect_notification'; +import { EffectsErrorHandler } from './effects_error_handler'; import { mergeEffects } from './effects_resolver'; import { onIdentifyEffectsKey, @@ -22,11 +23,17 @@ import { OnRunEffects, onInitEffects, } from './lifecycle_hooks'; +import { EFFECTS_ERROR_HANDLER } from './tokens'; import { getSourceForInstance } from './utils'; @Injectable() export class EffectSources extends Subject { - constructor(private errorHandler: ErrorHandler, private store: Store) { + constructor( + private errorHandler: ErrorHandler, + private store: Store, + @Inject(EFFECTS_ERROR_HANDLER) + private effectsErrorHandler: EffectsErrorHandler + ) { super(); } @@ -55,7 +62,9 @@ export class EffectSources extends Subject { }), mergeMap(source$ => source$.pipe( - exhaustMap(resolveEffectSource(this.errorHandler)), + exhaustMap( + resolveEffectSource(this.errorHandler, this.effectsErrorHandler) + ), map(output => { reportInvalidActions(output, this.errorHandler); return output.notification; @@ -83,10 +92,15 @@ function effectsInstance(sourceInstance: any) { } function resolveEffectSource( - errorHandler: ErrorHandler + errorHandler: ErrorHandler, + effectsErrorHandler: EffectsErrorHandler ): (sourceInstance: any) => Observable { return sourceInstance => { - const mergedEffects$ = mergeEffects(sourceInstance, errorHandler); + const mergedEffects$ = mergeEffects( + sourceInstance, + errorHandler, + effectsErrorHandler + ); if (isOnRunEffects(sourceInstance)) { return sourceInstance.ngrxOnRunEffects(mergedEffects$); diff --git a/modules/effects/src/effects_error_handler.ts b/modules/effects/src/effects_error_handler.ts new file mode 100644 index 0000000000..32d19bba3e --- /dev/null +++ b/modules/effects/src/effects_error_handler.ts @@ -0,0 +1,24 @@ +import { ErrorHandler } from '@angular/core'; +import { Action } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +export type EffectsErrorHandler = ( + observable$: Observable, + errorHandler: ErrorHandler +) => Observable; + +export const defaultEffectsErrorHandler: EffectsErrorHandler = < + T extends Action +>( + observable$: Observable, + errorHandler: ErrorHandler +): Observable => { + return observable$.pipe( + catchError(error => { + if (errorHandler) errorHandler.handleError(error); + // Return observable that produces this particular effect + return defaultEffectsErrorHandler(observable$, errorHandler); + }) + ); +}; diff --git a/modules/effects/src/effects_metadata.ts b/modules/effects/src/effects_metadata.ts index 292001fe6d..fa8f680027 100644 --- a/modules/effects/src/effects_metadata.ts +++ b/modules/effects/src/effects_metadata.ts @@ -6,9 +6,9 @@ export function getEffectsMetadata(instance: T): EffectsMetadata { return getSourceMetadata(instance).reduce( ( acc: EffectsMetadata, - { propertyName, dispatch, resubscribeOnError } + { propertyName, dispatch, useEffectsErrorHandler } ) => { - acc[propertyName] = { dispatch, resubscribeOnError }; + acc[propertyName] = { dispatch, useEffectsErrorHandler }; return acc; }, {} diff --git a/modules/effects/src/effects_module.ts b/modules/effects/src/effects_module.ts index 187f1dc753..26442a24c1 100644 --- a/modules/effects/src/effects_module.ts +++ b/modules/effects/src/effects_module.ts @@ -1,16 +1,22 @@ import { - NgModule, ModuleWithProviders, - Type, + NgModule, Optional, SkipSelf, + Type, } from '@angular/core'; -import { EffectSources } from './effect_sources'; import { Actions } from './actions'; -import { ROOT_EFFECTS, FEATURE_EFFECTS, _ROOT_EFFECTS_GUARD } from './tokens'; +import { EffectSources } from './effect_sources'; import { EffectsFeatureModule } from './effects_feature_module'; +import { defaultEffectsErrorHandler } from './effects_error_handler'; import { EffectsRootModule } from './effects_root_module'; import { EffectsRunner } from './effects_runner'; +import { + _ROOT_EFFECTS_GUARD, + EFFECTS_ERROR_HANDLER, + FEATURE_EFFECTS, + ROOT_EFFECTS, +} from './tokens'; @NgModule({}) export class EffectsModule { @@ -42,6 +48,10 @@ export class EffectsModule { useFactory: _provideForRootGuard, deps: [[EffectsRunner, new Optional(), new SkipSelf()]], }, + { + provide: EFFECTS_ERROR_HANDLER, + useValue: defaultEffectsErrorHandler, + }, EffectsRunner, EffectSources, Actions, diff --git a/modules/effects/src/effects_resolver.ts b/modules/effects/src/effects_resolver.ts index 8a95d4bf4d..05a8de1f47 100644 --- a/modules/effects/src/effects_resolver.ts +++ b/modules/effects/src/effects_resolver.ts @@ -1,15 +1,17 @@ import { Action } from '@ngrx/store'; import { merge, Notification, Observable } from 'rxjs'; -import { ignoreElements, map, materialize, catchError } from 'rxjs/operators'; +import { ignoreElements, map, materialize } from 'rxjs/operators'; import { EffectNotification } from './effect_notification'; import { getSourceMetadata } from './effects_metadata'; +import { EffectsErrorHandler } from './effects_error_handler'; import { getSourceForInstance } from './utils'; import { ErrorHandler } from '@angular/core'; export function mergeEffects( sourceInstance: any, - errorHandler?: ErrorHandler + globalErrorHandler: ErrorHandler, + effectsErrorHandler: EffectsErrorHandler ): Observable { const sourceName = getSourceForInstance(sourceInstance).constructor.name; @@ -17,22 +19,22 @@ export function mergeEffects( ({ propertyName, dispatch, - resubscribeOnError, + useEffectsErrorHandler, }): Observable => { const observable$: Observable = typeof sourceInstance[propertyName] === 'function' ? sourceInstance[propertyName]() : sourceInstance[propertyName]; - const resubscribable$ = resubscribeOnError - ? resubscribeInCaseOfError(observable$, errorHandler) + const effectAction$ = useEffectsErrorHandler + ? effectsErrorHandler(observable$, globalErrorHandler) : observable$; if (dispatch === false) { - return resubscribable$.pipe(ignoreElements()); + return effectAction$.pipe(ignoreElements()); } - const materialized$ = resubscribable$.pipe(materialize()); + const materialized$ = effectAction$.pipe(materialize()); return materialized$.pipe( map( @@ -50,16 +52,3 @@ export function mergeEffects( return merge(...observables$); } - -function resubscribeInCaseOfError( - observable$: Observable, - errorHandler?: ErrorHandler -): Observable { - return observable$.pipe( - catchError(error => { - if (errorHandler) errorHandler.handleError(error); - // Return observable that produces this particular effect - return resubscribeInCaseOfError(observable$, errorHandler); - }) - ); -} diff --git a/modules/effects/src/index.ts b/modules/effects/src/index.ts index e871f62ea3..c84586adca 100644 --- a/modules/effects/src/index.ts +++ b/modules/effects/src/index.ts @@ -3,6 +3,7 @@ export { EffectConfig } from './models'; export { Effect } from './effect_decorator'; export { getEffectsMetadata } from './effects_metadata'; export { mergeEffects } from './effects_resolver'; +export { EffectsErrorHandler } from './effects_error_handler'; export { EffectsMetadata, CreateEffectMetadata } from './models'; export { Actions, ofType } from './actions'; export { EffectsModule } from './effects_module'; @@ -14,6 +15,7 @@ export { rootEffectsInit, EffectsRootModule, } from './effects_root_module'; +export { EFFECTS_ERROR_HANDLER } from './tokens'; export { act } from './act'; export { OnIdentifyEffects, diff --git a/modules/effects/src/models.ts b/modules/effects/src/models.ts index 5996ff6bd1..1d3aac91dc 100644 --- a/modules/effects/src/models.ts +++ b/modules/effects/src/models.ts @@ -10,12 +10,12 @@ export interface EffectConfig { /** * Determines if the effect will be resubscribed to if an error occurs in the main actions stream. */ - resubscribeOnError?: boolean; + useEffectsErrorHandler?: boolean; } export const DEFAULT_EFFECT_CONFIG: Readonly> = { dispatch: true, - resubscribeOnError: true, + useEffectsErrorHandler: true, }; export const CREATE_EFFECT_METADATA_KEY = '__@ngrx/effects_create__'; diff --git a/modules/effects/src/tokens.ts b/modules/effects/src/tokens.ts index de3923be1e..666c666334 100644 --- a/modules/effects/src/tokens.ts +++ b/modules/effects/src/tokens.ts @@ -1,4 +1,5 @@ import { InjectionToken, Type } from '@angular/core'; +import { EffectsErrorHandler } from './effects_error_handler'; export const _ROOT_EFFECTS_GUARD = new InjectionToken( '@ngrx/effects Internal Root Guard' @@ -12,3 +13,6 @@ export const ROOT_EFFECTS = new InjectionToken[]>( export const FEATURE_EFFECTS = new InjectionToken( 'ngrx/effects: Feature Effects' ); +export const EFFECTS_ERROR_HANDLER = new InjectionToken( + 'ngrx/effects: Effects Error Handler' +); diff --git a/modules/store/testing/src/mock_reducer_manager.ts b/modules/store/testing/src/mock_reducer_manager.ts index b7fc1f6bb9..8c4431cf6c 100644 --- a/modules/store/testing/src/mock_reducer_manager.ts +++ b/modules/store/testing/src/mock_reducer_manager.ts @@ -9,7 +9,7 @@ export class MockReducerManager extends BehaviorSubject< constructor() { super(() => undefined); } - + addFeature(feature: any) { /* noop */ } diff --git a/projects/example-app/src/app/app.module.ts b/projects/example-app/src/app/app.module.ts index 6262bd8eb2..b9ad39081d 100644 --- a/projects/example-app/src/app/app.module.ts +++ b/projects/example-app/src/app/app.module.ts @@ -15,10 +15,7 @@ import { ROOT_REDUCERS, metaReducers } from '@example-app/reducers'; import { CoreModule } from '@example-app/core'; import { AppRoutingModule } from '@example-app/app-routing.module'; -import { - UserEffects, - RouterEffects -} from '@example-app/core/effects'; +import { UserEffects, RouterEffects } from '@example-app/core/effects'; import { AppComponent } from '@example-app/core/containers'; @NgModule({ diff --git a/projects/ngrx.io/content/guide/effects/lifecycle.md b/projects/ngrx.io/content/guide/effects/lifecycle.md index b17b132753..4c33011950 100644 --- a/projects/ngrx.io/content/guide/effects/lifecycle.md +++ b/projects/ngrx.io/content/guide/effects/lifecycle.md @@ -52,7 +52,7 @@ In some cases where particular RxJS operators are used, the new behavior might produce unexpected results. For example, if the `startWith` operator is within the effect's pipe then it will be triggered again. -To disable resubscriptions add `{resubscribeOnError: false}` to the `createEffect` +To disable resubscriptions add `{useEffectsErrorHandler: false}` to the `createEffect` metadata (second argument). @@ -81,7 +81,7 @@ export class AuthEffects { ) // Errors are handled and it is safe to disable resubscription ), - { resubscribeOnError: false } + { useEffectsErrorHandler: false } ); constructor( @@ -91,6 +91,58 @@ export class AuthEffects { } +### Customizing the Effects Error Handler + +The behavior of the default resubscription handler can be customized +by providing a custom handler using the `EFFECTS_ERROR_HANDLER` injection token. + +This allows you to provide a custom behavior, such as only retrying on +certain "retryable" errors, or with maximum number of retries. + + +```ts +import { Observable, throwError } from 'rxjs'; +import { retryWhen, mergeMap } from 'rxjs/operators'; +import { Action } from '@ngrx/store'; +import { EffectsModule, EFFECTS_ERROR_HANDLER } from '@ngrx/effects'; +import { MovieEffects } from './effects/movie.effects'; +import { CustomErrorHandler, isRetryable } from '../custom-error-handler'; + +export function effectResubscriptionHandler( + observable$: Observable, + errorHandler?: CustomErrorHandler +): Observable { + return observable$.pipe( + retryWhen(errors => + errors.pipe( + mergeMap(e => { + if (isRetryable(e)) { + return errorHandler.handleRetryableError(e); + } + + errorHandler.handleError(e); + return throwError(e); + }) + ) + ) + ); +} + +@NgModule({ + imports: [EffectsModule.forRoot([MovieEffects])], + providers: [ + { + provide: EFFECTS_ERROR_HANDLER, + useValue: effectResubscriptionHandler, + }, + { + provide: ErrorHandler, + useClass: CustomErrorHandler + } + ], +}) + + ## Controlling Effects ### OnInitEffects