2 changed files with 153 additions and 1 deletions
@ -0,0 +1,132 @@
@@ -0,0 +1,132 @@
|
||||
/** |
||||
* Connection Tracker Service |
||||
* Sends periodic heartbeats to the server to indicate active connection |
||||
*/ |
||||
|
||||
const HEARTBEAT_INTERVAL = 30 * 1000; // Send heartbeat every 30 seconds
|
||||
const SESSION_ID_KEY = 'connection_session_id'; |
||||
let heartbeatInterval: NodeJS.Timeout | null = null; |
||||
let isTracking = false; |
||||
let visibilityCleanup: (() => void) | null = null; |
||||
let currentVideo: { title: string; channelName: string } | null = null; |
||||
|
||||
/** |
||||
* Get or create a unique session ID for this browser window/tab |
||||
* Uses localStorage which is isolated per incognito window |
||||
*/ |
||||
function getSessionId(): string { |
||||
try { |
||||
let sessionId = localStorage.getItem(SESSION_ID_KEY); |
||||
if (!sessionId) { |
||||
// Generate a unique session ID for this window/tab
|
||||
sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; |
||||
localStorage.setItem(SESSION_ID_KEY, sessionId); |
||||
} |
||||
return sessionId; |
||||
} catch (error) { |
||||
// Fallback if localStorage is not available
|
||||
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Start sending heartbeats to the server |
||||
*/ |
||||
export function startConnectionTracking(): void { |
||||
if (isTracking) { |
||||
return; // Already tracking
|
||||
} |
||||
|
||||
isTracking = true; |
||||
|
||||
// Send initial heartbeat immediately
|
||||
sendHeartbeat(); |
||||
|
||||
// Then send periodic heartbeats
|
||||
heartbeatInterval = setInterval(() => { |
||||
// Only send heartbeat if page is visible (not backgrounded on mobile)
|
||||
if (!document.hidden) { |
||||
sendHeartbeat(); |
||||
} |
||||
}, HEARTBEAT_INTERVAL); |
||||
|
||||
// Handle page visibility changes (e.g., iPad locks, tab switches)
|
||||
// When page becomes visible again, send immediate heartbeat
|
||||
const handleVisibilityChange = () => { |
||||
if (!document.hidden && isTracking) { |
||||
// Page became visible - send heartbeat immediately
|
||||
sendHeartbeat(); |
||||
} |
||||
}; |
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange); |
||||
|
||||
// Store cleanup function
|
||||
visibilityCleanup = () => { |
||||
document.removeEventListener('visibilitychange', handleVisibilityChange); |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Stop sending heartbeats |
||||
*/ |
||||
export function stopConnectionTracking(): void { |
||||
if (heartbeatInterval) { |
||||
clearInterval(heartbeatInterval); |
||||
heartbeatInterval = null; |
||||
} |
||||
|
||||
// Clean up visibility change listener
|
||||
if (visibilityCleanup) { |
||||
visibilityCleanup(); |
||||
visibilityCleanup = null; |
||||
} |
||||
|
||||
isTracking = false; |
||||
} |
||||
|
||||
/** |
||||
* Get the current route/pathname |
||||
*/ |
||||
function getCurrentRoute(): string { |
||||
// Use window.location.pathname to get current route
|
||||
// This works outside React Router context
|
||||
return window.location.pathname || '/'; |
||||
} |
||||
|
||||
/** |
||||
* Set the current video being watched (call when video player opens) |
||||
*/ |
||||
export function setCurrentVideo(video: { title: string; channelName: string } | null): void { |
||||
currentVideo = video; |
||||
// Send immediate heartbeat when video changes
|
||||
if (isTracking) { |
||||
sendHeartbeat(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Send a heartbeat to the server with the session ID, current route, video info, and time limit usage |
||||
*/ |
||||
async function sendHeartbeat(): Promise<void> { |
||||
try { |
||||
const { settingsApi } = await import('./apiClient'); |
||||
const { getTimeUsedToday, getDailyLimitSync } = await import('./timeLimitService'); |
||||
|
||||
const sessionId = getSessionId(); |
||||
const route = getCurrentRoute(); |
||||
const timeUsed = getTimeUsedToday(); |
||||
const dailyLimit = getDailyLimitSync(); |
||||
|
||||
await settingsApi.heartbeat( |
||||
sessionId,
|
||||
route,
|
||||
currentVideo ? { title: currentVideo.title, channelName: currentVideo.channelName } : undefined, |
||||
{ timeUsed, dailyLimit } |
||||
); |
||||
} catch (error) { |
||||
// Silently fail - don't spam console with errors
|
||||
// Connection tracking is not critical for app functionality
|
||||
console.debug('Heartbeat failed:', error); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue