diff --git a/src/app/api/tunnel/status/route.js b/src/app/api/tunnel/status/route.js index c94df58d..8b8f8053 100644 --- a/src/app/api/tunnel/status/route.js +++ b/src/app/api/tunnel/status/route.js @@ -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 }); diff --git a/src/app/api/version/route.js b/src/app/api/version/route.js index 7e7f4a80..5635f9ae 100644 --- a/src/app/api/version/route.js +++ b/src/app/api/version/route.js @@ -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; diff --git a/src/shared/services/initializeApp.js b/src/shared/services/initializeApp.js index e2914e0d..094ee726 100644 --- a/src/shared/services/initializeApp.js +++ b/src/shared/services/initializeApp.js @@ -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;