Skip to content

Commit 878939a

Browse files
Fix: extension disconnected issue (#53)
* fix: make browseros controller singleton * fix: controllerBridge tracks all client connections and uses primary for updates * gitignore updates * minor: .env.example has url * controller-ext: remove exponential backoff to keep ti simple, remove un-unsed envs * Update .env.example Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent b76912e commit 878939a

File tree

9 files changed

+501
-577
lines changed

9 files changed

+501
-577
lines changed

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Remote config endpoint for BrowserOS server settings
2-
BROWSEROS_CONFIG_URL=
2+
# NOTE: create .env.dev for development environment and .env.prod for production environment
3+
BROWSEROS_CONFIG_URL=https://llm.browseros.com/api/browseros-server/config
34

45
# API key for LLM access used by Codex
56
BROWSEROS_API_KEY=

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ yarn-error.log*
77
lerna-debug.log*
88
.pnpm-debug.log*
99
.env.dev
10+
.env.prod
1011

1112
# Diagnostic reports (https://nodejs.org/api/report.html)
1213
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

packages/controller-ext/manifest.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,3 @@
3636
"128": "assets/icon128.png"
3737
}
3838
}
39-
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
/**
2+
* @license
3+
* Copyright 2025 BrowserOS
4+
* SPDX-License-Identifier: AGPL-3.0-or-later
5+
*/
6+
import {ActionRegistry} from '@/actions/ActionRegistry';
7+
import {CreateBookmarkAction} from '@/actions/bookmark/CreateBookmarkAction';
8+
import {GetBookmarksAction} from '@/actions/bookmark/GetBookmarksAction';
9+
import {RemoveBookmarkAction} from '@/actions/bookmark/RemoveBookmarkAction';
10+
import {CaptureScreenshotAction} from '@/actions/browser/CaptureScreenshotAction';
11+
import {ClearAction} from '@/actions/browser/ClearAction';
12+
import {ClickAction} from '@/actions/browser/ClickAction';
13+
import {ClickCoordinatesAction} from '@/actions/browser/ClickCoordinatesAction';
14+
import {ExecuteJavaScriptAction} from '@/actions/browser/ExecuteJavaScriptAction';
15+
import {GetAccessibilityTreeAction} from '@/actions/browser/GetAccessibilityTreeAction';
16+
import {GetInteractiveSnapshotAction} from '@/actions/browser/GetInteractiveSnapshotAction';
17+
import {GetPageLoadStatusAction} from '@/actions/browser/GetPageLoadStatusAction';
18+
import {GetSnapshotAction} from '@/actions/browser/GetSnapshotAction';
19+
import {InputTextAction} from '@/actions/browser/InputTextAction';
20+
import {ScrollDownAction} from '@/actions/browser/ScrollDownAction';
21+
import {ScrollToNodeAction} from '@/actions/browser/ScrollToNodeAction';
22+
import {ScrollUpAction} from '@/actions/browser/ScrollUpAction';
23+
import {SendKeysAction} from '@/actions/browser/SendKeysAction';
24+
import {TypeAtCoordinatesAction} from '@/actions/browser/TypeAtCoordinatesAction';
25+
import {CheckBrowserOSAction} from '@/actions/diagnostics/CheckBrowserOSAction';
26+
import {GetRecentHistoryAction} from '@/actions/history/GetRecentHistoryAction';
27+
import {SearchHistoryAction} from '@/actions/history/SearchHistoryAction';
28+
import {CloseTabAction} from '@/actions/tab/CloseTabAction';
29+
import {GetActiveTabAction} from '@/actions/tab/GetActiveTabAction';
30+
import {GetTabsAction} from '@/actions/tab/GetTabsAction';
31+
import {NavigateAction} from '@/actions/tab/NavigateAction';
32+
import {OpenTabAction} from '@/actions/tab/OpenTabAction';
33+
import {SwitchTabAction} from '@/actions/tab/SwitchTabAction';
34+
import {CONCURRENCY_CONFIG} from '@/config/constants';
35+
import type {ProtocolRequest, ProtocolResponse} from '@/protocol/types';
36+
import {ConnectionStatus} from '@/protocol/types';
37+
import {ConcurrencyLimiter} from '@/utils/ConcurrencyLimiter';
38+
import {logger} from '@/utils/Logger';
39+
import {RequestTracker} from '@/utils/RequestTracker';
40+
import {RequestValidator} from '@/utils/RequestValidator';
41+
import {ResponseQueue} from '@/utils/ResponseQueue';
42+
import {WebSocketClient} from '@/websocket/WebSocketClient';
43+
44+
/**
45+
* BrowserOS Controller
46+
*
47+
* Main controller class that orchestrates all components.
48+
* Message flow: WebSocket → Validator → Tracker → Limiter → Action → Response/Queue → WebSocket
49+
*/
50+
export class BrowserOSController {
51+
private wsClient: WebSocketClient;
52+
private requestTracker: RequestTracker;
53+
private concurrencyLimiter: ConcurrencyLimiter;
54+
private requestValidator: RequestValidator;
55+
private responseQueue: ResponseQueue;
56+
private actionRegistry: ActionRegistry;
57+
58+
constructor(port: number) {
59+
logger.info('Initializing BrowserOS Controller...');
60+
61+
this.requestTracker = new RequestTracker();
62+
this.concurrencyLimiter = new ConcurrencyLimiter(
63+
CONCURRENCY_CONFIG.maxConcurrent,
64+
CONCURRENCY_CONFIG.maxQueueSize,
65+
);
66+
this.requestValidator = new RequestValidator();
67+
this.responseQueue = new ResponseQueue();
68+
this.wsClient = new WebSocketClient(port);
69+
this.actionRegistry = new ActionRegistry();
70+
71+
this.registerActions();
72+
this.setupWebSocketHandlers();
73+
}
74+
75+
async start(): Promise<void> {
76+
logger.info('Starting BrowserOS Controller...');
77+
await this.wsClient.connect();
78+
}
79+
80+
stop(): void {
81+
logger.info('Stopping BrowserOS Controller...');
82+
this.wsClient.disconnect();
83+
this.requestTracker.destroy();
84+
this.requestValidator.destroy();
85+
this.responseQueue.clear();
86+
}
87+
88+
logStats(): void {
89+
const stats = this.getStats();
90+
logger.info('=== Controller Stats ===');
91+
logger.info(`Connection: ${stats.connection}`);
92+
logger.info(`Requests: ${JSON.stringify(stats.requests)}`);
93+
logger.info(`Concurrency: ${JSON.stringify(stats.concurrency)}`);
94+
logger.info(`Validator: ${JSON.stringify(stats.validator)}`);
95+
logger.info(`Response Queue: ${stats.responseQueue.size} queued`);
96+
}
97+
98+
getStats() {
99+
return {
100+
connection: this.wsClient.getStatus(),
101+
requests: this.requestTracker.getStats(),
102+
concurrency: this.concurrencyLimiter.getStats(),
103+
validator: this.requestValidator.getStats(),
104+
responseQueue: {
105+
size: this.responseQueue.size(),
106+
},
107+
};
108+
}
109+
110+
isConnected(): boolean {
111+
return this.wsClient.isConnected();
112+
}
113+
114+
private registerActions(): void {
115+
logger.info('Registering actions...');
116+
117+
this.actionRegistry.register('checkBrowserOS', new CheckBrowserOSAction());
118+
119+
this.actionRegistry.register('getActiveTab', new GetActiveTabAction());
120+
this.actionRegistry.register('getTabs', new GetTabsAction());
121+
this.actionRegistry.register('openTab', new OpenTabAction());
122+
this.actionRegistry.register('closeTab', new CloseTabAction());
123+
this.actionRegistry.register('switchTab', new SwitchTabAction());
124+
this.actionRegistry.register('navigate', new NavigateAction());
125+
126+
this.actionRegistry.register('getBookmarks', new GetBookmarksAction());
127+
this.actionRegistry.register('createBookmark', new CreateBookmarkAction());
128+
this.actionRegistry.register('removeBookmark', new RemoveBookmarkAction());
129+
130+
this.actionRegistry.register('searchHistory', new SearchHistoryAction());
131+
this.actionRegistry.register(
132+
'getRecentHistory',
133+
new GetRecentHistoryAction(),
134+
);
135+
136+
this.actionRegistry.register(
137+
'getInteractiveSnapshot',
138+
new GetInteractiveSnapshotAction(),
139+
);
140+
this.actionRegistry.register('click', new ClickAction());
141+
this.actionRegistry.register('inputText', new InputTextAction());
142+
this.actionRegistry.register('clear', new ClearAction());
143+
this.actionRegistry.register('scrollToNode', new ScrollToNodeAction());
144+
145+
this.actionRegistry.register(
146+
'captureScreenshot',
147+
new CaptureScreenshotAction(),
148+
);
149+
150+
this.actionRegistry.register('scrollDown', new ScrollDownAction());
151+
this.actionRegistry.register('scrollUp', new ScrollUpAction());
152+
153+
this.actionRegistry.register(
154+
'executeJavaScript',
155+
new ExecuteJavaScriptAction(),
156+
);
157+
this.actionRegistry.register('sendKeys', new SendKeysAction());
158+
this.actionRegistry.register(
159+
'getPageLoadStatus',
160+
new GetPageLoadStatusAction(),
161+
);
162+
this.actionRegistry.register('getSnapshot', new GetSnapshotAction());
163+
this.actionRegistry.register(
164+
'getAccessibilityTree',
165+
new GetAccessibilityTreeAction(),
166+
);
167+
this.actionRegistry.register(
168+
'clickCoordinates',
169+
new ClickCoordinatesAction(),
170+
);
171+
this.actionRegistry.register(
172+
'typeAtCoordinates',
173+
new TypeAtCoordinatesAction(),
174+
);
175+
176+
const actions = this.actionRegistry.getAvailableActions();
177+
logger.info(
178+
`Registered ${actions.length} action(s): ${actions.join(', ')}`,
179+
);
180+
}
181+
182+
private setupWebSocketHandlers(): void {
183+
this.wsClient.onMessage((message: ProtocolResponse) => {
184+
this.handleIncomingMessage(message);
185+
});
186+
187+
this.wsClient.onStatusChange((status: ConnectionStatus) => {
188+
this.handleStatusChange(status);
189+
});
190+
}
191+
192+
private handleIncomingMessage(message: ProtocolResponse): void {
193+
const rawMessage = message as any;
194+
195+
if (rawMessage.action) {
196+
this.processRequest(rawMessage).catch(error => {
197+
logger.error(
198+
`Unhandled error processing request ${rawMessage.id}: ${error}`,
199+
);
200+
});
201+
} else if (rawMessage.ok !== undefined) {
202+
logger.info(
203+
`Received server message: ${rawMessage.id} - ${rawMessage.ok ? 'success' : 'error'}`,
204+
);
205+
if (rawMessage.data) {
206+
logger.debug(`Server data: ${JSON.stringify(rawMessage.data)}`);
207+
}
208+
} else {
209+
logger.warn(
210+
`Received unknown message format: ${JSON.stringify(rawMessage)}`,
211+
);
212+
}
213+
}
214+
215+
private async processRequest(request: unknown): Promise<void> {
216+
let validatedRequest: ProtocolRequest;
217+
let requestId: string | undefined;
218+
219+
try {
220+
validatedRequest = this.requestValidator.validate(request);
221+
requestId = validatedRequest.id;
222+
223+
this.requestTracker.start(validatedRequest.id, validatedRequest.action);
224+
225+
await this.concurrencyLimiter.execute(async () => {
226+
this.requestTracker.markExecuting(validatedRequest.id);
227+
await this.executeAction(validatedRequest);
228+
});
229+
230+
this.requestTracker.complete(validatedRequest.id);
231+
this.requestValidator.markComplete(validatedRequest.id);
232+
} catch (error) {
233+
const errorMessage =
234+
error instanceof Error ? error.message : String(error);
235+
logger.error(`Request processing failed: ${errorMessage}`);
236+
237+
if (requestId) {
238+
this.requestTracker.complete(requestId, errorMessage);
239+
this.requestValidator.markComplete(requestId);
240+
241+
this.sendResponse({
242+
id: requestId,
243+
ok: false,
244+
error: errorMessage,
245+
});
246+
}
247+
}
248+
}
249+
250+
private async executeAction(request: ProtocolRequest): Promise<void> {
251+
logger.info(`Executing action: ${request.action} [${request.id}]`);
252+
253+
const actionResponse = await this.actionRegistry.dispatch(
254+
request.action,
255+
request.payload,
256+
);
257+
258+
this.sendResponse({
259+
id: request.id,
260+
ok: actionResponse.ok,
261+
data: actionResponse.data,
262+
error: actionResponse.error,
263+
});
264+
265+
const status = actionResponse.ok ? 'succeeded' : 'failed';
266+
logger.info(`Action ${status}: ${request.action} [${request.id}]`);
267+
}
268+
269+
private sendResponse(response: ProtocolResponse): void {
270+
try {
271+
if (this.wsClient.isConnected()) {
272+
this.wsClient.send(response);
273+
} else {
274+
logger.warn(`Not connected. Queueing response: ${response.id}`);
275+
this.responseQueue.enqueue(response);
276+
}
277+
} catch (error) {
278+
logger.error(`Failed to send response ${response.id}: ${error}`);
279+
this.responseQueue.enqueue(response);
280+
}
281+
}
282+
283+
private handleStatusChange(status: ConnectionStatus): void {
284+
logger.info(`Connection status changed: ${status}`);
285+
286+
if (status === ConnectionStatus.CONNECTED) {
287+
if (!this.responseQueue.isEmpty()) {
288+
logger.info(
289+
`Flushing ${this.responseQueue.size()} queued responses...`,
290+
);
291+
this.responseQueue.flush(response => {
292+
this.wsClient.send(response);
293+
});
294+
}
295+
}
296+
}
297+
}

0 commit comments

Comments
 (0)