311 lines
7.9 KiB
TypeScript
311 lines
7.9 KiB
TypeScript
/**
|
|
* Reload Loop Detection and Prevention Utility
|
|
* Advanced mobile Safari reload loop prevention system
|
|
*/
|
|
|
|
interface PageLoadInfo {
|
|
url: string;
|
|
timestamp: number;
|
|
userAgent: string;
|
|
loadCount: number;
|
|
}
|
|
|
|
interface ReloadLoopState {
|
|
enabled: boolean;
|
|
pageLoads: PageLoadInfo[];
|
|
blockedPages: Set<string>;
|
|
emergencyMode: boolean;
|
|
debugMode: boolean;
|
|
}
|
|
|
|
const RELOAD_LOOP_THRESHOLD = 5; // Max page loads in time window
|
|
const TIME_WINDOW = 10000; // 10 seconds
|
|
const EMERGENCY_BLOCK_TIME = 30000; // 30 seconds
|
|
const STORAGE_KEY = 'reload_loop_prevention';
|
|
|
|
/**
|
|
* Get or initialize reload loop state
|
|
*/
|
|
function getReloadLoopState(): ReloadLoopState {
|
|
if (typeof window === 'undefined') {
|
|
return {
|
|
enabled: false,
|
|
pageLoads: [],
|
|
blockedPages: new Set(),
|
|
emergencyMode: false,
|
|
debugMode: false
|
|
};
|
|
}
|
|
|
|
// Use sessionStorage for persistence across reloads
|
|
try {
|
|
const stored = sessionStorage.getItem(STORAGE_KEY);
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored);
|
|
return {
|
|
...parsed,
|
|
blockedPages: new Set(parsed.blockedPages || [])
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.warn('[reload-prevention] Failed to parse stored state:', error);
|
|
}
|
|
|
|
// Initialize new state
|
|
const initialState: ReloadLoopState = {
|
|
enabled: true,
|
|
pageLoads: [],
|
|
blockedPages: new Set(),
|
|
emergencyMode: false,
|
|
debugMode: process.env.NODE_ENV === 'development'
|
|
};
|
|
|
|
saveReloadLoopState(initialState);
|
|
return initialState;
|
|
}
|
|
|
|
/**
|
|
* Save reload loop state to sessionStorage
|
|
*/
|
|
function saveReloadLoopState(state: ReloadLoopState): void {
|
|
if (typeof window === 'undefined') return;
|
|
|
|
try {
|
|
const toStore = {
|
|
...state,
|
|
blockedPages: Array.from(state.blockedPages)
|
|
};
|
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
|
|
} catch (error) {
|
|
console.warn('[reload-prevention] Failed to save state:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record a page load and check for reload loops
|
|
*/
|
|
export function recordPageLoad(url: string): boolean {
|
|
const state = getReloadLoopState();
|
|
|
|
if (!state.enabled) {
|
|
return true; // Allow load
|
|
}
|
|
|
|
const now = Date.now();
|
|
const userAgent = navigator.userAgent || '';
|
|
|
|
// Clean old page loads
|
|
state.pageLoads = state.pageLoads.filter(
|
|
load => now - load.timestamp < TIME_WINDOW
|
|
);
|
|
|
|
// Find existing load for this URL
|
|
const existingLoad = state.pageLoads.find(load => load.url === url);
|
|
|
|
if (existingLoad) {
|
|
existingLoad.loadCount++;
|
|
existingLoad.timestamp = now;
|
|
} else {
|
|
state.pageLoads.push({
|
|
url,
|
|
timestamp: now,
|
|
userAgent,
|
|
loadCount: 1
|
|
});
|
|
}
|
|
|
|
// Check for reload loop
|
|
const urlLoads = state.pageLoads.filter(load => load.url === url);
|
|
const totalLoads = urlLoads.reduce((sum, load) => sum + load.loadCount, 0);
|
|
|
|
if (totalLoads >= RELOAD_LOOP_THRESHOLD) {
|
|
console.error(`[reload-prevention] Reload loop detected for ${url} (${totalLoads} loads)`);
|
|
|
|
// Block this page
|
|
state.blockedPages.add(url);
|
|
state.emergencyMode = true;
|
|
|
|
// Set emergency timeout
|
|
setTimeout(() => {
|
|
state.emergencyMode = false;
|
|
state.blockedPages.delete(url);
|
|
saveReloadLoopState(state);
|
|
console.log(`[reload-prevention] Emergency block lifted for ${url}`);
|
|
}, EMERGENCY_BLOCK_TIME);
|
|
|
|
saveReloadLoopState(state);
|
|
return false; // Block load
|
|
}
|
|
|
|
saveReloadLoopState(state);
|
|
return true; // Allow load
|
|
}
|
|
|
|
/**
|
|
* Check if a page is currently blocked
|
|
*/
|
|
export function isPageBlocked(url: string): boolean {
|
|
const state = getReloadLoopState();
|
|
return state.blockedPages.has(url);
|
|
}
|
|
|
|
/**
|
|
* Initialize reload loop prevention for a page
|
|
*/
|
|
export function initReloadLoopPrevention(pageName: string): boolean {
|
|
if (typeof window === 'undefined') {
|
|
return true;
|
|
}
|
|
|
|
const currentUrl = window.location.pathname;
|
|
const canLoad = recordPageLoad(currentUrl);
|
|
|
|
if (!canLoad) {
|
|
console.error(`[reload-prevention] Page load blocked: ${pageName} (${currentUrl})`);
|
|
|
|
// Show emergency message
|
|
showEmergencyMessage(pageName);
|
|
return false;
|
|
}
|
|
|
|
if (getReloadLoopState().debugMode) {
|
|
console.log(`[reload-prevention] Page load allowed: ${pageName} (${currentUrl})`);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Show emergency message when page is blocked
|
|
*/
|
|
function showEmergencyMessage(pageName: string): void {
|
|
const message = `
|
|
<div style="
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(163, 21, 21, 0.95);
|
|
color: white;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 10000;
|
|
font-family: Arial, sans-serif;
|
|
text-align: center;
|
|
padding: 20px;
|
|
">
|
|
<h1 style="font-size: 24px; margin-bottom: 20px;">Page Loading Temporarily Blocked</h1>
|
|
<p style="font-size: 16px; margin-bottom: 20px;">
|
|
Multiple rapid page loads detected for ${pageName}.<br>
|
|
This is a safety measure to prevent infinite loading loops.
|
|
</p>
|
|
<p style="font-size: 14px; margin-bottom: 30px;">
|
|
The block will be automatically lifted in 30 seconds.
|
|
</p>
|
|
<button onclick="location.href='/'" style="
|
|
background: white;
|
|
color: #a31515;
|
|
border: none;
|
|
padding: 12px 24px;
|
|
border-radius: 6px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
margin-right: 15px;
|
|
">Return to Home</button>
|
|
<button onclick="window.history.back()" style="
|
|
background: transparent;
|
|
color: white;
|
|
border: 2px solid white;
|
|
padding: 12px 24px;
|
|
border-radius: 6px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
">Go Back</button>
|
|
</div>
|
|
`;
|
|
|
|
document.body.innerHTML = message;
|
|
}
|
|
|
|
/**
|
|
* Clear reload loop prevention state (for testing)
|
|
*/
|
|
export function clearReloadLoopPrevention(): void {
|
|
if (typeof window === 'undefined') return;
|
|
|
|
try {
|
|
sessionStorage.removeItem(STORAGE_KEY);
|
|
console.log('[reload-prevention] State cleared');
|
|
} catch (error) {
|
|
console.warn('[reload-prevention] Failed to clear state:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current reload loop prevention status
|
|
*/
|
|
export function getReloadLoopStatus(): {
|
|
enabled: boolean;
|
|
emergencyMode: boolean;
|
|
blockedPages: string[];
|
|
pageLoads: PageLoadInfo[];
|
|
} {
|
|
const state = getReloadLoopState();
|
|
return {
|
|
enabled: state.enabled,
|
|
emergencyMode: state.emergencyMode,
|
|
blockedPages: Array.from(state.blockedPages),
|
|
pageLoads: state.pageLoads
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mobile Safari specific optimizations
|
|
*/
|
|
export function applyMobileSafariReloadLoopFixes(): void {
|
|
if (typeof window === 'undefined') return;
|
|
|
|
const userAgent = navigator.userAgent || '';
|
|
const isMobileSafari = /iPad|iPhone|iPod/.test(userAgent) && /Safari/i.test(userAgent) && !/Chrome/i.test(userAgent);
|
|
|
|
if (!isMobileSafari) return;
|
|
|
|
console.log('[reload-prevention] Applying mobile Safari specific fixes');
|
|
|
|
// Prevent Safari's aggressive caching that can cause reload loops
|
|
window.addEventListener('pageshow', (event) => {
|
|
if (event.persisted) {
|
|
console.log('[reload-prevention] Page restored from bfcache');
|
|
// Force a small delay to prevent immediate re-execution
|
|
setTimeout(() => {
|
|
console.log('[reload-prevention] bfcache restore handled');
|
|
}, 100);
|
|
}
|
|
});
|
|
|
|
// Handle Safari's navigation timing issues
|
|
window.addEventListener('beforeunload', () => {
|
|
// Mark navigation in progress to prevent reload loop detection
|
|
const state = getReloadLoopState();
|
|
state.enabled = false;
|
|
saveReloadLoopState(state);
|
|
});
|
|
|
|
// Re-enable after navigation completes
|
|
window.addEventListener('load', () => {
|
|
setTimeout(() => {
|
|
const state = getReloadLoopState();
|
|
state.enabled = true;
|
|
saveReloadLoopState(state);
|
|
}, 500);
|
|
});
|
|
}
|
|
|
|
// Auto-initialize on module load
|
|
if (typeof window !== 'undefined') {
|
|
applyMobileSafariReloadLoopFixes();
|
|
}
|