export const PROJECT_ACTION_KINDS = ["OPEN", "SAVE", "CLOSE"] as const; export type ProjectActionKind = typeof PROJECT_ACTION_KINDS[number]; export interface ProjectActionIdentity { actionId: string; kind: ProjectActionKind; } export interface ProjectActionMutexState { owner: ProjectActionIdentity | null; } export type ProjectActionConflictReason = | "REPEATED_OPEN" | "CONCURRENT_SAVE" | "CLOSE_DURING_SAVE" | "PROJECT_ACTION_EXCLUSIVE"; export interface ProjectActionConflict { code: "USER_ACTION_CONFLICT"; reason: ProjectActionConflictReason; requested: ProjectActionIdentity; owner: ProjectActionIdentity; } export type ProjectActionAcquireResult = | { granted: true; state: ProjectActionMutexState; conflict: null } | { granted: false; state: ProjectActionMutexState; conflict: ProjectActionConflict }; export type ProjectActionReleaseResult = | { released: true; state: ProjectActionMutexState; errorCode: null } | { released: false; state: ProjectActionMutexState; errorCode: "PROJECT_ACTION_LOCK_NOT_HELD" | "PROJECT_ACTION_LOCK_IDENTITY_MISMATCH"; }; export const PROJECT_ACTION_CONFLICT_MATRIX: Readonly>>> = { OPEN: { OPEN: true, SAVE: true, CLOSE: true }, SAVE: { OPEN: true, SAVE: true, CLOSE: true }, CLOSE: { OPEN: true, SAVE: true, CLOSE: true }, }; export function createProjectActionMutexState(): ProjectActionMutexState { return { owner: null }; } function conflictReason(requested: ProjectActionKind, owner: ProjectActionKind): ProjectActionConflictReason { if (requested === "OPEN" && owner === "OPEN") return "REPEATED_OPEN"; if (requested === "SAVE" && owner === "SAVE") return "CONCURRENT_SAVE"; if (requested === "CLOSE" && owner === "SAVE") return "CLOSE_DURING_SAVE"; return "PROJECT_ACTION_EXCLUSIVE"; } export function acquireProjectAction(state: ProjectActionMutexState, requested: ProjectActionIdentity): ProjectActionAcquireResult { const owner = state.owner; if (!owner) return { granted: true, state: { owner: requested }, conflict: null }; if (!PROJECT_ACTION_CONFLICT_MATRIX[requested.kind][owner.kind]) { return { granted: true, state: { owner: requested }, conflict: null }; } return { granted: false, state, conflict: { code: "USER_ACTION_CONFLICT", reason: conflictReason(requested.kind, owner.kind), requested, owner, }, }; } export function releaseProjectAction(state: ProjectActionMutexState, identity: ProjectActionIdentity): ProjectActionReleaseResult { if (!state.owner) return { released: false, state, errorCode: "PROJECT_ACTION_LOCK_NOT_HELD" }; if (state.owner.kind !== identity.kind || state.owner.actionId !== identity.actionId) { return { released: false, state, errorCode: "PROJECT_ACTION_LOCK_IDENTITY_MISMATCH" }; } return { released: true, state: createProjectActionMutexState(), errorCode: null }; }