@typescript-tea/core
Version:
The Elm Architecture for typescript
114 lines (102 loc) • 3.88 kB
text/typescript
/** @ignore */ /** */
/**
* Commands and Subscriptions are both effects and they can both be batched and mapped.
* This module handles the batching and mapping of both commands and subscriptions
* in a generic way.
* Each effect specifies a "home" that it belongs to. The effects can then be
* gathered by "home" and passed to the Effect Manager for that "home".
* This is an internal module which is not intended for outside usage.
* Please use only the Cmd and Sub modules externally.
*/
export type Effect<A> = BatchedEffect<A> | MappedEffect<A, unknown> | LeafEffect<A>;
export const InternalHome = "__internal";
export type InternalHome = typeof InternalHome;
export type LeafEffect<_A, Home = string> = {
readonly home: Home;
readonly type: string;
};
export type BatchedEffect<A> = {
readonly home: InternalHome;
readonly type: "Batched";
readonly list: ReadonlyArray<Effect<A>>;
};
export type MappedEffect<A1, A2> = {
readonly home: InternalHome;
readonly type: "Mapped";
readonly actionMapper: (a1: A1) => A2;
readonly original: BatchedEffect<A1> | MappedEffect<A1, A2> | LeafEffect<A1>;
};
export function batchEffects<A>(effects: ReadonlyArray<Effect<A> | undefined>): BatchedEffect<A> {
return {
home: InternalHome,
type: "Batched",
list: effects.filter((c) => c !== undefined) as ReadonlyArray<Effect<A>>,
};
}
export function mapEffect<A1, A2>(
actionMapper: (a1: A1) => A2,
c: BatchedEffect<A1> | MappedEffect<A1, A2> | LeafEffect<A1> | undefined
): MappedEffect<A1, A2> | undefined {
return c === undefined ? undefined : { home: InternalHome, type: "Mapped", actionMapper, original: c };
}
export type LeafEffectMapper<A1 = unknown, A2 = unknown> = (
actionMapper: (a1: A1) => A2,
effect: Effect<A1>
) => LeafEffect<A2>;
export type GatheredEffects<A> = {
// This type is mutable for efficency
// eslint-disable-next-line functional/prefer-readonly-type
[home: string]: {
// eslint-disable-next-line functional/prefer-readonly-type
readonly cmds: Array<LeafEffect<A>>;
// eslint-disable-next-line functional/prefer-readonly-type
readonly subs: Array<LeafEffect<A>>;
};
};
export type EffectMapper<A1 = unknown, A2 = unknown, THome = unknown> = {
readonly home: THome;
readonly mapCmd: LeafEffectMapper<A1, A2>;
readonly mapSub: LeafEffectMapper<A1, A2>;
};
/**
* This function is optimized for performance. Hence the ugly boolean
* and the mutable input param.
*/
export function gatherEffects<A>(
getEffectMapper: (home: string) => EffectMapper,
gatheredEffects: GatheredEffects<A>,
isCmd: boolean,
effect: Effect<unknown>,
actionMapper: ((a1: unknown) => unknown) | undefined = undefined
): void {
if (effect.home === InternalHome) {
const internalEffect = effect as BatchedEffect<unknown> | MappedEffect<unknown, unknown>;
switch (internalEffect.type) {
case "Batched": {
internalEffect.list.flatMap((c) => gatherEffects(getEffectMapper, gatheredEffects, isCmd, c, actionMapper));
return;
}
case "Mapped":
gatherEffects(
getEffectMapper,
gatheredEffects,
isCmd,
internalEffect.original,
actionMapper ? (a) => actionMapper(internalEffect.actionMapper(a)) : internalEffect.actionMapper
);
return;
default: {
const exhaustive: never = internalEffect;
throw new Error(`Invalid result type ${exhaustive}`);
}
}
} else {
const manager = getEffectMapper(effect.home);
if (!gatheredEffects[effect.home]) {
gatheredEffects[effect.home] = { cmds: [], subs: [] };
}
const list = isCmd ? gatheredEffects[effect.home].cmds : gatheredEffects[effect.home].subs;
const mapper = isCmd ? manager.mapCmd : manager.mapSub;
list.push(actionMapper ? mapper(actionMapper, effect) : effect);
}
}