@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.
- package/dist/board/core/file-preview.d.ts +18 -139
- package/dist/board/core/file-preview.js +23 -200
- package/dist/board/core/file-snapshot.d.ts +106 -0
- package/dist/board/core/file-snapshot.js +503 -0
- package/dist/board/index.d.ts +3 -2
- package/dist/board/index.js +3 -2
- package/dist/board/render/renderers/file-card-renderer.js +92 -18
- package/dist/chunks/http.d.ts +34 -3
- package/dist/chunks/websocket.d.ts +3 -0
- package/dist/chunks/websocket.js +97 -26
- package/dist/index.d.ts +57 -1
- package/dist/index.js +349 -20
- package/docs/app-runtime-guide.md +25 -3
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
-
import {
|
|
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 =
|
|
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,
|
|
222
|
-
color:
|
|
223
|
-
alpha: .
|
|
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")
|
|
229
|
-
|
|
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
|
|
246
|
-
const
|
|
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,
|
|
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 =
|
|
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
|
|
337
|
-
accentAlpha: .
|
|
410
|
+
accent: fileCategoryAccent(category, context.palette),
|
|
411
|
+
accentAlpha: .45
|
|
338
412
|
});
|
|
339
413
|
},
|
|
340
414
|
destroy: (container) => {
|
package/dist/chunks/http.d.ts
CHANGED
|
@@ -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;
|
package/dist/chunks/websocket.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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;
|