Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion packages/cloud/src/CloudService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { CloudSettingsService } from "./CloudSettingsService"
import { StaticSettingsService } from "./StaticSettingsService"
import { TelemetryClient } from "./TelemetryClient"
import { ShareService, TaskNotFoundError } from "./ShareService"
import { ConnectionMonitor } from "./ConnectionMonitor"
import { TelemetryQueueManager } from "./TelemetryQueueManager"

type AuthStateChangedPayload = CloudServiceEvents["auth-state-changed"][0]
type AuthUserInfoPayload = CloudServiceEvents["user-info"][0]
Expand All @@ -35,6 +37,8 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements vs
private settingsService: SettingsService | null = null
private telemetryClient: TelemetryClient | null = null
private shareService: ShareService | null = null
private connectionMonitor: ConnectionMonitor | null = null
private queueManager: TelemetryQueueManager | null = null
private isInitialized = false
private log: (...args: unknown[]) => void

Expand Down Expand Up @@ -87,9 +91,60 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements vs
this.settingsService = cloudSettingsService
}

this.telemetryClient = new TelemetryClient(this.authService, this.settingsService)
this.telemetryClient = new TelemetryClient(this.authService, this.settingsService, false, this.log)
this.shareService = new ShareService(this.authService, this.settingsService, this.log)

// Initialize connection monitor and queue manager
this.connectionMonitor = new ConnectionMonitor()
this.queueManager = TelemetryQueueManager.getInstance()

// Check if telemetry queue is enabled
let isQueueEnabled = true
try {
const { ContextProxy } = await import("../../../src/core/config/ContextProxy")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dynamic import for ContextProxy could fail. Consider adding more robust error handling:

Suggested change
const { ContextProxy } = await import("../../../src/core/config/ContextProxy")
try {
const { ContextProxy } = await import("../../../src/core/config/ContextProxy")
isQueueEnabled = ContextProxy.instance.getValue("telemetryQueueEnabled") ?? true
} catch (error) {
// Default to enabled if we can't access settings
this.log("[CloudService] Could not access telemetryQueueEnabled setting:", error)
isQueueEnabled = true
}

isQueueEnabled = ContextProxy.instance.getValue("telemetryQueueEnabled") ?? true
} catch (_error) {
// Default to enabled if we can't access settings
this.log("[CloudService] Could not access telemetryQueueEnabled setting, defaulting to enabled")
}

if (isQueueEnabled) {
// Set up connection monitoring with debouncing
let connectionRestoredDebounceTimer: NodeJS.Timeout | null = null
Copy link

Copilot AI Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debounce timer is declared in a closure but not properly cleaned up on dispose. This could lead to memory leaks if the service is disposed while a timer is pending.

Copilot uses AI. Check for mistakes.
const connectionRestoredDebounceDelay = 3000 // 3 seconds

this.connectionMonitor.onConnectionRestored(() => {
this.log("[CloudService] Connection restored, scheduling queue processing")

// Clear any existing timer
if (connectionRestoredDebounceTimer) {
clearTimeout(connectionRestoredDebounceTimer)
}

// Schedule queue processing with debounce
connectionRestoredDebounceTimer = setTimeout(() => {
this.queueManager
?.processQueue()
.then(() => {
this.log(
"[CloudService] Successfully processed queued events after connection restored",
)
})
.catch((error) => {
this.log("[CloudService] Error processing queue after connection restored:", error)
// Could implement retry logic here if needed in the future
})
}, connectionRestoredDebounceDelay)
})

// Start monitoring if authenticated
if (this.authService.isAuthenticated()) {
this.connectionMonitor.startMonitoring()
}
} else {
this.log("[CloudService] Telemetry queue is disabled")
}

try {
TelemetryService.instance.register(this.telemetryClient)
} catch (error) {
Expand Down Expand Up @@ -222,6 +277,34 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements vs
return this.shareService!.canShareTask()
}

// Connection Status

public isOnline(): boolean {
this.ensureInitialized()
return this.connectionMonitor?.getConnectionStatus() ?? true
}

public onConnectionRestored(callback: () => void): void {
this.ensureInitialized()
if (this.connectionMonitor) {
this.connectionMonitor.onConnectionRestored(callback)
}
}

