mirror of
https://github.com/tiramisulabs/seyfert.git
synced 2025-07-01 20:46:08 +00:00
feat: new ws & member's banner (#232)
* feat: new ws & member's banner * fix: comments
This commit is contained in:
parent
91bf30e4b3
commit
34216bd066
21
package.json
21
package.json
@ -5,7 +5,10 @@
|
||||
"main": "./lib/index.js",
|
||||
"module": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"files": ["lib/**", "deps/**"],
|
||||
"files": [
|
||||
"lib/**",
|
||||
"deps/**"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run clean && tsc --outDir ./lib",
|
||||
"prepublishOnly": "npm run build",
|
||||
@ -19,18 +22,18 @@
|
||||
"author": "MARCROCK22",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"magic-bytes.js": "^1.10.0",
|
||||
"ws": "^8.18.0"
|
||||
"magic-bytes.js": "^1.10.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.ts": ["biome check --write"]
|
||||
"*.ts": [
|
||||
"biome check --write"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.8.3",
|
||||
"@commitlint/cli": "^19.3.0",
|
||||
"@commitlint/config-conventional": "^19.2.2",
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/ws": "^8.5.11",
|
||||
"husky": "^9.1.1",
|
||||
"lint-staged": "^15.2.7",
|
||||
"rimraf": "5.0.9",
|
||||
@ -50,7 +53,13 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/tiramisulabs/seyfert"
|
||||
},
|
||||
"keywords": ["api", "discord", "bots", "typescript", "botdev"],
|
||||
"keywords": [
|
||||
"api",
|
||||
"discord",
|
||||
"bots",
|
||||
"typescript",
|
||||
"botdev"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@ -11,9 +11,6 @@ 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
|
||||
@ -40,9 +37,6 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^20.14.11
|
||||
version: 20.14.11
|
||||
'@types/ws':
|
||||
specifier: ^8.5.11
|
||||
version: 8.5.11
|
||||
husky:
|
||||
specifier: ^9.1.1
|
||||
version: 9.1.1
|
||||
@ -209,9 +203,6 @@ packages:
|
||||
'@types/node@20.14.11':
|
||||
resolution: {integrity: sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==}
|
||||
|
||||
'@types/ws@8.5.11':
|
||||
resolution: {integrity: sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==}
|
||||
|
||||
JSONStream@1.3.5:
|
||||
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
|
||||
hasBin: true
|
||||
@ -874,18 +865,6 @@ 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'}
|
||||
@ -1092,10 +1071,6 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/ws@8.5.11':
|
||||
dependencies:
|
||||
'@types/node': 20.14.11
|
||||
|
||||
JSONStream@1.3.5:
|
||||
dependencies:
|
||||
jsonparse: 1.3.1
|
||||
@ -1707,8 +1682,6 @@ snapshots:
|
||||
string-width: 7.2.0
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
ws@8.18.0: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yaml@2.4.5: {}
|
||||
|
1
src/cache/index.ts
vendored
1
src/cache/index.ts
vendored
@ -645,6 +645,7 @@ export class Cache {
|
||||
}
|
||||
function createMember(name: string): APIGuildMember {
|
||||
return {
|
||||
banner: null,
|
||||
avatar: 'xdxd',
|
||||
deaf: !false,
|
||||
flags: GuildMemberFlags.StartedHomeActions,
|
||||
|
@ -185,10 +185,10 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
|
||||
for (const g of packet.d.guilds) {
|
||||
this.__handleGuilds?.add(g.id);
|
||||
}
|
||||
await this.events?.execute(packet.t as never, packet, this as Client<true>, shardId);
|
||||
this.botId = packet.d.user.id;
|
||||
this.applicationId = packet.d.application.id;
|
||||
this.me = Transformers.ClientUser(this, packet.d.user, packet.d.application) as never;
|
||||
await this.events?.execute(packet.t as never, packet, this as Client<true>, shardId);
|
||||
if (
|
||||
!(
|
||||
this.__handleGuilds?.size &&
|
||||
|
@ -55,6 +55,7 @@ export class WorkerClient<Ready extends boolean = boolean> extends BaseClient {
|
||||
promises = new Map<string, { resolve: (value: any) => void; timeout: NodeJS.Timeout }>();
|
||||
|
||||
shards = new Map<number, Shard>();
|
||||
private __setServicesCache?: boolean;
|
||||
|
||||
declare options: WorkerClientOptions;
|
||||
|
||||
@ -85,6 +86,9 @@ export class WorkerClient<Ready extends boolean = boolean> extends BaseClient {
|
||||
};
|
||||
}) {
|
||||
super.setServices(rest);
|
||||
if (rest.cache) {
|
||||
this.__setServicesCache = true;
|
||||
}
|
||||
if (rest.handlers && 'events' in rest.handlers) {
|
||||
if (!rest.handlers.events) {
|
||||
this.events = undefined;
|
||||
@ -114,16 +118,26 @@ export class WorkerClient<Ready extends boolean = boolean> extends BaseClient {
|
||||
name: `[Worker #${workerData.workerId}]`,
|
||||
});
|
||||
|
||||
const adapter = new WorkerAdapter(workerData);
|
||||
if (this.options.postMessage) {
|
||||
adapter.postMessage = this.options.postMessage;
|
||||
if (this.__setServicesCache) {
|
||||
this.setServices({
|
||||
cache: {
|
||||
disabledCache: this.options.disabledCache,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const adapter = new WorkerAdapter(workerData);
|
||||
if (this.options.postMessage) {
|
||||
adapter.postMessage = this.options.postMessage;
|
||||
}
|
||||
this.setServices({
|
||||
cache: {
|
||||
adapter,
|
||||
disabledCache: this.options.disabledCache,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.setServices({
|
||||
cache: {
|
||||
adapter,
|
||||
disabledCache: this.options.disabledCache,
|
||||
},
|
||||
});
|
||||
|
||||
delete this.__setServicesCache;
|
||||
|
||||
if (workerData.debug) {
|
||||
this.debugger = new Logger({
|
||||
@ -394,10 +408,10 @@ export class WorkerClient<Ready extends boolean = boolean> extends BaseClient {
|
||||
for (const g of packet.d.guilds) {
|
||||
this.__handleGuilds?.add(g.id);
|
||||
}
|
||||
await this.events?.execute(packet.t as never, packet, this, shardId);
|
||||
this.botId = packet.d.user.id;
|
||||
this.applicationId = packet.d.application.id;
|
||||
this.me = Transformers.ClientUser(this, packet.d.user, packet.d.application) as never;
|
||||
await this.events?.execute(packet.t as never, packet, this, shardId);
|
||||
if (
|
||||
!(
|
||||
this.__handleGuilds?.size &&
|
||||
|
@ -195,14 +195,20 @@ export class GuildMember extends BaseGuildMember {
|
||||
avatarURL(options?: ImageOptions & { exclude?: false }): string;
|
||||
avatarURL(options?: ImageOptions & { exclude?: boolean }): string | null {
|
||||
if (!this.avatar) {
|
||||
return options?.exclude ? null : this.user.avatarURL();
|
||||
return options?.exclude ? null : this.user.avatarURL(options);
|
||||
}
|
||||
|
||||
return this.rest.cdn.guilds(this.guildId).users(this.id).avatars(this.avatar).get(options);
|
||||
}
|
||||
|
||||
bannerURL(options?: ImageOptions) {
|
||||
return this.user.bannerURL(options);
|
||||
bannerURL(options: ImageOptions & { exclude: true }): string | undefined | null;
|
||||
bannerURL(options?: ImageOptions & { exclude?: false }): string | undefined;
|
||||
bannerURL(options?: ImageOptions & { exclude?: boolean }): string | undefined | null {
|
||||
if (!this.banner) {
|
||||
return options?.exclude ? null : this.user.bannerURL(options);
|
||||
}
|
||||
|
||||
return this.rest.cdn.guilds(this.guildId).users(this.id).banners(this.banner).get(options);
|
||||
}
|
||||
|
||||
async fetchPermissions(force = false) {
|
||||
|
@ -674,6 +674,12 @@ export interface APIGuildMember {
|
||||
* See https://discord.com/developers/docs/resources/user#avatar-decoration-data-object
|
||||
*/
|
||||
avatar_decoration_data?: APIAvatarDecorationData | null;
|
||||
/**
|
||||
* The data for the member's guild banner
|
||||
*
|
||||
* See https://github.com/discord/discord-api-docs/discussions/4217
|
||||
*/
|
||||
banner: null | string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,26 +1,58 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import NodeWebSocket from 'ws';
|
||||
import { SeyfertWebSocket } from './socket/custom';
|
||||
|
||||
export class BaseSocket {
|
||||
private internal: NodeWebSocket | WebSocket;
|
||||
private internal: SeyfertWebSocket | WebSocket;
|
||||
|
||||
ping?: () => Promise<number>;
|
||||
|
||||
constructor(kind: 'ws' | 'bun', url: string) {
|
||||
this.internal = kind === 'ws' ? new NodeWebSocket(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 (data) {
|
||||
ws.__promises.delete(data);
|
||||
promise?.resolve();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
const ws = this.internal as WebSocket;
|
||||
this.ping = () => {
|
||||
return new Promise<number>(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);
|
||||
};
|
||||
//@ts-expect-error bun support
|
||||
ws.on('pong', listener);
|
||||
//@ts-expect-error bun support
|
||||
ws.ping(nonce);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
set onopen(callback: NodeWebSocket['onopen']) {
|
||||
set onopen(callback: SeyfertWebSocket['onopen']) {
|
||||
this.internal.onopen = callback;
|
||||
}
|
||||
|
||||
set onmessage(callback: NodeWebSocket['onmessage']) {
|
||||
set onmessage(callback: SeyfertWebSocket['onmessage']) {
|
||||
this.internal.onmessage = callback;
|
||||
}
|
||||
|
||||
set onclose(callback: NodeWebSocket['onclose']) {
|
||||
set onclose(callback: SeyfertWebSocket['onclose']) {
|
||||
this.internal.onclose = callback;
|
||||
}
|
||||
|
||||
set onerror(callback: NodeWebSocket['onerror']) {
|
||||
set onerror(callback: SeyfertWebSocket['onerror']) {
|
||||
this.internal.onerror = callback;
|
||||
}
|
||||
|
||||
@ -28,26 +60,10 @@ export class BaseSocket {
|
||||
return this.internal.send(data);
|
||||
}
|
||||
|
||||
close(...args: Parameters<NodeWebSocket['close']>) {
|
||||
// @ts-expect-error
|
||||
close(...args: Parameters<SeyfertWebSocket['close']>) {
|
||||
return this.internal.close(...args);
|
||||
}
|
||||
|
||||
async ping() {
|
||||
if (!('ping' in this.internal)) throw new Error('Unexpected: Method ping not implemented');
|
||||
return new Promise<number>(res => {
|
||||
const nonce = randomUUID();
|
||||
const start = performance.now();
|
||||
const listener = (data: Buffer) => {
|
||||
if (data.toString() !== nonce) return;
|
||||
(this.internal as NodeWebSocket).removeListener('pong', listener);
|
||||
res(performance.now() - start);
|
||||
};
|
||||
(this.internal as NodeWebSocket).on('pong', listener);
|
||||
(this.internal as NodeWebSocket).ping(nonce);
|
||||
});
|
||||
}
|
||||
|
||||
get readyState() {
|
||||
return this.internal.readyState;
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { inflateSync } from 'node:zlib';
|
||||
import type WS from 'ws';
|
||||
import { WebSocket, type CloseEvent, type ErrorEvent } from 'ws';
|
||||
import { type MakeRequired, MergeOptions, type Logger } from '../../common';
|
||||
import { properties } from '../constants';
|
||||
import { DynamicBucket } from '../structures';
|
||||
@ -67,7 +65,7 @@ export class Shard {
|
||||
}
|
||||
|
||||
get isOpen() {
|
||||
return this.websocket?.readyState === WebSocket.OPEN;
|
||||
return this.websocket?.readyState === 1 /*WebSocket.open*/;
|
||||
}
|
||||
|
||||
get gatewayURL() {
|
||||
@ -86,6 +84,7 @@ export class Shard {
|
||||
|
||||
ping() {
|
||||
if (!this.websocket) return Promise.resolve(Number.POSITIVE_INFINITY);
|
||||
//@ts-expect-error
|
||||
return this.websocket.ping();
|
||||
}
|
||||
|
||||
@ -102,9 +101,11 @@ export class Shard {
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: /\ bun lol
|
||||
this.websocket = new BaseSocket(typeof Bun === 'undefined' ? 'ws' : 'bun', this.currentGatewayURL);
|
||||
|
||||
this.websocket!.onmessage = (event: WS.MessageEvent) => this.handleMessage(event);
|
||||
this.websocket!.onmessage = ({ data }: { data: string }) => {
|
||||
this.handleMessage(data);
|
||||
};
|
||||
|
||||
this.websocket!.onclose = (event: WS.CloseEvent) => this.handleClosed(event);
|
||||
this.websocket!.onclose = (event: { code: number; reason: string }) => this.handleClosed(event);
|
||||
|
||||
this.websocket!.onerror = (event: ErrorEvent) => this.debugger?.error(event);
|
||||
|
||||
@ -264,12 +265,11 @@ export class Shard {
|
||||
}
|
||||
}
|
||||
|
||||
protected async handleClosed(close: CloseEvent) {
|
||||
protected async handleClosed(close: { code: number; reason: string }) {
|
||||
clearInterval(this.heart.nodeInterval);
|
||||
this.debugger?.warn(
|
||||
`[Shard #${this.id}] ${ShardSocketCloseCodes[close.code] ?? GatewayCloseCodes[close.code] ?? close.code} (${
|
||||
close.code
|
||||
})`,
|
||||
`[Shard #${this.id}] ${ShardSocketCloseCodes[close.code] ?? GatewayCloseCodes[close.code] ?? close.code} (${close.code})`,
|
||||
close.reason,
|
||||
);
|
||||
|
||||
switch (close.code) {
|
||||
@ -309,14 +309,14 @@ export class Shard {
|
||||
}
|
||||
|
||||
async close(code: number, reason: string) {
|
||||
if (this.websocket?.readyState !== WebSocket.OPEN) {
|
||||
if (!this.isOpen) {
|
||||
return this.debugger?.warn(`[Shard #${this.id}] Is not open`);
|
||||
}
|
||||
this.debugger?.warn(`[Shard #${this.id}] Called close`);
|
||||
this.websocket?.close(code, reason);
|
||||
}
|
||||
|
||||
protected handleMessage({ data }: WS.MessageEvent) {
|
||||
protected handleMessage(data: string | Buffer) {
|
||||
if (data instanceof Buffer) {
|
||||
data = inflateSync(data);
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ export class ShardManager extends Map<number, Shard> {
|
||||
handlePayload: this.options.handlePayload,
|
||||
properties: this.options.properties,
|
||||
debugger: this.debugger,
|
||||
compress: false,
|
||||
compress: this.options.compress ?? false,
|
||||
presence: this.options.presence?.(shardId, -1),
|
||||
});
|
||||
|
||||
|
313
src/websocket/discord/socket/custom.ts
Normal file
313
src/websocket/discord/socket/custom.ts
Normal file
@ -0,0 +1,313 @@
|
||||
import { randomBytes, createHash, randomUUID } from 'node:crypto';
|
||||
import { request } from 'node:https';
|
||||
import type { Socket } from 'node:net';
|
||||
import { createInflate, inflateSync } from 'node:zlib';
|
||||
|
||||
export class SeyfertWebSocket {
|
||||
socket?: Socket = undefined;
|
||||
hostname: string;
|
||||
path: string;
|
||||
__stored: Buffer[] = [];
|
||||
__opcode = 0;
|
||||
__body: Buffer[] = [];
|
||||
__promises = new Map<
|
||||
string,
|
||||
{
|
||||
resolve: () => void;
|
||||
reject: (reason?: any) => void;
|
||||
}
|
||||
>();
|
||||
__zlib?: ReturnType<typeof createInflate>;
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
public compress = true,
|
||||
) {
|
||||
const urlParts = new URL(url);
|
||||
this.hostname = urlParts.hostname || '';
|
||||
this.path = `${urlParts.pathname}${urlParts.search || ''}&encoding=json`;
|
||||
if (compress) {
|
||||
this.__zlib = createInflate();
|
||||
}
|
||||
this.connect();
|
||||
}
|
||||
|
||||
private connect() {
|
||||
const key = randomBytes(16).toString('base64');
|
||||
const req = request({
|
||||
//discord gateway hostname
|
||||
hostname: this.hostname,
|
||||
path: this.path,
|
||||
headers: {
|
||||
Connection: 'Upgrade',
|
||||
Upgrade: 'websocket',
|
||||
'Sec-WebSocket-Key': key,
|
||||
'Sec-WebSocket-Version': '13',
|
||||
},
|
||||
});
|
||||
|
||||
req.on('upgrade', (res, socket) => {
|
||||
const hash = createHash('sha1').update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest('base64');
|
||||
const accept = res.headers['sec-websocket-accept'];
|
||||
if (accept !== hash) {
|
||||
socket.end(() => {
|
||||
this.onerror(new Error('Invalid sec-websocket-accept header'));
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.socket = socket;
|
||||
|
||||
socket.on('readable', () => {
|
||||
this.handleReadable();
|
||||
});
|
||||
|
||||
socket.on('error', err => {
|
||||
this.onerror(err);
|
||||
});
|
||||
this.onopen();
|
||||
});
|
||||
|
||||
req.end();
|
||||
}
|
||||
|
||||
handleReadable() {
|
||||
// Keep reading until no data, this is useful when two payloads merges.
|
||||
while (this.socket?.readableLength) {
|
||||
// Read length without consuming the buffer
|
||||
let length = this.readBytes(1, 1) & 127;
|
||||
const slice = length === 126 ? 4 : length === 127 ? 10 : 2;
|
||||
// Check if frame/data is complete
|
||||
if (this.socket.readableLength < slice) return; // Wait to next cycle if not
|
||||
if (length > 125) {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
|
||||
// If length is 126/127, read extended payload length instead
|
||||
// Equivalent to readUint32BE
|
||||
length = this.readBytes(2, slice - 2);
|
||||
}
|
||||
// Read the frame, ignore data next to it, leave it to next `while` cycle
|
||||
const frame = this.socket.read(slice + length) as Buffer | null;
|
||||
if (!frame) return;
|
||||
// Get fin (0 | 1)
|
||||
const fin = frame[0] >> 7;
|
||||
// Read opcode (continuation, text, binary, close, ping, pong)
|
||||
let opcode = frame[0] & 15;
|
||||
// Cut frame to get payload
|
||||
let payload = frame.subarray(slice);
|
||||
// If fin is 0, store the data and wait to next `while` cycle to be fin=1
|
||||
if (fin === 0) {
|
||||
this.__stored.push(payload);
|
||||
// Only store opcode when is not 0 (continuation)
|
||||
if (opcode !== 0) {
|
||||
this.__opcode = opcode;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// When the message is from ws fragmentation, opcode of last message is 0
|
||||
// If so, merge all messages.
|
||||
if (opcode === 0) {
|
||||
this.__stored.push(payload);
|
||||
payload = Buffer.concat(this.__stored);
|
||||
opcode = this.__opcode;
|
||||
// Reset body
|
||||
this.__stored = [];
|
||||
// Reset opcode, should set as -1?
|
||||
this.__opcode = 0;
|
||||
}
|
||||
// Handle opcodes
|
||||
this.handleEvent(payload, opcode);
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(body: Buffer, opcode: number) {
|
||||
switch (opcode) {
|
||||
// text
|
||||
case 0x1:
|
||||
this.onmessage({ data: body.toString() });
|
||||
break;
|
||||
// binary
|
||||
case 0x2:
|
||||
this.onmessage({ data: inflateSync(body, { info: true }).buffer.toString() });
|
||||
break;
|
||||
// pong
|
||||
case 0x9:
|
||||
this.onping(body.toString());
|
||||
break;
|
||||
// ping
|
||||
case 0xa:
|
||||
this.onpong(body.toString());
|
||||
break;
|
||||
// close
|
||||
case 0x8:
|
||||
{
|
||||
const code = body.readUInt16BE(0);
|
||||
const reason = body.subarray(2).toString();
|
||||
this.onclose({ code, reason });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
send(data: string) {
|
||||
this._write(Buffer.from(data), 1);
|
||||
}
|
||||
|
||||
private _write(buffer: Buffer, opcode: number) {
|
||||
const length = buffer.length;
|
||||
let frame;
|
||||
// Kinda same logic as above, but client-side
|
||||
if (length < 126) {
|
||||
frame = Buffer.allocUnsafe(6 + length);
|
||||
frame[1] = 128 + length;
|
||||
} else if (length < 65536) {
|
||||
frame = Buffer.allocUnsafe(8 + length);
|
||||
frame[1] = 254;
|
||||
frame[2] = (length >> 8) & 255;
|
||||
frame[3] = length & 255;
|
||||
} else {
|
||||
frame = Buffer.allocUnsafe(14 + length);
|
||||
frame[1] = 255;
|
||||
frame.writeBigUint64BE(BigInt(length), 2);
|
||||
}
|
||||
frame[0] = 128 + opcode;
|
||||
frame.writeUint32BE(0, frame.length - length - 4);
|
||||
frame.set(buffer, frame.length - length);
|
||||
this.socket?.write(frame);
|
||||
}
|
||||
|
||||
onping(_data: string) {}
|
||||
|
||||
onpong(_data: string) {}
|
||||
|
||||
onopen() {}
|
||||
|
||||
onmessage(_payload: { data: string }) {}
|
||||
|
||||
onclose(_close: { code: number; reason: string }) {}
|
||||
|
||||
onerror(_err: unknown) {}
|
||||
|
||||
close(code: number, reason: string) {
|
||||
// alloc payload length
|
||||
const buffer = Buffer.alloc(2 + Buffer.byteLength(reason));
|
||||
// gateway close code
|
||||
buffer.writeUInt16BE(code, 0);
|
||||
// reason
|
||||
buffer.write(reason, 2);
|
||||
// message, close opcode
|
||||
this._write(buffer, 0x8);
|
||||
this.socket?.end();
|
||||
}
|
||||
|
||||
pong(data: string) {
|
||||
//send pong opcode (10)
|
||||
this._write(Buffer.from(data), 0xa);
|
||||
}
|
||||
|
||||
ping(data: string) {
|
||||
//send ping opcode (9)
|
||||
this._write(Buffer.from(data), 0x9);
|
||||
}
|
||||
|
||||
waitPing() {
|
||||
const id = this.#randomUUID();
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
|
||||
const start = performance.now();
|
||||
this.ping(id);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.__promises.set(id, {
|
||||
reject,
|
||||
resolve,
|
||||
});
|
||||
timeout = setTimeout(() => {
|
||||
resolve();
|
||||
}, 60e3);
|
||||
})
|
||||
.then(() => {
|
||||
return performance.now() - start;
|
||||
})
|
||||
.finally(() => {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
}
|
||||
|
||||
#randomUUID(): string {
|
||||
const id = randomUUID();
|
||||
if (this.__promises.has(id)) return this.#randomUUID();
|
||||
return id;
|
||||
}
|
||||
|
||||
get readyState() {
|
||||
return ['opening', 'open', 'closed', 'closed'].indexOf(this.socket?.readyState ?? 'closed');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param start Start calculating bytes from `start`
|
||||
* @param bits Num of bits since `start`
|
||||
* @returns
|
||||
*/
|
||||
|
||||
private readBytes(start: number, bits: number) {
|
||||
// @ts-expect-error this is private, thanks nodejs
|
||||
const readable = this.socket._readableState as
|
||||
| {
|
||||
bufferIndex: number;
|
||||
buffer: Buffer[];
|
||||
}
|
||||
| {
|
||||
buffer: {
|
||||
head: ReadableHeadData;
|
||||
};
|
||||
};
|
||||
// Num of bit read
|
||||
let bitIndex = 0;
|
||||
// Num of bit read counting since start
|
||||
let read = 0;
|
||||
// Bytes value
|
||||
let value = 0;
|
||||
// Node v20
|
||||
if ('bufferIndex' in readable) {
|
||||
// actual index of the buffer to read
|
||||
let blockIndex = readable.bufferIndex;
|
||||
// Buffer to read
|
||||
let block;
|
||||
while ((block = readable.buffer[blockIndex++])) {
|
||||
for (let i = 0; i < block.length; i++) {
|
||||
if (++bitIndex > start) {
|
||||
value *= 256; // shift 8 bits (1 byte) `*= 256 is faster than <<= 8`
|
||||
value += block[i]; // sum value to bits
|
||||
// Read until read all bits
|
||||
if (++read === bits) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} /* Support for olders versions*/ else {
|
||||
// readable.buffer is kinda a LinkedList
|
||||
let head: ReadableHeadData | undefined = readable.buffer.head;
|
||||
while (head) {
|
||||
for (let i = 0; i < head.data.length; i++) {
|
||||
if (++bitIndex > start) {
|
||||
value *= 256; // shift 8 bits (1 byte) `*= 256 is faster than <<= 8`
|
||||
value += head.data[i]; // sum value to bits
|
||||
// Read until read all bits
|
||||
if (++read === bits) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
// continue with next node
|
||||
head = head.next;
|
||||
}
|
||||
}
|
||||
throw new Error('Unexpected error, not enough bytes');
|
||||
}
|
||||
}
|
||||
|
||||
export type ReadableHeadData = {
|
||||
next?: ReadableHeadData;
|
||||
data: Buffer;
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user