68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
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<Record<OperationKey, OperationState>>;
|
|
|
|
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<OperationKey, readonly OperationKey[]>);
|
|
|
|
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<string, Promise<unknown>>();
|
|
|
|
function run<T>(key: OperationKey, action: () => T | Promise<T>, target = ''): Promise<T | false> {
|
|
const identity = `${key}:${target}`;
|
|
const existing = inFlight.get(identity);
|
|
if (existing) return existing as Promise<T>;
|
|
if (operationBlocked(operations, key)) return Promise.resolve(false);
|
|
|
|
operations = { ...operations, [key]: { status: 'running', startedAt: now(), target } };
|
|
onChange(operations);
|
|
|
|
const promise: Promise<T> = Promise.resolve()
|
|
.then(action)
|
|
.finally(() => {
|
|
const { [key]: completed, ...remaining } = operations;
|
|
operations = remaining;
|
|
inFlight.delete(identity);
|
|
onChange(operations);
|
|
});
|
|
inFlight.set(identity, promise as Promise<unknown>);
|
|
return promise;
|
|
}
|
|
|
|
return { run, getSnapshot: () => operations };
|
|
}
|