@neta-art/cohub 8.10.1 → 8.11.0

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.
@@ -1,5 +1,6 @@
1
- import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
- import { fileBaseName, filePreviewKind } from "../../core/file-preview.js";
1
+ import { BOARD_FONT_STACK, BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
+ import { fileCategory, fileStem } from "../../core/file-snapshot.js";
3
+ import { fileCategoryAccent, fileMetaLine, filePreviewKind, fileTypeLabel } from "../../core/file-preview.js";
3
4
  import { syncTextResolution } from "../text-resolution.js";
4
5
  import { positionShell } from "./base-card-renderer.js";
5
6
  import { drawFarPlate } from "./far-plate.js";
@@ -31,8 +32,12 @@ const TITLE_LINE = TITLE_SIZE * 1.35;
31
32
  const TITLE_MAX_LINES = 2;
32
33
  const EXCERPT_SIZE = 11;
33
34
  const EXCERPT_LINE = EXCERPT_SIZE * 1.45;
34
- const EXCERPT_MAX_LINES = 4;
35
+ const EXCERPT_MAX_LINES = 3;
36
+ const META_SIZE = 10;
37
+ const META_LINE = 14;
38
+ const TYPE_MARK_SIZE = 28;
35
39
  const GAP = 4;
40
+ const STRIPE = 2;
36
41
  /** Zoom below which the title is dropped (glyphs are sub-pixel). */
37
42
  const LOD_TITLE_ZOOM = .35;
38
43
  /** Zoom below which the excerpt is dropped. */
@@ -168,8 +173,13 @@ function sync(container, item, context) {
168
173
  const coverFailed = Boolean(key && !texture && context.hasError(key));
169
174
  const fileState = context.fileState(item.ref.path);
170
175
  const band = coverHeight(item, height);
176
+ const kind = filePreviewKind(item.snapshot);
177
+ const category = fileCategory(item.ref.path, item.snapshot?.mimeType);
178
+ const accent = fileCategoryAccent(category, context.palette);
171
179
  syncTextResolution(parts.title, parts.titleRes, context.zoom);
172
180
  syncTextResolution(parts.excerpt, parts.excerptRes, context.zoom);
181
+ syncTextResolution(parts.meta, parts.metaRes, context.zoom);
182
+ syncTextResolution(parts.typeMark, parts.typeMarkRes, context.zoom);
173
183
  const visualSig = [
174
184
  key ?? "",
175
185
  width,
@@ -180,9 +190,12 @@ function sync(container, item, context) {
180
190
  texture ? `${texture.width}x${texture.height}` : "none",
181
191
  coverFailed,
182
192
  fileState,
193
+ kind,
194
+ category,
183
195
  context.colorScheme,
184
196
  context.palette.surface,
185
- context.palette.hover
197
+ context.palette.hover,
198
+ accent
186
199
  ].join("|");
187
200
  if (visualSig !== parts.visualSig) {
188
201
  parts.visualSig = visualSig;
@@ -218,41 +231,67 @@ function sync(container, item, context) {
218
231
  }
219
232
  parts.cover.visible = showCover;
220
233
  parts.coverMask.visible = showCover;
221
- if (!band) parts.plate.rect(1, 1, 2, height - 2).fill({
222
- color: context.palette.muted,
223
- alpha: .35
234
+ if (!band) parts.plate.rect(1, 1, STRIPE, height - 2).fill({
235
+ color: accent,
236
+ alpha: .55
224
237
  });
225
238
  parts.title.visible = detail !== "plate";
226
239
  parts.excerpt.visible = detail === "full";
240
+ parts.meta.visible = detail === "full";
241
+ parts.typeMark.visible = kind === "blank" && detail !== "plate";
227
242
  }
228
- if (detail === "plate") return;
229
- const title = item.snapshot?.title || fileBaseName(item.ref.path);
243
+ if (detail === "plate") {
244
+ parts.typeMark.visible = false;
245
+ parts.meta.visible = false;
246
+ return;
247
+ }
248
+ const title = item.snapshot?.title || fileStem(item.ref.path);
230
249
  const excerpt = item.snapshot?.excerpt ?? "";
250
+ const meta = fileMetaLine(item.ref.path, item.snapshot?.size);
251
+ const mark = fileTypeLabel(item.ref.path);
231
252
  const innerWidth = Math.max(1, width - 20);
253
+ const showTypeMark = kind === "blank";
232
254
  const textSig = [
233
255
  title,
234
256
  excerpt,
257
+ meta,
258
+ mark,
235
259
  detail,
236
260
  innerWidth,
237
261
  band,
238
262
  height,
263
+ kind,
239
264
  context.palette.text,
240
- context.palette.muted
265
+ context.palette.muted,
266
+ accent
241
267
  ].join("|");
242
268
  if (textSig === parts.textSig) return;
243
269
  parts.textSig = textSig;
244
270
  const top = band > 0 ? band + PADDING * .8 : PADDING;
245
- const contentBottom = height - PADDING;
246
- const titleLines = linesInRoom(Math.max(0, contentBottom - top), TITLE_LINE, TITLE_MAX_LINES);
271
+ const metaReserve = detail === "full" && meta ? META_LINE : 0;
272
+ const contentBottom = height - PADDING - metaReserve;
273
+ let cursor = top;
274
+ if (showTypeMark) {
275
+ const markSize = Math.max(18, Math.min(TYPE_MARK_SIZE, Math.round(height * .22)));
276
+ parts.typeMark.style.fill = accent;
277
+ parts.typeMark.style.fontSize = markSize;
278
+ parts.typeMark.style.lineHeight = markSize * 1.1;
279
+ if (parts.typeMark.text !== mark) parts.typeMark.text = mark;
280
+ parts.typeMark.position.set(PADDING, cursor);
281
+ parts.typeMark.visible = true;
282
+ cursor += parts.typeMark.height + GAP;
283
+ } else parts.typeMark.visible = false;
284
+ const titleLines = linesInRoom(Math.max(0, contentBottom - cursor), TITLE_LINE, TITLE_MAX_LINES);
247
285
  parts.title.style.fill = context.palette.text;
248
286
  fitTextToLines(parts.title, title, titleLines, innerWidth);
249
- parts.title.position.set(PADDING, top);
287
+ parts.title.position.set(PADDING, cursor);
250
288
  parts.title.visible = titleLines > 0;
251
289
  if (detail !== "full") {
252
290
  parts.excerpt.visible = false;
291
+ parts.meta.visible = false;
253
292
  return;
254
293
  }
255
- const excerptTop = top + (titleLines > 0 ? parts.title.height + GAP : 0);
294
+ const excerptTop = cursor + (titleLines > 0 ? parts.title.height + GAP : 0);
256
295
  const excerptLines = linesInRoom(contentBottom - excerptTop, EXCERPT_LINE, EXCERPT_MAX_LINES);
257
296
  const showExcerpt = Boolean(excerpt) && excerptLines > 0;
258
297
  if (showExcerpt) {
@@ -261,6 +300,12 @@ function sync(container, item, context) {
261
300
  parts.excerpt.position.set(PADDING, excerptTop);
262
301
  }
263
302
  parts.excerpt.visible = showExcerpt;
303
+ if (meta) {
304
+ parts.meta.style.fill = context.palette.muted;
305
+ if (parts.meta.text !== meta) parts.meta.text = meta;
306
+ parts.meta.position.set(PADDING, height - PADDING - META_LINE + 2);
307
+ parts.meta.visible = true;
308
+ } else parts.meta.visible = false;
264
309
  }
265
310
  const fileCardRenderer = {
266
311
  id: "file-card",
@@ -302,8 +347,32 @@ const fileCardRenderer = {
302
347
  resolution,
303
348
  roundPixels: true
304
349
  });
350
+ const meta = new Text({
351
+ text: "",
352
+ style: {
353
+ fill: context.palette.muted,
354
+ fontFamily: BOARD_MONO_FONT_STACK,
355
+ fontSize: META_SIZE,
356
+ fontWeight: "500",
357
+ lineHeight: META_LINE
358
+ },
359
+ resolution,
360
+ roundPixels: true
361
+ });
362
+ const typeMark = new Text({
363
+ text: "",
364
+ style: {
365
+ fill: context.palette.muted,
366
+ fontFamily: BOARD_MONO_FONT_STACK,
367
+ fontSize: TYPE_MARK_SIZE,
368
+ fontWeight: "700",
369
+ lineHeight: TYPE_MARK_SIZE * 1.1
370
+ },
371
+ resolution,
372
+ roundPixels: true
373
+ });
305
374
  body.mask = clip;
306
- body.addChild(cover, coverMask, title, excerpt);
375
+ body.addChild(cover, coverMask, typeMark, title, excerpt, meta);
307
376
  root.addChild(plate, body, clip);
308
377
  partsByContainer.set(root, {
309
378
  root,
@@ -312,12 +381,16 @@ const fileCardRenderer = {
312
381
  clip,
313
382
  cover,
314
383
  coverMask,
384
+ typeMark,
315
385
  title,
316
386
  excerpt,
387
+ meta,
317
388
  visualSig: "",
318
389
  textSig: "",
319
390
  titleRes: { resolution },
320
- excerptRes: { resolution }
391
+ excerptRes: { resolution },
392
+ metaRes: { resolution },
393
+ typeMarkRes: { resolution }
321
394
  });
322
395
  if (item.type === "file") sync(root, item, context);
323
396
  return root;
@@ -330,11 +403,12 @@ const fileCardRenderer = {
330
403
  * would mean one draw call per distinct image and defeat the batch.
331
404
  */
332
405
  renderFar: (graphics, item, context) => {
406
+ const category = fileCategory(item.type === "file" ? item.ref.path : "", item.type === "file" ? item.snapshot?.mimeType : void 0);
333
407
  drawFarPlate(graphics, item.frame, {
334
408
  fill: context.palette.surface,
335
409
  fillAlpha: .96,
336
- accent: context.palette.muted,
337
- accentAlpha: .4
410
+ accent: fileCategoryAccent(category, context.palette),
411
+ accentAlpha: .45
338
412
  });
339
413
  },
340
414
  destroy: (container) => {
@@ -268,15 +268,23 @@ type AppPromotionEventKey = typeof APP_PROMOTION_EVENT_KEYS[number];
268
268
  //#region src/app-runtime.d.ts
269
269
  type AppRuntimeInvocationContext = {
270
270
  surface: "page" | "app" | "background" | "broker";
271
- source?: "desktop_command" | "user" | "route";
271
+ source?: "desktop_command" | "user" | "route" | "embed";
272
272
  spaceId?: string;
273
273
  sessionId?: string;
274
274
  turnId?: string;
275
275
  toolCallId?: string;
276
+ /**
277
+ * The App whose page embeds this one, when `source` is `embed`. Present only
278
+ * when that App's published content is served from the embedding frame.
279
+ */
280
+ embedder?: {
281
+ appId: string;
282
+ slug: string;
283
+ };
276
284
  };
277
285
  /** Current navigation context supplied by the embedding Cohub shell. */
278
286
  type AppRuntimeShellContext = {
279
- surface: "workspace" | "background" | "broker";
287
+ surface: "workspace" | "background" | "broker" | "embed";
280
288
  space: {
281
289
  id: string;
282
290
  name?: string | null;
@@ -348,6 +356,8 @@ interface AppRuntimeTransport {
348
356
  subscribeContextChanged?: (listener: AppContextChangedListener) => () => void;
349
357
  /** Whether this transport can address the embedding Cohub workspace. */
350
358
  supportsNavigation?: boolean;
359
+ /** Posts a one-way message to the host; no reply is expected. */
360
+ notify?: (message: Record<string, unknown>) => void;
351
361
  }
352
362
  /**
353
363
  * Bridge-mode transport: posts messages to `window.parent` (the Cohub host
@@ -360,6 +370,7 @@ declare class ParentBridgeTransport implements AppRuntimeTransport {
360
370
  private contextListeners;
361
371
  private contextListener;
362
372
  subscribeContextChanged(listener: AppContextChangedListener): () => void;
373
+ notify(message: Record<string, unknown>): void;
363
374
  request<T>(message: Record<string, unknown>, options?: AppRuntimeRequestOptions): Promise<T | null>;
364
375
  }
365
376
  /**
@@ -392,9 +403,10 @@ declare class PopupBrokerTransport implements AppRuntimeTransport {
392
403
  private resolveAppId;
393
404
  request<T>(message: Record<string, unknown>, options?: AppRuntimeRequestOptions): Promise<T | null>;
394
405
  }
395
- /** Outcome of {@link AppRuntimeApi.requestSpaceAuthorization}. */
406
+ /** Outcome of {@link AppRuntimeApi.requestSpaceAuthorization} / {@link AppRuntimeApi.requestCreateSpaceAuthorization}. */
396
407
  type AppRuntimeAuthorizationResult = {
397
408
  granted: boolean;
409
+ /** Picked or created Space. Null on deny. Set with `granted: false` when create persisted but did not provision. */
398
410
  space: {
399
411
  id: string;
400
412
  name: string | null;
@@ -426,6 +438,12 @@ declare class AppRuntimeApi {
426
438
  context(): Promise<AppRuntimeContext | null>;
427
439
  onContextChanged(listener: AppContextChangedListener): () => void;
428
440
  navigationOpen(target: AppNavigationTarget, call?: AppNavigationCall): Promise<AppNavigationOpenResponse>;
441
+ /**
442
+ * Asks the host to close this App: a workspace tab closes, an embedded page
443
+ * forwards the request to its embedder, a standalone page closes the tab.
444
+ * No-op in broker mode, where the App owns its own window.
445
+ */
446
+ requestClose(): void;
429
447
  getAccessToken(options?: {
430
448
  forceRefresh?: boolean;
431
449
  }): Promise<string | null>;
@@ -451,6 +469,19 @@ declare class AppRuntimeApi {
451
469
  reason?: string;
452
470
  alwaysAsk?: boolean;
453
471
  }): Promise<AppRuntimeAuthorizationResult>;
472
+ /**
473
+ * One consent: create a viewer-owned Space (full `CreateSpaceInput`, same
474
+ * as `spaces.create`) and grant the scopes on it. Never silent — each
475
+ * confirm mints a new Space. The host creates with the viewer's account
476
+ * token; the app never calls `POST /api/spaces` itself.
477
+ * `{ granted: false, space }` means the Space was created but not provisioned;
478
+ * no grant was issued. A viewer deny is `{ granted: false, space: null }`.
479
+ */
480
+ requestCreateSpaceAuthorization(input: {
481
+ scopes: Permission[];
482
+ space: CreateSpaceInput;
483
+ reason?: string;
484
+ }): Promise<AppRuntimeAuthorizationResult>;
454
485
  purchase(input: {
455
486
  productKey: string;
456
487
  purchaseAttemptId?: string;
@@ -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;