From ca28237d13e0852c39179f2a72d5e4bb7209a15c Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Wed, 7 Jan 2026 20:53:21 -0500 Subject: [PATCH] hopefully this solves random hanging --- src/app.tsx | 5 +- src/entry-client.tsx | 109 +++++++++++++++++- src/lib/deployment-detection.ts | 196 ++++++++++++++++++++++++++++++++ src/lib/token-refresh.ts | 23 +++- src/routes/account.tsx | 3 - vercel.json | 11 +- 6 files changed, 334 insertions(+), 13 deletions(-) create mode 100644 src/lib/deployment-detection.ts diff --git a/src/app.tsx b/src/app.tsx index 75ed738..f89a63a 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -19,7 +19,7 @@ import { createWindowWidth, isMobile } from "~/lib/resize-utils"; import { MOBILE_CONFIG } from "./config"; import CustomScrollbar from "./components/CustomScrollbar"; import { initPerformanceTracking } from "~/lib/performance-tracking"; -import { tokenRefreshManager } from "~/lib/token-refresh"; +import { startDeploymentMonitoring } from "~/lib/deployment-detection"; function AppLayout(props: { children: any }) { const { @@ -35,6 +35,9 @@ function AppLayout(props: { children: any }) { // Initialize performance tracking initPerformanceTracking(); + // Start monitoring for new deployments + startDeploymentMonitoring(); + const windowWidth = createWindowWidth(); createEffect(() => { diff --git a/src/entry-client.tsx b/src/entry-client.tsx index 22c7ee5..f633c0a 100644 --- a/src/entry-client.tsx +++ b/src/entry-client.tsx @@ -1,17 +1,108 @@ // @refresh reload import { mount, StartClient } from "@solidjs/start/client"; -// Handle chunk loading failures from stale cache +// Deployment version detection and chunk loading error handling +const RELOAD_STORAGE_KEY = "chunk-reload-count"; +const RELOAD_TIMESTAMP_KEY = "chunk-reload-timestamp"; +const MAX_RELOADS = 3; +const RELOAD_WINDOW_MS = 30000; // 30 seconds + +/** + * Check if we should attempt reload or show error + * Prevents infinite reload loops by tracking reload attempts + */ +function shouldAttemptReload(): boolean { + try { + const now = Date.now(); + const reloadCount = parseInt( + sessionStorage.getItem(RELOAD_STORAGE_KEY) || "0", + 10 + ); + const lastReloadTime = parseInt( + sessionStorage.getItem(RELOAD_TIMESTAMP_KEY) || "0", + 10 + ); + + // Reset counter if outside the time window + if (now - lastReloadTime > RELOAD_WINDOW_MS) { + sessionStorage.setItem(RELOAD_STORAGE_KEY, "0"); + sessionStorage.setItem(RELOAD_TIMESTAMP_KEY, now.toString()); + return true; + } + + // Check if we've exceeded max reloads + if (reloadCount >= MAX_RELOADS) { + console.error( + `Exceeded ${MAX_RELOADS} reload attempts in ${RELOAD_WINDOW_MS}ms. Stopping to prevent infinite loop.` + ); + return false; + } + + // Increment counter and allow reload + sessionStorage.setItem(RELOAD_STORAGE_KEY, (reloadCount + 1).toString()); + sessionStorage.setItem(RELOAD_TIMESTAMP_KEY, now.toString()); + return true; + } catch (e) { + // If sessionStorage fails, allow reload but log error + console.warn("Failed to access sessionStorage:", e); + return true; + } +} + +/** + * Handle chunk loading errors with smart reload logic + */ +function handleChunkError(source: string): void { + console.warn(`[Chunk Error] ${source} - chunk load failure detected`); + + if (shouldAttemptReload()) { + const reloadCount = sessionStorage.getItem(RELOAD_STORAGE_KEY) || "1"; + console.log( + `[Chunk Error] Attempting reload (${reloadCount}/${MAX_RELOADS})...` + ); + + // Add small delay to prevent race conditions + setTimeout(() => { + window.location.reload(); + }, 100); + } else { + // Show user-friendly error message + const errorDiv = document.createElement("div"); + errorDiv.style.cssText = ` + position: fixed; + top: 0; + left: 0; + right: 0; + background: #f59e0b; + color: #000; + padding: 16px; + text-align: center; + font-family: system-ui, -apple-system, sans-serif; + font-size: 14px; + z-index: 9999; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); + `; + errorDiv.innerHTML = ` + Update Required
+ A new version is available. Please refresh the page manually or + click here. + `; + document.body.appendChild(errorDiv); + } +} + +// Handle runtime chunk loading errors window.addEventListener("error", (event) => { if ( event.message?.includes("Importing a module script failed") || event.message?.includes("Failed to fetch dynamically imported module") ) { - console.warn("Chunk load error detected, reloading page..."); - window.location.reload(); + event.preventDefault(); + handleChunkError("error event"); } }); +// Handle promise-based chunk loading errors window.addEventListener("unhandledrejection", (event) => { if ( event.reason?.message?.includes("Importing a module script failed") || @@ -19,10 +110,18 @@ window.addEventListener("unhandledrejection", (event) => { "Failed to fetch dynamically imported module" ) ) { - console.warn("Chunk load error detected, reloading page..."); event.preventDefault(); - window.location.reload(); + handleChunkError("unhandled rejection"); } }); +// Clear reload counter on successful page load +window.addEventListener("load", () => { + // Only clear if we successfully loaded (we're past the critical chunk loading phase) + setTimeout(() => { + sessionStorage.removeItem(RELOAD_STORAGE_KEY); + sessionStorage.removeItem(RELOAD_TIMESTAMP_KEY); + }, 2000); +}); + mount(() => , document.getElementById("app")!); diff --git a/src/lib/deployment-detection.ts b/src/lib/deployment-detection.ts new file mode 100644 index 0000000..c946853 --- /dev/null +++ b/src/lib/deployment-detection.ts @@ -0,0 +1,196 @@ +/** + * Deployment Detection System + * Detects when the app has been updated on the server + */ + +const VERSION_CHECK_INTERVAL = 5 * 60 * 1000; // Check every 5 minutes +const VERSION_STORAGE_KEY = "app-version-hash"; + +/** + * Get a simple hash from the current page HTML + * This changes when deployment happens + */ +function getCurrentVersionHash(): string { + try { + // Use a combination of script tags to detect version + const scripts = Array.from(document.querySelectorAll("script[src]")) + .map((s) => (s as HTMLScriptElement).src) + .filter((src) => src.includes("/_build/")) + .sort() + .join(","); + + // Simple hash function + let hash = 0; + for (let i = 0; i < scripts.length; i++) { + const char = scripts.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32-bit integer + } + return hash.toString(36); + } catch (e) { + console.warn("[Version Detection] Failed to get version hash:", e); + return ""; + } +} + +/** + * Check if a new version is available + * Returns true if new version detected + */ +async function checkForNewVersion(): Promise { + try { + // Fetch current page HTML + const response = await fetch(window.location.pathname, { + method: "HEAD", + cache: "no-cache" + }); + + if (!response.ok) { + console.warn("[Version Detection] Health check failed:", response.status); + return false; + } + + // Check if ETag changed (Vercel sets this) + const newEtag = response.headers.get("etag"); + const storedEtag = sessionStorage.getItem("app-etag"); + + if (storedEtag && newEtag && storedEtag !== newEtag) { + console.log( + "[Version Detection] New version detected (ETag changed)", + storedEtag, + "→", + newEtag + ); + return true; + } + + // Store current ETag for future checks + if (newEtag) { + sessionStorage.setItem("app-etag", newEtag); + } + + return false; + } catch (error) { + console.warn("[Version Detection] Version check failed:", error); + return false; + } +} + +/** + * Show update notification to user + */ +function showUpdateNotification(): void { + // Only show once per session + if (sessionStorage.getItem("update-notification-shown")) { + return; + } + + sessionStorage.setItem("update-notification-shown", "true"); + + const notification = document.createElement("div"); + notification.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + background: #3b82f6; + color: white; + padding: 16px 24px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + font-family: system-ui, -apple-system, sans-serif; + font-size: 14px; + z-index: 9999; + max-width: 320px; + animation: slideIn 0.3s ease-out; + `; + + notification.innerHTML = ` + +
+
+ 🎉 New Update Available
+ A new version of the app is ready. +
+
+ + +
+
+ `; + + document.body.appendChild(notification); + + // Auto-remove after 30 seconds + setTimeout(() => { + if (notification.parentElement) { + notification.style.animation = "slideIn 0.3s ease-out reverse"; + setTimeout(() => notification.remove(), 300); + } + }, 30000); +} + +/** + * Start monitoring for new deployments + */ +export function startDeploymentMonitoring(): void { + if (typeof window === "undefined") return; + + // Store initial version + const initialVersion = getCurrentVersionHash(); + sessionStorage.setItem(VERSION_STORAGE_KEY, initialVersion); + + // Periodic version check + const intervalId = setInterval(async () => { + const hasNewVersion = await checkForNewVersion(); + if (hasNewVersion) { + showUpdateNotification(); + } + }, VERSION_CHECK_INTERVAL); + + // Check on visibility change (user returns to tab) + const handleVisibilityChange = async () => { + if (document.visibilityState === "visible") { + const hasNewVersion = await checkForNewVersion(); + if (hasNewVersion) { + showUpdateNotification(); + } + } + }; + + document.addEventListener("visibilitychange", handleVisibilityChange); + + // Cleanup function + if (typeof window !== "undefined") { + (window as any).__cleanupDeploymentMonitoring = () => { + clearInterval(intervalId); + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + } +} diff --git a/src/lib/token-refresh.ts b/src/lib/token-refresh.ts index 3248a5b..14811c5 100644 --- a/src/lib/token-refresh.ts +++ b/src/lib/token-refresh.ts @@ -211,9 +211,14 @@ class TokenRefreshManager { `[Token Refresh] Using rememberMe: ${rememberMe} (from refresh token cookie existence)` ); - const result = await api.auth.refreshToken.mutate({ - rememberMe - }); + const result = await Promise.race([ + api.auth.refreshToken.mutate({ + rememberMe + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Token refresh timeout")), 10000) + ) + ]); if (result.success) { console.log("[Token Refresh] Token refreshed successfully"); @@ -230,6 +235,18 @@ class TokenRefreshManager { } } catch (error) { console.error("[Token Refresh] Token refresh error:", error); + + // Don't redirect on timeout - might be deployment in progress + const isTimeout = + error instanceof Error && error.message.includes("timeout"); + if (isTimeout) { + console.warn( + "[Token Refresh] Timeout - server might be deploying, will retry on schedule" + ); + this.scheduleNextRefresh(); + return false; + } + this.handleRefreshFailure(); return false; } finally { diff --git a/src/routes/account.tsx b/src/routes/account.tsx index 180b9af..ea1e95d 100644 --- a/src/routes/account.tsx +++ b/src/routes/account.tsx @@ -15,9 +15,7 @@ import Input from "~/components/ui/Input"; import PasswordInput from "~/components/ui/PasswordInput"; import Button from "~/components/ui/Button"; import FormFeedback from "~/components/ui/FormFeedback"; - import type { UserProfile } from "~/types/user"; -import PasswordStrengthMeter from "~/components/PasswordStrengthMeter"; const getUserProfile = query(async (): Promise => { "use server"; @@ -672,7 +670,6 @@ export default function AccountPage() { } title="Please enter a valid email address" label={userProfile().email ? "Update Email" : "Add Email"} - containerClass="input-group mx-4" />