Skip to content

Commit ec24e57

Browse files
committed
feat: automatic retry in case of HTTP error 503 (Service Unavailable)
close #2
1 parent 4abbd0f commit ec24e57

File tree

1 file changed

+55
-12
lines changed

1 file changed

+55
-12
lines changed

src/APIClient.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import type {
1010
} from './APITypes';
1111

1212
const DEFAULT_SERVER = process.env.WOKWI_CLI_SERVER ?? 'wss://wokwi.com/api/ws/beta';
13+
const retryDelays = [1000, 2000, 5000, 10000, 20000];
1314

1415
export class APIClient {
15-
private readonly socket: WebSocket;
16+
private socket: WebSocket;
17+
private connectionAttempts = 0;
1618
private lastId = 0;
1719
private _running = false;
1820
private _lastNanos = 0;
@@ -27,21 +29,62 @@ export class APIClient {
2729
onEvent?: (event: APIEvent) => void;
2830

2931
constructor(readonly token: string, readonly server = DEFAULT_SERVER) {
30-
this.socket = new WebSocket(server, { headers: { Authorization: `Bearer ${token}` } });
31-
this.socket.addEventListener('message', ({ data }) => {
32-
if (typeof data === 'string') {
33-
const message = JSON.parse(data);
34-
this.processMessage(message);
35-
} else {
36-
console.error('Unsupported binary message');
37-
}
38-
});
39-
this.connected = new Promise((resolve, reject) => {
32+
this.socket = this.createSocket(token, server);
33+
this.connected = this.connectSocket(this.socket);
34+
}
35+
36+
private createSocket(token: string, server: string) {
37+
return new WebSocket(server, { headers: { Authorization: `Bearer ${token}` } });
38+
}
39+
40+
private async connectSocket(socket: WebSocket) {
41+
await new Promise((resolve, reject) => {
42+
socket.addEventListener('message', ({ data }) => {
43+
if (typeof data === 'string') {
44+
const message = JSON.parse(data);
45+
this.processMessage(message);
46+
} else {
47+
console.error('Unsupported binary message');
48+
}
49+
});
4050
this.socket.addEventListener('open', resolve);
41-
this.socket.addEventListener('error', reject);
51+
this.socket.on('unexpected-response', (req, res) => {
52+
this.socket.close();
53+
const ServiceUnavailable = 503;
54+
if (res.statusCode === ServiceUnavailable) {
55+
console.warn(
56+
`Connection to ${this.server} failed: ${res.statusMessage ?? ''} (${res.statusCode}).`
57+
);
58+
resolve(this.retryConnection());
59+
} else {
60+
reject(
61+
new Error(
62+
`Error connecting to ${this.server}: ${res.statusCode} ${res.statusMessage ?? ''}`
63+
)
64+
);
65+
}
66+
});
67+
this.socket.addEventListener('error', (event) => {
68+
reject(new Error(`Error connecting to ${this.server}: ${event.message}`));
69+
});
4270
});
4371
}
4472

73+
private async retryConnection() {
74+
const delay = retryDelays[this.connectionAttempts++];
75+
if (delay == null) {
76+
throw new Error(`Failed to connect to ${this.server}. Giving up.`);
77+
}
78+
79+
console.log(`Will retry in ${delay}ms...`);
80+
81+
await new Promise((resolve) => setTimeout(resolve, delay));
82+
83+
console.log(`Retrying connection to ${this.server}...`);
84+
this.socket = this.createSocket(this.token, this.server);
85+
await this.connectSocket(this.socket);
86+
}
87+
4588
async fileUpload(name: string, content: string | ArrayBuffer) {
4689
if (typeof content === 'string') {
4790
return await this.sendCommand('file:upload', { name, text: content });

0 commit comments

Comments
 (0)