Refactor VPN proxy client implementation
Build and Deploy Gateway / build-and-push (push) Failing after 2s
Build and Deploy Gateway / deploy (push) Has been skipped

This commit is contained in:
2026-08-09 00:41:52 +03:00
parent 32be4380a3
commit 34d8b681ad
190 changed files with 20064 additions and 9761 deletions
@@ -1,4 +1,27 @@
export const initialHarborState = {
import type { HarborClientState } from '../api/harborClient.js';
export type SyncErrorKind = 'incompatible-api' | 'control-unreachable' | 'fatal';
export interface HarborReducerState {
snapshot: HarborClientState | null;
pendingServerId: string;
transport: {
bootStatus: 'loading' | 'ready' | SyncErrorKind;
lastSuccessfulSyncAt: string | null;
consecutiveFailures: number;
stale: boolean;
error: { kind: SyncErrorKind; message: string } | null;
};
}
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: {
@@ -12,28 +35,22 @@ export const initialHarborState = {
export const STALE_FAILURE_THRESHOLD = 3;
export function compatibleSnapshot(snapshot) {
return snapshot?.apiVersion === 1 &&
Number.isSafeInteger(snapshot.revision) &&
typeof snapshot.selection?.desiredServerId === 'string' &&
Array.isArray(snapshot.servers);
}
export function classifySyncError(error) {
const status = Number(error?.status) || 0;
if (error?.code === 'INCOMPATIBLE_API' || status === 404) return 'incompatible-api';
if (error?.code === 'CONTROL_UNREACHABLE' || error?.name === 'TypeError' || status >= 500) return 'control-unreachable';
export function classifySyncError(error: unknown): SyncErrorKind {
const candidate = error && typeof error === 'object' ? error as Record<string, unknown> : {};
const status = Number(candidate.status) || 0;
if (candidate.code === 'INCOMPATIBLE_API' || status === 404) return 'incompatible-api';
if (candidate.code === 'CONTROL_UNREACHABLE' || candidate.name === 'TypeError' || status >= 500) return 'control-unreachable';
return 'fatal';
}
function reconcilePendingServer(pendingServerId, snapshot) {
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, action) {
export function harborReducer(current: HarborReducerState, action: HarborAction): HarborReducerState {
if (action.type === 'select-server') {
return action.serverId === current.pendingServerId
? current
@@ -65,7 +82,7 @@ export function harborReducer(current, action) {
),
error: {
kind: bootStatus,
message: action.error?.message || 'Неизвестная ошибка',
message: action.error instanceof Error ? action.error.message : 'Неизвестная ошибка',
},
},
};
@@ -1,4 +1,9 @@
export const OPERATION_CONFLICTS = Object.freeze({
export type OperationKey = 'connection' | 'serverApply' | 'subscriptionImport'
| 'subscriptionRefresh' | 'subscriptionDelete' | 'gatewayAuto' | 'routeRules';
export interface OperationState { status: 'running'; startedAt: 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'],
@@ -8,25 +13,29 @@ export const OPERATION_CONFLICTS = Object.freeze({
routeRules: ['connection', 'serverApply', 'subscriptionImport', 'subscriptionRefresh', 'subscriptionDelete', 'gatewayAuto'],
});
export function operationBlocked(operations, key) {
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 = () => {}, now = () => new Date().toISOString()) {
let operations = {};
const inFlight = new Map();
export function createOperationRegistry(
onChange: (operations: OperationRegistrySnapshot) => void = () => {},
now = () => new Date().toISOString(),
) {
let operations: OperationRegistrySnapshot = {};
const inFlight = new Map<OperationKey, Promise<unknown>>();
function run(key, action) {
if (inFlight.has(key)) return inFlight.get(key);
function run<T>(key: OperationKey, action: () => T | Promise<T>): Promise<T | false> {
const existing = inFlight.get(key);
if (existing) return existing as Promise<T>;
if (operationBlocked(operations, key)) return Promise.resolve(false);
operations = { ...operations, [key]: { status: 'running', startedAt: now() } };
onChange(operations);
const promise = Promise.resolve()
const promise: Promise<T> = Promise.resolve()
.then(action)
.finally(() => {
const { [key]: completed, ...remaining } = operations;
@@ -34,7 +43,7 @@ export function createOperationRegistry(onChange = () => {}, now = () => new Dat
inFlight.delete(key);
onChange(operations);
});
inFlight.set(key, promise);
inFlight.set(key, promise as Promise<unknown>);
return promise;
}