diff --git a/package.json b/package.json index bc5788b..331fbd5 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "author": "MARCROCK22", "license": "Apache-2.0", "dependencies": { - "magic-bytes.js": "^1.10.0" + "magic-bytes.js": "^1.10.0", + "ws": "^8.18.0" }, "lint-staged": { "*.ts": [ @@ -34,6 +35,7 @@ "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", "@types/node": "^20.14.11", + "@types/ws": "^8.5.12", "husky": "^9.1.1", "lint-staged": "^15.2.7", "rimraf": "5.0.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81d7718..9420f86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: magic-bytes.js: specifier: ^1.10.0 version: 1.10.0 + ws: + specifier: ^8.18.0 + version: 8.18.0 optionalDependencies: chokidar: specifier: ^3.6.0 @@ -37,6 +40,9 @@ importers: '@types/node': specifier: ^20.14.11 version: 20.14.11 + '@types/ws': + specifier: ^8.5.12 + version: 8.5.12 husky: specifier: ^9.1.1 version: 9.1.1 @@ -203,6 +209,9 @@ packages: '@types/node@20.14.11': resolution: {integrity: sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==} + '@types/ws@8.5.12': + resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -865,6 +874,18 @@ packages: resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} engines: {node: '>=18'} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -1071,6 +1092,10 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/ws@8.5.12': + dependencies: + '@types/node': 20.14.11 + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -1682,6 +1707,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.0 + ws@8.18.0: {} + y18n@5.0.8: {} yaml@2.4.5: {} diff --git a/src/websocket/discord/basesocket.ts b/src/websocket/discord/basesocket.ts index fc0ff83..2d41c10 100644 --- a/src/websocket/discord/basesocket.ts +++ b/src/websocket/discord/basesocket.ts @@ -1,58 +1,60 @@ import { randomUUID } from 'node:crypto'; -import { SeyfertWebSocket } from './socket/custom'; +// import { SeyfertWebSocket } from './socket/custom'; +import { WebSocket as NodeJSWebSocket } from 'ws'; export class BaseSocket { - private internal: SeyfertWebSocket | WebSocket; + private internal: NodeJSWebSocket | WebSocket; ping?: () => Promise; constructor(kind: 'ws' | 'bun', url: string) { - this.internal = kind === 'ws' ? new SeyfertWebSocket(url) : new WebSocket(url); + this.internal = kind === 'ws' ? new NodeJSWebSocket(url) : new WebSocket(url); + // this.internal = kind === 'ws' ? new SeyfertWebSocket(url) : new WebSocket(url); - if (kind === 'ws') { - const ws = this.internal as SeyfertWebSocket; - this.ping = ws.waitPing.bind(ws); - ws.onpong = data => { - const promise = ws.__promises.get(data); - if (promise) { - ws.__promises.delete(data); - promise?.resolve(); - } - }; - } else { - const ws = this.internal as WebSocket; - this.ping = () => { - return new Promise(res => { - const nonce = randomUUID(); - const start = performance.now(); - const listener = (data: Buffer) => { - if (data.toString() !== nonce) return; - //@ts-expect-error bun support - ws.removeListener('pong', listener); - res(performance.now() - start); - }; + // if (kind === 'ws') { + // const ws = this.internal as NodeJSWebSocket; + // this.ping = ws.waitPing.bind(ws); + // ws.onpong = data => { + // const promise = ws.__promises.get(data); + // if (promise) { + // ws.__promises.delete(data); + // promise?.resolve(); + // } + // }; + // } else { + const ws = this.internal as WebSocket; + this.ping = () => { + return new Promise(res => { + const nonce = randomUUID(); + const start = performance.now(); + const listener = (data: Buffer) => { + if (data.toString() !== nonce) return; //@ts-expect-error bun support - ws.on('pong', listener); - //@ts-expect-error bun support - ws.ping(nonce); - }); - }; - } + ws.removeListener('pong', listener); + res(performance.now() - start); + }; + //@ts-expect-error bun support + ws.on('pong', listener); + //@ts-expect-error bun support + ws.ping(nonce); + }); + }; + // } } - set onopen(callback: SeyfertWebSocket['onopen']) { + set onopen(callback: NodeJSWebSocket['onopen']) { this.internal.onopen = callback; } - set onmessage(callback: SeyfertWebSocket['onmessage']) { + set onmessage(callback: NodeJSWebSocket['onmessage']) { this.internal.onmessage = callback; } - set onclose(callback: SeyfertWebSocket['onclose']) { + set onclose(callback: NodeJSWebSocket['onclose']) { this.internal.onclose = callback; } - set onerror(callback: SeyfertWebSocket['onerror']) { + set onerror(callback: NodeJSWebSocket['onerror']) { this.internal.onerror = callback; } @@ -60,7 +62,8 @@ export class BaseSocket { return this.internal.send(data); } - close(...args: Parameters) { + close(...args: Parameters) { + //@ts-expect-error return this.internal.close(...args); } diff --git a/src/websocket/discord/shard.ts b/src/websocket/discord/shard.ts index b7870f2..92de029 100644 --- a/src/websocket/discord/shard.ts +++ b/src/websocket/discord/shard.ts @@ -25,7 +25,7 @@ export interface ShardHeart { export class Shard { debugger?: Logger; data: Partial | ShardData = { - resumeSeq: null, + resume_seq: null, }; websocket: BaseSocket | null = null; @@ -95,18 +95,20 @@ export class Shard { return; } + clearTimeout(this.heart.nodeInterval); + this.debugger?.debug(`[Shard #${this.id}] Connecting to ${this.currentGatewayURL}`); // @ts-expect-error @types/bun cause erros in compile // biome-ignore lint/correctness/noUndeclaredVariables: /\ bun lol this.websocket = new BaseSocket(typeof Bun === 'undefined' ? 'ws' : 'bun', this.currentGatewayURL); - + //@ts-expect-error this.websocket!.onmessage = ({ data }: { data: string | Buffer }) => { this.handleMessage(data); }; this.websocket!.onclose = (event: { code: number; reason: string }) => this.handleClosed(event); - + //@ts-expect-error this.websocket!.onerror = (event: ErrorEvent) => this.debugger?.error(event); this.websocket!.onopen = () => { @@ -150,14 +152,14 @@ export class Shard { } get resumable() { - return !!(this.data.resume_gateway_url && this.data.session_id && this.data.resumeSeq !== null); + return !!(this.data.resume_gateway_url && this.data.session_id && this.data.resume_seq !== null); } async resume() { await this.send(true, { op: GatewayOpcodes.Resume, d: { - seq: this.data.resumeSeq!, + seq: this.data.resume_seq!, session_id: this.data.session_id!, token: `Bot ${this.options.token}`, }, @@ -181,7 +183,7 @@ export class Shard { this.websocket!.send( JSON.stringify({ op: GatewayOpcodes.Heartbeat, - d: this.data.resumeSeq ?? null, + d: this.data.resume_seq ?? null, }), ); } @@ -199,10 +201,10 @@ export class Shard { async onpacket(packet: GatewayReceivePayload) { if (packet.s !== null) { - this.data.resumeSeq = packet.s; + this.data.resume_seq = packet.s; } - this.debugger?.debug(`[Shard #${this.id}]`, packet.t ? packet.t : GatewayOpcodes[packet.op], this.data.resumeSeq); + this.debugger?.debug(`[Shard #${this.id}]`, packet.t ? packet.t : GatewayOpcodes[packet.op], this.data.resume_seq); switch (packet.op) { case GatewayOpcodes.Hello: @@ -237,7 +239,7 @@ export class Shard { } await this.resume(); } else { - this.data.resumeSeq = 0; + this.data.resume_seq = 0; this.data.session_id = undefined; await this.identify(); } @@ -277,17 +279,22 @@ export class Shard { //Force disconnect, ignore break; case 1000: + case GatewayCloseCodes.UnknownOpcode: + case GatewayCloseCodes.InvalidSeq: + case GatewayCloseCodes.SessionTimedOut: + this.data.resume_seq = 0; + this.data.session_id = undefined; + this.data.resume_gateway_url = undefined; + await this.reconnect(); + break; case 1001: case 1006: case ShardSocketCloseCodes.ZombiedConnection: case GatewayCloseCodes.UnknownError: - case GatewayCloseCodes.UnknownOpcode: case GatewayCloseCodes.DecodeError: case GatewayCloseCodes.NotAuthenticated: case GatewayCloseCodes.AlreadyAuthenticated: - case GatewayCloseCodes.InvalidSeq: case GatewayCloseCodes.RateLimited: - case GatewayCloseCodes.SessionTimedOut: this.debugger?.info(`[Shard #${this.id}] Trying to reconnect`); await this.reconnect(); break; diff --git a/src/websocket/discord/shared.ts b/src/websocket/discord/shared.ts index e8143c1..a73c976 100644 --- a/src/websocket/discord/shared.ts +++ b/src/websocket/discord/shared.ts @@ -66,7 +66,7 @@ export interface WorkerManagerOptions extends Omit void; } >(); + __lastError: null | { + code: number; + reason: string; + } = null; constructor( url: string, @@ -55,6 +59,10 @@ export class SeyfertWebSocket { this.handleReadable(); }); + socket.on('close', () => { + this.handleClose(); + }); + socket.on('error', err => { this.onerror(err); }); @@ -135,15 +143,21 @@ export class SeyfertWebSocket { break; // close case 0x8: - { - const code = body.readUInt16BE(0); - const reason = body.subarray(2).toString(); - this.onclose({ code, reason }); - } + this.__lastError = { + code: body.readUInt16BE(0), + reason: body.subarray(2).toString(), + }; + this.socket?.destroy(); break; } } + handleClose() { + if (!this.__lastError) return this.connect(); + this.onclose(this.__lastError); + this.__lastError = null; + } + send(data: string) { this._write(Buffer.from(data), 1); }