Files
workinf_Blender_Wasm/web/protocol/user-action-state.ts
mes123456 7c16b279ae
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M7 workflows and release operations
2026-08-15 17:43:53 -04:00

110 lines
4.2 KiB
TypeScript

export const USER_ACTION_KINDS = ["IMPORT", "OPEN", "SAVE", "SAVE_AS", "EXPORT"] as const;
export type UserActionKind = typeof USER_ACTION_KINDS[number];
export const USER_ACTION_STATUSES = ["IDLE", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED"] as const;
export type UserActionStatus = typeof USER_ACTION_STATUSES[number];
export interface UserActionIdentity {
actionId: string;
kind: UserActionKind;
}
export interface UserActionState {
kind: UserActionKind;
status: UserActionStatus;
identity: UserActionIdentity | null;
errorCode: string | null;
}
export type UserActionEvent =
| { type: "START"; identity: UserActionIdentity }
| { type: "SUCCEED"; identity: UserActionIdentity }
| { type: "FAIL"; identity: UserActionIdentity; errorCode: string }
| { type: "CANCEL"; identity: UserActionIdentity; errorCode?: string }
| { type: "RESET"; kind: UserActionKind };
export type UserActionTransitionErrorCode =
| "USER_ACTION_KIND_MISMATCH"
| "USER_ACTION_IDENTITY_MISMATCH"
| "USER_ACTION_IDENTITY_REUSED"
| "USER_ACTION_INVALID_ERROR_CODE"
| "USER_ACTION_INVALID_TRANSITION";
export type UserActionTransitionResult =
| { ok: true; state: UserActionState }
| { ok: false; state: UserActionState; errorCode: UserActionTransitionErrorCode };
export type UserActionStates = Record<UserActionKind, UserActionState>;
export const USER_ACTION_ALLOWED_TRANSITIONS: Readonly<Record<UserActionStatus, readonly UserActionStatus[]>> = {
IDLE: ["RUNNING"],
RUNNING: ["SUCCEEDED", "FAILED", "CANCELLED"],
SUCCEEDED: ["IDLE", "RUNNING"],
FAILED: ["IDLE", "RUNNING"],
CANCELLED: ["IDLE", "RUNNING"],
};
function idleState(kind: UserActionKind): UserActionState {
return { kind, status: "IDLE", identity: null, errorCode: null };
}
export function createInitialUserActionStates(): UserActionStates {
return Object.fromEntries(USER_ACTION_KINDS.map((kind) => [kind, idleState(kind)])) as UserActionStates;
}
function rejected(state: UserActionState, errorCode: UserActionTransitionErrorCode): UserActionTransitionResult {
return { ok: false, state, errorCode };
}
function isStableErrorCode(value: string): boolean {
return /^[A-Z][A-Z0-9_]*$/.test(value);
}
export function transitionUserAction(state: UserActionState, event: UserActionEvent): UserActionTransitionResult {
if (event.type === "RESET") {
if (event.kind !== state.kind) return rejected(state, "USER_ACTION_KIND_MISMATCH");
if (!USER_ACTION_ALLOWED_TRANSITIONS[state.status].includes("IDLE")) {
return rejected(state, "USER_ACTION_INVALID_TRANSITION");
}
return { ok: true, state: idleState(state.kind) };
}
if (event.identity.kind !== state.kind) return rejected(state, "USER_ACTION_KIND_MISMATCH");
if (!event.identity.actionId) return rejected(state, "USER_ACTION_IDENTITY_MISMATCH");
if (event.type === "START") {
if (!USER_ACTION_ALLOWED_TRANSITIONS[state.status].includes("RUNNING")) {
return rejected(state, "USER_ACTION_INVALID_TRANSITION");
}
if (state.identity?.actionId === event.identity.actionId) {
return rejected(state, "USER_ACTION_IDENTITY_REUSED");
}
return {
ok: true,
state: { kind: state.kind, status: "RUNNING", identity: event.identity, errorCode: null },
};
}
if (state.status !== "RUNNING") return rejected(state, "USER_ACTION_INVALID_TRANSITION");
if (state.identity?.actionId !== event.identity.actionId) {
return rejected(state, "USER_ACTION_IDENTITY_MISMATCH");
}
if (event.type === "SUCCEED") {
return { ok: true, state: { ...state, status: "SUCCEEDED", errorCode: null } };
}
const errorCode = event.type === "CANCEL" ? event.errorCode ?? "USER_ACTION_CANCELLED" : event.errorCode;
if (!isStableErrorCode(errorCode)) return rejected(state, "USER_ACTION_INVALID_ERROR_CODE");
return {
ok: true,
state: { ...state, status: event.type === "CANCEL" ? "CANCELLED" : "FAILED", errorCode },
};
}
export function reduceUserActionStates(states: UserActionStates, event: UserActionEvent): UserActionStates {
const kind = event.type === "RESET" ? event.kind : event.identity.kind;
const result = transitionUserAction(states[kind], event);
return result.ok ? { ...states, [kind]: result.state } : states;
}