An Event Store for NgRx
import { Injectable } from "@angular/core"; | |
import { | |
Action, | |
ActionsSubject, | |
ReducerManager, | |
StateObservable, | |
Store, | |
} from "@ngrx/store"; | |
import { of, EMPTY, OperatorFunction } from "rxjs"; | |
import { flatMap } from "rxjs/operators"; | |
import { ActionReducer } from "@ngrx/store"; | |
export function args<T>() { | |
return ("args" as any) as T; | |
} | |
export interface Event extends Action { | |
readonly verb: string; | |
readonly source: string; | |
[other: string]: any; | |
} | |
export type EventCreatorParamless = () => Event; | |
export type EventCreator<ArgsType> = (args: ArgsType) => Event & ArgsType; | |
export interface VerbedEvent { | |
verb: string; | |
} | |
export type EventAssemblerParamless = (source: string) => Event; | |
export type EventAssembler<ArgsType> = ( | |
source: string, | |
args: ArgsType | |
) => Event & ArgsType; | |
export function toEvent(source: string, verb: string): Event { | |
return { | |
verb, | |
source, | |
type: `[${source}] ${verb}`, | |
}; | |
} | |
export function createEvent( | |
source: string, | |
verb: string | |
): EventCreatorParamless; | |
export function createEvent<ArgsType>( | |
source: string, | |
verb: string, | |
config: ArgsType | |
): EventCreator<ArgsType>; | |
export function createEvent<ArgsType>( | |
source: string, | |
verb: string, | |
config?: ArgsType | |
): EventCreatorParamless | EventCreator<ArgsType> { | |
if (!config) { | |
const creator: EventCreatorParamless = () => toEvent(source, verb); | |
((creator as any) as VerbedEvent).verb = verb; | |
return creator; | |
} else { | |
const creator = (params: ArgsType) => ({ | |
...params, | |
verb, | |
source, | |
type: `[${source}] ${verb}`, | |
}); | |
((creator as any) as VerbedEvent).verb = verb; | |
return creator; | |
} | |
} | |
export function prepareEvent(verb: string): EventAssemblerParamless; | |
export function prepareEvent<ArgsType>( | |
verb: string, | |
config: ArgsType | |
): EventAssembler<ArgsType>; | |
export function prepareEvent<ArgsType>( | |
verb: string, | |
config?: ArgsType | |
): EventAssemblerParamless | EventAssembler<ArgsType> { | |
if (!config) { | |
const assembler = (source: string) => toEvent(source, verb); | |
((assembler as any) as VerbedEvent).verb = verb; | |
return assembler; | |
} else { | |
const assembler = (source: string, prop: ArgsType) => ({ | |
...prop, | |
verb, | |
source, | |
type: `[${source}] ${verb}`, | |
}); | |
((assembler as any) as VerbedEvent).verb = verb; | |
return assembler; | |
} | |
} | |
export type EventReducer<StateType> = ( | |
state: StateType, | |
event: Event | |
) => StateType; | |
export interface When<StateType, EventType extends Event = Event> { | |
reducer: ActionReducer<StateType, EventType>; | |
verbs: string[]; | |
} | |
export function when<StateType, EventType extends Event = Event>( | |
verb: string, | |
reducer: ActionReducer<StateType, EventType> | |
): When<StateType, EventType>; | |
export function when<StateType, ArgsType, EventType extends Event = Event>( | |
event: EventAssembler<ArgsType>, | |
reducer: ActionReducer<StateType, EventType> | |
): When<StateType, EventType>; | |
export function when<StateType, ArgsType, EventType extends Event = Event>( | |
event: (string | EventAssembler<ArgsType>)[], | |
reducer: ActionReducer<StateType, EventType> | |
): When<StateType, EventType>; | |
export function when<StateType, ArgsType, EventType extends Event = Event>( | |
verbOrPreparedEvent: | |
| string | |
| EventAssembler<ArgsType> | |
| (string | EventAssembler<ArgsType>)[], | |
// prettier-ignore | |
reducer: ActionReducer<StateType, Event> | ActionReducer<StateType, EventType> | |
): When<StateType, EventType> { | |
const verbs = getVerbs(verbOrPreparedEvent); | |
return { | |
reducer, | |
verbs, | |
}; | |
} | |
function getVerbs<ArgsType>( | |
param: | |
| string | |
| EventAssembler<ArgsType> | |
| (string | EventAssembler<ArgsType>)[] | |
): string[] { | |
if (typeof param === "string") return [param]; | |
if (((param as unknown) as VerbedEvent).verb) | |
return [((param as unknown) as VerbedEvent).verb]; | |
if (Array.isArray(param)) { | |
return param.map((p: string | EventAssembler<ArgsType>) => | |
typeof p === "string" ? p : ((p as unknown) as VerbedEvent).verb | |
); | |
} | |
throw new Error("Incompatible event type"); | |
} | |
export function createEventReducer<StateType>( | |
initialState: StateType, | |
...whens: When<StateType>[] | |
): ActionReducer<StateType, Event> { | |
const map = new Map<string, ActionReducer<StateType, Event>>(); | |
for (let i = whens.length - 1; i >= 0; i--) { | |
if (!whens[i]) continue; | |
for (let j = whens[i].verbs.length - 1; j >= 0; j--) { | |
map.set(whens[i].verbs[j], whens[i].reducer); | |
} | |
} | |
return function (state: StateType = initialState, action: Event): StateType { | |
const reducer = map.get(action.verb); | |
return reducer ? reducer(state, action) : state; | |
}; | |
} | |
export function onEvent(verb: string): OperatorFunction<Action, Event>; | |
export function onEvent<ArgsType>( | |
expectedEvent: EventAssembler<ArgsType> | |
): OperatorFunction<Action, Event>; | |
export function onEvent<ArgsType>( | |
expectedEvenOrVerb: string | EventAssembler<ArgsType> | |
) { | |
const expectedVerb = | |
typeof expectedEvenOrVerb === "string" | |
? expectedEvenOrVerb | |
: ((expectedEvenOrVerb as any) as VerbedEvent).verb; | |
return flatMap((action: Action) => | |
(action as Event).verb === expectedVerb ? of(action as Event) : EMPTY | |
); | |
} | |
@Injectable({ providedIn: "root" }) | |
export class EventStore<StateType> extends Store<StateType> { | |
constructor( | |
state$: StateObservable, | |
actionsObserver: ActionsSubject, | |
reducerManager: ReducerManager | |
) { | |
super(state$, actionsObserver, reducerManager); | |
} | |
public dispatch(event: Action): void; | |
public dispatch(source: string, verb: string, args?: object): void; | |
public dispatch( | |
sourceOrEvent: string | Action, | |
verb?: string, | |
args?: object | |
): void { | |
if (typeof args !== "undefined") { | |
super.dispatch({ | |
...toEvent(sourceOrEvent as string, verb as string), | |
...args, | |
}); | |
} else if (typeof verb !== "undefined") { | |
super.dispatch(toEvent(sourceOrEvent as string, verb)); | |
} else { | |
super.dispatch(sourceOrEvent as Action); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment