src/state_machines/transition_validation.ts
Rama main · réplica de lectura del repositorio original.
import type { IgnoreReason, TransitionResult } from "./types";
export function describeTransitionValue(value: unknown): string {
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
export function transitionValuesAreEqual(
left: unknown,
right: unknown,
): boolean {
if (Object.is(left, right)) return true;
if (
typeof left !== "object" ||
left === null ||
typeof right !== "object" ||
right === null
) {
return false;
}
if (Array.isArray(left) || Array.isArray(right)) {
return (
Array.isArray(left) &&
Array.isArray(right) &&
left.length === right.length &&
left.every((value, index) =>
transitionValuesAreEqual(value, right[index]),
)
);
}
const leftRecord = left as Record<string, unknown>;
const rightRecord = right as Record<string, unknown>;
const leftKeys = Object.keys(leftRecord);
const rightKeys = Object.keys(rightRecord);
return (
leftKeys.length === rightKeys.length &&
leftKeys.every(
(key) =>
Object.hasOwn(rightRecord, key) &&
transitionValuesAreEqual(leftRecord[key], rightRecord[key]),
)
);
}
function validationFailure(
message: string,
context: {
state: unknown;
event: unknown;
result: unknown;
path: readonly unknown[];
},
): never {
throw new Error(
`${message}\nSource state: ${describeTransitionValue(context.state)}\nEvent: ${describeTransitionValue(context.event)}\nResult: ${describeTransitionValue(context.result)}\nExplored path: ${describeTransitionValue(context.path)}`,
);
}
/**
* Enforces the runtime contract at the pure transition boundary.
*
* Dispatchers report failures as programming errors and reject the invalid
* transaction before reservation or commit.
*/
export function validateTransitionResult<
State,
Event,
Command,
Reason extends IgnoreReason,
Outcome,
>(
previous: State,
event: Event,
result: TransitionResult<State, Command, Reason, Outcome>,
path: readonly Event[] = [],
): void {
const context = { state: previous, event, result, path };
if (typeof result !== "object" || result === null) {
validationFailure("Transition did not return a valid result", context);
}
if (!("state" in result)) {
validationFailure("Transition result must include a state", context);
}
if (result.kind === "ignored") {
if (result.state !== previous) {
validationFailure(
"Ignored transitions must retain the exact state reference",
context,
);
}
if (!("reason" in result) || typeof result.reason !== "string") {
validationFailure("Ignored transitions must include a reason", context);
}
if ("commands" in result || "outcomes" in result) {
validationFailure(
"Ignored transitions must not emit commands or outcomes",
context,
);
}
return;
}
if (result.kind !== "applied" || !Array.isArray(result.commands)) {
validationFailure("Transition did not return a valid result", context);
}
if (result.outcomes !== undefined && !Array.isArray(result.outcomes)) {
validationFailure("Transition outcomes must be an array", context);
}
if (
transitionValuesAreEqual(previous, result.state) &&
previous !== result.state
) {
validationFailure(
"Applied value-equal states must reuse the previous reference",
context,
);
}
}