///
import {
type Cell,
cell,
Default,
handler,
lift,
recipe,
str,
toSchema,
} from "commontools";
type HandlerKey = "increment" | "decrement" | "setExact";
interface TypedHandlerRecordArgs {
value: Default;
step: Default;
}
interface CounterChange {
action: HandlerKey | "init";
previous: number;
next: number;
}
type HandlerInvocationCounts = Record;
interface HandlerDescriptor {
key: HandlerKey;
label: string;
calls: number;
}
const sanitizeNumber = (value: unknown, fallback: number): number => {
if (typeof value !== "number" || !Number.isFinite(value)) {
return fallback;
}
const rounded = Math.round(value * 100) / 100;
return Number.isFinite(rounded) ? rounded : fallback;
};
const sanitizeCount = (value: unknown): number => {
if (typeof value !== "number" || !Number.isFinite(value)) {
return 0;
}
const integer = Math.trunc(value);
return integer >= 0 ? integer : 0;
};
const sanitizeCounts = (
record: HandlerInvocationCounts | undefined,
): HandlerInvocationCounts => {
const typed = record ?? {
increment: 0,
decrement: 0,
setExact: 0,
};
return {
increment: sanitizeCount(typed.increment),
decrement: sanitizeCount(typed.decrement),
setExact: sanitizeCount(typed.setExact),
};
};
const sanitizeChange = (value: CounterChange | undefined): CounterChange => {
if (!value) {
return { action: "init", previous: 0, next: 0 };
}
const action: HandlerKey | "init" = value.action === "increment" ||
value.action === "decrement" || value.action === "setExact"
? value.action
: "init";
return {
action,
previous: sanitizeNumber(value.previous, 0),
next: sanitizeNumber(value.next, 0),
};
};
const sanitizeHistory = (
entries: CounterChange[] | undefined,
): CounterChange[] => {
if (!Array.isArray(entries)) {
return [];
}
return entries.map((entry) => sanitizeChange(entry));
};
const normalizeStep = (value: unknown): number => {
const next = Math.abs(sanitizeNumber(value, 1));
return next > 0 ? next : 1;
};
const recordInvocation = (
action: HandlerKey,
change: { previous: number; next: number },
context: {
history: Cell;
lastChange: Cell;
counts: Cell;
snapshotId: Cell;
},
) => {
const entry: CounterChange = {
action,
previous: sanitizeNumber(change.previous, 0),
next: sanitizeNumber(change.next, 0),
};
const history = context.history.get();
const list = Array.isArray(history) ? [...history, entry] : [entry];
context.history.set(list);
context.lastChange.set(entry);
const currentCounts = sanitizeCounts(context.counts.get());
context.counts.set({
...currentCounts,
[action]: currentCounts[action] + 1,
});
const idSeed = sanitizeCount(context.snapshotId.get());
const nextSeed = idSeed + 1;
context.snapshotId.set(nextSeed);
};
const incrementCounter = handler(
(
event: { amount?: number } | undefined,
context: {
value: Cell;
step: Cell;
history: Cell;
lastChange: Cell;
counts: Cell;
snapshotId: Cell;
},
) => {
const previous = sanitizeNumber(context.value.get(), 0);
const baseStep = normalizeStep(context.step.get());
const requested = sanitizeNumber(event?.amount, baseStep);
const amount = requested === 0 ? baseStep : Math.abs(requested);
const next = previous + amount;
context.value.set(next);
recordInvocation("increment", { previous, next }, context);
},
);
const decrementCounter = handler(
(
event: { amount?: number } | undefined,
context: {
value: Cell;
step: Cell;
history: Cell;
lastChange: Cell;
counts: Cell;
snapshotId: Cell;
},
) => {
const previous = sanitizeNumber(context.value.get(), 0);
const baseStep = normalizeStep(context.step.get());
const requested = sanitizeNumber(event?.amount, baseStep);
const amount = requested === 0 ? baseStep : Math.abs(requested);
const next = previous - amount;
context.value.set(next);
recordInvocation("decrement", { previous, next }, context);
},
);
const setExactCounter = handler(
(
event: { value?: number } | undefined,
context: {
value: Cell;
history: Cell;
lastChange: Cell;
counts: Cell;
snapshotId: Cell;
},
) => {
const previous = sanitizeNumber(context.value.get(), 0);
const next = typeof event?.value === "number"
? sanitizeNumber(event.value, previous)
: previous;
context.value.set(next);
recordInvocation("setExact", { previous, next }, context);
},
);
type CounterHandlerRecord = {
increment: ReturnType;
decrement: ReturnType;
setExact: ReturnType;
};
export const counterWithTypedHandlerRecord = recipe(
"Counter With Typed Handler Record",
({ value, step }) => {
const history = cell([]);
const lastChange = cell({
action: "init",
previous: 0,
next: 0,
});
const counts = cell({
increment: 0,
decrement: 0,
setExact: 0,
});
const snapshotId = cell(0);
const sanitizedStep = lift(normalizeStep)(step);
const countsView = lift(sanitizeCounts)(counts);
const historyView = lift(sanitizeHistory)(history);
const lastChangeView = lift(sanitizeChange)(lastChange);
const countsLabel = lift((record: HandlerInvocationCounts | undefined) => {
const sanitized = sanitizeCounts(record);
return `inc:${sanitized.increment} dec:${sanitized.decrement} ` +
`set:${sanitized.setExact}`;
})(counts);
const lastChangeLabel = lift((change: CounterChange | undefined) => {
const sanitized = sanitizeChange(change);
return `${sanitized.action}:${sanitized.previous}->${sanitized.next}`;
})(lastChange);
const handlerCatalog = lift(
toSchema<
{
counts: Cell;
step: Cell;
}
>(),
toSchema(),
({ counts, step }) => {
const record = sanitizeCounts(counts.get());
const stepValue = normalizeStep(step.get());
return [
{
key: "increment" as const,
label: `Increment by ${stepValue}`,
calls: record.increment,
},
{
key: "decrement" as const,
label: `Decrement by ${stepValue}`,
calls: record.decrement,
},
{
key: "setExact" as const,
label: "Set exact value",
calls: record.setExact,
},
];
},
)({ counts, step: sanitizedStep });
const handlers: CounterHandlerRecord = {
increment: incrementCounter({
value,
step: sanitizedStep,
history,
lastChange,
counts,
snapshotId,
}),
decrement: decrementCounter({
value,
step: sanitizedStep,
history,
lastChange,
counts,
snapshotId,
}),
setExact: setExactCounter({
value,
history,
lastChange,
counts,
snapshotId,
}),
};
const summary = str`Value ${value} :: ${countsLabel}`;
return {
value,
step: sanitizedStep,
summary,
lastChange: lastChangeView,
lastChangeLabel,
history: historyView,
counts: countsView,
handlerCatalog,
handlers,
};
},
);