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 { NextResponse } from "next/server";
import { getTunnelStatus, getTailscaleStatus, getDownloadStatus } from "@/lib/tunnel"; 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() { export async function GET() {
try { try {
let probes = statusCache.value;
if (!probes || Date.now() - statusCache.fetchedAt >= STATUS_CACHE_TTL_MS) {
const [tunnel, tailscale] = await Promise.all([getTunnelStatus(), getTailscaleStatus()]); const [tunnel, tailscale] = await Promise.all([getTunnelStatus(), getTailscaleStatus()]);
probes = { tunnel, tailscale };
statusCache.value = probes;
statusCache.fetchedAt = Date.now();
}
const download = getDownloadStatus(); const download = getDownloadStatus();
return NextResponse.json({ tunnel, tailscale, download }); return NextResponse.json({ ...probes, download });
} catch (error) { } catch (error) {
console.error("Tunnel status error:", error); console.error("Tunnel status error:", error);
return NextResponse.json({ error: error.message }, { status: 500 }); 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" }; import pkg from "../../../../package.json" with { type: "json" };
const NPM_PACKAGE_NAME = "9router"; 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 // Fetch latest version from npm registry
function fetchLatestVersion() { function fetchLatestVersion() {
@@ -36,8 +40,20 @@ function compareVersions(a, b) {
return 0; 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() { export async function GET() {
const latestVersion = await fetchLatestVersion(); const latestVersion = await getLatestVersionCached();
const currentVersion = pkg.version; const currentVersion = pkg.version;
const hasUpdate = latestVersion ? compareVersions(latestVersion, currentVersion) > 0 : false; const hasUpdate = latestVersion ? compareVersions(latestVersion, currentVersion) > 0 : false;

View File

@@ -32,6 +32,10 @@ import { syncToJson as syncMitmAliasCache } from "@/lib/mitmAliasCache";
process.setMaxListeners(20); 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 // Survive Next.js hot reload
const g = global.__appSingleton ??= { const g = global.__appSingleton ??= {
signalHandlersRegistered: false, signalHandlersRegistered: false,
@@ -47,6 +51,34 @@ const g = global.__appSingleton ??= {
export async function initializeApp() { export async function initializeApp() {
try { try {
// 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 */ }
killCloudflared();
process.exit();
};
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
process.on("exit", () => { try { removeAllDNSEntriesSync(); } catch { /* ignore */ } });
g.signalHandlersRegistered = true;
}
setTunnelUnexpectedExitCallback(() => {
safeRestartTunnel("unexpected-exit").catch(() => {});
});
// 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(); await cleanupProviderConnections();
const settings = await getSettings(); const settings = await getSettings();
@@ -64,35 +96,15 @@ export async function initializeApp() {
safeRestartTailscale("startup").catch((e) => console.log("[InitApp] Tailscale resume failed:", e.message)); safeRestartTailscale("startup").catch((e) => console.log("[InitApp] Tailscale resume failed:", e.message));
} }
if (!g.signalHandlersRegistered) {
const cleanup = () => {
try { removeAllDNSEntriesSync(); } catch { /* best effort */ }
killCloudflared();
process.exit();
};
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
process.on("exit", () => { try { removeAllDNSEntriesSync(); } catch { /* ignore */ } });
g.signalHandlersRegistered = true;
}
ensureCloudflared().catch(() => {}); ensureCloudflared().catch(() => {});
// Sync mitmAlias DB → JSON cache so standalone MITM server can read it // Sync mitmAlias DB → JSON cache so standalone MITM server can read it
syncMitmAliasCache().catch(() => {}); syncMitmAliasCache().catch(() => {});
// Auto-respawn tunnel when cloudflared exits unexpectedly (e.g. network change drop)
setTunnelUnexpectedExitCallback(() => {
safeRestartTunnel("unexpected-exit").catch(() => {});
});
startWatchdog(); startWatchdog();
startNetworkMonitor(); startNetworkMonitor();
autoStartMitm(); autoStartMitm();
startQuotaAutoPing(); startQuotaAutoPing();
} catch (error) {
console.error("[InitApp] Error:", error);
}
} }
async function autoStartMitm() { async function autoStartMitm() {