@typescript-tea/core
Version:
The Elm Architecture for typescript
150 lines (133 loc) • 4.73 kB
text/typescript
import { Cmd } from "./cmd";
import { Sub } from "./sub";
import { Dispatch } from "./dispatch";
import { EffectManager, createGetEffectManager } from "./effect-manager";
import { GatheredEffects, gatherEffects } from "./effect";
/**
* A program represents the root of an application.
* @category Program
*/
export type Program<Init, State, Action, View> = {
readonly init: (init: Init) => readonly [State, Cmd<Action>?];
readonly update: (action: Action, state: State) => readonly [State, Cmd<Action>?];
readonly view: (props: { readonly state: State; readonly dispatch: Dispatch<Action> }) => View;
readonly subscriptions?: (state: State) => Sub<Action> | undefined;
};
/**
* This is the runtime that provides the main loop to run a Program.
* Given a Program and an array of EffectManagers it will start the program
* and progress the state each time the program calls update().
* You can use the returned function to terminate the program.
* @param program This is the program to run.
* @typeParam Init This is the type of the initial value passed to the program's init function.
* @category Program
*/
export function run<Init, State, Action, View>(
program: Program<Init, State, Action, View>,
init: Init,
render: (view: View) => void,
effectManagers: ReadonlyArray<EffectManager<string, unknown, unknown>> = []
): () => void {
const getEffectManager = createGetEffectManager(effectManagers);
const { update, view, subscriptions } = program;
let state: State;
const managerStates: { [home: string]: unknown } = {};
const managerTeardowns: Array<() => void> = [];
let isRunning = false;
let isProcessing = false;
const actionQueue: Array<{
dispatch: Dispatch<unknown>;
action: unknown;
}> = [];
// Init to an object that the appliction has no reference to so intial change always runs
let prevState = {};
function processActions(): void {
if (!isRunning || isProcessing) {
return;
}
isProcessing = true;
while (actionQueue.length > 0) {
const queuedAction = actionQueue.shift()!;
queuedAction.dispatch(queuedAction.action);
}
isProcessing = false;
}
const dispatchManager =
(home: string) =>
(action: Action): void => {
if (isRunning) {
const manager = getEffectManager(home);
const enqueueSelfAction = enqueueManagerAction(home);
managerStates[home] = manager.onSelfAction(
enqueueProgramAction,
enqueueSelfAction,
action,
managerStates[home]
);
}
};
function dispatchApp(action: Action): void {
if (isRunning) {
change(update(action, state));
}
}
const enqueueManagerAction =
(home: string) =>
(action: unknown): void => {
enqueueRaw(dispatchManager(home), action);
};
const enqueueProgramAction = (action: Action): void => {
enqueueRaw(dispatchApp, action);
};
function enqueueRaw(dispatch: Dispatch<Action>, action: unknown): void {
if (isRunning) {
actionQueue.push({ dispatch, action });
processActions();
}
}
function change(change: readonly [State, Cmd<Action>?]): void {
state = change[0];
const cmd = change[1];
const sub = subscriptions && subscriptions(state);
const gatheredEffects: GatheredEffects<Action> = {};
cmd && gatherEffects(getEffectManager, gatheredEffects, true, cmd); // eslint-disable-line @typescript-eslint/no-unused-expressions,no-unused-expressions
sub && gatherEffects(getEffectManager, gatheredEffects, false, sub); // eslint-disable-line @typescript-eslint/no-unused-expressions,no-unused-expressions
// Always call all effect managers so they get updated subscriptions even if there are no subscriptions anymore
for (const em of effectManagers) {
const home = em.home;
const { cmds, subs } = gatheredEffects[home] ?? { cmds: [], subs: [] };
const manager = getEffectManager(home);
managerStates[home] = manager.onEffects(
enqueueProgramAction,
enqueueManagerAction(home),
cmds,
subs,
managerStates[home]
);
}
if (state !== prevState) {
prevState = state;
render(view({ state, dispatch: enqueueProgramAction }));
}
}
function setup(): void {
for (const em of effectManagers) {
managerTeardowns.push(em.setup(enqueueProgramAction, enqueueManagerAction(em.home)));
}
}
function teardown(): void {
for (const mtd of managerTeardowns) {
mtd();
}
}
setup();
isRunning = true;
change(program.init(init));
processActions();
return function end(): void {
if (isRunning) {
isRunning = false;
teardown();
}
};
}