Last active
November 28, 2020 02:34
-
-
Save sameera/73718f45118f6607821db1aa183fef60 to your computer and use it in GitHub Desktop.
An Event Store for NgRx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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