232 lines
6.2 KiB
TypeScript
232 lines
6.2 KiB
TypeScript
// =====================================================
|
||
// 客戶端連線清理腳本
|
||
// =====================================================
|
||
|
||
export class ClientConnectionCleanup {
|
||
private static instance: ClientConnectionCleanup;
|
||
private isCleaning = false;
|
||
private clientId: string;
|
||
|
||
private constructor() {
|
||
this.clientId = this.generateClientId();
|
||
this.setupEventListeners();
|
||
}
|
||
|
||
public static getInstance(): ClientConnectionCleanup {
|
||
if (!ClientConnectionCleanup.instance) {
|
||
ClientConnectionCleanup.instance = new ClientConnectionCleanup();
|
||
}
|
||
return ClientConnectionCleanup.instance;
|
||
}
|
||
|
||
// 生成客戶端唯一標識符
|
||
private generateClientId(): string {
|
||
const timestamp = Date.now();
|
||
const random = Math.random().toString(36).substring(2);
|
||
return `client_${timestamp}_${random}`;
|
||
}
|
||
|
||
// 設置事件監聽器
|
||
private setupEventListeners() {
|
||
// 確保在瀏覽器環境中執行
|
||
if (typeof window === 'undefined') {
|
||
console.log('🖥️ 客戶端連線清理跳過(服務端環境)');
|
||
return;
|
||
}
|
||
|
||
// 頁面關閉前清理
|
||
window.addEventListener('beforeunload', () => {
|
||
this.cleanupOnUnload();
|
||
});
|
||
|
||
// 頁面隱藏時清理
|
||
document.addEventListener('visibilitychange', () => {
|
||
if (document.hidden) {
|
||
this.cleanupOnHidden();
|
||
}
|
||
});
|
||
|
||
// 頁面卸載時清理
|
||
window.addEventListener('unload', () => {
|
||
this.cleanupOnUnload();
|
||
});
|
||
|
||
// 定期清理(每5分鐘)
|
||
setInterval(() => {
|
||
this.periodicCleanup();
|
||
}, 5 * 60 * 1000);
|
||
|
||
console.log('🖥️ 客戶端連線清理已啟動');
|
||
}
|
||
|
||
// 頁面關閉前清理
|
||
private async cleanupOnUnload() {
|
||
if (this.isCleaning) return;
|
||
|
||
// 確保在瀏覽器環境中執行
|
||
if (typeof window === 'undefined') return;
|
||
|
||
this.isCleaning = true;
|
||
console.log('🔄 頁面關閉前清理連線...');
|
||
|
||
try {
|
||
// 使用 sendBeacon 確保請求能夠發送
|
||
const data = JSON.stringify({
|
||
action: 'cleanup-current',
|
||
clientId: this.clientId,
|
||
timestamp: Date.now()
|
||
});
|
||
|
||
// 嘗試使用 sendBeacon
|
||
if (navigator.sendBeacon) {
|
||
navigator.sendBeacon('/api/ip-cleanup', data);
|
||
console.log('✅ 使用 sendBeacon 發送清理請求');
|
||
} else {
|
||
// 備用方案:使用 fetch
|
||
await fetch('/api/ip-cleanup', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: data,
|
||
keepalive: true
|
||
});
|
||
console.log('✅ 使用 fetch 發送清理請求');
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 清理請求發送失敗:', error);
|
||
}
|
||
}
|
||
|
||
// 頁面隱藏時清理
|
||
private async cleanupOnHidden() {
|
||
// 確保在瀏覽器環境中執行
|
||
if (typeof window === 'undefined') return;
|
||
|
||
console.log('🔄 頁面隱藏時清理連線...');
|
||
|
||
try {
|
||
await fetch('/api/ip-cleanup', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
action: 'cleanup-current',
|
||
clientId: this.clientId,
|
||
reason: 'page-hidden'
|
||
})
|
||
});
|
||
console.log('✅ 頁面隱藏清理完成');
|
||
} catch (error) {
|
||
console.error('❌ 頁面隱藏清理失敗:', error);
|
||
}
|
||
}
|
||
|
||
// 定期清理
|
||
private async periodicCleanup() {
|
||
// 確保在瀏覽器環境中執行
|
||
if (typeof window === 'undefined') return;
|
||
|
||
console.log('🔄 定期清理連線...');
|
||
|
||
try {
|
||
const response = await fetch('/api/ip-cleanup?action=local-stats');
|
||
const data = await response.json();
|
||
|
||
if (data.success && data.data.trackedConnections > 10) {
|
||
// 如果追蹤的連線數超過10個,執行清理
|
||
await fetch('/api/ip-cleanup', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
action: 'cleanup-local',
|
||
clientId: this.clientId,
|
||
reason: 'periodic-cleanup'
|
||
})
|
||
});
|
||
console.log('✅ 定期清理完成');
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 定期清理失敗:', error);
|
||
}
|
||
}
|
||
|
||
// 手動清理
|
||
public async manualCleanup(): Promise<boolean> {
|
||
// 確保在瀏覽器環境中執行
|
||
if (typeof window === 'undefined') return false;
|
||
|
||
try {
|
||
const response = await fetch('/api/ip-cleanup', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
action: 'cleanup-current',
|
||
clientId: this.clientId,
|
||
reason: 'manual-cleanup'
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
console.log('✅ 手動清理完成:', data.message);
|
||
return true;
|
||
} else {
|
||
console.error('❌ 手動清理失敗:', data.error);
|
||
return false;
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 手動清理錯誤:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 獲取連線狀態
|
||
public async getConnectionStatus() {
|
||
// 確保在瀏覽器環境中執行
|
||
if (typeof window === 'undefined') return null;
|
||
|
||
try {
|
||
const response = await fetch('/api/ip-cleanup?action=status');
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
console.log('📊 連線狀態:', data.data);
|
||
return data.data;
|
||
} else {
|
||
console.error('❌ 獲取連線狀態失敗:', data.error);
|
||
return null;
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 獲取連線狀態錯誤:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 獲取客戶端 ID
|
||
public getClientId(): string {
|
||
return this.clientId;
|
||
}
|
||
}
|
||
|
||
// 導出單例實例
|
||
export const clientCleanup = ClientConnectionCleanup.getInstance();
|
||
|
||
// 在模組載入時自動初始化(只在瀏覽器環境中)
|
||
if (typeof window !== 'undefined') {
|
||
// 延遲初始化,確保 DOM 已載入
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
clientCleanup;
|
||
});
|
||
} else {
|
||
clientCleanup;
|
||
}
|
||
}
|