///
import {
type Cell,
cell,
Default,
derive,
handler,
lift,
recipe,
str,
} from "commontools";
interface MultiStepArgs {
value: Default;
phase: Default;
}
interface StartSequenceEvent {
label?: unknown;
}
interface StepEvent {
amount?: unknown;
note?: unknown;
}
interface CompleteEvent {
note?: unknown;
}
interface StepEntry {
index: number;
delta: number;
total: number;
note: string;
}
const toNumber = (input: unknown, fallback = 0): number => {
if (typeof input !== "number" || Number.isNaN(input)) {
return fallback;
}
return input;
};
const toSafeString = (input: unknown, fallback: string): string => {
if (typeof input !== "string" || input.trim().length === 0) {
return fallback;
}
return input.trim();
};
const sanitizeStepEntries = (input: unknown): StepEntry[] => {
if (!Array.isArray(input)) return [];
const result: StepEntry[] = [];
for (const value of input) {
if (!value || typeof value !== "object") continue;
const index = toNumber((value as StepEntry).index, Number.NaN);
const delta = toNumber((value as StepEntry).delta, Number.NaN);
const total = toNumber((value as StepEntry).total, Number.NaN);
const note = toSafeString((value as StepEntry).note, "step");
if (
Number.isFinite(index) &&
Number.isFinite(delta) &&
Number.isFinite(total)
) {
result.push({
index,
delta,
total,
note,
});
}
}
return result;
};
const startSequence = handler(
(
event: StartSequenceEvent | undefined,
context: {
phase: Cell;
value: Cell;
stepIndex: Cell;
stepLog: Cell;
},
) => {
const label = toSafeString(event?.label, "active");
const currentValue = context.value.get();
context.phase.set(label);
context.value.set(toNumber(currentValue, 0));
context.stepIndex.set(0);
context.stepLog.set([]);
},
);
const applyStep = handler(
(
event: StepEvent | undefined,
context: {
phase: Cell;
value: Cell;
stepIndex: Cell;
stepLog: Cell;
},
) => {
const currentPhase = context.phase.get();
const delta = toNumber(event?.amount, 1);
const note = toSafeString(
event?.note,
`step ${toSafeString(currentPhase, "active")}`,
);
const rawValue = context.value.get();
const current = toNumber(rawValue, 0);
const next = current + delta;
context.value.set(next);
const rawIndex = context.stepIndex.get();
const index = toNumber(rawIndex, 0) + 1;
context.stepIndex.set(index);
const log = sanitizeStepEntries(context.stepLog.get());
log.push({ index, delta, total: next, note });
context.stepLog.set(log);
},
);
const completeSequence = handler(
(
event: CompleteEvent | undefined,
context: {
phase: Cell;
value: Cell;
stepLog: Cell;
phaseHistory: Cell;
},
) => {
const phaseValue = toSafeString(context.phase.get(), "active");
const note = toSafeString(event?.note, "complete");
const steps = sanitizeStepEntries(context.stepLog.get());
const total = toNumber(context.value.get(), 0);
const history = Array.isArray(context.phaseHistory.get())
? context.phaseHistory.get()
: [];
const summary =
`${phaseValue} (${note}) steps: ${steps.length} total: ${total}`;
context.phaseHistory.set([...history, summary]);
},
);
export const counterWithScenarioDrivenSteps = recipe(
"Counter With Scenario Driven Multi Step Events",
({ value, phase }) => {
const stepIndex = cell(0);
const stepLog = cell([]);
const phaseHistory = cell([]);
const currentValue = lift((input: unknown) => toNumber(input, 0))(value);
const currentPhase = lift((input: unknown) => toSafeString(input, "idle"))(
phase,
);
const steps = lift(sanitizeStepEntries)(stepLog);
const completedPhases = lift((input: unknown) => {
if (!Array.isArray(input)) return [];
const result: string[] = [];
for (const value of input) {
result.push(toSafeString(value, "unknown phase"));
}
return result;
})(phaseHistory);
const stepCount = derive(steps, (entries) => entries.length);
const lastRecordedTotal = derive(
{ steps, current: currentValue },
({ steps, current }) => {
if (steps.length === 0) {
return current;
}
return steps[steps.length - 1].total;
},
);
const summary =
str`Phase ${currentPhase} total ${currentValue} over ${stepCount} steps`;
return {
value,
phase,
currentValue,
currentPhase,
stepCount,
steps,
lastRecordedTotal,
phases: completedPhases,
summary,
sequence: {
start: startSequence({ phase, value, stepIndex, stepLog }),
apply: applyStep({ phase, value, stepIndex, stepLog }),
complete: completeSequence({
phase,
value,
stepLog,
phaseHistory,
}),
},
};
},
);