Skip to content

Instantly share code, notes, and snippets.

@sameera
Last active November 28, 2020 02:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sameera/73718f45118f6607821db1aa183fef60 to your computer and use it in GitHub Desktop.
Save sameera/73718f45118f6607821db1aa183fef60 to your computer and use it in GitHub Desktop.
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