feat: integrate Socket.IO for real-time communication; add socket connection management and token refresh handling

This commit is contained in:
JSC
2025-07-27 13:44:00 +02:00
parent 6018a5c8c5
commit 5892d02e9f
10 changed files with 421 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
import { API_CONFIG } from './config'
import { createApiError, NetworkError, TimeoutError } from './errors'
import type { ApiClient, ApiRequestConfig, HttpMethod } from './types'
import { authEvents, AUTH_EVENTS } from '../events'
export class BaseApiClient implements ApiClient {
private refreshPromise: Promise<void> | null = null
@@ -147,6 +148,9 @@ export class BaseApiClient implements ApiClient {
if (!response.ok) {
throw createApiError(response, await this.safeParseJSON(response))
}
// Emit token refresh event for socket reconnection (reactive refresh)
authEvents.emit(AUTH_EVENTS.TOKEN_REFRESHED)
}
private handleAuthenticationFailure(): void {

43
src/lib/events.ts Normal file
View File

@@ -0,0 +1,43 @@
/**
* Simple event emitter for cross-component communication
*/
type EventHandler = (...args: any[]) => void
class EventEmitter {
private events: Map<string, EventHandler[]> = new Map()
on(event: string, handler: EventHandler): void {
if (!this.events.has(event)) {
this.events.set(event, [])
}
this.events.get(event)!.push(handler)
}
off(event: string, handler: EventHandler): void {
const handlers = this.events.get(event)
if (handlers) {
const index = handlers.indexOf(handler)
if (index > -1) {
handlers.splice(index, 1)
}
}
}
emit(event: string, ...args: any[]): void {
const handlers = this.events.get(event)
if (handlers) {
handlers.forEach(handler => handler(...args))
}
}
}
export const authEvents = new EventEmitter()
// Auth event types
export const AUTH_EVENTS = {
TOKEN_REFRESHED: 'token_refreshed',
TOKEN_EXPIRED: 'token_expired',
LOGIN_SUCCESS: 'login_success',
LOGOUT: 'logout',
} as const

View File

@@ -0,0 +1,149 @@
/**
* Token refresh manager for proactive token refresh
*/
import { authEvents, AUTH_EVENTS } from './events'
import { api } from './api'
export class TokenRefreshManager {
private refreshTimer: NodeJS.Timeout | null = null
private refreshInterval = 10 * 60 * 1000 // Refresh every 10 minutes (tokens expire in 15 min)
private isRefreshing = false
private isEnabled = false
/**
* Start proactive token refresh monitoring
*/
start(): void {
// Prevent multiple starts
if (this.isEnabled) {
return
}
this.isEnabled = true
this.scheduleNextRefresh()
// Listen for visibility changes to handle tab switching
document.addEventListener('visibilitychange', this.handleVisibilityChange)
// Listen for successful auth events to reschedule
authEvents.on(AUTH_EVENTS.LOGIN_SUCCESS, this.handleAuthSuccess)
authEvents.on(AUTH_EVENTS.TOKEN_REFRESHED, this.handleTokenRefreshed)
}
/**
* Stop proactive token refresh monitoring
*/
stop(): void {
if (!this.isEnabled) {
return
}
this.isEnabled = false
this.clearRefreshTimer()
document.removeEventListener('visibilitychange', this.handleVisibilityChange)
authEvents.off(AUTH_EVENTS.LOGIN_SUCCESS, this.handleAuthSuccess)
authEvents.off(AUTH_EVENTS.TOKEN_REFRESHED, this.handleTokenRefreshed)
}
/**
* Schedule the next token refresh using fixed interval
*/
private scheduleNextRefresh = (): void => {
if (!this.isEnabled) return
this.clearRefreshTimer()
this.refreshTimer = setTimeout(() => {
this.performProactiveRefresh()
}, this.refreshInterval)
}
/**
* Perform proactive token refresh
*/
private performProactiveRefresh = async (): Promise<void> => {
if (this.isRefreshing || !this.isEnabled) return
this.isRefreshing = true
try {
// Use the API client's refresh method directly
await api.auth.refreshToken()
authEvents.emit(AUTH_EVENTS.TOKEN_REFRESHED)
// Schedule next refresh immediately since we just completed one
this.scheduleNextRefresh()
} catch {
// If refresh fails, try again in 1 minute
this.refreshTimer = setTimeout(() => {
this.performProactiveRefresh()
}, 60 * 1000)
} finally {
this.isRefreshing = false
}
}
/**
* Handle tab visibility changes
*/
private handleVisibilityChange = (): void => {
if (!document.hidden) {
// Tab became active - check if we need to refresh immediately
this.checkTokenFreshnessOnActivation()
}
}
/**
* Check token freshness when tab becomes active
*/
private checkTokenFreshnessOnActivation = async (): Promise<void> => {
try {
// Try to make an API call to see if token is still valid
await api.auth.getMe()
// Token is still valid, reschedule based on remaining time
this.scheduleNextRefresh()
} catch (error: unknown) {
const errorObj = error as { status?: number; message?: string }
if (errorObj?.status === 401 || errorObj?.message?.includes('401')) {
// Token is expired, trigger refresh immediately
this.performProactiveRefresh()
}
}
}
/**
* Handle successful auth events
*/
private handleAuthSuccess = (): void => {
this.scheduleNextRefresh()
}
/**
* Handle token refresh events
*/
private handleTokenRefreshed = (): void => {
// Only reschedule if this wasn't our own refresh to avoid double scheduling
if (!this.isRefreshing) {
this.scheduleNextRefresh()
}
}
/**
* Clear the current refresh timer
*/
private clearRefreshTimer(): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer)
this.refreshTimer = null
}
}
}
// Global token refresh manager instance
export const tokenRefreshManager = new TokenRefreshManager()