diff --git a/modules/store/spec/runtime_checks.spec.ts b/modules/store/spec/runtime_checks.spec.ts index f0e6f581a7..9e2ee6698c 100644 --- a/modules/store/spec/runtime_checks.spec.ts +++ b/modules/store/spec/runtime_checks.spec.ts @@ -1,9 +1,16 @@ import * as ngCore from '@angular/core'; import { TestBed, fakeAsync, flush } from '@angular/core/testing'; -import { Store, StoreModule, META_REDUCERS, USER_RUNTIME_CHECKS } from '..'; +import { + Store, + StoreModule, + META_REDUCERS, + USER_RUNTIME_CHECKS, + createAction, +} from '..'; import { createActiveRuntimeChecks } from '../src/runtime_checks'; import { RuntimeChecks, Action } from '../src/models'; import * as metaReducers from '../src/meta-reducers'; +import { REGISTERED_ACTION_TYPES } from '../src/globals'; describe('Runtime checks:', () => { describe('createActiveRuntimeChecks:', () => { @@ -14,6 +21,7 @@ describe('Runtime checks:', () => { strictActionImmutability: true, strictStateImmutability: true, strictActionWithinNgZone: false, + strictActionTypeUniqueness: false, }); }); @@ -25,6 +33,7 @@ describe('Runtime checks:', () => { strictActionImmutability: false, strictStateImmutability: false, strictActionWithinNgZone: true, + strictActionTypeUniqueness: true, }) ).toEqual({ strictStateSerializability: true, @@ -32,6 +41,7 @@ describe('Runtime checks:', () => { strictActionImmutability: false, strictStateImmutability: false, strictActionWithinNgZone: true, + strictActionTypeUniqueness: true, }); }); @@ -44,6 +54,7 @@ describe('Runtime checks:', () => { strictActionImmutability: false, strictStateImmutability: false, strictActionWithinNgZone: false, + strictActionTypeUniqueness: false, }); }); @@ -55,6 +66,7 @@ describe('Runtime checks:', () => { strictStateSerializability: true, strictActionSerializability: true, strictActionWithinNgZone: true, + strictActionTypeUniqueness: true, }) ).toEqual({ strictStateSerializability: false, @@ -62,6 +74,7 @@ describe('Runtime checks:', () => { strictActionImmutability: false, strictStateImmutability: false, strictActionWithinNgZone: false, + strictActionTypeUniqueness: false, }); }); }); @@ -384,6 +397,47 @@ describe('Runtime checks:', () => { }); }); +fdescribe('ActionType uniqueness', () => { + beforeEach(() => { + REGISTERED_ACTION_TYPES.length = 0; + }); + + it('should throw when having no duplicate action types', () => { + createAction('action 1'); + createAction('action 1'); + + expect(() => { + const store = setupStore({ strictActionTypeUniqueness: true }); + }).toThrowError(/Action types are registered more than once/); + }); + + it('should not throw when having no duplicate action types', () => { + createAction('action 1'); + createAction('action 2'); + + expect(() => { + const store = setupStore({ strictActionTypeUniqueness: true }); + }).not.toThrowError(); + }); + + it('should not register action types if devMode is false', () => { + spyOn(ngCore, 'isDevMode').and.returnValue(false); + + createAction('action 1'); + createAction('action 1'); + + expect(REGISTERED_ACTION_TYPES.length).toBe(0); + }); + + it('should not be called when disabled', () => { + createAction('action 1'); + createAction('action 1'); + expect(() => { + const store = setupStore({ strictActionTypeUniqueness: false }); + }).not.toThrowError(); + }); +}); + function setupStore(runtimeChecks?: Partial): Store { TestBed.configureTestingModule({ imports: [ diff --git a/modules/store/src/action_creator.ts b/modules/store/src/action_creator.ts index 341bb5b79b..851a5fa18c 100644 --- a/modules/store/src/action_creator.ts +++ b/modules/store/src/action_creator.ts @@ -6,6 +6,8 @@ import { NotAllowedCheck, Props, } from './models'; +import { isDevMode } from '@angular/core'; +import { REGISTERED_ACTION_TYPES } from './globals'; // Action creators taken from ts-action library and modified a bit to better // fit current NgRx usage. Thank you Nicholas Jamieson (@cartant). @@ -101,6 +103,10 @@ export function createAction( type: T, config?: { _as: 'props' } | C ): ActionCreator { + if (isDevMode()) { + REGISTERED_ACTION_TYPES.push(type); + } + if (typeof config === 'function') { return defineType(type, (...args: any[]) => ({ ...config(...args), diff --git a/modules/store/src/globals.ts b/modules/store/src/globals.ts new file mode 100644 index 0000000000..37ae7e5af4 --- /dev/null +++ b/modules/store/src/globals.ts @@ -0,0 +1 @@ +export let REGISTERED_ACTION_TYPES: string[] = []; diff --git a/modules/store/src/models.ts b/modules/store/src/models.ts index 84eb35f5ef..d2f80c163f 100644 --- a/modules/store/src/models.ts +++ b/modules/store/src/models.ts @@ -120,4 +120,9 @@ export interface RuntimeChecks { * Verifies that actions are dispatched within NgZone */ strictActionWithinNgZone: boolean; + + /** + * Verifies that action types are not registered more than once + */ + strictActionTypeUniqueness: boolean; } diff --git a/modules/store/src/runtime_checks.ts b/modules/store/src/runtime_checks.ts index 55c516fa34..87b867be37 100644 --- a/modules/store/src/runtime_checks.ts +++ b/modules/store/src/runtime_checks.ts @@ -10,7 +10,10 @@ import { _ACTIVE_RUNTIME_CHECKS, META_REDUCERS, USER_RUNTIME_CHECKS, + _ACTION_TYPE_UNIQUENESS_CHECK, } from './tokens'; +import { REGISTERED_ACTION_TYPES } from './globals'; +import { RUNTIME_CHECK_URL } from './meta-reducers/utils'; export function createActiveRuntimeChecks( runtimeChecks?: Partial @@ -22,6 +25,7 @@ export function createActiveRuntimeChecks( strictStateImmutability: true, strictActionImmutability: true, strictActionWithinNgZone: false, + strictActionTypeUniqueness: false, ...runtimeChecks, }; } @@ -32,6 +36,7 @@ export function createActiveRuntimeChecks( strictStateImmutability: false, strictActionImmutability: false, strictActionWithinNgZone: false, + strictActionTypeUniqueness: false, }; } @@ -118,8 +123,37 @@ export function provideRuntimeChecks( ]; } +export function checkForActionTypeUniqueness(): Provider[] { + return [ + { + provide: _ACTION_TYPE_UNIQUENESS_CHECK, + multi: true, + deps: [_ACTIVE_RUNTIME_CHECKS], + useFactory: _actionTypeUniquenessCheck, + }, + ]; +} + export function _runtimeChecksFactory( runtimeChecks: RuntimeChecks ): RuntimeChecks { return runtimeChecks; } + +export function _actionTypeUniquenessCheck(config: RuntimeChecks) { + if (!config.strictActionTypeUniqueness) { + return; + } + + const duplicates = REGISTERED_ACTION_TYPES.filter( + (type, index, arr) => arr.indexOf(type) !== index + ); + + if (duplicates.length) { + throw new Error( + `Action types are registered more than once, ${duplicates + .map(type => `"${type}"`) + .join(', ')}. ${RUNTIME_CHECK_URL}#strictactiontypeuniqueness` + ); + } +} diff --git a/modules/store/src/store_module.ts b/modules/store/src/store_module.ts index 19ecc745fd..72c61180d1 100644 --- a/modules/store/src/store_module.ts +++ b/modules/store/src/store_module.ts @@ -37,6 +37,8 @@ import { USER_PROVIDED_META_REDUCERS, _RESOLVED_META_REDUCERS, _ROOT_STORE_GUARD, + _ACTIVE_RUNTIME_CHECKS, + _ACTION_TYPE_UNIQUENESS_CHECK, } from './tokens'; import { ACTIONS_SUBJECT_PROVIDERS, ActionsSubject } from './actions_subject'; import { @@ -50,7 +52,10 @@ import { } from './scanned_actions_subject'; import { STATE_PROVIDERS } from './state'; import { STORE_PROVIDERS, Store } from './store'; -import { provideRuntimeChecks } from './runtime_checks'; +import { + provideRuntimeChecks, + checkForActionTypeUniqueness, +} from './runtime_checks'; @NgModule({}) export class StoreRootModule { @@ -61,7 +66,10 @@ export class StoreRootModule { store: Store, @Optional() @Inject(_ROOT_STORE_GUARD) - guard: any + guard: any, + @Optional() + @Inject(_ACTION_TYPE_UNIQUENESS_CHECK) + actionCheck: any ) {} } @@ -71,7 +79,10 @@ export class StoreFeatureModule implements OnDestroy { @Inject(_STORE_FEATURES) private features: StoreFeature[], @Inject(FEATURE_REDUCERS) private featureReducers: ActionReducerMap[], private reducerManager: ReducerManager, - root: StoreRootModule + root: StoreRootModule, + @Optional() + @Inject(_ACTION_TYPE_UNIQUENESS_CHECK) + actionCheck: any ) { const feats = features.map((feature, index) => { const featureReducerCollection = featureReducers.shift(); @@ -166,6 +177,7 @@ export class StoreModule { STATE_PROVIDERS, STORE_PROVIDERS, provideRuntimeChecks(config.runtimeChecks), + checkForActionTypeUniqueness(), ], }; } @@ -238,6 +250,7 @@ export class StoreModule { ], useFactory: _createFeatureReducers, }, + checkForActionTypeUniqueness(), ], }; } diff --git a/modules/store/src/tokens.ts b/modules/store/src/tokens.ts index d0d6e1861d..224849bed7 100644 --- a/modules/store/src/tokens.ts +++ b/modules/store/src/tokens.ts @@ -86,3 +86,7 @@ export const _USER_RUNTIME_CHECKS = new InjectionToken( export const _ACTIVE_RUNTIME_CHECKS = new InjectionToken( '@ngrx/store Internal Runtime Checks' ); + +export const _ACTION_TYPE_UNIQUENESS_CHECK = new InjectionToken( + '@ngrx/store Check if Action types are unique' +);