///
import { Cell, Default, handler, lift, recipe, str } from "commontools";
interface FallbackDefaultsArgs {
slots: Default<(number | undefined)[], []>;
fallback: Default;
expectedLength: Default;
}
interface SlotUpdateEvent {
index?: number;
amount?: number;
value?: number;
}
const isFiniteNumber = (value: unknown): value is number =>
typeof value === "number" && Number.isFinite(value);
const sanitizeNumber = (value: unknown, fallback: number): number =>
isFiniteNumber(value) ? value : fallback;
const ensureArrayWithFallback = (
raw: unknown,
fallback: number,
requiredLength: number,
): number[] => {
const source = Array.isArray(raw) ? [...raw] : [];
if (source.length < requiredLength) {
source.length = requiredLength;
}
for (let index = 0; index < source.length; index++) {
source[index] = sanitizeNumber(source[index], fallback);
}
return source;
};
const updateSlot = handler(
(
event: SlotUpdateEvent | undefined,
context: {
slots: Cell<(number | undefined)[]>;
fallback: Cell;
expectedLength: Cell;
},
) => {
const rawIndex = event?.index;
if (!isFiniteNumber(rawIndex)) return;
const index = Math.max(0, Math.floor(rawIndex));
const fallbackValue = sanitizeNumber(context.fallback.get(), 0);
const expected = context.expectedLength.get();
const rawAmount = event?.amount;
const amount = isFiniteNumber(rawAmount) ? rawAmount : 1;
const rawSlots = context.slots.get();
const currentLength = Array.isArray(rawSlots) ? rawSlots.length : 0;
const requiredLength = Math.max(currentLength, expected, index + 1);
const normalized = ensureArrayWithFallback(
rawSlots,
fallbackValue,
requiredLength,
);
const rawValue = event?.value;
if (isFiniteNumber(rawValue)) {
normalized[index] = rawValue;
} else {
const baseValue = sanitizeNumber(normalized[index], fallbackValue);
normalized[index] = baseValue + amount;
}
context.slots.set(normalized);
},
);
export const counterWithFallbackDefaults = recipe(
"Counter With Fallback Defaults",
({ slots, fallback, expectedLength }) => {
const normalizedFallback = lift((value: number | undefined) =>
sanitizeNumber(value, 0)
)(fallback);
const normalizedExpected = lift((value: number | undefined) => {
if (isFiniteNumber(value) && value >= 0) {
return Math.floor(value);
}
return 0;
})(expectedLength);
const dense = lift(
(
input: {
raw: (number | undefined)[] | undefined;
fallback: number;
expected: number;
},
) => {
const base = Array.isArray(input.raw) ? input.raw : [];
const length = Math.max(base.length, input.expected);
const result: number[] = [];
for (let index = 0; index < length; index++) {
result.push(sanitizeNumber(base[index], input.fallback));
}
return result;
},
)({
raw: slots,
fallback: normalizedFallback,
expected: normalizedExpected,
});
const total = lift((entries: number[] | undefined) => {
if (!Array.isArray(entries) || entries.length === 0) return 0;
return entries.reduce((sum, value) => sum + value, 0);
})(dense);
const densePreview = lift((entries: number[] | undefined) => {
if (!Array.isArray(entries) || entries.length === 0) return "empty";
return entries.join(", ");
})(dense);
const label = str`Dense values [${densePreview}] total ${total}`;
const adjustSlot = updateSlot({
slots,
fallback: normalizedFallback,
expectedLength: normalizedExpected,
});
return {
slots,
fallback: normalizedFallback,
expectedLength: normalizedExpected,
dense,
densePreview,
total,
label,
updateSlot: adjustSlot,
increment: adjustSlot,
};
},
);