110 lines
4.2 KiB
TypeScript
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;
|
|
}
|