Refactor VPN proxy components and update related behavior
This commit is contained in:
@@ -4,7 +4,6 @@ export type SyncErrorKind = 'incompatible-api' | 'control-unreachable' | 'fatal'
|
||||
|
||||
export interface HarborReducerState {
|
||||
snapshot: HarborClientState | null;
|
||||
pendingServerId: string;
|
||||
transport: {
|
||||
bootStatus: 'loading' | 'ready' | SyncErrorKind;
|
||||
lastSuccessfulSyncAt: string | null;
|
||||
@@ -15,15 +14,12 @@ export interface HarborReducerState {
|
||||
}
|
||||
|
||||
export type HarborAction =
|
||||
| { type: 'select-server'; serverId: string }
|
||||
| { type: 'clear-pending-server' }
|
||||
| { type: 'retry-sync' }
|
||||
| { type: 'sync-failed'; error: unknown }
|
||||
| { type: 'sync-succeeded'; snapshot: HarborClientState; receivedAt: string };
|
||||
|
||||
export const initialHarborState: HarborReducerState = {
|
||||
snapshot: null,
|
||||
pendingServerId: '',
|
||||
transport: {
|
||||
bootStatus: 'loading',
|
||||
lastSuccessfulSyncAt: null,
|
||||
@@ -43,24 +39,7 @@ export function classifySyncError(error: unknown): SyncErrorKind {
|
||||
return 'fatal';
|
||||
}
|
||||
|
||||
function reconcilePendingServer(pendingServerId: string, snapshot: HarborClientState) {
|
||||
if (!pendingServerId || snapshot.selection.desiredServerId === pendingServerId) return '';
|
||||
return snapshot.servers.some((server) => server.id === pendingServerId)
|
||||
? pendingServerId
|
||||
: '';
|
||||
}
|
||||
|
||||
export function harborReducer(current: HarborReducerState, action: HarborAction): HarborReducerState {
|
||||
if (action.type === 'select-server') {
|
||||
return action.serverId === current.pendingServerId
|
||||
? current
|
||||
: { ...current, pendingServerId: action.serverId };
|
||||
}
|
||||
|
||||
if (action.type === 'clear-pending-server') {
|
||||
return current.pendingServerId ? { ...current, pendingServerId: '' } : current;
|
||||
}
|
||||
|
||||
if (action.type === 'retry-sync') {
|
||||
return current.snapshot ? current : {
|
||||
...current,
|
||||
@@ -95,9 +74,6 @@ export function harborReducer(current: HarborReducerState, action: HarborAction)
|
||||
|
||||
return {
|
||||
snapshot: newer ? snapshot : current.snapshot,
|
||||
pendingServerId: newer
|
||||
? reconcilePendingServer(current.pendingServerId, snapshot)
|
||||
: current.pendingServerId,
|
||||
transport: {
|
||||
bootStatus: 'ready',
|
||||
lastSuccessfulSyncAt: action.receivedAt,
|
||||
|
||||
+35
-21
@@ -1,23 +1,36 @@
|
||||
export type OperationKey = 'connection' | 'serverApply' | 'subscriptionImport'
|
||||
| 'subscriptionRefresh' | 'subscriptionDelete' | 'gatewayAuto' | 'routeRules';
|
||||
export interface OperationState { status: 'running'; startedAt: string }
|
||||
export type OperationKey = 'connection' | 'serverApply' | 'profileAdd' | 'profileRename'
|
||||
| 'profileSelect' | 'profileActivate' | 'profileRefresh' | 'profileDelete'
|
||||
| 'gatewayAuto' | 'routeRules';
|
||||
|
||||
export interface OperationState {
|
||||
status: 'running';
|
||||
startedAt: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
export type OperationRegistrySnapshot = Partial<Record<OperationKey, OperationState>>;
|
||||
|
||||
export const OPERATION_CONFLICTS: Readonly<Record<OperationKey, readonly OperationKey[]>> = Object.freeze({
|
||||
connection: ['serverApply', 'subscriptionImport', 'subscriptionRefresh', 'subscriptionDelete', 'gatewayAuto', 'routeRules'],
|
||||
serverApply: ['connection', 'subscriptionImport', 'subscriptionRefresh', 'subscriptionDelete', 'gatewayAuto', 'routeRules'],
|
||||
subscriptionImport: ['connection', 'serverApply', 'subscriptionRefresh', 'subscriptionDelete', 'gatewayAuto', 'routeRules'],
|
||||
subscriptionRefresh: ['connection', 'serverApply', 'subscriptionImport', 'subscriptionDelete', 'gatewayAuto', 'routeRules'],
|
||||
subscriptionDelete: ['connection', 'serverApply', 'subscriptionImport', 'subscriptionRefresh', 'gatewayAuto', 'routeRules'],
|
||||
gatewayAuto: ['connection', 'serverApply', 'subscriptionImport', 'subscriptionRefresh', 'subscriptionDelete', 'routeRules'],
|
||||
routeRules: ['connection', 'serverApply', 'subscriptionImport', 'subscriptionRefresh', 'subscriptionDelete', 'gatewayAuto'],
|
||||
});
|
||||
const OPERATION_KEYS: readonly OperationKey[] = [
|
||||
'connection',
|
||||
'serverApply',
|
||||
'profileAdd',
|
||||
'profileRename',
|
||||
'profileSelect',
|
||||
'profileActivate',
|
||||
'profileRefresh',
|
||||
'profileDelete',
|
||||
'gatewayAuto',
|
||||
'routeRules',
|
||||
];
|
||||
|
||||
// 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',
|
||||
);
|
||||
return OPERATION_CONFLICTS[key].some((conflict) => operations[conflict]?.status === 'running');
|
||||
}
|
||||
|
||||
export function createOperationRegistry(
|
||||
@@ -25,14 +38,15 @@ export function createOperationRegistry(
|
||||
now = () => new Date().toISOString(),
|
||||
) {
|
||||
let operations: OperationRegistrySnapshot = {};
|
||||
const inFlight = new Map<OperationKey, Promise<unknown>>();
|
||||
const inFlight = new Map<string, Promise<unknown>>();
|
||||
|
||||
function run<T>(key: OperationKey, action: () => T | Promise<T>): Promise<T | false> {
|
||||
const existing = inFlight.get(key);
|
||||
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() } };
|
||||
operations = { ...operations, [key]: { status: 'running', startedAt: now(), target } };
|
||||
onChange(operations);
|
||||
|
||||
const promise: Promise<T> = Promise.resolve()
|
||||
@@ -40,10 +54,10 @@ export function createOperationRegistry(
|
||||
.finally(() => {
|
||||
const { [key]: completed, ...remaining } = operations;
|
||||
operations = remaining;
|
||||
inFlight.delete(key);
|
||||
inFlight.delete(identity);
|
||||
onChange(operations);
|
||||
});
|
||||
inFlight.set(key, promise as Promise<unknown>);
|
||||
inFlight.set(identity, promise as Promise<unknown>);
|
||||
return promise;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user