Skip to content

Commit 358d4d0

Browse files
committed
Only trigger reconnection if the WS is not connected
1 parent 2e27a6b commit 358d4d0

File tree

3 files changed

+43
-23
lines changed

3 files changed

+43
-23
lines changed

src/api/coderApi.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
type OneWayWebSocketInit,
4747
} from "../websocket/oneWayWebSocket";
4848
import {
49+
ConnectionState,
4950
ReconnectingWebSocket,
5051
type SocketFactory,
5152
} from "../websocket/reconnectingWebSocket";
@@ -164,19 +165,22 @@ export class CoderApi extends Api implements vscode.Disposable {
164165

165166
/**
166167
* Watch for configuration changes that affect WebSocket connections.
167-
* When any watched setting changes, all active WebSockets are reconnected.
168+
* Skips connected sockets since they pick up new settings on next reconnect.
168169
*/
169170
private watchConfigChanges(): vscode.Disposable {
170171
const settings = webSocketConfigSettings.map((setting) => ({
171172
setting,
172173
getValue: () => vscode.workspace.getConfiguration().get(setting),
173174
}));
174175
return watchConfigurationChanges(settings, () => {
175-
if (this.reconnectingSockets.size > 0) {
176+
const socketsToReconnect = [...this.reconnectingSockets].filter(
177+
(socket) => socket.state !== ConnectionState.CONNECTED,
178+
);
179+
if (socketsToReconnect.length > 0) {
176180
this.output.info(
177-
`Configuration changed, reconnecting ${this.reconnectingSockets.size} WebSocket(s)`,
181+
`Configuration changed, reconnecting ${socketsToReconnect.length} WebSocket(s)`,
178182
);
179-
for (const socket of this.reconnectingSockets) {
183+
for (const socket of socketsToReconnect) {
180184
socket.reconnect();
181185
}
182186
}

src/websocket/reconnectingWebSocket.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export class ReconnectingWebSocket<
197197
/**
198198
* Returns the current connection state.
199199
*/
200-
get state(): string {
200+
get state(): ConnectionState {
201201
return this.#state;
202202
}
203203

test/unit/api/coderApi.test.ts

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -591,9 +591,11 @@ describe("CoderApi", () => {
591591

592592
const setupAutoOpeningWebSocket = () => {
593593
const sockets: Array<Partial<Ws>> = [];
594+
const handlers: Record<string, (...args: unknown[]) => void> = {};
594595
vi.mocked(Ws).mockImplementation(function (url: string | URL) {
595596
const mockWs = createMockWebSocket(String(url), {
596-
on: vi.fn((event, handler) => {
597+
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
598+
handlers[event] = handler;
597599
if (event === "open") {
598600
setImmediate(() => handler());
599601
}
@@ -603,12 +605,12 @@ describe("CoderApi", () => {
603605
sockets.push(mockWs);
604606
return mockWs as Ws;
605607
});
606-
return sockets;
608+
return { sockets, handlers };
607609
};
608610

609611
describe("Reconnection on Host/Token Changes", () => {
610612
it("triggers reconnection when session token changes", async () => {
611-
const sockets = setupAutoOpeningWebSocket();
613+
const { sockets } = setupAutoOpeningWebSocket();
612614
api = createApi(CODER_URL, AXIOS_TOKEN);
613615
await api.watchAgentMetadata(AGENT_ID);
614616

@@ -623,7 +625,7 @@ describe("CoderApi", () => {
623625
});
624626

625627
it("triggers reconnection when host changes", async () => {
626-
const sockets = setupAutoOpeningWebSocket();
628+
const { sockets } = setupAutoOpeningWebSocket();
627629
api = createApi(CODER_URL, AXIOS_TOKEN);
628630
const wsWrap = await api.watchAgentMetadata(AGENT_ID);
629631
expect(wsWrap.url).toContain(CODER_URL.replace("http", "ws"));
@@ -642,7 +644,7 @@ describe("CoderApi", () => {
642644
});
643645

644646
it("does not reconnect when token or host are unchanged", async () => {
645-
const sockets = setupAutoOpeningWebSocket();
647+
const { sockets } = setupAutoOpeningWebSocket();
646648
api = createApi(CODER_URL, AXIOS_TOKEN);
647649
await api.watchAgentMetadata(AGENT_ID);
648650

@@ -655,7 +657,7 @@ describe("CoderApi", () => {
655657
});
656658

657659
it("suspends sockets when host is set to empty string (logout)", async () => {
658-
const sockets = setupAutoOpeningWebSocket();
660+
const { sockets } = setupAutoOpeningWebSocket();
659661
api = createApi(CODER_URL, AXIOS_TOKEN);
660662
await api.watchAgentMetadata(AGENT_ID);
661663

@@ -668,7 +670,7 @@ describe("CoderApi", () => {
668670
});
669671

670672
it("does not reconnect when setting token after clearing host", async () => {
671-
const sockets = setupAutoOpeningWebSocket();
673+
const { sockets } = setupAutoOpeningWebSocket();
672674
api = createApi(CODER_URL, AXIOS_TOKEN);
673675
await api.watchAgentMetadata(AGENT_ID);
674676

@@ -682,7 +684,7 @@ describe("CoderApi", () => {
682684
});
683685

684686
it("setCredentials sets both host and token together", async () => {
685-
const sockets = setupAutoOpeningWebSocket();
687+
const { sockets } = setupAutoOpeningWebSocket();
686688
api = createApi(CODER_URL, AXIOS_TOKEN);
687689
await api.watchAgentMetadata(AGENT_ID);
688690

@@ -695,7 +697,7 @@ describe("CoderApi", () => {
695697
});
696698

697699
it("setCredentials suspends when host is cleared", async () => {
698-
const sockets = setupAutoOpeningWebSocket();
700+
const { sockets } = setupAutoOpeningWebSocket();
699701
api = createApi(CODER_URL, AXIOS_TOKEN);
700702
await api.watchAgentMetadata(AGENT_ID);
701703

@@ -796,29 +798,26 @@ describe("CoderApi", () => {
796798
describe("Configuration Change Reconnection", () => {
797799
const tick = () => new Promise((resolve) => setImmediate(resolve));
798800

799-
it("reconnects sockets when watched config value changes", async () => {
801+
it("does not reconnect connected sockets when config value changes", async () => {
800802
mockConfig.set("coder.insecure", false);
801-
const sockets = setupAutoOpeningWebSocket();
803+
const { sockets } = setupAutoOpeningWebSocket();
802804
api = createApi(CODER_URL, AXIOS_TOKEN);
803805
await api.watchAgentMetadata(AGENT_ID);
804806
await tick(); // Wait for open event to fire (socket becomes CONNECTED)
805807

806808
mockConfig.set("coder.insecure", true);
807809
await tick();
808810

809-
expect(sockets[0].close).toHaveBeenCalledWith(
810-
1000,
811-
"Replacing connection",
812-
);
813-
expect(sockets).toHaveLength(2);
811+
expect(sockets[0].close).not.toHaveBeenCalled();
812+
expect(sockets).toHaveLength(1);
814813
});
815814

816815
it.each([
817816
["unchanged value", "coder.insecure", false],
818817
["unrelated setting", "unrelated.setting", "new-value"],
819818
])("does not reconnect for %s", async (_desc, key, value) => {
820819
mockConfig.set("coder.insecure", false);
821-
const sockets = setupAutoOpeningWebSocket();
820+
const { sockets } = setupAutoOpeningWebSocket();
822821
api = createApi(CODER_URL, AXIOS_TOKEN);
823822
await api.watchAgentMetadata(AGENT_ID);
824823
await tick(); // Wait for open event to fire (socket becomes CONNECTED)
@@ -832,7 +831,7 @@ describe("CoderApi", () => {
832831

833832
it("stops watching after dispose", async () => {
834833
mockConfig.set("coder.insecure", false);
835-
const sockets = setupAutoOpeningWebSocket();
834+
const { sockets } = setupAutoOpeningWebSocket();
836835
api = createApi(CODER_URL, AXIOS_TOKEN);
837836
await api.watchAgentMetadata(AGENT_ID);
838837
await tick(); // Wait for open event to fire (socket becomes CONNECTED)
@@ -843,6 +842,23 @@ describe("CoderApi", () => {
843842

844843
expect(sockets).toHaveLength(1);
845844
});
845+
846+
it("reconnects sockets in AWAITING_RETRY state when config changes", async () => {
847+
mockConfig.set("coder.insecure", false);
848+
const { sockets, handlers } = setupAutoOpeningWebSocket();
849+
api = createApi(CODER_URL, AXIOS_TOKEN);
850+
await api.watchAgentMetadata(AGENT_ID);
851+
852+
// Trigger close with abnormal code to put socket in AWAITING_RETRY
853+
handlers["close"]?.({ code: 1006, reason: "Abnormal closure" });
854+
await tick();
855+
856+
mockConfig.set("coder.insecure", true);
857+
await tick();
858+
859+
expect(sockets[0].close).toHaveBeenCalled();
860+
expect(sockets).toHaveLength(2);
861+
});
846862
});
847863
});
848864

0 commit comments

Comments
 (0)