fix: improve network status handling and probe logic

This commit is contained in:
shuaiplus
2026-06-13 17:05:30 +08:00
parent a06cb0ed71
commit b4dfb0409b
5 changed files with 35 additions and 29 deletions
+1 -6
View File
@@ -23,7 +23,6 @@ export default function NetworkStatusBadge() {
const Icon = status === 'online' ? Wifi : WifiOff;
useEffect(() => {
let cancelled = false;
let timer = 0;
const checkService = async () => {
@@ -31,10 +30,7 @@ export default function NetworkStatusBadge() {
setCurrentNetworkStatus('offline');
return;
}
const reachable = await probeNodeWardenService();
if (!cancelled) {
setCurrentNetworkStatus(reachable ? 'online' : 'offline');
}
await probeNodeWardenService();
};
const scheduleNextCheck = () => {
@@ -62,7 +58,6 @@ export default function NetworkStatusBadge() {
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
cancelled = true;
unsubscribe();
window.clearTimeout(timer);
window.removeEventListener('online', handleOnline);
+3
View File
@@ -9,6 +9,7 @@ import type {
TokenSuccess,
} from '../types';
import type { AccountPasskeyAssertion, AccountPasskeyPrfKeySet } from '../account-passkeys';
import { recordNodeWardenReachable, recordNodeWardenUnreachable } from '../network-status';
import { parseJson, type AuthedFetch, type SessionSetter } from './shared';
const SESSION_KEY = 'nodewarden.web.session.v4';
@@ -474,6 +475,7 @@ export function createAuthedFetch(getSession: () => SessionState | null, setSess
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
try {
const response = await fetch(input, { ...init, headers });
recordNodeWardenReachable();
if (response.status !== 429 && (response.status < 500 || response.status >= 600)) {
return response;
}
@@ -484,6 +486,7 @@ export function createAuthedFetch(getSession: () => SessionState | null, setSess
} catch (error) {
lastError = error;
if (attempt === maxAttempts - 1) {
recordNodeWardenUnreachable();
throw error;
}
}
+8 -18
View File
@@ -279,20 +279,16 @@ export async function hydrateLockedSession(
fallbackProfile: Profile | null = null
): Promise<{ session: SessionState | null; profile: Profile | null }> {
const hasOfflineUnlock = hasOfflineUnlockRecord(session.email);
let serviceReachable = true;
if (hasOfflineUnlock) {
serviceReachable = await probeNodeWardenService();
if (!serviceReachable) {
return {
session,
profile: fallbackProfile || loadOfflineProfileSnapshot(session.email),
};
}
if (hasOfflineUnlock && browserReportsOffline()) {
return {
session,
profile: fallbackProfile || loadOfflineProfileSnapshot(session.email),
};
}
const refreshedSession = await maybeRefreshSession(session);
if (!refreshedSession?.accessToken) {
if (hasOfflineUnlock && !serviceReachable) {
if (hasOfflineUnlock && (browserReportsOffline() || !(await probeNodeWardenService()))) {
return {
session,
profile: fallbackProfile || loadOfflineProfileSnapshot(session.email),
@@ -571,14 +567,8 @@ export async function performUnlock(
}
};
if (hasOfflineUnlock) {
if (browserReportsOffline()) {
return unlockOffline();
}
const serviceReachable = await probeNodeWardenService();
if (!serviceReachable) {
return unlockOffline();
}
if (hasOfflineUnlock && browserReportsOffline()) {
return unlockOffline();
}
let token: TokenSuccess | { TwoFactorProviders?: unknown; error_description?: string; error?: string };
+23 -3
View File
@@ -1,12 +1,14 @@
export type NetworkStatus = 'online' | 'offline';
const STATUS_PROBE_TIMEOUT_MS = 3500;
const STATUS_PROBE_TIMEOUT_MS = 8000;
const STATUS_PROBE_CACHE_MS = 5000;
const PROBE_FAILURES_BEFORE_OFFLINE = 2;
const listeners = new Set<(status: NetworkStatus) => void>();
let currentStatus: NetworkStatus = getInitialNetworkStatus();
let pendingProbe: Promise<boolean> | null = null;
let lastProbeAt = 0;
let lastProbeResult = currentStatus === 'online';
let consecutiveProbeFailures = 0;
export function browserReportsOffline(): boolean {
return typeof navigator !== 'undefined' && navigator.onLine === false;
@@ -35,8 +37,23 @@ export function subscribeNetworkStatus(listener: (status: NetworkStatus) => void
};
}
export function recordNodeWardenReachable(): void {
consecutiveProbeFailures = 0;
lastProbeResult = true;
setCurrentNetworkStatus('online');
}
export function recordNodeWardenUnreachable(): void {
lastProbeResult = false;
consecutiveProbeFailures += 1;
if (browserReportsOffline() || consecutiveProbeFailures >= PROBE_FAILURES_BEFORE_OFFLINE) {
setCurrentNetworkStatus('offline');
}
}
export async function probeNodeWardenService(): Promise<boolean> {
if (browserReportsOffline()) {
consecutiveProbeFailures = PROBE_FAILURES_BEFORE_OFFLINE;
setCurrentNetworkStatus('offline');
return false;
}
@@ -68,8 +85,11 @@ export async function probeNodeWardenService(): Promise<boolean> {
.catch(() => false)
.then((result) => {
lastProbeAt = Date.now();
lastProbeResult = result;
setCurrentNetworkStatus(result ? 'online' : 'offline');
if (result) {
recordNodeWardenReachable();
} else {
recordNodeWardenUnreachable();
}
return result;
})
.finally(() => {
-2
View File
@@ -1,5 +1,4 @@
import type { Send } from './types';
import { getCurrentNetworkStatus } from './network-status';
import type { DecryptSendsArgs, DecryptVaultCoreArgs, DecryptVaultCoreResult } from './vault-decrypt';
type WorkerSuccess<T> = { id: number; ok: true; result: T };
@@ -13,7 +12,6 @@ const pending = new Map<number, { resolve: (value: any) => void; reject: (error:
function getWorker(): Worker | null {
if (typeof Worker === 'undefined') return null;
if (worker) return worker;
if (getCurrentNetworkStatus() === 'offline') return null;
worker = new Worker(new URL('../workers/vault-decrypt.worker.ts', import.meta.url), { type: 'module' });
worker.addEventListener('message', (event: MessageEvent<WorkerResponse<unknown>>) => {
const message = event.data;