80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
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<Record<ProjectActionKind, Readonly<Record<ProjectActionKind, boolean>>>> = {
|
|
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 };
|
|
}
|