100 lines
2.9 KiB
TypeScript
100 lines
2.9 KiB
TypeScript
import { computed, readonly, ref } from "vue";
|
|
import { heartbeatSession } from "../api/authClient";
|
|
import { getToken } from "../utils/auth";
|
|
|
|
export type ConnectionStatus = "checking" | "online" | "degraded" | "offline";
|
|
|
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
|
|
const status = ref<ConnectionStatus>("checking");
|
|
const latencyMs = ref<number | null>(null);
|
|
const lastSuccessAt = ref<number | null>(null);
|
|
const lastCheckedAt = ref<number | null>(null);
|
|
let consumerCount = 0;
|
|
let heartbeatTimer: number | undefined;
|
|
let heartbeatPromise: Promise<void> | null = null;
|
|
let listenersRegistered = false;
|
|
|
|
const checkConnection = async () => {
|
|
if (typeof navigator !== "undefined" && !navigator.onLine) {
|
|
status.value = "offline";
|
|
latencyMs.value = null;
|
|
lastCheckedAt.value = Date.now();
|
|
return;
|
|
}
|
|
if (heartbeatPromise) return heartbeatPromise;
|
|
heartbeatPromise = (async () => {
|
|
const startedAt = performance.now();
|
|
try {
|
|
const token = getToken();
|
|
if (!token) {
|
|
status.value = "offline";
|
|
latencyMs.value = null;
|
|
return;
|
|
}
|
|
await heartbeatSession(token);
|
|
latencyMs.value = Math.max(1, Math.round(performance.now() - startedAt));
|
|
lastSuccessAt.value = Date.now();
|
|
status.value = "online";
|
|
} catch (error: any) {
|
|
latencyMs.value = null;
|
|
status.value = error?.response ? "degraded" : "offline";
|
|
} finally {
|
|
lastCheckedAt.value = Date.now();
|
|
heartbeatPromise = null;
|
|
}
|
|
})();
|
|
return heartbeatPromise;
|
|
};
|
|
|
|
const handleOnline = () => {
|
|
status.value = "checking";
|
|
void checkConnection();
|
|
};
|
|
|
|
const handleOffline = () => {
|
|
status.value = "offline";
|
|
latencyMs.value = null;
|
|
lastCheckedAt.value = Date.now();
|
|
};
|
|
|
|
const registerNetworkListeners = () => {
|
|
if (listenersRegistered || typeof window === "undefined") return;
|
|
listenersRegistered = true;
|
|
window.addEventListener("online", handleOnline);
|
|
window.addEventListener("offline", handleOffline);
|
|
};
|
|
|
|
const startConnectionMonitor = () => {
|
|
consumerCount += 1;
|
|
registerNetworkListeners();
|
|
if (consumerCount === 1) {
|
|
void checkConnection();
|
|
heartbeatTimer = window.setInterval(() => void checkConnection(), HEARTBEAT_INTERVAL_MS);
|
|
}
|
|
return () => {
|
|
consumerCount = Math.max(0, consumerCount - 1);
|
|
if (consumerCount === 0 && heartbeatTimer) {
|
|
window.clearInterval(heartbeatTimer);
|
|
heartbeatTimer = undefined;
|
|
}
|
|
};
|
|
};
|
|
|
|
const statusLabel = computed(() => {
|
|
if (status.value === "online") return "在线";
|
|
if (status.value === "degraded") return "连接异常";
|
|
if (status.value === "offline") return "已断开";
|
|
return "检测中";
|
|
});
|
|
|
|
export const useConnectionStatus = () => ({
|
|
status: readonly(status),
|
|
statusLabel,
|
|
latencyMs: readonly(latencyMs),
|
|
lastSuccessAt: readonly(lastSuccessAt),
|
|
lastCheckedAt: readonly(lastCheckedAt),
|
|
checkConnection,
|
|
startConnectionMonitor,
|
|
});
|