diff --git a/libs/ngrx-toolkit/src/lib/with-redux.spec.ts b/libs/ngrx-toolkit/src/lib/with-redux.spec.ts index 5f34c87..1267052 100644 --- a/libs/ngrx-toolkit/src/lib/with-redux.spec.ts +++ b/libs/ngrx-toolkit/src/lib/with-redux.spec.ts @@ -12,6 +12,7 @@ import { HttpTestingController, provideHttpClientTesting, } from '@angular/common/http/testing'; +import { Action } from 'ngrx-toolkit'; interface Flight { id: number; @@ -97,4 +98,45 @@ describe('with redux', () => { controller.verify(); }); }); + + it('should allow multiple effects listening to the same action', () => { + const FlightsStore = signalStore( + withState({ flights: [] as Flight[], effect1: false, effect2: false }), + withRedux({ + actions: { + init: noPayload, + updateEffect1: payload<{ value: boolean }>(), + updateEffect2: payload<{ value: boolean }>(), + }, + reducer(actions, on) { + on(actions.updateEffect1, (state, { value }) => { + patchState(state, { effect1: value }); + }); + + on(actions.updateEffect2, (state, { value }) => { + patchState(state, { effect2: value }); + }); + }, + effects(actions, create) { + return { + init1$: create(actions.init).pipe( + map(() => actions.updateEffect1({ value: true })), + ), + init2$: create(actions.init).pipe( + map(() => actions.updateEffect2({ value: true })), + ), + }; + }, + }), + ); + + const flightStore = TestBed.configureTestingModule({ + providers: [FlightsStore], + }).inject(FlightsStore); + + flightStore.init({}); + + expect(flightStore.effect1()).toBe(true); + expect(flightStore.effect2()).toBe(true); + }); }); diff --git a/libs/ngrx-toolkit/src/lib/with-redux.ts b/libs/ngrx-toolkit/src/lib/with-redux.ts index 2ee234d..c73c5f5 100644 --- a/libs/ngrx-toolkit/src/lib/with-redux.ts +++ b/libs/ngrx-toolkit/src/lib/with-redux.ts @@ -13,7 +13,7 @@ type Payload = Record; type ActionFn< Type extends string = string, - ActionPayload extends Payload = Payload + ActionPayload extends Payload = Payload, > = ((payload: ActionPayload) => ActionPayload & { type: Type }) & { type: Type; }; @@ -24,7 +24,7 @@ export type ActionsFnSpecs = Record; type ActionFnCreator = { [ActionName in keyof Spec]: (( - payload: Spec[ActionName] + payload: Spec[ActionName], ) => Spec[ActionName] & { type: ActionName }) & { type: ActionName & string }; }; @@ -55,15 +55,15 @@ export const noPayload = {}; type ReducerFunction = ( state: State, - action: ActionFnPayload + action: ActionFnPayload, ) => void; type ReducerFactory = ( actions: StateActionFns, on: ( action: ReducerAction, - reducerFn: ReducerFunction - ) => void + reducerFn: ReducerFunction, + ) => void, ) => void; /** Effect **/ @@ -71,18 +71,27 @@ type ReducerFactory = ( type EffectsFactory = ( actions: StateActionFns, create: ( - action: EffectAction - ) => Observable> + action: EffectAction, + ) => Observable>, ) => Record>; +// internal types + +/** + * Record which holds all effects for a specific action type. + * The values are Subject which the effect are subscribed to. + * `createActionFns` will call next on these subjects. + */ +type EffectsRegistry = Record>[]>; + function createActionFns( actionFnSpecs: Spec, reducerRegistry: Record< string, (state: unknown, payload: ActionFnPayload) => void >, - effectsRegistry: Record>>, - state: unknown + effectsRegistry: EffectsRegistry, + state: unknown, ) { const actionFns: Record = {}; @@ -93,12 +102,14 @@ function createActionFns( if (reducer) { (reducer as (state: unknown, payload: unknown) => void)( state, - fullPayload as unknown + fullPayload as unknown, ); } - const effectSubject = effectsRegistry[type]; - if (effectSubject) { - (effectSubject as unknown as Subject).next(fullPayload); + const effectSubjects = effectsRegistry[type]; + if (effectSubjects?.length) { + for (const effectSubject of effectSubjects) { + (effectSubject as unknown as Subject).next(fullPayload); + } } return fullPayload; }; @@ -115,8 +126,8 @@ function createPublicAndAllActionsFns( string, (state: unknown, payload: ActionFnPayload) => void >, - effectsRegistry: Record>>, - state: unknown + effectsRegistry: EffectsRegistry, + state: unknown, ): { all: ActionFns; publics: ActionFns } { if ('public' in actionFnSpecs || 'private' in actionFnSpecs) { const privates = actionFnSpecs['private'] || {}; @@ -129,13 +140,13 @@ function createPublicAndAllActionsFns( privates, reducerRegistry, effectsRegistry, - state + state, ); const publicActionFns = createActionFns( publics, reducerRegistry, effectsRegistry, - state + state, ); return { @@ -148,7 +159,7 @@ function createPublicAndAllActionsFns( actionFnSpecs, reducerRegistry, effectsRegistry, - state + state, ); return { all: actionFns, publics: actionFns }; @@ -160,11 +171,11 @@ function fillReducerRegistry( reducerRegistry: Record< string, (state: unknown, payload: ActionFnPayload) => void - > + >, ) { function on( action: { type: string }, - reducerFn: (state: unknown, payload: ActionFnPayload) => void + reducerFn: (state: unknown, payload: ActionFnPayload) => void, ) { reducerRegistry[action.type] = reducerFn; } @@ -177,11 +188,14 @@ function fillReducerRegistry( function fillEffects( effects: EffectsFactory, actionFns: ActionFns, - effectsRegistry: Record>> = {} + effectsRegistry: EffectsRegistry = {}, ): Observable[] { function create(action: { type: string }) { const subject = new Subject>(); - effectsRegistry[action.type] = subject; + if (!(action.type in effectsRegistry)) { + effectsRegistry[action.type] = []; + } + effectsRegistry[action.type].push(subject); return subject.asObservable(); } @@ -197,18 +211,19 @@ function processRedux( actionFnSpecs: Spec, reducer: ReducerFactory, effects: EffectsFactory, - store: unknown + store: unknown, ) { const reducerRegistry: Record< string, (state: unknown, payload: ActionFnPayload) => void > = {}; - const effectsRegistry: Record>> = {}; + const effectsRegistry: Record>[]> = + {}; const actionsMap = createPublicAndAllActionsFns( actionFnSpecs, reducerRegistry, effectsRegistry, - store + store, ); const actionFns = actionsMap.all; const publicActionsFns = actionsMap.publics; @@ -237,7 +252,7 @@ export function withRedux< Spec extends ActionsFnSpecs, Input extends SignalStoreFeatureResult, StateActionFns extends ActionFnsCreator = ActionFnsCreator, - PublicStoreActionFns extends PublicActionFns = PublicActionFns + PublicStoreActionFns extends PublicActionFns = PublicActionFns, >(redux: { actions: Spec; reducer: ReducerFactory>; @@ -251,7 +266,7 @@ export function withRedux< redux.actions, redux.reducer as ReducerFactory, redux.effects as EffectsFactory, - store + store, ); return { ...store,