developing #10
|
|
@ -44,6 +44,7 @@ const RECONNECT_BASE_DELAY_MS = 1000
|
||||||
const RECONNECT_MAX_DELAY_MS = 30_000
|
const RECONNECT_MAX_DELAY_MS = 30_000
|
||||||
const INACTIVITY_TIMEOUT_MS = 5 * 60_000 // 5 minutes of inactivity before pausing
|
const INACTIVITY_TIMEOUT_MS = 5 * 60_000 // 5 minutes of inactivity before pausing
|
||||||
const CONNECTION_TIMEOUT_MS = 15_000 // 15s timeout for initial connection
|
const CONNECTION_TIMEOUT_MS = 15_000 // 15s timeout for initial connection
|
||||||
|
const STALE_THRESHOLD_MS = 60_000 // 60s of inactivity = force fresh reconnect on return
|
||||||
|
|
||||||
export class RealtimeConnection {
|
export class RealtimeConnection {
|
||||||
private channel: RealtimeChannel | null = null
|
private channel: RealtimeChannel | null = null
|
||||||
|
|
@ -58,6 +59,7 @@ export class RealtimeConnection {
|
||||||
private callbacks: RealtimeCallbacks
|
private callbacks: RealtimeCallbacks
|
||||||
private isDestroyed = false
|
private isDestroyed = false
|
||||||
private isPaused = false
|
private isPaused = false
|
||||||
|
private lastActivityTime = Date.now()
|
||||||
private supabase = createClient()
|
private supabase = createClient()
|
||||||
|
|
||||||
constructor(projectId: string, userId: string, displayName: string, callbacks: RealtimeCallbacks) {
|
constructor(projectId: string, userId: string, displayName: string, callbacks: RealtimeCallbacks) {
|
||||||
|
|
@ -70,6 +72,7 @@ export class RealtimeConnection {
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
if (this.isDestroyed) return
|
if (this.isDestroyed) return
|
||||||
this.isPaused = false
|
this.isPaused = false
|
||||||
|
this.lastActivityTime = Date.now()
|
||||||
this.callbacks.onConnectionStateChange('connecting')
|
this.callbacks.onConnectionStateChange('connecting')
|
||||||
this.resetInactivityTimer()
|
this.resetInactivityTimer()
|
||||||
this.clearConnectionTimer()
|
this.clearConnectionTimer()
|
||||||
|
|
@ -244,15 +247,43 @@ export class RealtimeConnection {
|
||||||
/**
|
/**
|
||||||
* Notify that user activity has occurred, resetting the inactivity timer.
|
* Notify that user activity has occurred, resetting the inactivity timer.
|
||||||
* If the connection was paused due to inactivity, it will resume.
|
* If the connection was paused due to inactivity, it will resume.
|
||||||
|
* If the connection was idle for a significant period, force a fresh reconnect
|
||||||
|
* to ensure the WebSocket isn't stale.
|
||||||
*/
|
*/
|
||||||
notifyActivity(): void {
|
notifyActivity(): void {
|
||||||
if (this.isDestroyed) return
|
if (this.isDestroyed) return
|
||||||
|
const now = Date.now()
|
||||||
|
const idleDuration = now - this.lastActivityTime
|
||||||
|
this.lastActivityTime = now
|
||||||
this.resetInactivityTimer()
|
this.resetInactivityTimer()
|
||||||
|
|
||||||
if (this.isPaused) {
|
if (this.isPaused) {
|
||||||
this.resume()
|
this.resume()
|
||||||
|
} else if (idleDuration > STALE_THRESHOLD_MS && this.channel) {
|
||||||
|
// Connection may appear alive but WebSocket could be stale after idle.
|
||||||
|
// Force a fresh reconnect to ensure broadcasts actually work.
|
||||||
|
this.forceReconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force a fresh reconnect by tearing down the current channel and reconnecting.
|
||||||
|
*/
|
||||||
|
private async forceReconnect(): Promise<void> {
|
||||||
|
if (this.isDestroyed || this.isPaused) return
|
||||||
|
this.stopHeartbeat()
|
||||||
|
this.clearReconnectTimer()
|
||||||
|
this.clearConnectionTimer()
|
||||||
|
|
||||||
|
if (this.channel) {
|
||||||
|
this.supabase.removeChannel(this.channel)
|
||||||
|
this.channel = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
await this.connect()
|
||||||
|
}
|
||||||
|
|
||||||
getIsPaused(): boolean {
|
getIsPaused(): boolean {
|
||||||
return this.isPaused
|
return this.isPaused
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue