feat: integrate Socket.IO for real-time communication; add socket connection management and token refresh handling
This commit is contained in:
@@ -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
43
src/lib/events.ts
Normal 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
|
||||
149
src/lib/token-refresh-manager.ts
Normal file
149
src/lib/token-refresh-manager.ts
Normal 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()
|
||||
Reference in New Issue
Block a user