Handle stale sync state in client UI
This commit is contained in:
92
src/web/state/harborReducer.js
Normal file
92
src/web/state/harborReducer.js
Normal file
@@ -0,0 +1,92 @@
|
||||
export const initialHarborState = {
|
||||
snapshot: null,
|
||||
pendingServerId: '',
|
||||
transport: {
|
||||
bootStatus: 'loading',
|
||||
lastSuccessfulSyncAt: null,
|
||||
consecutiveFailures: 0,
|
||||
stale: false,
|
||||
error: null,
|
||||
},
|
||||
};
|
||||
|
||||
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?.name === 'TypeError' || status >= 500) return 'control-unreachable';
|
||||
return 'fatal';
|
||||
}
|
||||
|
||||
function reconcilePendingServer(pendingServerId, snapshot) {
|
||||
if (!pendingServerId || snapshot.selection.desiredServerId === pendingServerId) return '';
|
||||
return snapshot.servers.some((server) => server.id === pendingServerId)
|
||||
? pendingServerId
|
||||
: '';
|
||||
}
|
||||
|
||||
export function harborReducer(current, action) {
|
||||
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,
|
||||
transport: { ...current.transport, bootStatus: 'loading', error: null },
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'sync-failed') {
|
||||
const consecutiveFailures = current.transport.consecutiveFailures + 1;
|
||||
const bootStatus = classifySyncError(action.error);
|
||||
return {
|
||||
...current,
|
||||
transport: {
|
||||
...current.transport,
|
||||
bootStatus: current.snapshot ? 'ready' : bootStatus,
|
||||
consecutiveFailures,
|
||||
stale: Boolean(current.snapshot) && (
|
||||
bootStatus === 'incompatible-api' || consecutiveFailures >= STALE_FAILURE_THRESHOLD
|
||||
),
|
||||
error: {
|
||||
kind: bootStatus,
|
||||
message: action.error?.message || 'Неизвестная ошибка',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type !== 'sync-succeeded') return current;
|
||||
|
||||
const snapshot = action.snapshot;
|
||||
const newer = !current.snapshot || snapshot.revision > current.snapshot.revision;
|
||||
|
||||
return {
|
||||
snapshot: newer ? snapshot : current.snapshot,
|
||||
pendingServerId: newer
|
||||
? reconcilePendingServer(current.pendingServerId, snapshot)
|
||||
: current.pendingServerId,
|
||||
transport: {
|
||||
bootStatus: 'ready',
|
||||
lastSuccessfulSyncAt: action.receivedAt,
|
||||
consecutiveFailures: 0,
|
||||
stale: false,
|
||||
error: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user