///
import { Cell, cell, Default, handler, lift, recipe, str } from "commontools";
interface PersistedState {
value?: number;
step?: number;
history?: (number | null | undefined)[];
}
interface MetadataArgs {
label?: string;
}
interface PersistenceInitialArgs {
state: Default<
PersistedState,
{ value: 0; step: 1; history: [] }
>;
metadata: Default;
}
interface NormalizedState {
value: number;
step: number;
history: number[];
}
interface PersistedChange {
reason: "initial" | "increment";
previous: number;
next: number;
amount: number;
step: number;
historyLength: number;
}
interface IncrementEvent {
amount?: number;
step?: number;
}
const DEFAULT_NORMALIZED_STATE: NormalizedState = {
value: 0,
step: 1,
history: [0],
};
const isFiniteNumber = (value: unknown): value is number =>
typeof value === "number" && Number.isFinite(value);
const sanitizeStep = (value: unknown, fallback: number): number => {
if (isFiniteNumber(value) && Math.abs(value) > 0) {
return value;
}
return fallback;
};
const sanitizeHistory = (
value: (number | null | undefined)[] | undefined,
fallbackValue: number,
): number[] => {
if (!Array.isArray(value)) {
return [fallbackValue];
}
const sanitized: number[] = [];
for (const entry of value) {
if (isFiniteNumber(entry)) {
sanitized.push(entry);
}
}
if (sanitized.length === 0) {
sanitized.push(fallbackValue);
}
return sanitized;
};
const normalizeState = (
input: PersistedState | undefined,
): NormalizedState => {
const explicitValue = isFiniteNumber(input?.value)
? input.value as number
: undefined;
const step = sanitizeStep(input?.step, DEFAULT_NORMALIZED_STATE.step);
const history = sanitizeHistory(
input?.history,
explicitValue ?? DEFAULT_NORMALIZED_STATE.value,
);
const value = explicitValue ?? history[history.length - 1];
if (history[history.length - 1] !== value) {
history.push(value);
} else {
history[history.length - 1] = value;
}
return { value, step, history };
};
export const counterPersistenceViaInitialArguments = recipe<
PersistenceInitialArgs
>(
"Counter Persistence Via Initial Arguments",
({ state, metadata }) => {
const lastChange = cell({
reason: "initial",
previous: DEFAULT_NORMALIZED_STATE.value,
next: DEFAULT_NORMALIZED_STATE.value,
amount: 0,
step: DEFAULT_NORMALIZED_STATE.step,
historyLength: DEFAULT_NORMALIZED_STATE.history.length,
});
const normalizedState = lift((raw: PersistedState | undefined) => {
const sanitized = normalizeState(raw);
return sanitized;
})(state);
const normalizedMetadata = lift((input: MetadataArgs | undefined) => {
const raw = typeof input?.label === "string" ? input.label.trim() : "";
const label = raw.length > 0 ? raw : "Persisted counter";
return { label };
})(metadata);
const currentValue = normalizedState.key("value");
const currentStep = normalizedState.key("step");
const historyView = normalizedState.key("history");
const initializationStatus = lift(
(
state: NormalizedState,
): "default" | "restored" => {
const matchesDefault = state.value === DEFAULT_NORMALIZED_STATE.value &&
state.step === DEFAULT_NORMALIZED_STATE.step &&
state.history.length ===
DEFAULT_NORMALIZED_STATE.history.length &&
state.history[state.history.length - 1] ===
DEFAULT_NORMALIZED_STATE.value;
return matchesDefault ? "default" : "restored";
},
)(normalizedState);
const historyPreview = lift((entries: number[]) =>
entries.map((value, index) => `${index}:${value}`).join(" | ")
)(historyView);
const labelCell = normalizedMetadata.key("label");
const summary =
str`${labelCell}: value ${currentValue} (mode ${initializationStatus})`;
const details = str`${summary} history ${historyPreview}`;
const applyIncrement = handler(
(
event: IncrementEvent | undefined,
context: {
state: Cell;
current: Cell;
lastChange: Cell;
},
) => {
const current = context.current.get();
const step = sanitizeStep(event?.step, current.step);
const amount = isFiniteNumber(event?.amount)
? event.amount as number
: step;
const nextValue = current.value + amount;
const nextHistory = [...current.history, nextValue];
const nextState: PersistedState = {
value: nextValue,
step,
history: nextHistory,
};
context.state.set(nextState);
context.lastChange.set({
reason: "increment",
previous: current.value,
next: nextValue,
amount,
step,
historyLength: nextHistory.length,
});
},
);
return {
state,
metadata: normalizedMetadata,
normalizedState,
value: currentValue,
step: currentStep,
history: historyView,
historyPreview,
initializationStatus,
summary,
details,
lastPersistedChange: lastChange,
increment: applyIncrement({
state,
current: normalizedState,
lastChange,
}),
};
},
);