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 { 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(() => {
|
||||||
|
|||||||
@@ -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")!);
|
||||||
|
|||||||
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)`
|
`[Token Refresh] Using rememberMe: ${rememberMe} (from refresh token cookie existence)`
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await api.auth.refreshToken.mutate({
|
const result = await Promise.race([
|
||||||
rememberMe
|
api.auth.refreshToken.mutate({
|
||||||
});
|
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 {
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
11
vercel.json
11
vercel.json
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user