public onConnectionLost(callback: () => void): void {
this.ensureInitialized()
if (this.connectionMonitor) {
this.connectionMonitor.onConnectionLost(callback)
}
}

public removeConnectionListener(event: "connection-restored" | "connection-lost", callback: () => void): void {
this.ensureInitialized()
if (this.connectionMonitor) {
this.connectionMonitor.removeListener(event, callback)
}
}

// Lifecycle

public dispose(): void {
Expand All @@ -235,6 +318,9 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements vs
}
this.settingsService.dispose()
}
if (this.connectionMonitor) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: Memory leak - The connectionRestoredDebounceTimer declared on line 113 is not cleaned up in the dispose() method. This could cause memory leaks if the service is disposed while a timer is pending.

Consider adding cleanup in the dispose method:

Suggested change
if (this.connectionMonitor) {
if (this.connectionMonitor) {
this.connectionMonitor.dispose();
}
// Clean up any pending debounce timer
if (connectionRestoredDebounceTimer) {
clearTimeout(connectionRestoredDebounceTimer);
}

this.connectionMonitor.dispose()
}

this.isInitialized = false
}
Expand Down
105 changes: 105 additions & 0 deletions packages/cloud/src/ConnectionMonitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { EventEmitter } from "events"
import { getRooCodeApiUrl } from "./Config"

export class ConnectionMonitor extends EventEmitter {
private isOnline = true
private checkInterval: NodeJS.Timeout | null = null
private readonly healthCheckEndpoint = "/api/health"
private readonly defaultCheckInterval = 30000 // 30 seconds

constructor() {
super()
}

/**
* Check if the connection to the API is available
*/
public async checkConnection(): Promise<boolean> {
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000) // 5 second timeout
Copy link

Copilot AI Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout value of 5000ms is hardcoded. Consider making this configurable or storing it as a class constant for better maintainability.

Suggested change
const timeoutId = setTimeout(() => controller.abort(), 5000) // 5 second timeout
const timeoutId = setTimeout(() => controller.abort(), this.defaultTimeoutMs) // 5 second timeout

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout value is still hardcoded as 5000ms. Consider making this configurable by adding a class constant:

Suggested change
const timeoutId = setTimeout(() => controller.abort(), 5000) // 5 second timeout
private readonly defaultTimeoutMs = 5000
const timeoutId = setTimeout(() => controller.abort(), this.defaultTimeoutMs)


const response = await fetch(`${getRooCodeApiUrl()}${this.healthCheckEndpoint}`, {
method: "GET",
signal: controller.signal,
})

clearTimeout(timeoutId)

const wasOffline = !this.isOnline
this.isOnline = response.ok

// Emit event if connection status changed from offline to online
if (wasOffline && this.isOnline) {
this.emit("connection-restored")
}

return this.isOnline
} catch (_error) {
const wasOnline = this.isOnline
this.isOnline = false

// Emit event if connection status changed from online to offline
if (wasOnline && !this.isOnline) {
this.emit("connection-lost")
}

return false
}
}

/**
* Get current connection status
*/
public getConnectionStatus(): boolean {
return this.isOnline
}

/**
* Register a callback for when connection is restored
*/
public onConnectionRestored(callback: () => void): void {
this.on("connection-restored", callback)
}

/**
* Register a callback for when connection is lost
*/
public onConnectionLost(callback: () => void): void {
this.on("connection-lost", callback)
}

/**
* Start monitoring the connection
*/
public startMonitoring(intervalMs: number = this.defaultCheckInterval): void {
// Stop any existing monitoring
this.stopMonitoring()

// Initial check
this.checkConnection()

// Set up periodic checks
this.checkInterval = setInterval(() => {
this.checkConnection()
}, intervalMs)
}

/**
* Stop monitoring the connection
*/
public stopMonitoring(): void {
if (this.checkInterval) {
clearInterval(this.checkInterval)
this.checkInterval = null
}
}

/**
* Clean up resources
*/
public dispose(): void {
this.stopMonitoring()
this.removeAllListeners()
}
}
Loading
Loading