@neta-art/cohub 8.10.1 → 8.10.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.
@@ -18,6 +18,7 @@ type WebsocketClientOptions = {
18
18
  autoReconnect?: boolean;
19
19
  reconnectBaseDelayMs?: number;
20
20
  reconnectMaxDelayMs?: number;
21
+ connectTimeoutMs?: number;
21
22
  pingIntervalMs?: number;
22
23
  pongTimeoutMs?: number;
23
24
  debug?: boolean;
@@ -99,6 +100,7 @@ declare class WebsocketClient {
99
100
  private readonly autoReconnect;
100
101
  private readonly reconnectBaseDelayMs;
101
102
  private readonly reconnectMaxDelayMs;
103
+ private readonly connectTimeoutMs;
102
104
  private readonly pingIntervalMs;
103
105
  private readonly pongTimeoutMs;
104
106
  private readonly debug;
@@ -114,6 +116,7 @@ declare class WebsocketClient {
114
116
  private retryAuthClose;
115
117
  private manuallyClosed;
116
118
  private connectPromise;
119
+ private pendingConnectReject;
117
120
  private authWaiter;
118
121
  private awaitingPong;
119
122
  private lastPingRequestId;
@@ -61,11 +61,16 @@ const normalizeOptions = (options = {}) => ({
61
61
  autoReconnect: options.autoReconnect !== false,
62
62
  reconnectBaseDelayMs: options.reconnectBaseDelayMs ?? 1e3,
63
63
  reconnectMaxDelayMs: options.reconnectMaxDelayMs ?? 15e3,
64
+ connectTimeoutMs: normalizeConnectTimeoutMs(options.connectTimeoutMs ?? 15e3),
64
65
  pingIntervalMs: options.pingIntervalMs ?? 2e4,
65
66
  pongTimeoutMs: options.pongTimeoutMs ?? 15e3,
66
67
  debug: options.debug === true
67
68
  });
68
69
  const formatCloseMessage = (code, reason) => `WebSocket closed: ${code ?? 0} ${reason || ""}`.trim();
70
+ const normalizeConnectTimeoutMs = (value) => {
71
+ if (!Number.isInteger(value) || value < 1 || value > 2147483647) throw new Error("connectTimeoutMs must be an integer between 1 and 2147483647");
72
+ return value;
73
+ };
69
74
  const AUTH_CLOSE_CODE = 4003;
70
75
  const AUTH_CLOSE_REASON = "authentication failed";
71
76
  const isRetryableCloseCode = (code) => {
@@ -135,6 +140,7 @@ var WebsocketClient = class {
135
140
  autoReconnect;
136
141
  reconnectBaseDelayMs;
137
142
  reconnectMaxDelayMs;
143
+ connectTimeoutMs;
138
144
  pingIntervalMs;
139
145
  pongTimeoutMs;
140
146
  debug;
@@ -150,6 +156,7 @@ var WebsocketClient = class {
150
156
  retryAuthClose = false;
151
157
  manuallyClosed = false;
152
158
  connectPromise = null;
159
+ pendingConnectReject = null;
153
160
  authWaiter = null;
154
161
  awaitingPong = false;
155
162
  lastPingRequestId = null;
@@ -170,6 +177,7 @@ var WebsocketClient = class {
170
177
  this.autoReconnect = normalized.autoReconnect;
171
178
  this.reconnectBaseDelayMs = normalized.reconnectBaseDelayMs;
172
179
  this.reconnectMaxDelayMs = normalized.reconnectMaxDelayMs;
180
+ this.connectTimeoutMs = normalized.connectTimeoutMs;
173
181
  this.pingIntervalMs = normalized.pingIntervalMs;
174
182
  this.pongTimeoutMs = normalized.pongTimeoutMs;
175
183
  this.debug = normalized.debug;
@@ -204,19 +212,70 @@ var WebsocketClient = class {
204
212
  const ws = new this.WebSocketImpl(this.url);
205
213
  this.ws = ws;
206
214
  let settled = false;
215
+ let closed = false;
216
+ let connectTimer = null;
217
+ const clearConnectTimer = () => {
218
+ if (connectTimer === null) return;
219
+ clearTimeout(connectTimer);
220
+ connectTimer = null;
221
+ };
207
222
  const rejectOnce = (error) => {
208
223
  if (settled) return;
224
+ clearConnectTimer();
209
225
  settled = true;
210
226
  this.connectPromise = null;
227
+ if (this.pendingConnectReject === rejectOnce) this.pendingConnectReject = null;
211
228
  reject(error);
212
229
  };
213
230
  const resolveOnce = () => {
214
231
  if (settled) return;
232
+ clearConnectTimer();
215
233
  settled = true;
216
234
  this.connectPromise = null;
235
+ if (this.pendingConnectReject === rejectOnce) this.pendingConnectReject = null;
217
236
  resolve();
218
237
  };
238
+ const isCurrentConnection = () => this.ws === ws && !closed;
239
+ const cleanupSocket = () => {
240
+ ws.onopen = null;
241
+ ws.onmessage = null;
242
+ ws.onerror = null;
243
+ ws.onclose = null;
244
+ };
245
+ const handleClose = (event) => {
246
+ if (closed || this.ws !== ws) return;
247
+ closed = true;
248
+ clearConnectTimer();
249
+ this.stopPingLoop();
250
+ const wasConnecting = this.state === "connecting" || this.state === "reconnecting";
251
+ this.state = "closed";
252
+ this.ws = null;
253
+ this.compactStreamContexts.clear();
254
+ this.patchStreamBuffers.clear();
255
+ this.resetRoomSubscriptions();
256
+ const closeError = new Error(formatCloseMessage(event.code, event.reason));
257
+ this.rejectAuthWaiter(closeError);
258
+ const retryAuthClose = event.code === AUTH_CLOSE_CODE && this.retryAuthClose;
259
+ this.retryAuthClose = false;
260
+ const willReconnect = !this.manuallyClosed && this.autoReconnect && (retryAuthClose || isRetryableCloseCode(event.code));
261
+ this.log("closed", {
262
+ code: event.code,
263
+ reason: event.reason,
264
+ willReconnect,
265
+ wasConnecting
266
+ });
267
+ this.emit("close", {
268
+ code: event.code,
269
+ reason: event.reason,
270
+ willReconnect
271
+ });
272
+ cleanupSocket();
273
+ if (wasConnecting) rejectOnce(closeError);
274
+ if (willReconnect) this.scheduleReconnect(event.code, event.reason);
275
+ };
219
276
  ws.onopen = async () => {
277
+ if (!isCurrentConnection()) return;
278
+ clearConnectTimer();
220
279
  try {
221
280
  this.log("connected", {
222
281
  url: this.url,
@@ -225,12 +284,14 @@ var WebsocketClient = class {
225
284
  });
226
285
  this.startPingLoop();
227
286
  await this.authenticate();
287
+ if (!isCurrentConnection()) return;
228
288
  this.state = "open";
229
289
  this.restoreRoomSubscriptions();
230
290
  this.reconnectAttempt = 0;
231
291
  this.emit("open", { connectionId: this.connectionId });
232
292
  resolveOnce();
233
293
  } catch (error) {
294
+ if (!isCurrentConnection()) return;
234
295
  const authError = error instanceof Error ? error : /* @__PURE__ */ new Error("authentication failed");
235
296
  const recoverable = Boolean(this.getAccessToken && this.autoReconnect && !this.manuallyClosed && !this.authReconnectAttempted);
236
297
  this.retryAuthClose = recoverable;
@@ -247,41 +308,37 @@ var WebsocketClient = class {
247
308
  }
248
309
  };
249
310
  ws.onmessage = (event) => {
311
+ if (!isCurrentConnection()) return;
250
312
  this.handleMessage(event.data);
251
313
  };
252
314
  ws.onerror = (error) => {
315
+ if (!isCurrentConnection()) return;
253
316
  this.emit("error", {
254
317
  error,
255
318
  recoverable: !this.manuallyClosed
256
319
  });
257
320
  };
258
321
  ws.onclose = (event) => {
259
- this.stopPingLoop();
260
- const wasConnecting = this.state === "connecting" || this.state === "reconnecting";
261
- this.state = "closed";
262
- this.ws = null;
263
- this.compactStreamContexts.clear();
264
- this.patchStreamBuffers.clear();
265
- this.resetRoomSubscriptions();
266
- const closeError = new Error(formatCloseMessage(event.code, event.reason));
267
- this.rejectAuthWaiter(closeError);
268
- const retryAuthClose = event.code === AUTH_CLOSE_CODE && this.retryAuthClose;
269
- this.retryAuthClose = false;
270
- const willReconnect = !this.manuallyClosed && this.autoReconnect && (retryAuthClose || isRetryableCloseCode(event.code));
271
- this.log("closed", {
272
- code: event.code,
273
- reason: event.reason,
274
- willReconnect,
275
- wasConnecting
276
- });
277
- this.emit("close", {
278
- code: event.code,
279
- reason: event.reason,
280
- willReconnect
281
- });
282
- if (wasConnecting) rejectOnce(closeError);
283
- if (willReconnect) this.scheduleReconnect(event.code, event.reason);
322
+ handleClose(event);
284
323
  };
324
+ this.pendingConnectReject = rejectOnce;
325
+ connectTimer = setTimeout(() => {
326
+ if (!isCurrentConnection() || settled) return;
327
+ const reason = "connect timeout";
328
+ const error = /* @__PURE__ */ new Error(`WebSocket ${reason} after ${this.connectTimeoutMs}ms`);
329
+ this.emit("error", {
330
+ error,
331
+ recoverable: !this.manuallyClosed && this.autoReconnect
332
+ });
333
+ try {
334
+ ws.close(4002, reason);
335
+ } finally {
336
+ handleClose({
337
+ code: 4002,
338
+ reason
339
+ });
340
+ }
341
+ }, this.connectTimeoutMs);
285
342
  });
286
343
  return this.connectPromise;
287
344
  }
@@ -291,9 +348,23 @@ var WebsocketClient = class {
291
348
  this.stopPingLoop();
292
349
  this.state = "closed";
293
350
  this.rejectAuthWaiter(/* @__PURE__ */ new Error("disconnected"));
294
- this.ws?.close(code, reason);
351
+ const ws = this.ws;
295
352
  this.ws = null;
353
+ ws?.close(code, reason);
354
+ this.pendingConnectReject?.(/* @__PURE__ */ new Error(`WebSocket disconnected: ${reason}`));
355
+ this.pendingConnectReject = null;
296
356
  this.connectPromise = null;
357
+ if (ws) {
358
+ ws.onopen = null;
359
+ ws.onmessage = null;
360
+ ws.onerror = null;
361
+ ws.onclose = null;
362
+ this.emit("close", {
363
+ code,
364
+ reason,
365
+ willReconnect: false
366
+ });
367
+ }
297
368
  this.authReconnectAttempted = false;
298
369
  this.forceRefreshOnNextAuth = false;
299
370
  this.retryAuthClose = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub",
3
- "version": "8.10.1",
3
+ "version": "8.10.2",
4
4
  "description": "Cohub SDK for spaces, sessions, boards, and realtime agent collaboration.",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,