From d467593bbd4a34547f04a9d0af230e4ac504bb70 Mon Sep 17 00:00:00 2001 From: markostanimirovic Date: Tue, 18 Apr 2023 02:17:19 +0200 Subject: [PATCH] fix(effects): run user provided effects defined as injection token Closes #3848 --- modules/effects/spec/integration.spec.ts | 30 +++++++++++++++++++++++- modules/effects/spec/utils.spec.ts | 16 +++++++++++++ modules/effects/src/effects_module.ts | 24 ++++++++++++------- modules/effects/src/tokens.ts | 6 ++--- modules/effects/src/utils.ts | 11 ++++++++- 5 files changed, 74 insertions(+), 13 deletions(-) diff --git a/modules/effects/spec/integration.spec.ts b/modules/effects/spec/integration.spec.ts index 3eccb9766c..688e7a214e 100644 --- a/modules/effects/spec/integration.spec.ts +++ b/modules/effects/spec/integration.spec.ts @@ -1,4 +1,4 @@ -import { NgModule, Injectable } from '@angular/core'; +import { NgModule, Injectable, InjectionToken } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { Router } from '@angular/router'; @@ -113,6 +113,34 @@ describe('NgRx Effects Integration spec', () => { expect(functionalEffectRun).toHaveBeenCalledTimes(2); }); + it('runs user provided effects defined as injection token', () => { + const userProvidedEffectRun = jest.fn(); + + const TOKEN_EFFECTS = new InjectionToken('Token Effects', { + providedIn: 'root', + factory: () => ({ + userProvidedEffect$: createEffect( + () => concat(of('ngrx'), NEVER).pipe(tap(userProvidedEffectRun)), + { dispatch: false } + ), + }), + }); + + TestBed.configureTestingModule({ + imports: [StoreModule.forRoot({}), EffectsModule.forRoot([])], + providers: [ + { + provide: USER_PROVIDED_EFFECTS, + useValue: [TOKEN_EFFECTS], + multi: true, + }, + ], + }); + TestBed.inject(EffectSources); + + expect(userProvidedEffectRun).toHaveBeenCalledTimes(1); + }); + describe('actions', () => { const createDispatchedReducer = (dispatchedActions: string[] = []) => diff --git a/modules/effects/spec/utils.spec.ts b/modules/effects/spec/utils.spec.ts index 5cc341f772..e146529fca 100644 --- a/modules/effects/spec/utils.spec.ts +++ b/modules/effects/spec/utils.spec.ts @@ -3,7 +3,9 @@ import { getSourceForInstance, isClass, isClassInstance, + isToken, } from '../src/utils'; +import { InjectionToken } from '@angular/core'; describe('getSourceForInstance', () => { it('gets the prototype for an instance of a source', () => { @@ -55,3 +57,17 @@ describe('getClasses', () => { expect(classes).toEqual([C1, C2, C3]); }); }); + +describe('isToken', () => { + it('returns true for a class', () => { + expect(isToken(class C {})).toBe(true); + }); + + it('returns true for an injection token', () => { + expect(isToken(new InjectionToken('foo'))).toBe(true); + }); + + it('returns false for a record', () => { + expect(isToken({ foo: 'bar' })).toBe(false); + }); +}); diff --git a/modules/effects/src/effects_module.ts b/modules/effects/src/effects_module.ts index ff31a59434..f6408da208 100644 --- a/modules/effects/src/effects_module.ts +++ b/modules/effects/src/effects_module.ts @@ -1,4 +1,10 @@ -import { inject, ModuleWithProviders, NgModule, Type } from '@angular/core'; +import { + inject, + InjectionToken, + ModuleWithProviders, + NgModule, + Type, +} from '@angular/core'; import { EffectsFeatureModule } from './effects_feature_module'; import { EffectsRootModule } from './effects_root_module'; import { EffectsRunner } from './effects_runner'; @@ -11,7 +17,7 @@ import { USER_PROVIDED_EFFECTS, } from './tokens'; import { FunctionalEffect } from './models'; -import { getClasses, isClass } from './utils'; +import { getClasses, isToken } from './utils'; @NgModule({}) export class EffectsModule { @@ -94,9 +100,11 @@ export class EffectsModule { function createEffectsInstances( effectsGroups: Array | Record>[], - userProvidedEffectsGroups: Type[][] + userProvidedEffectsGroups: Array | InjectionToken>[] ): unknown[] { - const effects: Array | Record> = []; + const effects: Array< + Type | Record | InjectionToken + > = []; for (const effectsGroup of effectsGroups) { effects.push(...effectsGroup); @@ -106,10 +114,10 @@ function createEffectsInstances( effects.push(...userProvidedEffectsGroup); } - return effects.map((effectsClassOrRecord) => - isClass(effectsClassOrRecord) - ? inject(effectsClassOrRecord) - : effectsClassOrRecord + return effects.map((effectsTokenOrRecord) => + isToken(effectsTokenOrRecord) + ? inject(effectsTokenOrRecord) + : effectsTokenOrRecord ); } diff --git a/modules/effects/src/tokens.ts b/modules/effects/src/tokens.ts index 30a00ace16..b43aef6d28 100644 --- a/modules/effects/src/tokens.ts +++ b/modules/effects/src/tokens.ts @@ -8,9 +8,9 @@ import { FunctionalEffect } from './models'; export const _ROOT_EFFECTS_GUARD = new InjectionToken( '@ngrx/effects Internal Root Guard' ); -export const USER_PROVIDED_EFFECTS = new InjectionToken[][]>( - '@ngrx/effects User Provided Effects' -); +export const USER_PROVIDED_EFFECTS = new InjectionToken< + Array | InjectionToken>[] +>('@ngrx/effects User Provided Effects'); export const _ROOT_EFFECTS = new InjectionToken< [Array | Record>] >('@ngrx/effects Internal Root Effects'); diff --git a/modules/effects/src/utils.ts b/modules/effects/src/utils.ts index f15539ca8a..761f0faf71 100644 --- a/modules/effects/src/utils.ts +++ b/modules/effects/src/utils.ts @@ -1,4 +1,4 @@ -import { Type } from '@angular/core'; +import { InjectionToken, Type } from '@angular/core'; export function getSourceForInstance(instance: T): T { return Object.getPrototypeOf(instance); @@ -22,6 +22,15 @@ export function getClasses( return classesAndRecords.filter(isClass); } +export function isToken( + tokenOrRecord: + | Type + | InjectionToken + | Record +): tokenOrRecord is Type | InjectionToken { + return tokenOrRecord instanceof InjectionToken || isClass(tokenOrRecord); +} + // TODO: replace with RxJS interfaces when possible // needs dependency on RxJS >=7 export interface NextNotification {