refactor(api): implement caching for tunnel and version status endpoints

This commit is contained in:
decolua
2026-07-09 15:08:30 +07:00
parent 20b442b708
commit a4c5fa4e14
3 changed files with 70 additions and 30 deletions

View File

@@ -1,11 +1,23 @@
import { NextResponse } from "next/server";
import { getTunnelStatus, getTailscaleStatus, getDownloadStatus } from "@/lib/tunnel";
const STATUS_CACHE_TTL_MS = 3000; // coalesce rapid polls; underlying probes already cache 10s
// Survive hot reload; one cache per process. Only tunnel/tailscale probes are cached —
// download progress stays live so the enable/download UI updates smoothly.
const statusCache = (global.__tunnelStatusCache ??= { value: null, fetchedAt: 0 });
export async function GET() {
try {
const [tunnel, tailscale] = await Promise.all([getTunnelStatus(), getTailscaleStatus()]);
let probes = statusCache.value;
if (!probes || Date.now() - statusCache.fetchedAt >= STATUS_CACHE_TTL_MS) {
const [tunnel, tailscale] = await Promise.all([getTunnelStatus(), getTailscaleStatus()]);
probes = { tunnel, tailscale };
statusCache.value = probes;
statusCache.fetchedAt = Date.now();
}
const download = getDownloadStatus();
return NextResponse.json({ tunnel, tailscale, download });
return NextResponse.json({ ...probes, download });
} catch (error) {
console.error("Tunnel status error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });

View File

@@ -2,6 +2,10 @@ import https from "https";
import pkg from "../../../../package.json" with { type: "json" };
const NPM_PACKAGE_NAME = "9router";
const VERSION_CACHE_TTL_MS = 3600000; // cache npm latest lookup for 1h
// Survive hot reload; one cache per process
const versionCache = (global.__npmVersionCache ??= { value: null, fetchedAt: 0 });
// Fetch latest version from npm registry
function fetchLatestVersion() {
@@ -36,8 +40,20 @@ function compareVersions(a, b) {
return 0;
}
async function getLatestVersionCached() {
if (versionCache.value && Date.now() - versionCache.fetchedAt < VERSION_CACHE_TTL_MS) {
return versionCache.value;
}
const latest = await fetchLatestVersion();
if (latest) {
versionCache.value = latest;
versionCache.fetchedAt = Date.now();
}
return latest;
}
export async function GET() {
const latestVersion = await fetchLatestVersion();
const latestVersion = await getLatestVersionCached();
const currentVersion = pkg.version;
const hasUpdate = latestVersion ? compareVersions(latestVersion, currentVersion) > 0 : false;

View File

@@ -32,6 +32,10 @@ import { syncToJson as syncMitmAliasCache } from "@/lib/mitmAliasCache";
process.setMaxListeners(20);
// Defer heavy startup work so the first HTTP request (login → dashboard) isn't
// starved by DB cleanup, cloudflared download, lsof/DNS probes and OAuth pings.
const STARTUP_DEFER_MS = 3000;
// Survive Next.js hot reload
const g = global.__appSingleton ??= {
signalHandlersRegistered: false,
@@ -47,23 +51,8 @@ const g = global.__appSingleton ??= {
export async function initializeApp() {
try {
await cleanupProviderConnections();
const settings = await getSettings();
// Auto-resume tunnel (once per process)
if (settings.tunnelEnabled && !g.tunnelAutoResumed) {
g.tunnelAutoResumed = true;
console.log("[InitApp] Tunnel was enabled, auto-resuming...");
safeRestartTunnel("startup").catch((e) => console.log("[InitApp] Tunnel resume failed:", e.message));
}
// Auto-resume tailscale (once per process)
if (settings.tailscaleEnabled && !g.tailscaleAutoResumed) {
g.tailscaleAutoResumed = true;
console.log("[InitApp] Tailscale was enabled, auto-resuming...");
safeRestartTailscale("startup").catch((e) => console.log("[InitApp] Tailscale resume failed:", e.message));
}
// Register cleanup + exit-respawn callback immediately so signals and
// unexpected cloudflared exits are handled even during the deferred window.
if (!g.signalHandlersRegistered) {
const cleanup = () => {
try { removeAllDNSEntriesSync(); } catch { /* best effort */ }
@@ -76,25 +65,48 @@ export async function initializeApp() {
g.signalHandlersRegistered = true;
}
ensureCloudflared().catch(() => {});
// Sync mitmAlias DB → JSON cache so standalone MITM server can read it
syncMitmAliasCache().catch(() => {});
// Auto-respawn tunnel when cloudflared exits unexpectedly (e.g. network change drop)
setTunnelUnexpectedExitCallback(() => {
safeRestartTunnel("unexpected-exit").catch(() => {});
});
startWatchdog();
startNetworkMonitor();
autoStartMitm();
startQuotaAutoPing();
// Defer the heavy work — nothing here blocks incoming requests.
setTimeout(() => {
runHeavyStartup().catch((e) => console.error("[InitApp] deferred startup failed:", e.message));
}, STARTUP_DEFER_MS);
} catch (error) {
console.error("[InitApp] Error:", error);
}
}
async function runHeavyStartup() {
await cleanupProviderConnections();
const settings = await getSettings();
// Auto-resume tunnel (once per process)
if (settings.tunnelEnabled && !g.tunnelAutoResumed) {
g.tunnelAutoResumed = true;
console.log("[InitApp] Tunnel was enabled, auto-resuming...");
safeRestartTunnel("startup").catch((e) => console.log("[InitApp] Tunnel resume failed:", e.message));
}
// Auto-resume tailscale (once per process)
if (settings.tailscaleEnabled && !g.tailscaleAutoResumed) {
g.tailscaleAutoResumed = true;
console.log("[InitApp] Tailscale was enabled, auto-resuming...");
safeRestartTailscale("startup").catch((e) => console.log("[InitApp] Tailscale resume failed:", e.message));
}
ensureCloudflared().catch(() => {});
// Sync mitmAlias DB → JSON cache so standalone MITM server can read it
syncMitmAliasCache().catch(() => {});
startWatchdog();
startNetworkMonitor();
autoStartMitm();
startQuotaAutoPing();
}
async function autoStartMitm() {
if (g.mitmStartInProgress) return;
g.mitmStartInProgress = true;