Refactor VPN proxy client implementation
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user