export type OperationKey = 'connection' | 'serverApply' | 'profileAdd' | 'profileRename' | 'profileSelect' | 'profileActivate' | 'profileRefresh' | 'profileDelete' | 'gatewayAuto' | 'routeRules' | 'diagnosticsSettings' | 'failover'; export interface OperationState { status: 'running'; startedAt: string; target: string; } export type OperationRegistrySnapshot = Partial>; const OPERATION_KEYS: readonly OperationKey[] = [ 'connection', 'serverApply', 'profileAdd', 'profileRename', 'profileSelect', 'profileActivate', 'profileRefresh', 'profileDelete', 'gatewayAuto', 'routeRules', 'diagnosticsSettings', 'failover', ]; // The backend has one canonical revision and one mutation queue, so the UI mirrors that lock. export const OPERATION_CONFLICTS = Object.freeze(Object.fromEntries( OPERATION_KEYS.map((key) => [key, OPERATION_KEYS.filter((candidate) => candidate !== key)]), ) as unknown as Record); export function operationBlocked(operations: OperationRegistrySnapshot, key: OperationKey) { if (operations[key]?.status === 'running') return true; return OPERATION_CONFLICTS[key].some((conflict) => operations[conflict]?.status === 'running'); } export function createOperationRegistry( onChange: (operations: OperationRegistrySnapshot) => void = () => {}, now = () => new Date().toISOString(), ) { let operations: OperationRegistrySnapshot = {}; const inFlight = new Map>(); function run(key: OperationKey, action: () => T | Promise, target = ''): Promise { const identity = `${key}:${target}`; const existing = inFlight.get(identity); if (existing) return existing as Promise; if (operationBlocked(operations, key)) return Promise.resolve(false); operations = { ...operations, [key]: { status: 'running', startedAt: now(), target } }; onChange(operations); const promise: Promise = Promise.resolve() .then(action) .finally(() => { const { [key]: completed, ...remaining } = operations; operations = remaining; inFlight.delete(identity); onChange(operations); }); inFlight.set(identity, promise as Promise); return promise; } return { run, getSnapshot: () => operations }; }