Handle stale sync state in client UI
This commit is contained in:
@@ -1,33 +1,53 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useReducer, useRef, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './styles.css';
|
||||
import { api } from './api.js';
|
||||
import { ClientOverviewPage } from './components/ClientOverviewPage.jsx';
|
||||
import { BootStatePage, StaleBanner } from './components/SyncStatus.jsx';
|
||||
import {
|
||||
compatibleSnapshot,
|
||||
harborReducer,
|
||||
initialHarborState,
|
||||
} from './state/harborReducer.js';
|
||||
|
||||
function App() {
|
||||
const previewReady = new URLSearchParams(window.location.search).has('preview-ready');
|
||||
const [state, setState] = useState(null);
|
||||
const [{ snapshot: state, pendingServerId: pendingTag, transport }, dispatch] = useReducer(
|
||||
harborReducer,
|
||||
initialHarborState,
|
||||
);
|
||||
const [subscriptionUrl, setSubscriptionUrl] = useState('');
|
||||
const [pendingTag, setPendingTag] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const pollGeneration = useRef(0);
|
||||
|
||||
function syncState(data) {
|
||||
setState(data);
|
||||
setPendingTag((current) => (
|
||||
(data.servers || []).some((server) => server.tag === current)
|
||||
? current
|
||||
: data.selectedTag || ''
|
||||
));
|
||||
function setPendingTag(serverId) {
|
||||
dispatch({ type: 'select-server', serverId });
|
||||
}
|
||||
|
||||
async function loadState() {
|
||||
syncState(await api.state());
|
||||
async function loadState({ retry = false } = {}) {
|
||||
if (retry) dispatch({ type: 'retry-sync' });
|
||||
const generation = pollGeneration.current;
|
||||
try {
|
||||
const snapshot = await api.state();
|
||||
if (!compatibleSnapshot(snapshot)) {
|
||||
const incompatible = new Error('Ожидался Harbor state apiVersion 1');
|
||||
incompatible.code = 'INCOMPATIBLE_API';
|
||||
throw incompatible;
|
||||
}
|
||||
if (generation === pollGeneration.current) {
|
||||
dispatch({ type: 'sync-succeeded', snapshot, receivedAt: new Date().toISOString() });
|
||||
}
|
||||
} catch (requestError) {
|
||||
if (generation === pollGeneration.current) {
|
||||
dispatch({ type: 'sync-failed', error: requestError });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadState().catch((err) => setError(err.message));
|
||||
const timer = setInterval(() => loadState().catch(() => {}), 5000);
|
||||
loadState();
|
||||
const timer = setInterval(loadState, 5000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
@@ -44,10 +64,7 @@ function App() {
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
const result = await action();
|
||||
if (result?.state) syncState(result.state);
|
||||
else await loadState();
|
||||
return result;
|
||||
return await applyMutation(action);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
@@ -56,34 +73,50 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function applyMutation(action) {
|
||||
pollGeneration.current += 1;
|
||||
const result = await action();
|
||||
if (!result?.state) throw new Error('Harbor API не вернул state snapshot');
|
||||
if (!compatibleSnapshot(result.state)) throw new Error('Harbor API не вернул state snapshot v1');
|
||||
dispatch({
|
||||
type: 'sync-succeeded',
|
||||
snapshot: result.state,
|
||||
receivedAt: new Date().toISOString(),
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async function fetchSubscription() {
|
||||
return run(async () => {
|
||||
const data = await api.subscription.fetch(subscriptionUrl);
|
||||
setPendingTag('');
|
||||
dispatch({ type: 'clear-pending-server' });
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshSubscription() {
|
||||
const data = await api.subscription.refresh();
|
||||
syncState(data.state || data);
|
||||
setPendingTag(data.selectedTag || '');
|
||||
return data;
|
||||
try {
|
||||
return await applyMutation(api.subscription.refresh);
|
||||
} catch (refreshError) {
|
||||
setError(refreshError.message);
|
||||
throw refreshError;
|
||||
}
|
||||
}
|
||||
|
||||
async function forgetSubscription() {
|
||||
return run(async () => {
|
||||
const data = await api.subscription.forget();
|
||||
setSubscriptionUrl('');
|
||||
setPendingTag('');
|
||||
dispatch({ type: 'clear-pending-server' });
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
if (!state) return <div className="app-loading">Harbor</div>;
|
||||
if (!state) return <BootStatePage transport={transport} onRetry={() => loadState({ retry: true })} />;
|
||||
|
||||
return (
|
||||
<div className={`app client-app${state.mode === 'gateway' ? ' is-gateway-app' : ''}`}>
|
||||
<StaleBanner transport={transport} onRetry={() => loadState({ retry: true })} />
|
||||
<div className="app-body client-mode">
|
||||
<main className="app-main">
|
||||
<ClientOverviewPage
|
||||
|
||||
Reference in New Issue
Block a user