Skip to content

Commit c01321f

Browse files
committed
Refactoring websockets and coderApi changes
1 parent e9198e2 commit c01321f

File tree

3 files changed

+43
-52
lines changed

3 files changed

+43
-52
lines changed

src/api/coderApi.ts

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -144,48 +144,39 @@ export class CoderApi extends Api implements vscode.Disposable {
144144
watchTargets: string[],
145145
options?: ClientOptions,
146146
) => {
147-
const apiRoute = "/api/v2/notifications/inbox/watch";
148-
return this.createReconnectingSocket(
149-
() =>
150-
this.createOneWayWebSocket<GetInboxNotificationResponse>({
151-
apiRoute,
152-
searchParams: {
153-
format: "plaintext",
154-
templates: watchTemplates.join(","),
155-
targets: watchTargets.join(","),
156-
},
157-
options,
158-
}),
159-
apiRoute,
147+
return this.createReconnectingSocket(() =>
148+
this.createOneWayWebSocket<GetInboxNotificationResponse>({
149+
apiRoute: "/api/v2/notifications/inbox/watch",
150+
searchParams: {
151+
format: "plaintext",
152+
templates: watchTemplates.join(","),
153+
targets: watchTargets.join(","),
154+
},
155+
options,
156+
}),
160157
);
161158
};
162159

163160
watchWorkspace = async (workspace: Workspace, options?: ClientOptions) => {
164-
const apiRoute = `/api/v2/workspaces/${workspace.id}/watch-ws`;
165-
return this.createReconnectingSocket(
166-
() =>
167-
this.createStreamWithSseFallback({
168-
apiRoute,
169-
fallbackApiRoute: `/api/v2/workspaces/${workspace.id}/watch`,
170-
options,
171-
}),
172-
apiRoute,
161+
return this.createReconnectingSocket(() =>
162+
this.createStreamWithSseFallback({
163+
apiRoute: `/api/v2/workspaces/${workspace.id}/watch-ws`,
164+
fallbackApiRoute: `/api/v2/workspaces/${workspace.id}/watch`,
165+
options,
166+
}),
173167
);
174168
};
175169

176170
watchAgentMetadata = async (
177171
agentId: WorkspaceAgent["id"],
178172
options?: ClientOptions,
179173
) => {
180-
const apiRoute = `/api/v2/workspaceagents/${agentId}/watch-metadata-ws`;
181-
return this.createReconnectingSocket(
182-
() =>
183-
this.createStreamWithSseFallback({
184-
apiRoute,
185-
fallbackApiRoute: `/api/v2/workspaceagents/${agentId}/watch-metadata`,
186-
options,
187-
}),
188-
apiRoute,
174+
return this.createReconnectingSocket(() =>
175+
this.createStreamWithSseFallback({
176+
apiRoute: `/api/v2/workspaceagents/${agentId}/watch-metadata-ws`,
177+
fallbackApiRoute: `/api/v2/workspaceagents/${agentId}/watch-metadata`,
178+
options,
179+
}),
189180
);
190181
};
191182

@@ -403,12 +394,10 @@ export class CoderApi extends Api implements vscode.Disposable {
403394
*/
404395
private async createReconnectingSocket<TData>(
405396
socketFactory: SocketFactory<TData>,
406-
apiRoute: string,
407397
): Promise<ReconnectingWebSocket<TData>> {
408398
const reconnectingSocket = await ReconnectingWebSocket.create<TData>(
409399
socketFactory,
410400
this.output,
411-
apiRoute,
412401
undefined,
413402
() => this.reconnectingSockets.delete(reconnectingSocket),
414403
);

src/websocket/reconnectingWebSocket.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export class ReconnectingWebSocket<TData = unknown>
2727
{
2828
readonly #socketFactory: SocketFactory<TData>;
2929
readonly #logger: Logger;
30-
readonly #apiRoute: string;
3130
readonly #options: Required<ReconnectingWebSocketOptions>;
3231
readonly #eventHandlers: {
3332
[K in WebSocketEventType]: Set<EventHandler<TData, K>>;
@@ -39,6 +38,7 @@ export class ReconnectingWebSocket<TData = unknown>
3938
};
4039

4140
#currentSocket: UnidirectionalStream<TData> | null = null;
41+
#lastRoute = "unknown"; // Cached route for logging when socket is closed
4242
#backoffMs: number;
4343
#reconnectTimeoutId: NodeJS.Timeout | null = null;
4444
#isSuspended = false; // Temporary pause, can be resumed via reconnect()
@@ -50,13 +50,11 @@ export class ReconnectingWebSocket<TData = unknown>
5050
private constructor(
5151
socketFactory: SocketFactory<TData>,
5252
logger: Logger,
53-
apiRoute: string,
5453
options: ReconnectingWebSocketOptions = {},
5554
onDispose?: () => void,
5655
) {
5756
this.#socketFactory = socketFactory;
5857
this.#logger = logger;
59-
this.#apiRoute = apiRoute;
6058
this.#options = {
6159
initialBackoffMs: options.initialBackoffMs ?? 250,
6260
maxBackoffMs: options.maxBackoffMs ?? 30000,
@@ -69,14 +67,12 @@ export class ReconnectingWebSocket<TData = unknown>
6967
static async create<TData>(
7068
socketFactory: SocketFactory<TData>,
7169
logger: Logger,
72-
apiRoute: string,
7370
options: ReconnectingWebSocketOptions = {},
7471
onDispose?: () => void,
7572
): Promise<ReconnectingWebSocket<TData>> {
7673
const instance = new ReconnectingWebSocket<TData>(
7774
socketFactory,
7875
logger,
79-
apiRoute,
8076
options,
8177
onDispose,
8278
);
@@ -88,6 +84,19 @@ export class ReconnectingWebSocket<TData = unknown>
8884
return this.#currentSocket?.url ?? "";
8985
}
9086

87+
/**
88+
* Extract the route (pathname + search) from the current socket URL for logging.
89+
* Falls back to the last known route when the socket is closed.
90+
*/
91+
get #route(): string {
92+
const socketUrl = this.#currentSocket?.url;
93+
if (!socketUrl) {
94+
return this.#lastRoute;
95+
}
96+
const url = new URL(socketUrl);
97+
return url.pathname + url.search;
98+
}
99+
91100
addEventListener<TEvent extends WebSocketEventType>(
92101
event: TEvent,
93102
callback: EventHandler<TData, TEvent>,
@@ -174,6 +183,7 @@ export class ReconnectingWebSocket<TData = unknown>
174183

175184
const socket = await this.#socketFactory();
176185
this.#currentSocket = socket;
186+
this.#lastRoute = this.#route;
177187

178188
socket.addEventListener("open", (event) => {
179189
this.#backoffMs = this.#options.initialBackoffMs;
@@ -193,7 +203,7 @@ export class ReconnectingWebSocket<TData = unknown>
193203
const errorMessage = event.error?.message ?? event.message ?? "";
194204
if (this.isUnrecoverableHttpError(errorMessage)) {
195205
this.#logger.error(
196-
`Unrecoverable HTTP error for ${this.#apiRoute}: ${errorMessage}`,
206+
`Unrecoverable HTTP error for ${this.#route}: ${errorMessage}`,
197207
);
198208
this.suspend();
199209
}
@@ -247,7 +257,7 @@ export class ReconnectingWebSocket<TData = unknown>
247257
const delayMs = Math.max(0, this.#backoffMs + jitter);
248258

249259
this.#logger.debug(
250-
`Reconnecting WebSocket in ${Math.round(delayMs)}ms for ${this.#apiRoute}`,
260+
`Reconnecting WebSocket in ${Math.round(delayMs)}ms for ${this.#route}`,
251261
);
252262

253263
this.#reconnectTimeoutId = setTimeout(() => {
@@ -267,7 +277,7 @@ export class ReconnectingWebSocket<TData = unknown>
267277
handler(eventData);
268278
} catch (error) {
269279
this.#logger.error(
270-
`Error in ${event} handler for ${this.#apiRoute}`,
280+
`Error in ${event} handler for ${this.#route}`,
271281
error,
272282
);
273283
}
@@ -285,17 +295,14 @@ export class ReconnectingWebSocket<TData = unknown>
285295

286296
if (this.isUnrecoverableHttpError(error)) {
287297
this.#logger.error(
288-
`Unrecoverable HTTP error during connection for ${this.#apiRoute}`,
298+
`Unrecoverable HTTP error during connection for ${this.#route}`,
289299
error,
290300
);
291301
this.suspend();
292302
return;
293303
}
294304

295-
this.#logger.warn(
296-
`WebSocket connection failed for ${this.#apiRoute}`,
297-
error,
298-
);
305+
this.#logger.warn(`WebSocket connection failed for ${this.#route}`, error);
299306
this.scheduleReconnect();
300307
}
301308

test/unit/websocket/reconnectingWebSocket.test.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,7 @@ describe("ReconnectingWebSocket", () => {
9191
});
9292

9393
await expect(
94-
ReconnectingWebSocket.create(
95-
factory,
96-
createMockLogger(),
97-
"/api/test",
98-
),
94+
ReconnectingWebSocket.create(factory, createMockLogger()),
9995
).rejects.toThrow(`Unexpected server response: ${statusCode}`);
10096

10197
// Should not retry after unrecoverable HTTP error
@@ -598,7 +594,6 @@ async function fromFactory<T>(
598594
return await ReconnectingWebSocket.create(
599595
factory,
600596
createMockLogger(),
601-
"/random/api",
602597
undefined,
603598
onDispose,
604599
);

0 commit comments

Comments
 (0)