/// import { type Cell, cell, Default, handler, lift, recipe, str, } from "commontools"; type CounterState = "idle" | "running" | "paused" | "complete"; interface EnumerationArgs { state: Default; value: Default; } interface TransitionRecord { from: CounterState; to: CounterState; kind: "advance" | "retreat" | "reset" | "tick"; note: string; } interface TransitionEvent { note?: unknown; } interface TickEvent { amount?: unknown; note?: unknown; } const STATE_SEQUENCE: readonly CounterState[] = [ "idle", "running", "paused", "complete", ] as const; const clampState = (input: unknown): CounterState => { if (input === "running" || input === "paused" || input === "complete") { return input; } return "idle"; }; const toNumber = (input: unknown, fallback = 0): number => { if (typeof input !== "number" || Number.isNaN(input)) { return fallback; } return input; }; const toNote = (input: unknown, fallback: string): string => { return typeof input === "string" && input.length > 0 ? input : fallback; }; const recordTransition = ( history: Cell, sequence: Cell, record: TransitionRecord, ) => { const current = history.get(); const list = Array.isArray(current) ? current : []; history.set([...list, record]); const currentId = toNumber(sequence.get(), 0); const nextId = currentId + 1; sequence.set(nextId); }; const advanceState = handler( ( event: TransitionEvent | undefined, context: { state: Cell; history: Cell; sequence: Cell; }, ) => { const current = clampState(context.state.get()); const index = STATE_SEQUENCE.indexOf(current); const next = index < STATE_SEQUENCE.length - 1 ? STATE_SEQUENCE[index + 1] : current; if (next === current) { return; } context.state.set(next); recordTransition(context.history, context.sequence, { from: current, to: next, kind: "advance", note: toNote(event?.note, `advance:${current}->${next}`), }); }, ); const retreatState = handler( ( event: TransitionEvent | undefined, context: { state: Cell; history: Cell; sequence: Cell; }, ) => { const current = clampState(context.state.get()); const index = STATE_SEQUENCE.indexOf(current); const next = index > 0 ? STATE_SEQUENCE[index - 1] : current; if (next === current) { return; } context.state.set(next); recordTransition(context.history, context.sequence, { from: current, to: next, kind: "retreat", note: toNote(event?.note, `retreat:${current}->${next}`), }); }, ); const resetState = handler( ( event: TransitionEvent | undefined, context: { state: Cell; value: Cell; history: Cell; sequence: Cell; }, ) => { const current = clampState(context.state.get()); context.state.set("idle"); context.value.set(0); recordTransition(context.history, context.sequence, { from: current, to: "idle", kind: "reset", note: toNote(event?.note, `reset:${current}`), }); }, ); const tickValue = handler( ( event: TickEvent | undefined, context: { state: Cell; value: Cell; history: Cell; sequence: Cell; }, ) => { const current = clampState(context.state.get()); if (current !== "running") { return; } const amount = toNumber(event?.amount, 1); const currentValue = toNumber(context.value.get(), 0); const nextValue = currentValue + amount; context.value.set(nextValue); recordTransition(context.history, context.sequence, { from: current, to: current, kind: "tick", note: toNote(event?.note, `tick:+${amount}=${nextValue}`), }); }, ); export const counterWithEnumerationState = recipe( "Counter With Enumeration State", ({ state, value }) => { const transitions = cell([]); const transitionSequence = cell(0); const normalizedState = lift( (input: CounterState | undefined) => clampState(input), )(state); const normalizedValue = lift((input: number | undefined) => toNumber(input, 0) )(value); const transitionView = lift( (input: TransitionRecord[] | undefined) => Array.isArray(input) ? input : [], )(transitions); const transitionCount = lift((list: TransitionRecord[] | undefined) => Array.isArray(list) ? list.length : 0 )(transitionView); const stateIndex = lift((current: CounterState) => STATE_SEQUENCE.indexOf(current) )(normalizedState); const canAdvance = lift((index: number) => index < STATE_SEQUENCE.length - 1 )(stateIndex); const canRetreat = lift((index: number) => index > 0)(stateIndex); const isRunning = lift((current: CounterState) => current === "running")( normalizedState, ); const phaseLabel = str`state:${normalizedState} index:${stateIndex} value:${normalizedValue}`; const summary = str`transitions:${transitionCount} running:${isRunning}`; return { state: normalizedState, value: normalizedValue, stateIndex, canAdvance, canRetreat, isRunning, phaseLabel, summary, transitions: transitionView, transitionCount, advance: advanceState({ state, history: transitions, sequence: transitionSequence, }), retreat: retreatState({ state, history: transitions, sequence: transitionSequence, }), reset: resetState({ state, value, history: transitions, sequence: transitionSequence, }), tick: tickValue({ state, value, history: transitions, sequence: transitionSequence, }), }; }, );