Refactor VPN proxy components and update related behavior
Build and Deploy Gateway / build-and-push (push) Successful in 20s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-11 01:27:46 +03:00
parent c89e56942a
commit aa9c959368
58 changed files with 4234 additions and 2755 deletions
-24
View File
@@ -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
View File
@@ -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;
}