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