hopefully this solves random hanging
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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 = `
|
||||
<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) => {
|
||||
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(() => <StartClient />, document.getElementById("app")!);
|
||||
|
||||
196
src/lib/deployment-detection.ts
Normal file
196
src/lib/deployment-detection.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<never>((_, 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 {
|
||||
|
||||
@@ -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<UserProfile | null> => {
|
||||
"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"
|
||||
/>
|
||||
<Show
|
||||
when={
|
||||
|
||||
11
vercel.json
11
vercel.json
@@ -1,11 +1,20 @@
|
||||
{
|
||||
"headers": [
|
||||
{
|
||||
"source": "/",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=0, must-revalidate"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=0, stale-while-revalidate=60"
|
||||
"value": "public, max-age=0, must-revalidate"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user