ExtensionBridgeService.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. import crypto from "crypto"
  2. import {
  3. type TaskProviderLike,
  4. type TaskLike,
  5. type CloudUserInfo,
  6. type ExtensionBridgeCommand,
  7. type TaskBridgeCommand,
  8. ConnectionState,
  9. ExtensionSocketEvents,
  10. TaskSocketEvents,
  11. } from "@roo-code/types"
  12. import { SocketConnectionManager } from "./SocketConnectionManager.js"
  13. import { ExtensionManager } from "./ExtensionManager.js"
  14. import { TaskManager } from "./TaskManager.js"
  15. export interface ExtensionBridgeServiceOptions {
  16. userId: string
  17. socketBridgeUrl: string
  18. token: string
  19. provider: TaskProviderLike
  20. sessionId?: string
  21. }
  22. export class ExtensionBridgeService {
  23. private static instance: ExtensionBridgeService | null = null
  24. // Core
  25. private readonly userId: string
  26. private readonly socketBridgeUrl: string
  27. private readonly token: string
  28. private readonly provider: TaskProviderLike
  29. private readonly instanceId: string
  30. // Managers
  31. private connectionManager: SocketConnectionManager
  32. private extensionManager: ExtensionManager
  33. private taskManager: TaskManager
  34. // Reconnection
  35. private readonly MAX_RECONNECT_ATTEMPTS = Infinity
  36. private readonly RECONNECT_DELAY = 1_000
  37. private readonly RECONNECT_DELAY_MAX = 30_000
  38. public static getInstance(): ExtensionBridgeService | null {
  39. return ExtensionBridgeService.instance
  40. }
  41. public static async createInstance(options: ExtensionBridgeServiceOptions) {
  42. console.log("[ExtensionBridgeService] createInstance")
  43. ExtensionBridgeService.instance = new ExtensionBridgeService(options)
  44. await ExtensionBridgeService.instance.initialize()
  45. return ExtensionBridgeService.instance
  46. }
  47. public static resetInstance() {
  48. if (ExtensionBridgeService.instance) {
  49. console.log("[ExtensionBridgeService] resetInstance")
  50. ExtensionBridgeService.instance.disconnect().catch(() => {})
  51. ExtensionBridgeService.instance = null
  52. }
  53. }
  54. public static async handleRemoteControlState(
  55. userInfo: CloudUserInfo | null,
  56. remoteControlEnabled: boolean | undefined,
  57. options: ExtensionBridgeServiceOptions,
  58. logger?: (message: string) => void,
  59. ) {
  60. if (userInfo?.extensionBridgeEnabled && remoteControlEnabled) {
  61. const existingService = ExtensionBridgeService.getInstance()
  62. if (!existingService) {
  63. try {
  64. const service = await ExtensionBridgeService.createInstance(options)
  65. const state = service.getConnectionState()
  66. logger?.(`[ExtensionBridgeService#handleRemoteControlState] Instance created (state: ${state})`)
  67. if (state !== ConnectionState.CONNECTED) {
  68. logger?.(
  69. `[ExtensionBridgeService#handleRemoteControlState] Service is not connected yet, will retry in background`,
  70. )
  71. }
  72. } catch (error) {
  73. const message = `[ExtensionBridgeService#handleRemoteControlState] Failed to create instance: ${
  74. error instanceof Error ? error.message : String(error)
  75. }`
  76. logger?.(message)
  77. console.error(message)
  78. }
  79. } else {
  80. const state = existingService.getConnectionState()
  81. if (state === ConnectionState.FAILED || state === ConnectionState.DISCONNECTED) {
  82. logger?.(
  83. `[ExtensionBridgeService#handleRemoteControlState] Existing service is ${state}, attempting reconnection`,
  84. )
  85. existingService.reconnect().catch((error) => {
  86. const message = `[ExtensionBridgeService#handleRemoteControlState] Reconnection failed: ${
  87. error instanceof Error ? error.message : String(error)
  88. }`
  89. logger?.(message)
  90. console.error(message)
  91. })
  92. }
  93. }
  94. } else {
  95. const existingService = ExtensionBridgeService.getInstance()
  96. if (existingService) {
  97. try {
  98. await existingService.disconnect()
  99. ExtensionBridgeService.resetInstance()
  100. logger?.(`[ExtensionBridgeService#handleRemoteControlState] Service disconnected and reset`)
  101. } catch (error) {
  102. const message = `[ExtensionBridgeService#handleRemoteControlState] Failed to disconnect and reset instance: ${
  103. error instanceof Error ? error.message : String(error)
  104. }`
  105. logger?.(message)
  106. console.error(message)
  107. }
  108. }
  109. }
  110. }
  111. private constructor(options: ExtensionBridgeServiceOptions) {
  112. this.userId = options.userId
  113. this.socketBridgeUrl = options.socketBridgeUrl
  114. this.token = options.token
  115. this.provider = options.provider
  116. this.instanceId = options.sessionId || crypto.randomUUID()
  117. this.connectionManager = new SocketConnectionManager({
  118. url: this.socketBridgeUrl,
  119. socketOptions: {
  120. query: {
  121. token: this.token,
  122. clientType: "extension",
  123. instanceId: this.instanceId,
  124. },
  125. transports: ["websocket", "polling"],
  126. reconnection: true,
  127. reconnectionAttempts: this.MAX_RECONNECT_ATTEMPTS,
  128. reconnectionDelay: this.RECONNECT_DELAY,
  129. reconnectionDelayMax: this.RECONNECT_DELAY_MAX,
  130. },
  131. onConnect: () => this.handleConnect(),
  132. onDisconnect: () => this.handleDisconnect(),
  133. onReconnect: () => this.handleReconnect(),
  134. })
  135. this.extensionManager = new ExtensionManager(this.instanceId, this.userId, this.provider)
  136. this.taskManager = new TaskManager()
  137. }
  138. private async initialize() {
  139. // Populate the app and git properties before registering the instance.
  140. await this.provider.getTelemetryProperties()
  141. await this.connectionManager.connect()
  142. this.setupSocketListeners()
  143. }
  144. private setupSocketListeners() {
  145. const socket = this.connectionManager.getSocket()
  146. if (!socket) {
  147. console.error("[ExtensionBridgeService] Socket not available")
  148. return
  149. }
  150. // Remove any existing listeners first to prevent duplicates.
  151. socket.off(ExtensionSocketEvents.RELAYED_COMMAND)
  152. socket.off(TaskSocketEvents.RELAYED_COMMAND)
  153. socket.off("connected")
  154. socket.on(ExtensionSocketEvents.RELAYED_COMMAND, (message: ExtensionBridgeCommand) => {
  155. console.log(
  156. `[ExtensionBridgeService] on(${ExtensionSocketEvents.RELAYED_COMMAND}) -> ${message.type} for ${message.instanceId}`,
  157. )
  158. this.extensionManager?.handleExtensionCommand(message)
  159. })
  160. socket.on(TaskSocketEvents.RELAYED_COMMAND, (message: TaskBridgeCommand) => {
  161. console.log(
  162. `[ExtensionBridgeService] on(${TaskSocketEvents.RELAYED_COMMAND}) -> ${message.type} for ${message.taskId}`,
  163. )
  164. this.taskManager.handleTaskCommand(message)
  165. })
  166. }
  167. private async handleConnect() {
  168. const socket = this.connectionManager.getSocket()
  169. if (!socket) {
  170. console.error("[ExtensionBridgeService] Socket not available after connect")
  171. return
  172. }
  173. await this.extensionManager.onConnect(socket)
  174. await this.taskManager.onConnect(socket)
  175. }
  176. private handleDisconnect() {
  177. this.extensionManager.onDisconnect()
  178. this.taskManager.onDisconnect()
  179. }
  180. private async handleReconnect() {
  181. const socket = this.connectionManager.getSocket()
  182. if (!socket) {
  183. console.error("[ExtensionBridgeService] Socket not available after reconnect")
  184. return
  185. }
  186. // Re-setup socket listeners to ensure they're properly configured
  187. // after automatic reconnection (Socket.IO's built-in reconnection)
  188. // The socket.off() calls in setupSocketListeners prevent duplicates
  189. this.setupSocketListeners()
  190. await this.extensionManager.onReconnect(socket)
  191. await this.taskManager.onReconnect(socket)
  192. }
  193. // Task API
  194. public async subscribeToTask(task: TaskLike): Promise<void> {
  195. const socket = this.connectionManager.getSocket()
  196. if (!socket || !this.connectionManager.isConnected()) {
  197. console.warn("[ExtensionBridgeService] Cannot subscribe to task: not connected. Will retry when connected.")
  198. this.taskManager.addPendingTask(task)
  199. const state = this.connectionManager.getConnectionState()
  200. if (state === ConnectionState.DISCONNECTED || state === ConnectionState.FAILED) {
  201. this.initialize()
  202. }
  203. return
  204. }
  205. await this.taskManager.subscribeToTask(task, socket)
  206. }
  207. public async unsubscribeFromTask(taskId: string): Promise<void> {
  208. const socket = this.connectionManager.getSocket()
  209. if (!socket) {
  210. return
  211. }
  212. await this.taskManager.unsubscribeFromTask(taskId, socket)
  213. }
  214. // Shared API
  215. public getConnectionState(): ConnectionState {
  216. return this.connectionManager.getConnectionState()
  217. }
  218. public async disconnect(): Promise<void> {
  219. await this.extensionManager.cleanup(this.connectionManager.getSocket())
  220. await this.taskManager.cleanup(this.connectionManager.getSocket())
  221. await this.connectionManager.disconnect()
  222. ExtensionBridgeService.instance = null
  223. }
  224. public async reconnect(): Promise<void> {
  225. await this.connectionManager.reconnect()
  226. // After a manual reconnect, we have a new socket instance
  227. // so we need to set up listeners again.
  228. this.setupSocketListeners()
  229. }
  230. }