hopefully this solves random hanging

This commit is contained in:
Michael Freno
2026-01-07 20:53:21 -05:00
parent 547bc6c452
commit ca28237d13
6 changed files with 334 additions and 13 deletions

View File

@@ -19,7 +19,7 @@ import { createWindowWidth, isMobile } from "~/lib/resize-utils";
import { MOBILE_CONFIG } from "./config"; import { MOBILE_CONFIG } from "./config";
import CustomScrollbar from "./components/CustomScrollbar"; import CustomScrollbar from "./components/CustomScrollbar";
import { initPerformanceTracking } from "~/lib/performance-tracking"; import { initPerformanceTracking } from "~/lib/performance-tracking";
import { tokenRefreshManager } from "~/lib/token-refresh"; import { startDeploymentMonitoring } from "~/lib/deployment-detection";
function AppLayout(props: { children: any }) { function AppLayout(props: { children: any }) {
const { const {
@@ -35,6 +35,9 @@ function AppLayout(props: { children: any }) {
// Initialize performance tracking // Initialize performance tracking
initPerformanceTracking(); initPerformanceTracking();
// Start monitoring for new deployments
startDeploymentMonitoring();
const windowWidth = createWindowWidth(); const windowWidth = createWindowWidth();
createEffect(() => { createEffect(() => {

View File

@@ -1,17 +1,108 @@
// @refresh reload // @refresh reload
import { mount, StartClient } from "@solidjs/start/client"; 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 = `
<strong>Update Required</strong><br>
A new version is available. Please refresh the page manually or
<a href="javascript:void(0)" onclick="location.reload()" style="color: #000; text-decoration: underline; font-weight: bold;">click here</a>.
`;
document.body.appendChild(errorDiv);
}
}
// Handle runtime chunk loading errors
window.addEventListener("error", (event) => { window.addEventListener("error", (event) => {
if ( if (
event.message?.includes("Importing a module script failed") || event.message?.includes("Importing a module script failed") ||
event.message?.includes("Failed to fetch dynamically imported module") event.message?.includes("Failed to fetch dynamically imported module")
) { ) {
console.warn("Chunk load error detected, reloading page..."); event.preventDefault();
window.location.reload(); handleChunkError("error event");
} }
}); });
// Handle promise-based chunk loading errors
window.addEventListener("unhandledrejection", (event) => { window.addEventListener("unhandledrejection", (event) => {
if ( if (
event.reason?.message?.includes("Importing a module script failed") || event.reason?.message?.includes("Importing a module script failed") ||
@@ -19,10 +110,18 @@ window.addEventListener("unhandledrejection", (event) => {
"Failed to fetch dynamically imported module" "Failed to fetch dynamically imported module"
) )
) { ) {
console.warn("Chunk load error detected, reloading page...");
event.preventDefault(); 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(() => <StartClient />, document.getElementById("app")!); mount(() => <StartClient />, document.getElementById("app")!);

View File

@@ -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<boolean> {
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 = `
<style>
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
</style>
<div style="display: flex; flex-direction: column; gap: 12px;">
<div>
<strong>🎉 New Update Available</strong><br>
<span style="font-size: 13px; opacity: 0.9;">A new version of the app is ready.</span>
</div>
<div style="display: flex; gap: 8px;">
<button onclick="location.reload()" style="
background: white;
color: #3b82f6;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
flex: 1;
">Update Now</button>
<button onclick="this.closest('div').parentElement.parentElement.remove()" style="
background: rgba(255,255,255,0.2);
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
">Later</button>
</div>
</div>
`;
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);
};
}
}

View File

@@ -211,9 +211,14 @@ class TokenRefreshManager {
`[Token Refresh] Using rememberMe: ${rememberMe} (from refresh token cookie existence)` `[Token Refresh] Using rememberMe: ${rememberMe} (from refresh token cookie existence)`
); );
const result = await api.auth.refreshToken.mutate({ const result = await Promise.race([
api.auth.refreshToken.mutate({
rememberMe rememberMe
}); }),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Token refresh timeout")), 10000)
)
]);
if (result.success) { if (result.success) {
console.log("[Token Refresh] Token refreshed successfully"); console.log("[Token Refresh] Token refreshed successfully");
@@ -230,6 +235,18 @@ class TokenRefreshManager {
} }
} catch (error) { } catch (error) {
console.error("[Token Refresh] Token refresh error:", 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(); this.handleRefreshFailure();
return false; return false;
} finally { } finally {

View File

@@ -15,9 +15,7 @@ import Input from "~/components/ui/Input";
import PasswordInput from "~/components/ui/PasswordInput"; import PasswordInput from "~/components/ui/PasswordInput";
import Button from "~/components/ui/Button"; import Button from "~/components/ui/Button";
import FormFeedback from "~/components/ui/FormFeedback"; import FormFeedback from "~/components/ui/FormFeedback";
import type { UserProfile } from "~/types/user"; import type { UserProfile } from "~/types/user";
import PasswordStrengthMeter from "~/components/PasswordStrengthMeter";
const getUserProfile = query(async (): Promise<UserProfile | null> => { const getUserProfile = query(async (): Promise<UserProfile | null> => {
"use server"; "use server";
@@ -672,7 +670,6 @@ export default function AccountPage() {
} }
title="Please enter a valid email address" title="Please enter a valid email address"
label={userProfile().email ? "Update Email" : "Add Email"} label={userProfile().email ? "Update Email" : "Add Email"}
containerClass="input-group mx-4"
/> />
<Show <Show
when={ when={

View File

@@ -1,11 +1,20 @@
{ {
"headers": [ "headers": [
{
"source": "/",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=0, must-revalidate"
}
]
},
{ {
"source": "/(.*)", "source": "/(.*)",
"headers": [ "headers": [
{ {
"key": "Cache-Control", "key": "Cache-Control",
"value": "public, max-age=0, stale-while-revalidate=60" "value": "public, max-age=0, must-revalidate"
} }
] ]
}, },