@vex-chat/libvex 6.5.1 → 6.5.3
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/Client.d.ts +57 -51
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +140 -40
- package/dist/Client.js.map +1 -1
- package/package.json +1 -1
- package/src/Client.ts +218 -102
package/src/Client.ts
CHANGED
|
@@ -350,18 +350,6 @@ import { uuidToUint8 } from "./utils/uint8uuid.js";
|
|
|
350
350
|
|
|
351
351
|
const _protocolMsgRegex = /��\w+:\w+��/g;
|
|
352
352
|
|
|
353
|
-
/**
|
|
354
|
-
* Permission is a permission to a resource.
|
|
355
|
-
*
|
|
356
|
-
* Common fields:
|
|
357
|
-
* - `permissionID`: unique permission row ID
|
|
358
|
-
* - `userID`: user receiving this grant
|
|
359
|
-
* - `resourceID`: target server/channel/etc.
|
|
360
|
-
* - `resourceType`: type string for the resource
|
|
361
|
-
* - `powerLevel`: authorization level
|
|
362
|
-
*/
|
|
363
|
-
export type { Permission } from "@vex-chat/types";
|
|
364
|
-
|
|
365
353
|
/**
|
|
366
354
|
* @ignore
|
|
367
355
|
*/
|
|
@@ -378,27 +366,6 @@ export interface Channels {
|
|
|
378
366
|
userList: (channelID: string) => Promise<User[]>;
|
|
379
367
|
}
|
|
380
368
|
|
|
381
|
-
/**
|
|
382
|
-
* Device record associated with a user account.
|
|
383
|
-
*
|
|
384
|
-
* Common fields:
|
|
385
|
-
* - `deviceID`: unique device identifier
|
|
386
|
-
* - `owner`: owning user ID
|
|
387
|
-
* - `signKey`: signing public key
|
|
388
|
-
* - `name`: user-facing device name
|
|
389
|
-
* - `lastLogin`: last login timestamp string
|
|
390
|
-
* - `deleted`: soft-delete flag
|
|
391
|
-
*/
|
|
392
|
-
export type { Device } from "@vex-chat/types";
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Public passkey record returned by `client.passkeys.list()` and
|
|
396
|
-
* `client.passkeys.finishRegistration()`. Server-private fields
|
|
397
|
-
* (credential ID, public key, COSE algorithm, signature counter) are
|
|
398
|
-
* never exposed.
|
|
399
|
-
*/
|
|
400
|
-
export type { Passkey } from "@vex-chat/types";
|
|
401
|
-
|
|
402
369
|
/**
|
|
403
370
|
* ClientOptions are the options you can pass into the client.
|
|
404
371
|
*/
|
|
@@ -436,8 +403,41 @@ export interface ClientOptions {
|
|
|
436
403
|
unsafeHttp?: boolean;
|
|
437
404
|
}
|
|
438
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Permission is a permission to a resource.
|
|
408
|
+
*
|
|
409
|
+
* Common fields:
|
|
410
|
+
* - `permissionID`: unique permission row ID
|
|
411
|
+
* - `userID`: user receiving this grant
|
|
412
|
+
* - `resourceID`: target server/channel/etc.
|
|
413
|
+
* - `resourceType`: type string for the resource
|
|
414
|
+
* - `powerLevel`: authorization level
|
|
415
|
+
*/
|
|
416
|
+
export type { Permission } from "@vex-chat/types";
|
|
417
|
+
|
|
439
418
|
export type DeviceRegistrationResult = Device | PendingDeviceRegistration;
|
|
440
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Device record associated with a user account.
|
|
422
|
+
*
|
|
423
|
+
* Common fields:
|
|
424
|
+
* - `deviceID`: unique device identifier
|
|
425
|
+
* - `owner`: owning user ID
|
|
426
|
+
* - `signKey`: signing public key
|
|
427
|
+
* - `name`: user-facing device name
|
|
428
|
+
* - `lastLogin`: last login timestamp string
|
|
429
|
+
* - `deleted`: soft-delete flag
|
|
430
|
+
*/
|
|
431
|
+
export type { Device } from "@vex-chat/types";
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Public passkey record returned by `client.passkeys.list()` and
|
|
435
|
+
* `client.passkeys.finishRegistration()`. Server-private fields
|
|
436
|
+
* (credential ID, public key, COSE algorithm, signature counter) are
|
|
437
|
+
* never exposed.
|
|
438
|
+
*/
|
|
439
|
+
export type { Passkey } from "@vex-chat/types";
|
|
440
|
+
|
|
441
441
|
/**
|
|
442
442
|
* @ignore
|
|
443
443
|
*/
|
|
@@ -556,6 +556,29 @@ export interface Files {
|
|
|
556
556
|
retrieve: (fileID: string, key: string) => Promise<FileResponse | null>;
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
/**
|
|
560
|
+
* @ignore
|
|
561
|
+
*/
|
|
562
|
+
export interface Invites {
|
|
563
|
+
/** Creates an invite for a server and duration. */
|
|
564
|
+
create: (serverID: string, duration: string) => Promise<Invite>;
|
|
565
|
+
/** Redeems an invite and returns the created permission grant. */
|
|
566
|
+
redeem: (inviteID: string) => Promise<Permission>;
|
|
567
|
+
/** Lists active invites for a server. */
|
|
568
|
+
retrieve: (serverID: string) => Promise<Invite[]>;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Keys are a pair of ed25519 public and private keys,
|
|
573
|
+
* encoded as hex strings.
|
|
574
|
+
*/
|
|
575
|
+
export interface Keys {
|
|
576
|
+
/** Secret Ed25519 key as hex. Store securely. */
|
|
577
|
+
private: string;
|
|
578
|
+
/** Public Ed25519 key as hex. */
|
|
579
|
+
public: string;
|
|
580
|
+
}
|
|
581
|
+
|
|
559
582
|
/**
|
|
560
583
|
* Channel is a chat channel on a server.
|
|
561
584
|
*
|
|
@@ -581,29 +604,6 @@ export type { Server } from "@vex-chat/types";
|
|
|
581
604
|
*/
|
|
582
605
|
export type { ServerChannelBootstrap } from "@vex-chat/types";
|
|
583
606
|
|
|
584
|
-
/**
|
|
585
|
-
* @ignore
|
|
586
|
-
*/
|
|
587
|
-
export interface Invites {
|
|
588
|
-
/** Creates an invite for a server and duration. */
|
|
589
|
-
create: (serverID: string, duration: string) => Promise<Invite>;
|
|
590
|
-
/** Redeems an invite and returns the created permission grant. */
|
|
591
|
-
redeem: (inviteID: string) => Promise<Permission>;
|
|
592
|
-
/** Lists active invites for a server. */
|
|
593
|
-
retrieve: (serverID: string) => Promise<Invite[]>;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* Keys are a pair of ed25519 public and private keys,
|
|
598
|
-
* encoded as hex strings.
|
|
599
|
-
*/
|
|
600
|
-
export interface Keys {
|
|
601
|
-
/** Secret Ed25519 key as hex. Store securely. */
|
|
602
|
-
private: string;
|
|
603
|
-
/** Public Ed25519 key as hex. */
|
|
604
|
-
public: string;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
607
|
/**
|
|
608
608
|
* @ignore
|
|
609
609
|
*/
|
|
@@ -753,6 +753,59 @@ export interface RetryRequest {
|
|
|
753
753
|
source: "decrypt_failure" | "server_notify";
|
|
754
754
|
}
|
|
755
755
|
|
|
756
|
+
function compareInboxEntries(
|
|
757
|
+
a: [Uint8Array, MailWS, string],
|
|
758
|
+
b: [Uint8Array, MailWS, string],
|
|
759
|
+
): number {
|
|
760
|
+
const timeCmp = a[2].localeCompare(b[2]);
|
|
761
|
+
if (timeCmp !== 0) {
|
|
762
|
+
return timeCmp;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const aMail = a[1];
|
|
766
|
+
const bMail = b[1];
|
|
767
|
+
if (aMail.sender !== bMail.sender) {
|
|
768
|
+
return aMail.sender.localeCompare(bMail.sender, "en");
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const typeCmp = aMail.mailType - bMail.mailType;
|
|
772
|
+
if (typeCmp !== 0) {
|
|
773
|
+
return typeCmp;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (
|
|
777
|
+
aMail.mailType === MailType.subsequent &&
|
|
778
|
+
bMail.mailType === MailType.subsequent
|
|
779
|
+
) {
|
|
780
|
+
const aHeader = tryDecodeRatchetHeader(aMail.extra);
|
|
781
|
+
const bHeader = tryDecodeRatchetHeader(bMail.extra);
|
|
782
|
+
if (aHeader && bHeader) {
|
|
783
|
+
const dhCmp = XUtils.encodeHex(aHeader.dhPub).localeCompare(
|
|
784
|
+
XUtils.encodeHex(bHeader.dhPub),
|
|
785
|
+
"en",
|
|
786
|
+
);
|
|
787
|
+
if (dhCmp !== 0) {
|
|
788
|
+
return dhCmp;
|
|
789
|
+
}
|
|
790
|
+
const pnCmp = aHeader.pn - bHeader.pn;
|
|
791
|
+
if (pnCmp !== 0) {
|
|
792
|
+
return pnCmp;
|
|
793
|
+
}
|
|
794
|
+
return aHeader.n - bHeader.n;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return aMail.nonce.toString().localeCompare(bMail.nonce.toString(), "en");
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function tryDecodeRatchetHeader(extra: Uint8Array) {
|
|
802
|
+
try {
|
|
803
|
+
return decodeRatchetHeader(extra);
|
|
804
|
+
} catch {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
756
809
|
/** Zod schema matching the {@link Message} interface for forwarded-message decode. */
|
|
757
810
|
const messageSchema: z.ZodType<Message> = z.object({
|
|
758
811
|
authorID: z.string(),
|
|
@@ -1344,6 +1397,8 @@ export class Client {
|
|
|
1344
1397
|
retrieve: this.fetchUser.bind(this),
|
|
1345
1398
|
};
|
|
1346
1399
|
|
|
1400
|
+
private autoReconnectEnabled = false;
|
|
1401
|
+
|
|
1347
1402
|
private readonly cryptoProfile: CryptoProfile;
|
|
1348
1403
|
|
|
1349
1404
|
private readonly database: Storage;
|
|
@@ -1390,18 +1445,19 @@ export class Client {
|
|
|
1390
1445
|
private readonly notFoundUsers = new Map<string, number>();
|
|
1391
1446
|
|
|
1392
1447
|
private readonly options?: ClientOptions | undefined;
|
|
1393
|
-
|
|
1394
1448
|
private pingInterval: null | ReturnType<typeof setTimeout> = null;
|
|
1395
1449
|
/**
|
|
1396
1450
|
* Bumped when the WebSocket is torn down and re-opened so the previous
|
|
1397
1451
|
* `postAuth` loop exits instead of overlapping a new one.
|
|
1398
1452
|
*/
|
|
1399
1453
|
private postAuthVersion = 0;
|
|
1400
|
-
|
|
1401
1454
|
private readonly prefixes:
|
|
1402
1455
|
| { HTTP: "http://"; WS: "ws://" }
|
|
1403
1456
|
| { HTTP: "https://"; WS: "wss://" };
|
|
1404
1457
|
private reading: boolean = false;
|
|
1458
|
+
private reconnectAttempt = 0;
|
|
1459
|
+
private reconnectPromise: null | Promise<void> = null;
|
|
1460
|
+
private reconnectTimer: null | ReturnType<typeof setTimeout> = null;
|
|
1405
1461
|
private retentionPurgeDebounce: null | ReturnType<typeof setTimeout> = null;
|
|
1406
1462
|
private readonly seenMailIDs: Set<string> = new Set();
|
|
1407
1463
|
private readonly sessionHealBackoffUntil = new Map<string, number>();
|
|
@@ -1711,6 +1767,11 @@ export class Client {
|
|
|
1711
1767
|
*/
|
|
1712
1768
|
public async close(muteEvent = false): Promise<void> {
|
|
1713
1769
|
this.manuallyClosing = true;
|
|
1770
|
+
this.autoReconnectEnabled = false;
|
|
1771
|
+
if (this.reconnectTimer) {
|
|
1772
|
+
clearTimeout(this.reconnectTimer);
|
|
1773
|
+
this.reconnectTimer = null;
|
|
1774
|
+
}
|
|
1714
1775
|
this.httpAbortController.abort();
|
|
1715
1776
|
this.socket.close();
|
|
1716
1777
|
await this.database.close();
|
|
@@ -1776,6 +1837,7 @@ export class Client {
|
|
|
1776
1837
|
const { deviceToken } = decodeAxios(ConnectResponseCodec, res.data);
|
|
1777
1838
|
this.http.defaults.headers.common["X-Device-Token"] = deviceToken;
|
|
1778
1839
|
|
|
1840
|
+
this.autoReconnectEnabled = true;
|
|
1779
1841
|
this.initSocket();
|
|
1780
1842
|
// Yield the event loop so the WS open callback fires and sends the
|
|
1781
1843
|
// auth message before OTK generation starts. OTK top-up is best-effort
|
|
@@ -1979,45 +2041,20 @@ export class Client {
|
|
|
1979
2041
|
* newly-registered second device.
|
|
1980
2042
|
*/
|
|
1981
2043
|
public async reconnectWebsocket(): Promise<void> {
|
|
1982
|
-
this.
|
|
1983
|
-
|
|
1984
|
-
clearInterval(this.pingInterval);
|
|
1985
|
-
this.pingInterval = null;
|
|
2044
|
+
if (this.isManualCloseInFlight()) {
|
|
2045
|
+
throw new WebSocketNotOpenError(this.socket.readyState);
|
|
1986
2046
|
}
|
|
1987
|
-
this.
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
const t = setTimeout(() => {
|
|
1991
|
-
this.off("connected", onC);
|
|
1992
|
-
reject(
|
|
1993
|
-
new Error(
|
|
1994
|
-
"reconnectWebsocket: timed out waiting for authorized",
|
|
1995
|
-
),
|
|
1996
|
-
);
|
|
1997
|
-
}, 15_000);
|
|
1998
|
-
const onC = () => {
|
|
1999
|
-
clearTimeout(t);
|
|
2000
|
-
this.off("connected", onC);
|
|
2001
|
-
resolve();
|
|
2002
|
-
};
|
|
2003
|
-
this.on("connected", onC);
|
|
2004
|
-
try {
|
|
2005
|
-
this.initSocket();
|
|
2006
|
-
} catch (err: unknown) {
|
|
2007
|
-
clearTimeout(t);
|
|
2008
|
-
this.off("connected", onC);
|
|
2009
|
-
const e =
|
|
2010
|
-
err instanceof Error
|
|
2011
|
-
? err
|
|
2012
|
-
: new Error(String(err), { cause: err });
|
|
2013
|
-
reject(e);
|
|
2014
|
-
}
|
|
2015
|
-
});
|
|
2016
|
-
} catch (e: unknown) {
|
|
2017
|
-
throw e instanceof Error ? e : new Error(String(e), { cause: e });
|
|
2047
|
+
if (this.reconnectTimer) {
|
|
2048
|
+
clearTimeout(this.reconnectTimer);
|
|
2049
|
+
this.reconnectTimer = null;
|
|
2018
2050
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
2051
|
+
if (this.reconnectPromise) {
|
|
2052
|
+
return this.reconnectPromise;
|
|
2053
|
+
}
|
|
2054
|
+
this.reconnectPromise = this.reconnectWebsocketOnce().finally(() => {
|
|
2055
|
+
this.reconnectPromise = null;
|
|
2056
|
+
});
|
|
2057
|
+
return this.reconnectPromise;
|
|
2021
2058
|
}
|
|
2022
2059
|
|
|
2023
2060
|
/**
|
|
@@ -3017,7 +3054,7 @@ export class Client {
|
|
|
3017
3054
|
const rawInbox = z
|
|
3018
3055
|
.array(mailInboxEntry)
|
|
3019
3056
|
.parse(msgpack.decode(mailBuffer));
|
|
3020
|
-
const inbox = rawInbox.sort(
|
|
3057
|
+
const inbox = rawInbox.sort(compareInboxEntries);
|
|
3021
3058
|
|
|
3022
3059
|
if (libvexDebugDmEnabled()) {
|
|
3023
3060
|
const did = (() => {
|
|
@@ -3062,8 +3099,9 @@ export class Client {
|
|
|
3062
3099
|
fetchErr,
|
|
3063
3100
|
);
|
|
3064
3101
|
}
|
|
3102
|
+
} finally {
|
|
3103
|
+
this.fetchingMail = false;
|
|
3065
3104
|
}
|
|
3066
|
-
this.fetchingMail = false;
|
|
3067
3105
|
}
|
|
3068
3106
|
|
|
3069
3107
|
private async getMessageHistory(userID: string): Promise<Message[]> {
|
|
@@ -3220,7 +3258,6 @@ export class Client {
|
|
|
3220
3258
|
}
|
|
3221
3259
|
case "mail":
|
|
3222
3260
|
await this.getMail();
|
|
3223
|
-
this.fetchingMail = false;
|
|
3224
3261
|
break;
|
|
3225
3262
|
case "permission":
|
|
3226
3263
|
this.emitter.emit(
|
|
@@ -3323,12 +3360,14 @@ export class Client {
|
|
|
3323
3360
|
}
|
|
3324
3361
|
if (!this.manuallyClosing) {
|
|
3325
3362
|
this.emitter.emit("disconnect");
|
|
3363
|
+
this.scheduleReconnect();
|
|
3326
3364
|
}
|
|
3327
3365
|
});
|
|
3328
3366
|
|
|
3329
3367
|
this.socket.on("error", (_error: Error) => {
|
|
3330
3368
|
if (!this.manuallyClosing) {
|
|
3331
3369
|
this.emitter.emit("disconnect");
|
|
3370
|
+
this.scheduleReconnect();
|
|
3332
3371
|
}
|
|
3333
3372
|
});
|
|
3334
3373
|
|
|
@@ -3363,6 +3402,7 @@ export class Client {
|
|
|
3363
3402
|
"Received unauthorized message from server.",
|
|
3364
3403
|
);
|
|
3365
3404
|
case "authorized":
|
|
3405
|
+
this.reconnectAttempt = 0;
|
|
3366
3406
|
this.emitter.emit("connected");
|
|
3367
3407
|
this.postAuth().catch(ignoreSocketTeardown);
|
|
3368
3408
|
break;
|
|
@@ -3654,7 +3694,6 @@ export class Client {
|
|
|
3654
3694
|
try {
|
|
3655
3695
|
await this.getMail();
|
|
3656
3696
|
count++;
|
|
3657
|
-
this.fetchingMail = false;
|
|
3658
3697
|
|
|
3659
3698
|
if (count > 10) {
|
|
3660
3699
|
void this.negotiateOTK();
|
|
@@ -4234,7 +4273,9 @@ export class Client {
|
|
|
4234
4273
|
},
|
|
4235
4274
|
);
|
|
4236
4275
|
}
|
|
4237
|
-
|
|
4276
|
+
if (failureCount >= 2) {
|
|
4277
|
+
healSession();
|
|
4278
|
+
}
|
|
4238
4279
|
if (failureCount === 1) {
|
|
4239
4280
|
this.emitter.emit("retryRequest", {
|
|
4240
4281
|
mailID: mail.mailID,
|
|
@@ -4367,6 +4408,48 @@ export class Client {
|
|
|
4367
4408
|
}
|
|
4368
4409
|
}
|
|
4369
4410
|
|
|
4411
|
+
private async reconnectWebsocketOnce(): Promise<void> {
|
|
4412
|
+
this.postAuthVersion++;
|
|
4413
|
+
if (this.pingInterval) {
|
|
4414
|
+
clearInterval(this.pingInterval);
|
|
4415
|
+
this.pingInterval = null;
|
|
4416
|
+
}
|
|
4417
|
+
this.socket.close();
|
|
4418
|
+
try {
|
|
4419
|
+
await new Promise<void>((resolve, reject) => {
|
|
4420
|
+
const t = setTimeout(() => {
|
|
4421
|
+
this.off("connected", onC);
|
|
4422
|
+
reject(
|
|
4423
|
+
new Error(
|
|
4424
|
+
"reconnectWebsocket: timed out waiting for authorized",
|
|
4425
|
+
),
|
|
4426
|
+
);
|
|
4427
|
+
}, 15_000);
|
|
4428
|
+
const onC = () => {
|
|
4429
|
+
clearTimeout(t);
|
|
4430
|
+
this.off("connected", onC);
|
|
4431
|
+
resolve();
|
|
4432
|
+
};
|
|
4433
|
+
this.on("connected", onC);
|
|
4434
|
+
try {
|
|
4435
|
+
this.initSocket();
|
|
4436
|
+
} catch (err: unknown) {
|
|
4437
|
+
clearTimeout(t);
|
|
4438
|
+
this.off("connected", onC);
|
|
4439
|
+
const e =
|
|
4440
|
+
err instanceof Error
|
|
4441
|
+
? err
|
|
4442
|
+
: new Error(String(err), { cause: err });
|
|
4443
|
+
reject(e);
|
|
4444
|
+
}
|
|
4445
|
+
});
|
|
4446
|
+
} catch (e: unknown) {
|
|
4447
|
+
throw e instanceof Error ? e : new Error(String(e), { cause: e });
|
|
4448
|
+
}
|
|
4449
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
4450
|
+
await this.negotiateOTK();
|
|
4451
|
+
}
|
|
4452
|
+
|
|
4370
4453
|
private async redeemInvite(inviteID: string): Promise<Permission> {
|
|
4371
4454
|
const res = await this.http.patch(
|
|
4372
4455
|
this.getHost() + "/invite/" + inviteID,
|
|
@@ -4601,6 +4684,25 @@ export class Client {
|
|
|
4601
4684
|
}
|
|
4602
4685
|
}
|
|
4603
4686
|
|
|
4687
|
+
private scheduleReconnect(): void {
|
|
4688
|
+
if (
|
|
4689
|
+
!this.autoReconnectEnabled ||
|
|
4690
|
+
this.isManualCloseInFlight() ||
|
|
4691
|
+
this.reconnectPromise ||
|
|
4692
|
+
this.reconnectTimer
|
|
4693
|
+
) {
|
|
4694
|
+
return;
|
|
4695
|
+
}
|
|
4696
|
+
const delayMs = Math.min(30_000, 250 * 2 ** this.reconnectAttempt);
|
|
4697
|
+
this.reconnectAttempt = Math.min(this.reconnectAttempt + 1, 8);
|
|
4698
|
+
this.reconnectTimer = setTimeout(() => {
|
|
4699
|
+
this.reconnectTimer = null;
|
|
4700
|
+
this.reconnectWebsocket().catch(() => {
|
|
4701
|
+
this.scheduleReconnect();
|
|
4702
|
+
});
|
|
4703
|
+
}, delayMs);
|
|
4704
|
+
}
|
|
4705
|
+
|
|
4604
4706
|
private scheduleRetentionPurge(): void {
|
|
4605
4707
|
if (this.retentionPurgeDebounce) {
|
|
4606
4708
|
clearTimeout(this.retentionPurgeDebounce);
|
|
@@ -4623,7 +4725,8 @@ export class Client {
|
|
|
4623
4725
|
throw new WebSocketNotOpenError(this.socket.readyState);
|
|
4624
4726
|
}
|
|
4625
4727
|
if (this.socket.readyState === 2 || this.socket.readyState === 3) {
|
|
4626
|
-
|
|
4728
|
+
await this.reconnectWebsocket();
|
|
4729
|
+
continue;
|
|
4627
4730
|
}
|
|
4628
4731
|
if (elapsed >= maxWaitMs) {
|
|
4629
4732
|
throw new WebSocketNotOpenError(this.socket.readyState);
|
|
@@ -4644,7 +4747,20 @@ export class Client {
|
|
|
4644
4747
|
// discarded callers (`pong`, `ping`) can `.catch(ignore)` the
|
|
4645
4748
|
// teardown without an unhandled rejection, and real callers
|
|
4646
4749
|
// can choose to retry on the next reconnect.
|
|
4647
|
-
|
|
4750
|
+
const packed = XUtils.packMessage(msg, header);
|
|
4751
|
+
try {
|
|
4752
|
+
this.socket.send(packed);
|
|
4753
|
+
} catch (err: unknown) {
|
|
4754
|
+
if (
|
|
4755
|
+
err instanceof WebSocketNotOpenError &&
|
|
4756
|
+
!this.isManualCloseInFlight()
|
|
4757
|
+
) {
|
|
4758
|
+
await this.reconnectWebsocket();
|
|
4759
|
+
this.socket.send(packed);
|
|
4760
|
+
return;
|
|
4761
|
+
}
|
|
4762
|
+
throw err;
|
|
4763
|
+
}
|
|
4648
4764
|
}
|
|
4649
4765
|
|
|
4650
4766
|
private async sendGroupMessage(
|