-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathws-client.js
More file actions
101 lines (90 loc) · 2.78 KB
/
Copy pathws-client.js
File metadata and controls
101 lines (90 loc) · 2.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// WebSocket client, exponential-backoff reconnect (1/2/4/8s, cap 30s).
const BACKOFF_SCHEDULE_MS = [1_000, 2_000, 4_000, 8_000];
const BACKOFF_MAX_MS = 30_000;
export class WsClient {
constructor({ url, onOpen, onClose, onMessage }) {
this.url = url;
this.onOpen = onOpen || (() => {});
this.onClose = onClose || (() => {});
this.onMessage = onMessage || (() => {});
this.socket = null;
this.attempt = 0;
this.reconnectTimer = null;
this.stopped = false;
}
start() {
this.stopped = false;
this._connect();
}
stop() {
this.stopped = true;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.socket) {
try { this.socket.close(1000, 'module disabled'); } catch {}
this.socket = null;
}
}
isOpen() {
return this.socket?.readyState === WebSocket.OPEN;
}
send(message) {
if (!this.isOpen()) {
console.warn(`[foundry-bridge] cannot send - socket not open. Dropping ${message?.method || 'message'}.`);
return false;
}
try {
this.socket.send(JSON.stringify(message));
return true;
} catch (err) {
console.error('[foundry-bridge] send failed:', err);
return false;
}
}
_connect() {
if (this.stopped) return;
let s;
try {
s = new WebSocket(this.url);
} catch (err) {
console.error('[foundry-bridge] WebSocket constructor threw:', err);
this._scheduleReconnect();
return;
}
this.socket = s;
s.addEventListener('open', () => {
this.attempt = 0;
try { this.onOpen(); } catch (err) { console.error('[foundry-bridge] onOpen threw:', err); }
});
s.addEventListener('message', (ev) => {
let msg;
try {
msg = JSON.parse(ev.data);
} catch (err) {
console.error('[foundry-bridge] dropping malformed frame:', err);
return;
}
try { this.onMessage(msg); } catch (err) { console.error('[foundry-bridge] onMessage threw:', err); }
});
s.addEventListener('close', (ev) => {
this.socket = null;
try { this.onClose({ code: ev.code, reason: ev.reason }); } catch {}
if (!this.stopped) this._scheduleReconnect();
});
// 'error' precedes 'close'; reconnect handled on close.
s.addEventListener('error', () => {});
}
_scheduleReconnect() {
if (this.stopped) return;
const idx = Math.min(this.attempt, BACKOFF_SCHEDULE_MS.length - 1);
const delay = this.attempt < BACKOFF_SCHEDULE_MS.length ? BACKOFF_SCHEDULE_MS[idx] : BACKOFF_MAX_MS;
this.attempt++;
console.log(`[foundry-bridge] reconnecting in ${delay}ms (attempt ${this.attempt})`);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this._connect();
}, delay);
}
}