///
import {
Cell,
cell,
Default,
derive,
handler,
lift,
recipe,
str,
} from "commontools";
type LoadingState = {
status: "loading";
attempts: number;
note: string;
};
type ReadyState = {
status: "ready";
attempts: number;
note: string;
value: number;
history: number[];
};
type CounterUnionState = LoadingState | ReadyState;
interface ComplexUnionArgs {
state: Default<
CounterUnionState,
{ status: "loading"; attempts: 0; note: "booting" }
>;
initialValue: Default;
}
const READY_NOTE = "ready";
const pushTransition = (
logCell: Cell,
entry: string,
) => {
const current = logCell.get();
const history = Array.isArray(current) ? current : [];
logCell.set([...history, entry]);
};
const startLoading = handler(
(
event: { note?: string } | undefined,
context: {
state: Cell;
history: Cell;
},
) => {
const nextNote = typeof event?.note === "string" ? event.note : "booting";
const current = context.state.get();
const attempts = current?.status === "loading"
? current.attempts + 1
: (current?.attempts ?? 0) + 1;
context.state.set({
status: "loading",
attempts,
note: nextNote,
});
pushTransition(
context.history,
`loading:${attempts}:${nextNote}`,
);
},
);
const completeLoading = handler(
(
event: { value?: number; note?: string } | undefined,
context: {
state: Cell;
initialValue: Cell;
history: Cell;
readyNote: Cell;
},
) => {
const fallback = context.initialValue.get();
const base = typeof event?.value === "number"
? event.value
: typeof fallback === "number"
? fallback
: 0;
const readyLabel = typeof event?.note === "string"
? event.note
: context.readyNote.get();
const attempts = context.state.get()?.attempts ?? 0;
const readyState: ReadyState = {
status: "ready",
attempts,
note: readyLabel ?? "ready",
value: base,
history: [base],
};
context.state.set(readyState);
pushTransition(
context.history,
`ready:${readyState.value}:${readyLabel}`,
);
},
);
const incrementReady = handler(
(
event: { amount?: number; note?: string } | undefined,
context: {
state: Cell;
history: Cell;
},
) => {
const current = context.state.get();
if (!current || current.status !== "ready") {
return;
}
const amount = typeof event?.amount === "number" ? event.amount : 1;
const note = typeof event?.note === "string" ? event.note : current.note;
const nextValue = current.value + amount;
const nextHistory = [...current.history, nextValue];
const nextState: ReadyState = {
status: "ready",
attempts: current.attempts,
note,
value: nextValue,
history: nextHistory,
};
context.state.set(nextState);
pushTransition(
context.history,
`increment:${nextValue}:${note}`,
);
},
);
export const counterWithComplexUnionState = recipe(
"Counter With Complex Union State",
({ state, initialValue }) => {
const defaultReadyNote = cell(READY_NOTE);
const unionState = lift((value: CounterUnionState | undefined) => {
if (!value) {
return {
status: "loading" as const,
attempts: 0,
note: "booting",
} satisfies LoadingState;
}
if (value.status === "loading") {
return {
status: "loading" as const,
attempts: value.attempts,
note: value.note,
} satisfies LoadingState;
}
return {
status: "ready" as const,
attempts: value.attempts,
note: value.note,
value: value.value,
history: Array.isArray(value.history) ? value.history : [value.value],
} satisfies ReadyState;
})(state);
const transitions = cell([]);
const mode = derive(unionState, (current) => current.status);
const readyValue = lift((current: CounterUnionState) =>
current.status === "ready" ? current.value : 0
)(unionState);
const historyView = lift((current: CounterUnionState) =>
current.status === "ready" ? current.history : []
)(unionState);
const attemptCount = lift((current: CounterUnionState) => current.attempts)(
unionState,
);
const historyCount = lift((items: number[] | undefined) =>
Array.isArray(items) ? items.length : 0
)(historyView);
const summary =
str`mode:${mode} value:${readyValue} attempts:${attemptCount} history:${historyCount}`;
return {
state: unionState,
mode,
readyValue,
historyView,
attemptCount,
summary,
transitions,
load: completeLoading({
state,
initialValue,
history: transitions,
readyNote: defaultReadyNote,
}),
increment: incrementReady({ state, history: transitions }),
reset: startLoading({ state, history: transitions }),
};
},
);