@botcord/botcord 0.3.2-beta.20260407032921 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ws-client.ts CHANGED
@@ -28,17 +28,30 @@ interface WsClientOptions {
28
28
  };
29
29
  }
30
30
 
31
+ export type WsConnectionStatus = "disconnected" | "connecting" | "authenticated" | "reconnecting";
32
+
33
+ export interface WsClientEntry {
34
+ stop: () => void;
35
+ getStatus: () => WsConnectionStatus;
36
+ }
37
+
31
38
  // Use lazy initialization to avoid TDZ errors when jiti resolves
32
39
  // the dynamic import("./ws-client.js") before the module body completes.
33
- let _activeWsClients: Map<string, { stop: () => void }> | undefined;
40
+ let _activeWsClients: Map<string, WsClientEntry> | undefined;
34
41
  function getActiveWsClients() {
35
42
  return (_activeWsClients ??= new Map());
36
43
  }
37
44
 
45
+ /** Get the current WS connection status for an account. */
46
+ export function getWsStatus(accountId: string): WsConnectionStatus {
47
+ const entry = getActiveWsClients().get(accountId);
48
+ return entry ? entry.getStatus() : "disconnected";
49
+ }
50
+
38
51
  // Reconnect backoff: 1s, 2s, 4s, 8s, 16s, 30s max
39
52
  const RECONNECT_BACKOFF = [1000, 2000, 4000, 8000, 16000, 30000];
40
53
 
41
- export function startWsClient(opts: WsClientOptions): { stop: () => void } {
54
+ export function startWsClient(opts: WsClientOptions): WsClientEntry {
42
55
  // Stop any existing client for this account before creating a new one
43
56
  const existing = getActiveWsClients().get(opts.accountId);
44
57
  if (existing) existing.stop();
@@ -55,6 +68,7 @@ export function startWsClient(opts: WsClientOptions): { stop: () => void } {
55
68
  let pendingUpdate = false;
56
69
  let keepaliveTimer: ReturnType<typeof setInterval> | null = null;
57
70
  const KEEPALIVE_INTERVAL = 20_000; // 20s — well under Caddy/proxy 30s timeout
71
+ let status: WsConnectionStatus = "connecting";
58
72
 
59
73
  async function fetchAndDispatch() {
60
74
  if (processing) {
@@ -101,6 +115,7 @@ export function startWsClient(opts: WsClientOptions): { stop: () => void } {
101
115
  const hubUrl = client.getHubUrl();
102
116
  const wsUrl = buildHubWebSocketUrl(hubUrl);
103
117
 
118
+ status = "connecting";
104
119
  log?.info(`[${dp}] WebSocket connecting to ${wsUrl}`);
105
120
  ws = new WebSocket(wsUrl);
106
121
 
@@ -114,6 +129,7 @@ export function startWsClient(opts: WsClientOptions): { stop: () => void } {
114
129
  const msg = JSON.parse(data.toString());
115
130
  switch (msg.type) {
116
131
  case "auth_ok":
132
+ status = "authenticated";
117
133
  log?.info(`[${dp}] WebSocket authenticated as ${msg.agent_id}`);
118
134
  reconnectAttempt = 0; // Reset backoff on successful auth
119
135
  consecutiveAuthFailures = 0; // Reset auth failure counter
@@ -175,9 +191,11 @@ export function startWsClient(opts: WsClientOptions): { stop: () => void } {
175
191
  if (code === 4001) {
176
192
  consecutiveAuthFailures++;
177
193
  if (consecutiveAuthFailures >= MAX_AUTH_FAILURES) {
194
+ status = "disconnected";
178
195
  log?.error(`[${dp}] WebSocket auth failed ${consecutiveAuthFailures} times consecutively, stopping reconnect`);
179
196
  return;
180
197
  }
198
+ status = "reconnecting";
181
199
  log?.warn(`[${dp}] WebSocket auth failed (${consecutiveAuthFailures}/${MAX_AUTH_FAILURES}), force-refreshing token before reconnect`);
182
200
  // Await token refresh so the next connect() picks up the new token
183
201
  try {
@@ -210,12 +228,14 @@ export function startWsClient(opts: WsClientOptions): { stop: () => void } {
210
228
  const delay =
211
229
  RECONNECT_BACKOFF[Math.min(reconnectAttempt, RECONNECT_BACKOFF.length - 1)];
212
230
  reconnectAttempt++;
231
+ status = "reconnecting";
213
232
  log?.info(`[${dp}] WebSocket reconnecting in ${delay}ms (attempt ${reconnectAttempt})`);
214
233
  reconnectTimer = setTimeout(connect, delay);
215
234
  }
216
235
 
217
236
  function stop() {
218
237
  running = false;
238
+ status = "disconnected";
219
239
  if (reconnectTimer) clearTimeout(reconnectTimer);
220
240
  if (keepaliveTimer) { clearInterval(keepaliveTimer); keepaliveTimer = null; }
221
241
  if (ws) {
@@ -229,10 +249,14 @@ export function startWsClient(opts: WsClientOptions): { stop: () => void } {
229
249
  getActiveWsClients().delete(accountId);
230
250
  }
231
251
 
252
+ function getStatus(): WsConnectionStatus {
253
+ return status;
254
+ }
255
+
232
256
  // Start connection
233
257
  connect();
234
258
 
235
- const entry = { stop };
259
+ const entry: WsClientEntry = { stop, getStatus };
236
260
  getActiveWsClients().set(accountId, entry);
237
261
 
238
262
  abortSignal?.addEventListener("abort", stop, { once: true });