From 02b8bd0d288c2f3ced9be6447bb319e57c4c680d Mon Sep 17 00:00:00 2001 From: socram03 Date: Wed, 8 Nov 2023 20:16:28 -0400 Subject: [PATCH] fix(ws): gateway resume now it works correctly feat(ws): more usesfull methods in sharder fix(helpers): data arrays fixed ci: change rome to biome Co-authored-by: MARCROCK22 --- rome.json => biome.json | 5 +- bun.lockb | Bin 0 -> 152128 bytes package.json | 136 ++--- packages/common/src/Collection.ts | 4 +- packages/common/src/Util.ts | 126 ++--- packages/core/src/events/handler.ts | 6 +- packages/core/src/managers/ChannelManager.ts | 22 +- packages/core/src/managers/GuildManager.ts | 64 +-- .../core/src/managers/InteractionManager.ts | 2 +- packages/core/src/managers/MemberManager.ts | 8 +- packages/core/src/managers/UserManager.ts | 32 +- packages/core/src/session.ts | 30 +- packages/helpers/src/Collector.ts | 34 +- packages/helpers/src/MessageEmbed.ts | 2 +- packages/helpers/src/Permissions.ts | 16 +- packages/helpers/src/Utils.ts | 8 +- .../commands/contextMenu/ContextCommand.ts | 4 +- packages/helpers/src/commands/index.ts | 6 +- .../src/commands/slash/SlashCommand.ts | 12 +- .../src/commands/slash/SlashCommandOption.ts | 24 +- packages/helpers/src/components/ActionRow.ts | 12 +- .../helpers/src/components/BaseComponent.ts | 2 +- .../helpers/src/components/MessageButton.ts | 4 +- packages/helpers/src/components/SelectMenu.ts | 3 +- packages/helpers/src/components/TextInput.ts | 6 +- packages/helpers/src/components/index.ts | 10 +- packages/helpers/src/index.ts | 10 +- packages/rest/src/REST.ts | 14 +- packages/rest/src/Routes/gateway.ts | 2 +- packages/rest/src/index.ts | 8 +- packages/ws/README.md | 4 +- packages/ws/src/SharedTypes.ts | 21 +- packages/ws/src/constants/index.ts | 52 +- packages/ws/src/discord/heartbeater.ts | 122 +++++ packages/ws/src/discord/index.ts | 3 + packages/ws/src/discord/shard.ts | 315 ++++++++++++ packages/ws/src/discord/sharder.ts | 160 ++++++ .../ws/src/{gateway => discord}/shared.ts | 116 +++-- packages/ws/src/gateway/index.ts | 3 - packages/ws/src/gateway/shard.ts | 477 ------------------ packages/ws/src/gateway/sharder.ts | 86 ---- packages/ws/src/index.ts | 6 +- packages/ws/src/structures/dynamic_bucket.ts | 138 ----- packages/ws/src/structures/index.ts | 340 ++++++++++++- packages/ws/src/structures/lists.ts | 153 ------ .../ws/src/structures/sequential_bucket.ts | 54 -- 46 files changed, 1313 insertions(+), 1349 deletions(-) rename rome.json => biome.json (88%) create mode 100755 bun.lockb create mode 100644 packages/ws/src/discord/heartbeater.ts create mode 100644 packages/ws/src/discord/index.ts create mode 100644 packages/ws/src/discord/shard.ts create mode 100644 packages/ws/src/discord/sharder.ts rename packages/ws/src/{gateway => discord}/shared.ts (88%) delete mode 100644 packages/ws/src/gateway/index.ts delete mode 100644 packages/ws/src/gateway/shard.ts delete mode 100644 packages/ws/src/gateway/sharder.ts delete mode 100644 packages/ws/src/structures/dynamic_bucket.ts delete mode 100644 packages/ws/src/structures/lists.ts delete mode 100644 packages/ws/src/structures/sequential_bucket.ts diff --git a/rome.json b/biome.json similarity index 88% rename from rome.json rename to biome.json index f0db417..becafb4 100644 --- a/rome.json +++ b/biome.json @@ -10,15 +10,12 @@ "suspicious": { "noExplicitAny": "off", "noAssignInExpressions": "off" - }, - "nursery": { - "useCamelCase": "off" } } }, "formatter": { "enabled": true, - "indentSize": 2, + "indentWidth": 2, "indentStyle": "space", "lineWidth": 140, "formatWithErrors": true diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..55716bb2115c70cab49a949debe337ea1b4ef36d GIT binary patch literal 152128 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≀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.0.0", - "node": ">=16.0.0" - }, - "devDependencies": { - "@changesets/cli": "^2.24.1", - "@types/node": "^18.6.3", - "rome": "^12.0.0", - "turbo": "^1.4.2", - "typescript": "^5.0.4" - }, - "packageManager": "npm@8.19.4", - "bugs": { - "url": "https://github.com/oasisjs/biscuit" - }, - "keywords": [ - "api", - "discord", - "bots", - "typescript", - "botdev" - ], - "license": "Apache-2.0", - "author": "Yuzuru ", - "contributors": [ - { - "name": "Yuzuru", - "url": "https://github.com/yuzudev", - "author": true - }, - { - "name": "miia", - "url": "https://github.com/dragurimu" - }, - { - "name": "n128", - "url": "https://github.com/nicolito128" - }, - { - "name": "socram03", - "url": "https://github.com/socram03" - }, - { - "name": "Drylozu", - "url": "https://github.com/Drylozu" - }, - { - "name": "FreeAoi", - "url": "https://github.com/FreeAoi" - } - ], - "homepage": "https://biscuitjs.com", - "repository": { - "type": "git", - "url": "git+https://github.com/oasisjs/biscuit.git" - } + "name": "@biscuitland/core", + "workspaces": [ + "packages/*" + ], + "scripts": { + "build": "turbo run build", + "clean": "turbo run clean", + "check": "biome check ./packages/", + "check:apply": "biome check --apply --max-diagnostics 200 --quote-style single --trailing-comma none ./packages/", + "lint": "biome format --write --quote-style single --trailing-comma none ./packages/", + "dev": "turbo run dev --parallel" + }, + "engines": { + "npm": ">=7.0.0", + "node": ">=16.0.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.3.3", + "@changesets/cli": "^2.24.1", + "@types/node": "^18.6.3", + "turbo": "^1.4.2", + "typescript": "^5.0.4" + }, + "packageManager": "npm@8.19.4", + "bugs": { + "url": "https://github.com/oasisjs/biscuit" + }, + "keywords": [ + "api", + "discord", + "bots", + "typescript", + "botdev" + ], + "license": "Apache-2.0", + "author": "Yuzuru ", + "contributors": [ + { + "name": "Yuzuru", + "url": "https://github.com/yuzudev", + "author": true + }, + { + "name": "miia", + "url": "https://github.com/dragurimu" + }, + { + "name": "n128", + "url": "https://github.com/nicolito128" + }, + { + "name": "socram03", + "url": "https://github.com/socram03" + }, + { + "name": "Drylozu", + "url": "https://github.com/Drylozu" + }, + { + "name": "FreeAoi", + "url": "https://github.com/FreeAoi" + } + ], + "homepage": "https://biscuitjs.com", + "repository": { + "type": "git", + "url": "git+https://github.com/oasisjs/biscuit.git" + } } diff --git a/packages/common/src/Collection.ts b/packages/common/src/Collection.ts index bb15a2a..48bf42b 100644 --- a/packages/common/src/Collection.ts +++ b/packages/common/src/Collection.ts @@ -6,7 +6,7 @@ export class Collection extends Map { */ maxSize: number | undefined; /** Handler to remove items from the collection every so often. */ - sweeper: (CollectionSweeper & { intervalId?: NodeJS.Timer }) | undefined; + sweeper: (CollectionSweeper & { intervalId?: NodeJS.Timeout }) | undefined; constructor(entries?: (ReadonlyArray | null) | Map, options?: CollectionOptions) { super(entries ?? []); @@ -23,6 +23,7 @@ export class Collection extends Map { this.sweeper = options; this.sweeper.intervalId = setInterval(() => { + // biome-ignore lint/complexity/noForEach: this.forEach((value, key) => { if (!this.sweeper?.filter(value, key)) return; @@ -98,6 +99,7 @@ export class Collection extends Map { /** Find all elements in this collection that match the given pattern. */ filter(callback: (value: V, key: K) => boolean): Collection { const relevant = new Collection(); + // biome-ignore lint/complexity/noForEach: this.forEach((value, key) => { if (callback(value, key)) relevant.set(key, value); }); diff --git a/packages/common/src/Util.ts b/packages/common/src/Util.ts index 65e2910..042620d 100644 --- a/packages/common/src/Util.ts +++ b/packages/common/src/Util.ts @@ -1,51 +1,27 @@ -import { setTimeout } from "node:timers/promises"; -import { ObjectToLower, ObjectToSnake } from "./Types"; +import { setTimeout } from 'node:timers/promises'; +import { ObjectToLower, ObjectToSnake } from './Types'; -const isPlainObject = (value: any) => { - return ( - (value !== null && - typeof value === "object" && - typeof value.constructor === "function" && - // rome-ignore lint/suspicious/noPrototypeBuiltins: js tricks - (value.constructor.prototype.hasOwnProperty("isPrototypeOf") || Object.getPrototypeOf(value.constructor.prototype) === null)) || - (value !== undefined && Object.getPrototypeOf(value) === null) +export function isObject(o: any) { + return o && typeof o === 'object' && !Array.isArray(o); +} + +export function Options(defaults: any, ...options: any[]): T { + const option = options.shift(); + if (!option) return defaults; + + return Options( + { + ...option, + ...Object.fromEntries( + Object.entries(defaults).map(([key, value]) => [ + key, + isObject(value) ? Options(value, option?.[key] || {}) : option?.[key] || value + ]) + ) + }, + ...options ); -}; - -const isObject = (o: any) => { - return !!o && typeof o === "object" && !Array.isArray(o); -}; - -export const Options = (defaults: any, ...options: any[]): T => { - if (!options.length) { - return defaults; - } - - const source = options.shift(); - - // This prevents default options from being intercepted by `Object.assign` - const $ = { ...defaults }; - - if (isObject($) && isPlainObject(source)) { - Object.entries(source).forEach(([key, value]) => { - if (typeof value === "undefined") { - return; - } - - if (isPlainObject(value)) { - if (!(key in $)) { - Object.assign($, { [key]: {} }); - } - - Options($[key], value); - } else { - Object.assign($, { [key]: value }); - } - }); - } - - return Options($, ...options); -}; +} /** * Convert a camelCase object to snake_case. * @param target The object to convert. @@ -55,18 +31,18 @@ export function toSnakeCase>(target: Obj): Objec const result = {}; for (const [key, value] of Object.entries(target)) { switch (typeof value) { - case "string": - case "bigint": - case "boolean": - case "function": - case "number": - case "symbol": - case "undefined": + case 'string': + case 'bigint': + case 'boolean': + case 'function': + case 'number': + case 'symbol': + case 'undefined': result[ReplaceRegex.snake(key)] = value; break; - case "object": + case 'object': if (Array.isArray(value)) { - result[ReplaceRegex.snake(key)] = value.map((prop) => (typeof prop === "object" && prop ? toSnakeCase(prop) : prop)); + result[ReplaceRegex.snake(key)] = value.map((prop) => (typeof prop === 'object' && prop ? toSnakeCase(prop) : prop)); break; } if (isObject(value)) { @@ -93,18 +69,18 @@ export function toCamelCase>(target: Obj): Objec const result = {}; for (const [key, value] of Object.entries(target)) { switch (typeof value) { - case "string": - case "bigint": - case "boolean": - case "function": - case "symbol": - case "number": - case "undefined": + case 'string': + case 'bigint': + case 'boolean': + case 'function': + case 'symbol': + case 'number': + case 'undefined': result[ReplaceRegex.camel(key)] = value; break; - case "object": + case 'object': if (Array.isArray(value)) { - result[ReplaceRegex.camel(key)] = value.map((prop) => (typeof prop === "object" && prop ? toCamelCase(prop) : prop)); + result[ReplaceRegex.camel(key)] = value.map((prop) => (typeof prop === 'object' && prop ? toCamelCase(prop) : prop)); break; } if (isObject(value)) { @@ -128,7 +104,7 @@ export const ReplaceRegex = { }, snake: (s: string) => { return s.replace(/[A-Z]/g, (a) => `_${a.toLowerCase()}`); - }, + } }; // https://github.com/discordeno/discordeno/blob/main/packages/utils/src/colors.ts @@ -168,9 +144,9 @@ export function getColorEnabled(): boolean { */ function code(open: number[], close: number): Code { return { - open: `\x1b[${open.join(";")}m`, + open: `\x1b[${open.join(';')}m`, close: `\x1b[${close}m`, - regexp: new RegExp(`\\x1b\\[${close}m`, "g"), + regexp: new RegExp(`\\x1b\\[${close}m`, 'g') }; } @@ -559,7 +535,7 @@ export function bgRgb8(str: string, color: number): string { * @param color code */ export function rgb24(str: string, color: number | Rgb): string { - if (typeof color === "number") { + if (typeof color === 'number') { return run(str, code([38, 2, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff], 39)); } return run(str, code([38, 2, clampAndTruncate(color.r), clampAndTruncate(color.g), clampAndTruncate(color.b)], 39)); @@ -581,7 +557,7 @@ export function rgb24(str: string, color: number | Rgb): string { * @param color code */ export function bgRgb24(str: string, color: number | Rgb): string { - if (typeof color === "number") { + if (typeof color === 'number') { return run(str, code([48, 2, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff], 49)); } return run(str, code([48, 2, clampAndTruncate(color.r), clampAndTruncate(color.g), clampAndTruncate(color.b)], 49)); @@ -590,10 +566,10 @@ export function bgRgb24(str: string, color: number | Rgb): string { // https://github.com/chalk/ansi-regex/blob/02fa893d619d3da85411acc8fd4e2eea0e95a9d9/index.js const ANSI_PATTERN = new RegExp( [ - "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", - "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))", - ].join("|"), - "g", + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))' + ].join('|'), + 'g' ); /** @@ -601,9 +577,9 @@ const ANSI_PATTERN = new RegExp( * @param string to remove ANSI escape codes from */ export function stripColor(string: string): string { - return string.replace(ANSI_PATTERN, ""); + return string.replace(ANSI_PATTERN, ''); } -export function delay(time: number, result?: T) { +export function delay(time: number, result?: T): Promise { return setTimeout(time, result); } diff --git a/packages/core/src/events/handler.ts b/packages/core/src/events/handler.ts index 76b74a3..ff1c4da 100644 --- a/packages/core/src/events/handler.ts +++ b/packages/core/src/events/handler.ts @@ -1,11 +1,11 @@ -import type { GatewayEvents } from "@biscuitland/ws"; -import type { Session } from "../index"; +import type { GatewayEvents } from '@biscuitland/ws'; +import type { Session } from '../index'; export function actionHandler([session, payload, shardId]: Parameters) { // @ts-expect-error At this point, typescript sucks session.emit(payload.t, payload.d, shardId); // @ts-expect-error At this point, typescript sucks - session.emit("RAW", payload.d, shardId); + session.emit('RAW', payload.d, shardId); } export type ActionHandler = ( diff --git a/packages/core/src/managers/ChannelManager.ts b/packages/core/src/managers/ChannelManager.ts index 85229ce..5dfa652 100644 --- a/packages/core/src/managers/ChannelManager.ts +++ b/packages/core/src/managers/ChannelManager.ts @@ -1,23 +1,23 @@ import type { APIChannel, - RESTPostAPIChannelMessageJSONBody, - RESTPatchAPIChannelJSONBody, - RESTGetAPIChannelThreadsArchivedQuery, RESTGetAPIChannelMessageReactionUsersQuery, + RESTGetAPIChannelThreadMemberQuery, + RESTGetAPIChannelThreadMembersQuery, + RESTGetAPIChannelThreadsArchivedQuery, + RESTPatchAPIChannelJSONBody, RESTPatchAPIChannelMessageJSONBody, - RESTPostAPIChannelMessagesBulkDeleteJSONBody, - RESTPutAPIChannelPermissionJSONBody, - RESTPostAPIChannelInviteJSONBody, + RESTPatchAPIStageInstanceJSONBody, RESTPostAPIChannelFollowersJSONBody, - RESTPutAPIChannelRecipientJSONBody, + RESTPostAPIChannelInviteJSONBody, + RESTPostAPIChannelMessageJSONBody, + RESTPostAPIChannelMessagesBulkDeleteJSONBody, RESTPostAPIChannelMessagesThreadsJSONBody, RESTPostAPIChannelThreadsJSONBody, RESTPostAPIChannelThreadsResult, - RESTPostAPIGuildForumThreadsJSONBody, - RESTGetAPIChannelThreadMembersQuery, - RESTGetAPIChannelThreadMemberQuery, RESTPostAPIChannelWebhookJSONBody, - RESTPatchAPIStageInstanceJSONBody + RESTPostAPIGuildForumThreadsJSONBody, + RESTPutAPIChannelPermissionJSONBody, + RESTPutAPIChannelRecipientJSONBody } from '@biscuitland/common'; import type { RawFile } from '@biscuitland/rest'; diff --git a/packages/core/src/managers/GuildManager.ts b/packages/core/src/managers/GuildManager.ts index 8a42c18..55db2e0 100644 --- a/packages/core/src/managers/GuildManager.ts +++ b/packages/core/src/managers/GuildManager.ts @@ -1,45 +1,45 @@ -import type { Session } from '../session'; import type { - GuildMFALevel, APIGuildChannel, GuildChannelType, - RESTPostAPIGuildPruneJSONBody, - RESTPostAPIGuildsJSONBody, - RESTPatchAPIGuildJSONBody, - RESTPostAPIGuildChannelJSONBody, - RESTPatchAPIGuildChannelPositionsJSONBody, - RESTGetAPIGuildBansQuery, - RESTPutAPIGuildBanJSONBody, - RESTPostAPIGuildRoleJSONBody, - RESTPatchAPIGuildRolePositionsJSONBody, - RESTPatchAPIGuildRoleJSONBody, - RESTPatchAPIGuildWidgetSettingsJSONBody, - RESTPatchAPIGuildWelcomeScreenJSONBody, - RESTGetAPIGuildPruneCountQuery, + GuildMFALevel, RESTGetAPIAuditLogQuery, - RESTPostAPIAutoModerationRuleJSONBody, - RESTPatchAPIAutoModerationRuleJSONBody, - RESTPostAPITemplateCreateGuildJSONBody, + RESTGetAPIGuildBansQuery, RESTGetAPIGuildMembersQuery, RESTGetAPIGuildMembersSearchQuery, - RESTPatchAPICurrentGuildMemberJSONBody, - RESTPutAPIGuildMemberJSONBody, - RESTPatchAPIGuildMemberJSONBody, - RESTGetAPIGuildWidgetImageQuery, - RESTPatchAPIGuildEmojiJSONBody, - RESTPostAPIGuildEmojiJSONBody, - RESTPatchAPIGuildVoiceStateUserJSONBody, - RESTPatchAPIGuildVoiceStateCurrentMemberJSONBody, - RESTPatchAPIGuildStickerJSONBody, - RESTPostAPIGuildStickerFormDataBody, - RESTGetAPIGuildScheduledEventsQuery, - RESTPatchAPIGuildScheduledEventJSONBody, - RESTPostAPIGuildScheduledEventJSONBody, + RESTGetAPIGuildPruneCountQuery, RESTGetAPIGuildScheduledEventQuery, RESTGetAPIGuildScheduledEventUsersQuery, + RESTGetAPIGuildScheduledEventsQuery, + RESTGetAPIGuildWidgetImageQuery, + RESTPatchAPIAutoModerationRuleJSONBody, + RESTPatchAPICurrentGuildMemberJSONBody, + RESTPatchAPIGuildChannelPositionsJSONBody, + RESTPatchAPIGuildEmojiJSONBody, + RESTPatchAPIGuildJSONBody, + RESTPatchAPIGuildMemberJSONBody, + RESTPatchAPIGuildRoleJSONBody, + RESTPatchAPIGuildRolePositionsJSONBody, + RESTPatchAPIGuildScheduledEventJSONBody, + RESTPatchAPIGuildStickerJSONBody, RESTPatchAPIGuildTemplateJSONBody, - RESTPostAPIGuildTemplatesJSONBody + RESTPatchAPIGuildVoiceStateCurrentMemberJSONBody, + RESTPatchAPIGuildVoiceStateUserJSONBody, + RESTPatchAPIGuildWelcomeScreenJSONBody, + RESTPatchAPIGuildWidgetSettingsJSONBody, + RESTPostAPIAutoModerationRuleJSONBody, + RESTPostAPIGuildChannelJSONBody, + RESTPostAPIGuildEmojiJSONBody, + RESTPostAPIGuildPruneJSONBody, + RESTPostAPIGuildRoleJSONBody, + RESTPostAPIGuildScheduledEventJSONBody, + RESTPostAPIGuildStickerFormDataBody, + RESTPostAPIGuildTemplatesJSONBody, + RESTPostAPIGuildsJSONBody, + RESTPostAPITemplateCreateGuildJSONBody, + RESTPutAPIGuildBanJSONBody, + RESTPutAPIGuildMemberJSONBody } from '@biscuitland/common'; +import type { Session } from '../session'; export class GuildManager { readonly session!: Session; diff --git a/packages/core/src/managers/InteractionManager.ts b/packages/core/src/managers/InteractionManager.ts index 03759ec..46d0529 100644 --- a/packages/core/src/managers/InteractionManager.ts +++ b/packages/core/src/managers/InteractionManager.ts @@ -3,8 +3,8 @@ import type { RESTPostAPIInteractionCallbackJSONBody, RESTPostAPIInteractionFollowupJSONBody } from '@biscuitland/common'; -import type { Session } from '..'; import type { RawFile } from '@biscuitland/rest'; +import type { Session } from '..'; export class InteractionManager { readonly session!: Session; diff --git a/packages/core/src/managers/MemberManager.ts b/packages/core/src/managers/MemberManager.ts index 53fcd5c..a588184 100644 --- a/packages/core/src/managers/MemberManager.ts +++ b/packages/core/src/managers/MemberManager.ts @@ -1,6 +1,6 @@ -import type { APIGuildMember, MakeRequired } from "@biscuitland/common"; -import type { ImageOptions, Session } from "../index"; -import { formatImageURL } from "../index"; +import type { APIGuildMember, MakeRequired } from '@biscuitland/common'; +import type { ImageOptions, Session } from '../index'; +import { formatImageURL } from '../index'; export class MemberManager { constructor(private readonly session: Session) {} @@ -14,6 +14,6 @@ export class MemberManager { } } -export type DynamicMember = MakeRequired & { +export type DynamicMember = MakeRequired & { guild_id: string; }; diff --git a/packages/core/src/managers/UserManager.ts b/packages/core/src/managers/UserManager.ts index bb8f8f2..d0a9c7c 100644 --- a/packages/core/src/managers/UserManager.ts +++ b/packages/core/src/managers/UserManager.ts @@ -2,21 +2,21 @@ import type { APIUser, RESTGetAPICurrentUserGuildsQuery, RESTPatchAPICurrentUserJSONBody, - RESTPutAPICurrentUserApplicationRoleConnectionJSONBody, -} from "@biscuitland/common"; -import type { ImageOptions, Session } from "../index"; -import { formatImageURL } from "../index"; + RESTPutAPICurrentUserApplicationRoleConnectionJSONBody +} from '@biscuitland/common'; +import type { ImageOptions, Session } from '../index'; +import { formatImageURL } from '../index'; export class UserManager { readonly session!: Session; constructor(session: Session) { - Object.defineProperty(this, "session", { + Object.defineProperty(this, 'session', { value: session, - writable: false, + writable: false }); } - get(userId = "@me") { + get(userId = '@me') { return this.session.api.users(userId).get(); } @@ -29,36 +29,36 @@ export class UserManager { } createDM(userId: string) { - return this.session.api.users("@me").channels.post({ body: { recipient_id: userId } }); + return this.session.api.users('@me').channels.post({ body: { recipient_id: userId } }); } editCurrent(body: RESTPatchAPICurrentUserJSONBody) { - return this.session.api.users("@me").patch({ - body, + return this.session.api.users('@me').patch({ + body }); } getGuilds(query?: RESTGetAPICurrentUserGuildsQuery) { - return this.session.api.users("@me").guilds.get({ query }); + return this.session.api.users('@me').guilds.get({ query }); } getGuildMember(guildId: string) { - return this.session.api.users("@me").guilds(guildId).member.get(); + return this.session.api.users('@me').guilds(guildId).member.get(); } leaveGuild(guildId: string) { - return this.session.api.users("@me").guilds(guildId).delete(); + return this.session.api.users('@me').guilds(guildId).delete(); } getConnections() { - return this.session.api.users("@me").connections.get(); + return this.session.api.users('@me').connections.get(); } getRoleConnections(applicationId: string) { - return this.session.api.users("@me").applications(applicationId)["role-connection"].get(); + return this.session.api.users('@me').applications(applicationId)['role-connection'].get(); } updateRoleConnection(applicationId: string, body: RESTPutAPICurrentUserApplicationRoleConnectionJSONBody) { - return this.session.api.users("@me").applications(applicationId)["role-connection"].put({ body }); + return this.session.api.users('@me').applications(applicationId)['role-connection'].put({ body }); } } diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index d8147fa..7ca9f11 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -1,10 +1,10 @@ -import { GatewayIntentBits, Identify, When } from "@biscuitland/common"; -import type { BiscuitRESTOptions, CDNRoutes, Routes } from "@biscuitland/rest"; -import { BiscuitREST, CDN, Router } from "@biscuitland/rest"; -import { GatewayEvents, GatewayManager, GatewayManagerOptions } from "@biscuitland/ws"; -import EventEmitter2 from "eventemitter2"; -import { MainManager, getBotIdFromToken } from "."; -import { Handler, actionHandler } from "./events/handler"; +import { GatewayIntentBits, Identify, When } from '@biscuitland/common'; +import type { BiscuitRESTOptions, CDNRoutes, Routes } from '@biscuitland/rest'; +import { BiscuitREST, CDN, Router } from '@biscuitland/rest'; +import { GatewayEvents, ShardManager, ShardManagerOptions } from '@biscuitland/ws'; +import EventEmitter2 from 'eventemitter2'; +import { MainManager, getBotIdFromToken } from '.'; +import { Handler, actionHandler } from './events/handler'; export class Session extends EventEmitter2 { constructor(public options: BiscuitOptions) { @@ -18,7 +18,7 @@ export class Session extends EventEmitter2 { api: Routes; cdn: CDNRoutes; managers: MainManager; - gateway!: When; + gateway!: When; private _applicationId?: string; private _botId?: string; @@ -67,7 +67,7 @@ export class Session extends EventEmitter2 { if (!rest) { return new BiscuitREST({ token: this.options.token, - ...this.options.defaultRestOptions, + ...this.options.defaultRestOptions }); } @@ -75,14 +75,14 @@ export class Session extends EventEmitter2 { return rest; } - throw new Error("[CORE] REST not found"); + throw new Error('[CORE] REST not found'); } async start() { // alias fixed `this` on handlePayload const ctx = this as Session; - ctx.gateway = new GatewayManager({ + ctx.gateway = new ShardManager({ token: this.options.token, intents: this.options.intents ?? 0, info: this.options.defaultGatewayOptions?.info ?? (await this.api.gateway.bot.get()), @@ -92,10 +92,10 @@ export class Session extends EventEmitter2 { // @ts-expect-error actionHandler([ctx, { t, d }, shard]); }, - ...this.options.defaultGatewayOptions, + ...this.options.defaultGatewayOptions }); - ctx.once("READY", (payload) => { + ctx.once('READY', (payload) => { const { user, application } = payload; this.botId = user.id; this.applicationId = application.id; @@ -110,12 +110,12 @@ export class Session extends EventEmitter2 { } } -export type HandlePayload = Pick["handlePayload"]; +export type HandlePayload = Pick['handlePayload']; export interface BiscuitOptions { token: string; intents: number | GatewayIntentBits; rest?: BiscuitREST; defaultRestOptions?: Partial; - defaultGatewayOptions?: Identify>>; + defaultGatewayOptions?: Identify>>; } diff --git a/packages/helpers/src/Collector.ts b/packages/helpers/src/Collector.ts index f612ffc..507922e 100644 --- a/packages/helpers/src/Collector.ts +++ b/packages/helpers/src/Collector.ts @@ -1,7 +1,7 @@ -import { MakeRequired, Options } from "@biscuitland/common"; -import { Handler, type Session } from "@biscuitland/core"; -import { GatewayEvents } from "@biscuitland/ws"; -import { EventEmitter } from "node:events"; +import { EventEmitter } from 'node:events'; +import { MakeRequired, Options } from '@biscuitland/common'; +import { Handler, type Session } from '@biscuitland/core'; +import { GatewayEvents } from '@biscuitland/ws'; interface CollectorOptions { event: `${E}`; @@ -13,19 +13,19 @@ interface CollectorOptions { export const DEFAULT_OPTIONS = { filter: () => true, - max: -1, + max: -1 }; export enum CollectorStatus { Idle = 0, Started = 1, - Ended = 2, + Ended = 2 } export class EventCollector extends EventEmitter { collected = new Set[0]>(); status: CollectorStatus = CollectorStatus.Idle; - options: MakeRequired, "filter" | "max">; + options: MakeRequired, 'filter' | 'max'>; private timeout: NodeJS.Timeout | null = null; constructor(readonly session: Session, rawOptions: CollectorOptions) { @@ -36,24 +36,24 @@ export class EventCollector extends EventEmitter start() { this.session.setMaxListeners(this.session.getMaxListeners() + 1); this.session.on(this.options.event, (...args: unknown[]) => this.collect(...(args as Parameters))); - this.timeout = setTimeout(() => this.stop("time"), this.options.idle ?? this.options.time); + this.timeout = setTimeout(() => this.stop('time'), this.options.idle ?? this.options.time); } private collect(...args: Parameters) { if (this.options.filter?.(...args)) { this.collected.add(args[0]); - this.emit("collect", ...args); + this.emit('collect', ...args); } if (this.options.idle) { if (this.timeout) clearTimeout(this.timeout); - this.timeout = setTimeout(() => this.stop("time"), this.options.idle); + this.timeout = setTimeout(() => this.stop('time'), this.options.idle); } - if (this.collected.size >= this.options.max!) this.stop("max"); + if (this.collected.size >= this.options.max!) this.stop('max'); } - stop(reason = "User stopped") { + stop(reason = 'User stopped') { if (this.status === CollectorStatus.Ended) return; if (this.timeout) clearTimeout(this.timeout); @@ -62,17 +62,17 @@ export class EventCollector extends EventEmitter this.session.setMaxListeners(this.session.getMaxListeners() - 1); this.status = CollectorStatus.Ended; - this.emit("end", reason, this.collected); + this.emit('end', reason, this.collected); } - on(event: "collect", listener: (...args: Parameters) => unknown): this; - on(event: "end", listener: (reason: string | null | undefined, collected: Set[0]>) => void): this; + on(event: 'collect', listener: (...args: Parameters) => unknown): this; + on(event: 'end', listener: (reason: string | null | undefined, collected: Set[0]>) => void): this; on(event: string, listener: unknown): this { return super.on(event, listener as () => unknown); } - once(event: "collect", listener: (...args: Parameters) => unknown): this; - once(event: "end", listener: (reason: string | null | undefined, collected: Set[0]>) => void): this; + once(event: 'collect', listener: (...args: Parameters) => unknown): this; + once(event: 'end', listener: (reason: string | null | undefined, collected: Set[0]>) => void): this; once(event: string, listener: unknown): this { return super.once(event, listener as () => unknown); } diff --git a/packages/helpers/src/MessageEmbed.ts b/packages/helpers/src/MessageEmbed.ts index 414c64f..f7352b1 100644 --- a/packages/helpers/src/MessageEmbed.ts +++ b/packages/helpers/src/MessageEmbed.ts @@ -1,4 +1,4 @@ -import { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter, ObjectToLower, TypeArray, toSnakeCase } from "@biscuitland/common"; +import { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter, ObjectToLower, TypeArray, toSnakeCase } from '@biscuitland/common'; export class MessageEmbed { constructor(public data: Partial = {}) { diff --git a/packages/helpers/src/Permissions.ts b/packages/helpers/src/Permissions.ts index d9a0856..a5339a7 100644 --- a/packages/helpers/src/Permissions.ts +++ b/packages/helpers/src/Permissions.ts @@ -1,4 +1,4 @@ -import { PermissionFlagsBits } from "@biscuitland/common"; +import { PermissionFlagsBits } from '@biscuitland/common'; export type PermissionsStrings = keyof typeof PermissionFlagsBits; export type PermissionResolvable = bigint | PermissionsStrings | PermissionsStrings[] | PermissionsStrings | PermissionsStrings[]; @@ -89,17 +89,17 @@ export class Permissions { static resolve(bit: PermissionResolvable): bigint { switch (typeof bit) { - case "bigint": + case 'bigint': return bit; - case "number": + case 'number': return BigInt(bit); - case "string": + case 'string': return BigInt(Permissions.Flags[bit]); - case "object": + case 'object': return Permissions.resolve( bit - .map((p) => (typeof p === "string" ? BigInt(Permissions.Flags[p]) : BigInt(p))) - .reduce((acc, cur) => acc | cur, Permissions.None), + .map((p) => (typeof p === 'string' ? BigInt(Permissions.Flags[p]) : BigInt(p))) + .reduce((acc, cur) => acc | cur, Permissions.None) ); default: throw new TypeError(`Cannot resolve permission: ${bit}`); @@ -125,7 +125,7 @@ export class Permissions { } toJSON(): { fields: string[] } { - const fields = Object.keys(Permissions.Flags).filter((bit) => typeof bit === "number" && this.has(bit)); + const fields = Object.keys(Permissions.Flags).filter((bit) => typeof bit === 'number' && this.has(bit)); return { fields }; } diff --git a/packages/helpers/src/Utils.ts b/packages/helpers/src/Utils.ts index df0589b..4bc3e95 100644 --- a/packages/helpers/src/Utils.ts +++ b/packages/helpers/src/Utils.ts @@ -1,4 +1,4 @@ -import { APIMessageActionRowComponent, APIModalActionRowComponent, ComponentType } from "@biscuitland/common"; +import { APIMessageActionRowComponent, APIModalActionRowComponent, ComponentType } from '@biscuitland/common'; import { ChannelSelectMenu, MentionableSelectMenu, @@ -6,9 +6,9 @@ import { ModalTextInput, RoleSelectMenu, StringSelectMenu, - UserSelectMenu, -} from "./components"; -import { BaseComponent } from "./components/BaseComponent"; + UserSelectMenu +} from './components'; +import { BaseComponent } from './components/BaseComponent'; export function createComponent(data: APIMessageActionRowComponent): HelperComponents; export function createComponent(data: APIModalActionRowComponent): HelperComponents; diff --git a/packages/helpers/src/commands/contextMenu/ContextCommand.ts b/packages/helpers/src/commands/contextMenu/ContextCommand.ts index a60239d..7200efb 100644 --- a/packages/helpers/src/commands/contextMenu/ContextCommand.ts +++ b/packages/helpers/src/commands/contextMenu/ContextCommand.ts @@ -1,5 +1,5 @@ -import { ApplicationCommandType, LocalizationMap, RESTPostAPIContextMenuApplicationCommandsJSONBody } from "@biscuitland/common"; -import { PermissionResolvable, Permissions } from "../../Permissions"; +import { ApplicationCommandType, LocalizationMap, RESTPostAPIContextMenuApplicationCommandsJSONBody } from '@biscuitland/common'; +import { PermissionResolvable, Permissions } from '../../Permissions'; export type ContextCommandType = ApplicationCommandType.Message | ApplicationCommandType.User; diff --git a/packages/helpers/src/commands/index.ts b/packages/helpers/src/commands/index.ts index 973a0bb..47bc1e4 100644 --- a/packages/helpers/src/commands/index.ts +++ b/packages/helpers/src/commands/index.ts @@ -1,3 +1,3 @@ -export * from "./contextMenu/ContextCommand"; -export * from "./slash/SlashCommand"; -export * from "./slash/SlashCommandOption"; +export * from './contextMenu/ContextCommand'; +export * from './slash/SlashCommand'; +export * from './slash/SlashCommandOption'; diff --git a/packages/helpers/src/commands/slash/SlashCommand.ts b/packages/helpers/src/commands/slash/SlashCommand.ts index ced21cc..c79fe32 100644 --- a/packages/helpers/src/commands/slash/SlashCommand.ts +++ b/packages/helpers/src/commands/slash/SlashCommand.ts @@ -1,7 +1,7 @@ -import { ApplicationCommandType, RESTPostAPIChatInputApplicationCommandsJSONBody } from "@biscuitland/common"; -import { Mixin } from "ts-mixer"; -import { PermissionResolvable, Permissions } from "../../Permissions"; -import { AllSlashOptions, SlashSubcommandGroupOption, SlashSubcommandOption } from "./SlashCommandOption"; +import { ApplicationCommandType, RESTPostAPIChatInputApplicationCommandsJSONBody } from '@biscuitland/common'; +import { Mixin } from 'ts-mixer'; +import { PermissionResolvable, Permissions } from '../../Permissions'; +import { AllSlashOptions, SlashSubcommandGroupOption, SlashSubcommandOption } from './SlashCommandOption'; class SlashCommandB { constructor(public data: Partial = {}) {} @@ -33,7 +33,7 @@ class SlashCommandB { return this; } - addRawOption(option: ReturnType) { + addRawOption(option: ReturnType) { this.data.options ??= []; // @ts-expect-error discord-api-types bad typing, again this.data.options.push(option); @@ -42,7 +42,7 @@ class SlashCommandB { toJSON(): RESTPostAPIChatInputApplicationCommandsJSONBody { return { ...this.data, - type: ApplicationCommandType.ChatInput, + type: ApplicationCommandType.ChatInput } as RESTPostAPIChatInputApplicationCommandsJSONBody & { type: ApplicationCommandType.ChatInput; }; diff --git a/packages/helpers/src/commands/slash/SlashCommandOption.ts b/packages/helpers/src/commands/slash/SlashCommandOption.ts index 39f6ffd..f78b693 100644 --- a/packages/helpers/src/commands/slash/SlashCommandOption.ts +++ b/packages/helpers/src/commands/slash/SlashCommandOption.ts @@ -1,26 +1,26 @@ import { - APIApplicationCommandIntegerOption as AACIO, - APIApplicationCommandNumberOption as AACNO, - APIApplicationCommandSubcommandOption as AACSCO, - APIApplicationCommandSubcommandGroupOption as AACSGO, - APIApplicationCommandStringOption as AACSO, APIApplicationCommandAttachmentOption, APIApplicationCommandBooleanOption, APIApplicationCommandChannelOption, + APIApplicationCommandIntegerOption as AACIO, APIApplicationCommandMentionableOption, + APIApplicationCommandNumberOption as AACNO, APIApplicationCommandOption, APIApplicationCommandOptionBase, APIApplicationCommandOptionChoice, APIApplicationCommandRoleOption, + APIApplicationCommandStringOption as AACSO, + APIApplicationCommandSubcommandGroupOption as AACSGO, + APIApplicationCommandSubcommandOption as AACSCO, APIApplicationCommandUserOption, ApplicationCommandOptionType, ChannelType, LocalizationMap, RestToKeys, TypeArray, - When, -} from "@biscuitland/common"; -import { OptionValuesLength } from "../../"; + When +} from '@biscuitland/common'; +import { OptionValuesLength } from '../../'; export type SlashBaseOptionTypes = | Exclude @@ -53,7 +53,7 @@ export abstract class SlashBaseOption { return this; } - addLocalizations(locals: RestToKeys<[LocalizationMap, "name", "description"]>): this { + addLocalizations(locals: RestToKeys<[LocalizationMap, 'name', 'description']>): this { this.data.name_localizations = locals.name; this.data.description_localizations = locals.description; return this; @@ -102,7 +102,7 @@ export class SlashStringOption extends SlashRequir addChoices(choices: TypeArray>): SlashStringOption { const ctx = this as SlashStringOption; ctx.data.choices ??= []; - ctx.data.choices.concat(choices); + ctx.data.choices = ctx.data.choices!.concat(choices); return ctx; } @@ -148,7 +148,7 @@ export class SlashNumberOption extends SlashRequir addChoices(choices: TypeArray>): SlashNumberOption { const ctx = this as SlashNumberOption; ctx.data.choices ??= []; - ctx.data.choices.concat(choices); + ctx.data.choices = ctx.data.choices.concat(choices); return ctx; } @@ -333,7 +333,7 @@ export class SlashSubcommandGroupOption extends SlashBaseOption) { + addRawOption(option: ReturnType) { this.data.options ??= []; this.data.options.push(option); } diff --git a/packages/helpers/src/components/ActionRow.ts b/packages/helpers/src/components/ActionRow.ts index a46355b..dfe6f55 100644 --- a/packages/helpers/src/components/ActionRow.ts +++ b/packages/helpers/src/components/ActionRow.ts @@ -1,6 +1,6 @@ -import { APIActionRowComponent, APIMessageActionRowComponent, ComponentType, TypeArray } from "@biscuitland/common"; -import { MessageComponents, createComponent } from "../Utils"; -import { BaseComponent } from "./BaseComponent"; +import { APIActionRowComponent, APIMessageActionRowComponent, ComponentType, TypeArray } from '@biscuitland/common'; +import { MessageComponents, createComponent } from '../Utils'; +import { BaseComponent } from './BaseComponent'; export class MessageActionRow extends BaseComponent> { constructor({ components, ...data }: Partial> = {}) { @@ -10,7 +10,7 @@ export class MessageActionRow extends BaseComponent components: T[]; addComponents(component: TypeArray): this { - this.components.concat(component); + this.components = this.components.concat(component); return this; } @@ -22,7 +22,7 @@ export class MessageActionRow extends BaseComponent toJSON(): APIActionRowComponent { return { ...this.data, - components: this.components.map((c) => c.toJSON()), - } as APIActionRowComponent>; + components: this.components.map((c) => c.toJSON()) + } as APIActionRowComponent>; } } diff --git a/packages/helpers/src/components/BaseComponent.ts b/packages/helpers/src/components/BaseComponent.ts index 2bb8f09..c88dcf9 100644 --- a/packages/helpers/src/components/BaseComponent.ts +++ b/packages/helpers/src/components/BaseComponent.ts @@ -1,4 +1,4 @@ -import { APIBaseComponent, ComponentType } from "@biscuitland/common"; +import { APIBaseComponent, ComponentType } from '@biscuitland/common'; export abstract class BaseComponent> = APIBaseComponent,> { constructor(public data: Partial) {} diff --git a/packages/helpers/src/components/MessageButton.ts b/packages/helpers/src/components/MessageButton.ts index 98310b8..9c03f36 100644 --- a/packages/helpers/src/components/MessageButton.ts +++ b/packages/helpers/src/components/MessageButton.ts @@ -1,5 +1,5 @@ -import { APIButtonComponentBase, APIMessageComponentEmoji, ButtonStyle, ComponentType, When } from "@biscuitland/common"; -import { BaseComponent } from "./BaseComponent"; +import { APIButtonComponentBase, APIMessageComponentEmoji, ButtonStyle, ComponentType, When } from '@biscuitland/common'; +import { BaseComponent } from './BaseComponent'; export type ButtonStylesForID = Exclude; diff --git a/packages/helpers/src/components/SelectMenu.ts b/packages/helpers/src/components/SelectMenu.ts index 0a49894..2267dbc 100644 --- a/packages/helpers/src/components/SelectMenu.ts +++ b/packages/helpers/src/components/SelectMenu.ts @@ -73,7 +73,7 @@ export class StringSelectMenu extends SelectMenu { addOption(option: TypeArray): this { this.data.options ??= []; - this.data.options.concat(option); + this.data.options = this.data.options.concat(option); return this; } @@ -84,6 +84,7 @@ export class StringSelectMenu extends SelectMenu { } export class StringSelectOption { + // biome-ignore lint/nursery/noEmptyBlockStatements: constructor(public data: Partial = {}) {} setLabel(label: string): this { diff --git a/packages/helpers/src/components/TextInput.ts b/packages/helpers/src/components/TextInput.ts index abdb4e3..ade6b72 100644 --- a/packages/helpers/src/components/TextInput.ts +++ b/packages/helpers/src/components/TextInput.ts @@ -1,6 +1,6 @@ -import { APITextInputComponent, ComponentType, TextInputStyle } from "@biscuitland/common"; -import { OptionValuesLength } from ".."; -import { BaseComponent } from "./BaseComponent"; +import { APITextInputComponent, ComponentType, TextInputStyle } from '@biscuitland/common'; +import { OptionValuesLength } from '..'; +import { BaseComponent } from './BaseComponent'; export class ModalTextInput extends BaseComponent { constructor(data: Partial = {}) { diff --git a/packages/helpers/src/components/index.ts b/packages/helpers/src/components/index.ts index 554cf5e..f76ad29 100644 --- a/packages/helpers/src/components/index.ts +++ b/packages/helpers/src/components/index.ts @@ -1,5 +1,5 @@ -export * from "./ActionRow"; -export * from "./BaseComponent"; -export * from "./MessageButton"; -export * from "./SelectMenu"; -export * from "./TextInput"; +export * from './ActionRow'; +export * from './BaseComponent'; +export * from './MessageButton'; +export * from './SelectMenu'; +export * from './TextInput'; diff --git a/packages/helpers/src/index.ts b/packages/helpers/src/index.ts index fd99747..b6dd0e5 100644 --- a/packages/helpers/src/index.ts +++ b/packages/helpers/src/index.ts @@ -1,5 +1,5 @@ -export * from "./MessageEmbed"; -export * from "./Permissions"; -export * from "./Utils"; -export * from "./commands"; -export * from "./components"; +export * from './MessageEmbed'; +export * from './Permissions'; +export * from './Utils'; +export * from './commands'; +export * from './components'; diff --git a/packages/rest/src/REST.ts b/packages/rest/src/REST.ts index 8c0a978..9cf5ec0 100644 --- a/packages/rest/src/REST.ts +++ b/packages/rest/src/REST.ts @@ -1,8 +1,8 @@ +import type { Identify } from '@biscuitland/common'; import type { RawFile, RequestData } from '@discordjs/rest'; import { REST } from '@discordjs/rest'; -import type { Identify } from '@biscuitland/common'; import type { RequestMethod } from './Router'; -import { Routes } from './Routes'; + export class BiscuitREST { api: REST; constructor(public options: BiscuitRESTOptions) { @@ -72,12 +72,12 @@ export type RequestObject, Q = (M extends `${RequestMethod.Get}` ? unknown : { - body?: B; - files?: RawFile[]; - }); + body?: B; + files?: RawFile[]; + }); export type RestArguments = any> = M extends RequestMethod.Get ? Q extends never - ? RequestObject - : never + ? RequestObject + : never : RequestObject; diff --git a/packages/rest/src/Routes/gateway.ts b/packages/rest/src/Routes/gateway.ts index 68bf446..de46831 100644 --- a/packages/rest/src/Routes/gateway.ts +++ b/packages/rest/src/Routes/gateway.ts @@ -1,4 +1,4 @@ -import { RESTGetAPIGatewayResult, RESTGetAPIGatewayBotResult } from '@biscuitland/common'; +import { RESTGetAPIGatewayBotResult, RESTGetAPIGatewayResult } from '@biscuitland/common'; import { RestArguments } from '../REST'; import { RequestMethod } from '../Router'; diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index 747b44a..d39c3b6 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -1,5 +1,5 @@ -export * from './Routes/'; -export * from './Router'; -export * from './REST'; +export { REST, type RawFile } from '@discordjs/rest'; export * from './CDN'; -export { type RawFile, REST } from '@discordjs/rest'; +export * from './REST'; +export * from './Router'; +export * from './Routes'; diff --git a/packages/ws/README.md b/packages/ws/README.md index d61623d..959df29 100644 --- a/packages/ws/README.md +++ b/packages/ws/README.md @@ -23,7 +23,7 @@ yarn add @biscuitland/ws ## Example ```ts -import { GatewayManager } from "@biscuitland/ws"; +import { ShardManager } from "@biscuitland/ws"; import { BiscuitREST, Router } from "@biscuitland/rest"; import { GatewayIntentBits } from "@biscuitland/common"; @@ -36,7 +36,7 @@ const api = new Router(rest).createProxy(); const connection = await api.gateway.bot.get(); // gateway bot code ↓ - const ws = new GatewayManager({ + const ws = new ShardManager({ token, intents, connection, diff --git a/packages/ws/src/SharedTypes.ts b/packages/ws/src/SharedTypes.ts index 10fcd6a..e26a653 100644 --- a/packages/ws/src/SharedTypes.ts +++ b/packages/ws/src/SharedTypes.ts @@ -11,7 +11,6 @@ import type { GatewayAutoModerationActionExecutionDispatchData, GatewayChannelPinsUpdateDispatchData, GatewayChannelUpdateDispatchData, - GatewayDispatchEvents, GatewayGuildBanAddDispatchData, GatewayGuildBanRemoveDispatchData, GatewayGuildCreateDispatchData, @@ -56,13 +55,15 @@ import type { GatewayVoiceStateUpdateData, GatewayWebhooksUpdateDispatchData, PresenceUpdateStatus, - RestToKeys, -} from "@biscuitland/common"; + RestToKeys +} from '@biscuitland/common'; + +import { GatewayDispatchEvents } from '@biscuitland/common'; /** https://discord.com/developers/docs/topics/gateway-events#update-presence */ export interface StatusUpdate { /** The user's activities */ - activities?: Omit[]; + activities?: Omit[]; /** The user's new status */ status: PresenceUpdateStatus; } @@ -79,7 +80,7 @@ export interface UpdateVoiceState { self_deaf: boolean; } -export type ShardStatusUpdate = Pick; +export type ShardStatusUpdate = Pick; export interface RequestGuildMembersOptions extends GatewayRequestGuildMembersDataWithQuery, GatewayRequestGuildMembersDataWithUserIds {} @@ -96,7 +97,7 @@ export type AtLeastOne< T, U = { [K in keyof T]: Pick; - }, + } > = Partial & U[keyof U]; export type ClientUser = { bot: true } & APIUser; @@ -155,7 +156,7 @@ export type StageSameEvents = RestToKeys< APIStageInstance, GatewayDispatchEvents.StageInstanceCreate, GatewayDispatchEvents.StageInstanceUpdate, - GatewayDispatchEvents.StageInstanceDelete, + GatewayDispatchEvents.StageInstanceDelete ] >; @@ -167,7 +168,7 @@ export type GuildScheduledUserSameEvents = RestToKeys< [ GatewayGuildScheduledEventUserRemoveDispatchData, GatewayDispatchEvents.GuildScheduledEventUserRemove, - GatewayDispatchEvents.GuildScheduledEventUserAdd, + GatewayDispatchEvents.GuildScheduledEventUserAdd ] >; @@ -176,7 +177,7 @@ export type GuildScheduledSameEvents = RestToKeys< APIGuildScheduledEvent, GatewayDispatchEvents.GuildScheduledEventCreate, GatewayDispatchEvents.GuildScheduledEventDelete, - GatewayDispatchEvents.GuildScheduledEventUpdate, + GatewayDispatchEvents.GuildScheduledEventUpdate ] >; @@ -189,7 +190,7 @@ export type AutoModetaractionRuleEvents = RestToKeys< APIAutoModerationRule, GatewayDispatchEvents.AutoModerationRuleCreate, GatewayDispatchEvents.AutoModerationRuleDelete, - GatewayDispatchEvents.AutoModerationRuleUpdate, + GatewayDispatchEvents.AutoModerationRuleUpdate ] >; diff --git a/packages/ws/src/constants/index.ts b/packages/ws/src/constants/index.ts index 032d3e6..4501f98 100644 --- a/packages/ws/src/constants/index.ts +++ b/packages/ws/src/constants/index.ts @@ -1,24 +1,24 @@ -import type { GatewayDispatchPayload } from "@biscuitland/common"; -import type { GatewayManagerOptions } from "../gateway"; +import type { GatewayDispatchPayload } from '@biscuitland/common'; +import { ShardManagerOptions } from '../discord'; const COMPRESS = false; const properties = { os: process.platform, - browser: "Biscuit", - device: "Biscuit", + browser: 'Biscuit', + device: 'Biscuit' }; -const GatewayManagerDefaults: Partial = { +const ShardManagerDefaults: Partial = { totalShards: 1, spawnShardDelay: 5300, debug: false, intents: 0, properties: properties, version: 10, - handlePayload: function (shardId: number, packet: GatewayDispatchPayload): void { + handlePayload: (shardId: number, packet: GatewayDispatchPayload): void => { console.info(`Packet ${packet.t} on shard ${shardId}`); - }, + } }; export interface IdentifyProperties { @@ -51,41 +51,7 @@ enum ShardState { /** Shard is trying to resume a session with the gateway. */ Resuming = 5, /** Shard got shut down studied or due to a not (self) fixable error and may not attempt to reconnect on its own. */ - Offline = 6, + Offline = 6 } -function isObject(o: any) { - return o && typeof o === "object" && !Array.isArray(o); -} - -function Options(defaults: any, ...options: any[]): T { - const option = options.shift(); - if (!option) return defaults; - - return Options( - { - ...option, - ...Object.fromEntries( - Object.entries(defaults).map(([key, value]) => [ - key, - isObject(value) ? Options(value, option?.[key] || {}) : option?.[key] || value, - ]), - ), - }, - ...options, - ); -} - -// nutella truco -function OptionsD>(o: O) { - return function (target: { new (...args: any[]): any }) { - return class extends target { - constructor(...args: any[]) { - super(); - this.options = Options(o, ...args.filter(isObject)); - } - }; - }; -} - -export { COMPRESS, GatewayManagerDefaults, Options, OptionsD, ShardState, isObject, properties }; +export { COMPRESS, ShardManagerDefaults, ShardState, properties }; diff --git a/packages/ws/src/discord/heartbeater.ts b/packages/ws/src/discord/heartbeater.ts new file mode 100644 index 0000000..9650cfa --- /dev/null +++ b/packages/ws/src/discord/heartbeater.ts @@ -0,0 +1,122 @@ +import { GatewayHeartbeatRequest, GatewayHello, GatewayOpcodes, GatewayReceivePayload } from '@biscuitland/common'; +import { Shard } from './shard.js'; +import { ShardSocketCloseCodes } from './shared.js'; + +export interface ShardHeart { + /** Whether or not the heartbeat was acknowledged by Discord in time. */ + ack: boolean; + /** Interval between heartbeats requested by Discord. */ + interval: number; + /** Id of the interval, which is used for sending the heartbeats. */ + intervalId?: NodeJS.Timeout; + /** Unix (in milliseconds) timestamp when the last heartbeat ACK was received from Discord. */ + lastAck?: number; + /** Unix timestamp (in milliseconds) when the last heartbeat was sent. */ + lastBeat?: number; + /** Round trip time (in milliseconds) from Shard to Discord and back. + * Calculated using the heartbeat system. + * Note: this value is undefined until the first heartbeat to Discord has happened. + */ + rtt?: number; + /** Id of the timeout which is used for sending the first heartbeat to Discord since it's "special". */ + timeoutId?: NodeJS.Timeout; + /** internal value */ + toString(): string; +} + +export class ShardHeartBeater { + heart: ShardHeart = { + ack: false, + interval: 30_000 + }; + // biome-ignore lint/nursery/noEmptyBlockStatements: + constructor(public shard: Shard) {} + + acknowledge(ack = true) { + this.heart.ack = ack; + } + + handleHeartbeat(_packet: Extract) { + this.shard.logger.debug(`[Shard #${this.shard.id}] received hearbeat event`); + this.heartbeat(false); + } + + /** + * sends a heartbeat whenever its needed + * fails if heart.interval is null + */ + heartbeat(acknowledgeAck: boolean) { + if (acknowledgeAck) { + if (!this.heart.lastAck) { + this.shard.logger.debug(`[Shard #${this.shard.id}] Heartbeat not acknowledged.`); + this.shard.close(ShardSocketCloseCodes.ZombiedConnection, 'Zombied connection, did not receive an heartbeat ACK in time.'); + this.shard.identify(true); + } + this.heart.lastAck = undefined; + } + + this.heart.lastBeat = Date.now(); + + // avoid creating a bucket here + this.shard.websocket?.send( + JSON.stringify({ + op: GatewayOpcodes.Heartbeat, + d: this.shard.data.resumeSeq + }) + ); + } + + stopHeartbeating() { + clearInterval(this.heart.intervalId); + clearTimeout(this.heart.timeoutId); + } + + startHeartBeating() { + this.shard.logger.debug(`[Shard #${this.shard.id}] scheduling heartbeat!`); + + if (!this.shard.isOpen()) return; + + // The first heartbeat needs to be send with a random delay between `0` and `interval` + // Using a `setTimeout(_, jitter)` here to accomplish that. + // `Math.random()` can be `0` so we use `0.5` if this happens + // Reference: https://discord.com/developers/docs/topics/gateway#heartbeating + const jitter = Math.ceil(this.heart.interval * (Math.random() || 0.5)); + + this.heart.timeoutId = setTimeout(() => { + // send a heartbeat + this.heartbeat(false); + this.heart.intervalId = setInterval(() => { + this.acknowledge(false); + this.heartbeat(false); + }, this.heart.interval); + }, jitter); + } + + handleHello(packet: GatewayHello) { + if (packet.d.heartbeat_interval > 0) { + if (this.heart.interval != null) { + this.stopHeartbeating(); + } + + this.heart.interval = packet.d.heartbeat_interval; + this.heart.intervalId = setInterval(() => { + this.acknowledge(false); + this.heartbeat(false); + }, this.heart.interval); + } + + this.startHeartBeating(); + } + + onpacket(packet: GatewayReceivePayload) { + switch (packet.op) { + case GatewayOpcodes.Heartbeat: + return this.handleHeartbeat(packet); + case GatewayOpcodes.Hello: + return this.handleHello(packet); + case GatewayOpcodes.HeartbeatAck: + this.acknowledge(); + return (this.heart.lastAck = Date.now()); + } + } +} diff --git a/packages/ws/src/discord/index.ts b/packages/ws/src/discord/index.ts new file mode 100644 index 0000000..d46579a --- /dev/null +++ b/packages/ws/src/discord/index.ts @@ -0,0 +1,3 @@ +export * from './shard'; +export * from './sharder'; +export * from './shared'; diff --git a/packages/ws/src/discord/shard.ts b/packages/ws/src/discord/shard.ts new file mode 100644 index 0000000..2080ec9 --- /dev/null +++ b/packages/ws/src/discord/shard.ts @@ -0,0 +1,315 @@ +import { + GATEWAY_BASE_URL, + GatewayCloseCodes, + GatewayDispatchEvents, + GatewayDispatchPayload, + GatewayOpcodes, + GatewayReadyDispatchData, + GatewayReceivePayload, + GatewaySendPayload, + type Logger, +} from "@biscuitland/common"; +import { setTimeout as delay } from "node:timers/promises"; +import { inflateSync } from "node:zlib"; +import WS, { WebSocket, type CloseEvent } from "ws"; +import { ShardState, properties } from "../constants"; +import { DynamicBucket, PriorityQueue } from "../structures"; +import { ShardHeartBeater } from "./heartbeater.js"; +import { ShardData, ShardOptions, ShardSocketCloseCodes } from "./shared.js"; + +export class Shard { + logger: Logger; + data: Partial | ShardData; + websocket: WebSocket | null = null; + heartbeater: ShardHeartBeater; + bucket: DynamicBucket; + offlineSendQueue = new PriorityQueue<(_?: unknown) => void>(); + constructor(public id: number, protected options: ShardOptions) { + this.options.ratelimitOptions ??= { + rateLimitResetInterval: 60_000, + maxRequestsPerRateLimitTick: 120, + }; + this.logger = options.logger; + this.data = { + resumeSeq: null, + resume_gateway_url: GATEWAY_BASE_URL, + }; + + this.heartbeater = new ShardHeartBeater(this); + + const safe = this.calculateSafeRequests(); + this.bucket = new DynamicBucket({ + limit: safe, + refillAmount: safe, + refillInterval: 6e4, + logger: this.logger, + }); + } + + isOpen() { + this.logger.fatal(`[Shard #${this.id}]`, "isOpen", this.websocket?.readyState === WebSocket.OPEN); + return this.websocket?.readyState === WebSocket.OPEN; + } + + /** + * the state of the current shard + */ + get state() { + return this.data.shardState ?? ShardState.Offline; + } + + set state(st: ShardState) { + this.data.shardState = st; + } + + get gatewayURL() { + return this.data.resume_gateway_url ?? this.options.info.url; + } + + connect() { + this.logger.fatal(`[Shard #${this.id}]`, "Connect", this.state); + if (![ShardState.Resuming, ShardState.Identifying].includes(this.state)) { + this.state = ShardState.Connecting; + } + + this.websocket = new WebSocket(this.gatewayURL); + + this.websocket!.onmessage = (event) => this.handleMessage(event); + + this.websocket!.onclose = (event) => this.handleClosed(event); + + this.websocket!.onerror = (event) => this.logger.error(event); + + return new Promise((resolve, reject) => { + const timer = setTimeout(reject, 30_000); + this.websocket!.onopen = () => { + if (![ShardState.Resuming, ShardState.Identifying].includes(this.state)) { + this.state = ShardState.Unidentified; + } + + clearTimeout(timer); + resolve(this); + }; + + this.heartbeater = new ShardHeartBeater(this); + }); + } + + checkOffline(priority: number) { + // biome-ignore lint/style/noArguments: + // biome-ignore lint/correctness/noUndeclaredVariables: + this.logger.fatal(`[Shard #${this.id}]`, "checkOffline", ...arguments); + if (!this.isOpen()) { + return new Promise((resolve) => this.offlineSendQueue.push(resolve, priority)); + } + return Promise.resolve(); + } + + async identify(justTry = false) { + this.logger.debug(`[Shard #${this.id}] ${justTry ? "Trying " : ""}on identify ${this.isOpen()}`); + + if (this.isOpen()) { + if (justTry) return; + this.logger.debug(`[Shard #${this.id}] CLOSING EXISTING SHARD`); + this.close(ShardSocketCloseCodes.ReIdentifying, "Re-identifying closure of old connection."); + } + + this.state = ShardState.Identifying; + + if (!this.isOpen()) { + await this.connect(); + } + + this.send(0, { + op: GatewayOpcodes.Identify, + d: { + token: `Bot ${this.options.token}`, + compress: this.options.compress, + properties, + shard: [this.id, this.options.info.shards], + intents: this.options.intents, + }, + }); + } + + resume() { + this.logger.fatal(`[Shard #${this.id}]`, "Resuming"); + this.state = ShardState.Resuming; + const data = { + seq: this.data.resumeSeq!, + session_id: this.data.session_id!, + token: `Bot ${this.options.token}`, + }; + console.log({ data }); + return this.send(0, { d: data, op: GatewayOpcodes.Resume }); + } + + /** + * Send a message to Discord Gateway. + * sets up the buckets aswell for every path + * these buckets are dynamic memory however a good practice is to use 'WebSocket.send' directly + * in simpler terms, do not use where we don't want buckets + */ + async send(priority: number, message: T) { + // biome-ignore lint/style/noArguments: + // biome-ignore lint/correctness/noUndeclaredVariables: + this.logger.fatal(`[Shard #${this.id}]`, "Send", ...arguments); + // Before acquiring a token from the bucket, check whether the shard is currently offline or not. + // Else bucket and token wait time just get wasted. + await this.checkOffline(priority); + + // pause the function execution for the bucket to be acquired + await this.bucket.acquire(priority); + + // It's possible, that the shard went offline after a token has been acquired from the bucket. + await this.checkOffline(priority); + + // send the payload at last + this.websocket?.send(JSON.stringify(message)); + } + + protected handleMessage({ data }: WS.MessageEvent) { + if (data instanceof Buffer) { + data = inflateSync(data); + } + /** + * Idk why, but Bun sends this event when websocket connects. + * MessageEvent { + * type: "message", + * data: "Already authenticated." + * } + */ + if ((data as string).startsWith("{")) data = JSON.parse(data as string); + + const packet = data as unknown as GatewayReceivePayload; + + // emit other events + this.onpacket(packet); + } + + async onpacket(packet: GatewayReceivePayload | GatewayDispatchPayload) { + if (packet.s !== null) { + this.data.resumeSeq = packet.s; + } + + this.logger.debug(`[Shard #${this.id}]`, packet.t, packet.op); + + this.heartbeater.onpacket(packet); + + switch (packet.op) { + case GatewayOpcodes.Hello: + if (this.data.session_id) { + await this.resume(); + } else { + // await this.identify(true); + } + break; + case GatewayOpcodes.Reconnect: + this.disconnect(); + await this.connect(); + // await this.resume(); + break; + case GatewayOpcodes.InvalidSession: { + const resumable = packet.d as boolean; + // We need to wait for a random amount of time between 1 and 5 + // Reference: https://discord.com/developers/docs/topics/gateway#resuming + // el delay es el tipico timoeut promise, hazmelo pls + //yo con un import { setTimeout as delay } from 'node:timers/promises'; en la mochila + await delay(Math.floor((Math.random() * 4 + 1) * 1000)); + + if (!resumable) { + this.data.resumeSeq = 0; + this.data.session_id = undefined; + await this.identify(true); + break; + } + await this.resume(); + break; + } + } + + switch (packet.t) { + case GatewayDispatchEvents.Resumed: + this.state = ShardState.Connected; + this.offlineSendQueue.toArray().map((resolve: () => any) => resolve()); + break; + case GatewayDispatchEvents.Ready: { + const payload = packet.d as GatewayReadyDispatchData; + this.data.resume_gateway_url = payload.resume_gateway_url; + this.data.session_id = payload.session_id; + this.state = ShardState.Connected; + this.offlineSendQueue.toArray().map((resolve: () => any) => resolve()); + this.options.handlePayload(this.id, packet); + break; + } + default: + this.options.handlePayload(this.id, packet as GatewayDispatchPayload); + break; + } + } + + close(code: number, reason: string) { + if (this.websocket?.readyState !== WebSocket.OPEN) return; + this.websocket?.close(code, reason); + } + + disconnect() { + // biome-ignore lint/style/noArguments: + // biome-ignore lint/correctness/noUndeclaredVariables: + this.logger.info(`[Shard #${this.id}]`, "Disconnect", ...arguments); + this.close(ShardSocketCloseCodes.Shutdown, "Shard down request"); + this.state = ShardState.Offline; + } + + protected async handleClosed(close: CloseEvent) { + this.heartbeater.stopHeartbeating(); + + switch (close.code) { + case ShardSocketCloseCodes.Shutdown: + case ShardSocketCloseCodes.ReIdentifying: + case ShardSocketCloseCodes.Resharded: + case ShardSocketCloseCodes.ResumeClosingOldConnection: + case ShardSocketCloseCodes.ZombiedConnection: + this.state = ShardState.Disconnected; + return; + + case GatewayCloseCodes.UnknownOpcode: + case GatewayCloseCodes.NotAuthenticated: + case GatewayCloseCodes.InvalidSeq: + case GatewayCloseCodes.RateLimited: + case GatewayCloseCodes.SessionTimedOut: + this.logger.debug(`[Shard #${this.id}] Gateway connection closing requiring re-identify. Code: ${close.code}`); + this.state = ShardState.Identifying; + + return this.identify(); + case GatewayCloseCodes.AuthenticationFailed: + case GatewayCloseCodes.InvalidShard: + case GatewayCloseCodes.ShardingRequired: + case GatewayCloseCodes.InvalidAPIVersion: + case GatewayCloseCodes.InvalidIntents: + case GatewayCloseCodes.DisallowedIntents: + this.state = ShardState.Offline; + + throw new Error(close.reason || "Discord gave no reason! GG! You broke Discord!"); + // Gateway connection closes on which a resume is allowed. + default: + console.log(close.code); + this.logger.info(`[Shard #${this.id}] closed shard #${this.id}. Resuming...`); + this.state = ShardState.Resuming; + + this.disconnect(); + await this.connect(); + } + } + + /** Calculate the amount of requests which can safely be made per rate limit interval, before the gateway gets disconnected due to an exceeded rate limit. */ + calculateSafeRequests(): number { + // * 2 adds extra safety layer for discords OP 1 requests that we need to respond to + const safeRequests = + this.options.ratelimitOptions!.maxRequestsPerRateLimitTick - + Math.ceil(this.options.ratelimitOptions!.rateLimitResetInterval / this.heartbeater!.heart.interval) * 2; + + if (safeRequests < 0) return 0; + return safeRequests; + } +} diff --git a/packages/ws/src/discord/sharder.ts b/packages/ws/src/discord/sharder.ts new file mode 100644 index 0000000..f960059 --- /dev/null +++ b/packages/ws/src/discord/sharder.ts @@ -0,0 +1,160 @@ +import { + APIGatewayBotInfo, + Collection, + GatewayOpcodes, + GatewayUpdatePresence, + GatewayVoiceStateUpdate, + LogLevels, + Logger, + ObjectToLower, + Options, + toSnakeCase, +} from "@biscuitland/common"; +import { ShardManagerDefaults } from "../constants"; +import { SequentialBucket } from "../structures"; +import { Shard } from "./shard.js"; +import { ShardManagerOptions } from "./shared"; + +export class ShardManager extends Collection { + connectQueue: SequentialBucket; + options: Required; + logger: Logger; + + constructor(options: ShardManagerOptions) { + super(); + this.options = Options>(ShardManagerDefaults, options, { info: { shards: options.totalShards } }); + + this.connectQueue = new SequentialBucket(this.concurrency); + + this.logger = new Logger({ + active: this.options.debug, + name: "[ShardManager]", + logLevel: LogLevels.Debug, + }); + } + + get remaining(): number { + return this.options.info.session_start_limit.remaining; + } + + get concurrency(): number { + return this.options.info.session_start_limit.max_concurrency; + } + + calculeShardId(guildId: string) { + return Number((BigInt(guildId) >> 22n) % BigInt(this.options.totalShards)); + } + + spawn(shardId: number) { + this.logger.info(`Spawn shard ${shardId}`); + let shard = this.get(shardId); + + shard ??= new Shard(shardId, { + token: this.options.token, + intents: this.options.intents, + info: Options(this.options.info, { shards: this.options.totalShards }), + handlePayload: this.options.handlePayload, + properties: this.options.properties, + logger: this.logger, + compress: false, + }); + + this.set(shardId, shard); + + return shard; + } + + async spawnShards(): Promise { + const buckets = this.spawnBuckets(); + + this.logger.info("Spawn shards"); + for (const bucket of buckets) { + for (const shard of bucket) { + if (!shard) break; + this.logger.info(`${shard.id} add to connect queue`); + await this.connectQueue.push(shard.identify.bind(shard, false)); + } + } + } + + /* + * spawns buckets in order + * https://discord.com/developers/docs/topics/gateway#sharding-max-concurrency + */ + spawnBuckets(): Shard[][] { + this.logger.info("Preparing buckets"); + const chunks = SequentialBucket.chunk(new Array(this.options.totalShards), this.concurrency); + + // biome-ignore lint/complexity/noForEach: i mean is the same thing, but we need the index; + chunks.forEach((arr: any[], index: number) => { + for (let i = 0; i < arr.length; i++) { + const id = i + (index > 0 ? index * this.concurrency : 0); + chunks[index][i] = this.spawn(id); + } + }); + this.logger.info(`${chunks.length} buckets created`); + return chunks; + } + + forceIdentify(shardId: number) { + this.logger.info(`Shard #${shardId} force identify`); + return this.spawn(shardId).identify(); + } + + disconnect(shardId: number) { + this.logger.info(`Force disconnect shard ${shardId}`); + return this.get(shardId)?.disconnect(); + } + + disconnectAll() { + this.logger.info("Disconnect all shards"); + // biome-ignore lint/complexity/noForEach: In maps, for each and for of have same performance + return this.forEach((shard) => shard.disconnect()); + } + + setShardPresence(shardId: number, payload: GatewayUpdatePresence["d"]) { + this.logger.info(`Shard #${shardId} update presence`); + return this.get(shardId)?.send(1, { + op: GatewayOpcodes.PresenceUpdate, + d: payload, + }); + } + setPresence(payload: GatewayUpdatePresence["d"]): Promise | undefined { + return new Promise((resolve) => { + // biome-ignore lint/complexity/noForEach: In maps, for each and for of have same performance + this.forEach((shard) => { + this.setShardPresence(shard.id, payload); + }, this); + resolve(); + }); + } + + joinVoice(guild_id: string, channel_id: string, options: ObjectToLower>) { + const shardId = this.calculeShardId(guild_id); + this.logger.info(`Shard #${shardId} join voice ${channel_id} in ${guild_id}`); + + return this.get(shardId)?.send(1, { + op: GatewayOpcodes.VoiceStateUpdate, + d: { + guild_id, + channel_id, + ...toSnakeCase(options), + }, + }); + } + + leaveVoice(guild_id: string) { + const shardId = this.calculeShardId(guild_id); + this.logger.info(`Shard #${shardId} leave voice in ${guild_id}`); + + return this.get(shardId)?.send(1, { + op: GatewayOpcodes.VoiceStateUpdate, + d: { + guild_id, + channel_id: null, + self_mute: false, + self_deaf: false, + }, + }); + } +} diff --git a/packages/ws/src/gateway/shared.ts b/packages/ws/src/discord/shared.ts similarity index 88% rename from packages/ws/src/gateway/shared.ts rename to packages/ws/src/discord/shared.ts index 0aa0281..220a1f4 100644 --- a/packages/ws/src/gateway/shared.ts +++ b/packages/ws/src/discord/shared.ts @@ -1,5 +1,48 @@ -import type { APIGatewayBotInfo, GatewayDispatchPayload, GatewayIntentBits } from "@biscuitland/common"; -import type { IdentifyProperties, ShardState } from "../constants/index"; +import { APIGatewayBotInfo, GatewayDispatchPayload, GatewayIntentBits, Logger } from '@biscuitland/common'; +import { IdentifyProperties, ShardState } from '../constants'; + +export interface ShardManagerOptions extends ShardDetails { + /** Important data which is used by the manager to connect shards to the gateway. */ + info: APIGatewayBotInfo; + /** + * Delay in milliseconds to wait before spawning next shard. OPTIMAL IS ABOVE 5100. YOU DON'T WANT TO HIT THE RATE LIMIT!!! + * @default 5300 + */ + spawnShardDelay?: number; + /** + * Total amount of shards your bot uses. Useful for zero-downtime updates or resharding. + * @default 1 + */ + totalShards?: number; + /** + * The payload handlers for messages on the shard. + */ + handlePayload(shardId: number, packet: GatewayDispatchPayload): unknown; + /** + * wheter to send debug information to the console + */ + debug?: boolean; +} + +export interface ShardData { + /** state */ + shardState: ShardState; + + /** resume seq to resume connections */ + resumeSeq: number | null; + + /** + * resume_gateway_url is the url to resume the connection + * @link https://discord.com/developers/docs/topics/gateway#ready-event + */ + resume_gateway_url?: string; + + /** + * session_id is the unique session id of the gateway + * do not mistake with the biscuit client which is named Session + */ + session_id?: string; +} export interface ShardDetails { /** Bot token which is used to connect to Discord */ @@ -24,6 +67,17 @@ export interface ShardDetails { properties?: IdentifyProperties; } +export interface ShardOptions extends ShardDetails { + info: APIGatewayBotInfo; + handlePayload(shardId: number, packet: GatewayDispatchPayload): unknown; + ratelimitOptions?: { + maxRequestsPerRateLimitTick: number; + rateLimitResetInterval: number; + }; + logger: Logger; + compress: boolean; +} + export enum ShardSocketCloseCodes { /** A regular Shard shutdown. */ Shutdown = 3000, @@ -38,61 +92,5 @@ export enum ShardSocketCloseCodes { /** Special close code reserved for Discordeno's zero-downtime resharding system. */ Resharded = 3065, /** Shard is re-identifying therefore the old connection needs to be closed. */ - ReIdentifying = 3066, -} - -export interface ShardOptions extends ShardDetails { - info: APIGatewayBotInfo; - handlePayload(shardId: number, packet: GatewayDispatchPayload): unknown; - debug?: boolean; - ratelimitOptions?: { - maxRequestsPerRateLimitTick: number; - rateLimitResetInterval: number; - }; -} - -export interface ShardData { - /** the id of the current shard */ - id: number; - - /** state */ - shardState: ShardState; - - /** resume seq to resume connections */ - resumeSeq: number | null; - - /** - * resume_gateway_url is the url to resume the connection - * @link https://discord.com/developers/docs/topics/gateway#ready-event - */ - resume_gateway_url?: string; - - /** - * session_id is the unique session id of the gateway - * do not mistake with the biscuit client which is named Session - */ - session_id?: string; -} - -export interface GatewayManagerOptions extends ShardDetails { - /** Important data which is used by the manager to connect shards to the gateway. */ - info: APIGatewayBotInfo; - /** - * Delay in milliseconds to wait before spawning next shard. OPTIMAL IS ABOVE 5100. YOU DON'T WANT TO HIT THE RATE LIMIT!!! - * @default 5300 - */ - spawnShardDelay?: number; - /** - * Total amount of shards your bot uses. Useful for zero-downtime updates or resharding. - * @default 1 - */ - totalShards?: number; - /** - * The payload handlers for messages on the shard. - */ - handlePayload(shardId: number, packet: GatewayDispatchPayload): unknown; - /** - * wheter to send debug information to the console - */ - debug?: boolean; + ReIdentifying = 3066 } diff --git a/packages/ws/src/gateway/index.ts b/packages/ws/src/gateway/index.ts deleted file mode 100644 index e057f33..0000000 --- a/packages/ws/src/gateway/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./shard"; -export * from "./sharder"; -export * from "./shared"; diff --git a/packages/ws/src/gateway/shard.ts b/packages/ws/src/gateway/shard.ts deleted file mode 100644 index 784a5ff..0000000 --- a/packages/ws/src/gateway/shard.ts +++ /dev/null @@ -1,477 +0,0 @@ -import type { - GatewayDispatchPayload, - GatewayHeartbeatRequest, - GatewayHello, - GatewayReceivePayload, - GatewaySendPayload, - When, -} from "@biscuitland/common"; - -import { - GATEWAY_BASE_URL, - GatewayVersion as GATEWAY_VERSION, - GatewayCloseCodes, - GatewayDispatchEvents, - GatewayOpcodes, - Logger, -} from "@biscuitland/common"; -import { inflateSync } from "node:zlib"; -import { WebSocket, type MessageEvent } from "ws"; -import { COMPRESS, ShardState, properties } from "../constants/index"; -import { DynamicBucket, PriorityQueue } from "../structures/index"; -import type { ShardData, ShardOptions } from "./shared"; - -export interface ShardHeart { - /** Whether or not the heartbeat was acknowledged by Discord in time. */ - ack: boolean; - /** Interval between heartbeats requested by Discord. */ - interval: number; - /** Id of the interval, which is used for sending the heartbeats. */ - intervalId?: NodeJS.Timer; - /** Unix (in milliseconds) timestamp when the last heartbeat ACK was received from Discord. */ - lastAck?: number; - /** Unix timestamp (in milliseconds) when the last heartbeat was sent. */ - lastBeat?: number; - /** Round trip time (in milliseconds) from Shard to Discord and back. - * Calculated using the heartbeat system. - * Note: this value is undefined until the first heartbeat to Discord has happened. - */ - rtt?: number; - /** Id of the timeout which is used for sending the first heartbeat to Discord since it's "special". */ - timeoutId?: NodeJS.Timeout; - /** internal value */ - toString(): string; -} - -export class ShardHeartbeater { - shard: Shard; - heart: ShardHeart; - - constructor(shard: Shard) { - this.shard = shard; - this.heart = { - ack: false, - interval: 30_000, - }; - } - - acknowledge(ack = true) { - this.heart.ack = ack; - } - - async handleHeartbeat(_packet: Extract) { - this.shard.logger.debug("received hearbeat event"); - this.heartbeat(false); - } - - async handleHello(packet: GatewayHello) { - if (packet.d.heartbeat_interval > 0) { - if (this.heart.interval != null) { - this.stopHeartbeating(); - } - - this.heart.interval = packet.d.heartbeat_interval; - this.heart.intervalId = setInterval(() => { - this.acknowledge(false); - this.heartbeat(false); - }, this.heart.interval); - } - - const interval: number = packet.d.heartbeat_interval; - return this.startHeartbeating(interval); - } - - async onpacket(packet: GatewayReceivePayload) { - switch (packet.op) { - case GatewayOpcodes.Hello: { - return this.handleHello(packet); - } - case GatewayOpcodes.Heartbeat: { - return this.handleHeartbeat(packet); - } - case GatewayOpcodes.HeartbeatAck: { - this.acknowledge(); - return (this.heart.lastAck = Date.now()); - } - } - } - - /** - * sends a heartbeat whenever its needed - * fails if heart.interval is null - */ - heartbeat(acknowledgeAck: boolean) { - if (acknowledgeAck) { - if (!this.heart.lastAck) this.shard.disconnect({ reconnect: () => this.shard.resume() }); - this.heart.lastAck = undefined; - } - - this.heart.lastBeat = Date.now(); - - // avoid creating a bucket here - this.shard.websocket.send( - JSON.stringify({ - op: GatewayOpcodes.Heartbeat, - d: this.shard.data.resumeSeq, - }), - ); - } - - /** - * wait the first interval upon receiving the hello event, then start heartbiting - * interval * jitter - * @param interval the packet interval - */ - async startHeartbeating(interval: number) { - this.heart.interval = interval; - this.shard.logger.debug("scheduling heartbeat!"); - - // The first heartbeat needs to be send with a random delay between `0` and `interval` - // Using a `setTimeout(_, jitter)` here to accomplish that. - // `Math.random()` can be `0` so we use `0.5` if this happens - // Reference: https://discord.com/developers/docs/topics/gateway#heartbeating - const jitter = Math.ceil(this.heart.interval * (Math.random() || 0.5)); - - return (this.heart.timeoutId = setTimeout(() => { - // send a heartbeat - this.heartbeat(false); - this.heart.intervalId = setInterval(() => { - this.acknowledge(false); - this.heartbeat(false); - }, this.heart.interval); - }, jitter)); - } - - stopHeartbeating() { - clearTimeout(this.heart.intervalId); - return clearTimeout(this.heart.timeoutId); - } -} - -export class Shard { - constructor(id: number, protected options: ShardOptions) { - this.options.ratelimitOptions ??= { - rateLimitResetInterval: 60_000, - maxRequestsPerRateLimitTick: 120, - }; - this.data = { - id, - resume_gateway_url: GATEWAY_BASE_URL, - resumeSeq: null, - } as never; - this.websocket = null as never; - this.ready = false; - this.handlePayload = (packet) => { - // do not -> - // decorate this function - if (packet.t === "READY") { - this.logger.debug("received ready event"); - this.data.resume_gateway_url = `${packet.d.resume_gateway_url}?v=${GATEWAY_VERSION}&encoding=json`; - this.logger.debug("switch resume url"); - this.data.session_id = packet.d.session_id; - } - - this.options.handlePayload(this.data.id ?? id, packet); - }; - - // we create an empty heartbeater just to get the interval variable - this.heartbeater = new ShardHeartbeater(this as Shard) as never; - - const safe = this.calculateSafeRequests(); - - this.bucket = new DynamicBucket({ - limit: safe, - refillAmount: safe, - refillInterval: 6e4, - debug: this.options.debug, - }); - this.logger = new Logger({ - name: "[Shard]", - active: this.options.debug, - logLevel: 0, - }); - } - - websocket: When; - heartbeater: When; - data: When>; - handlePayload: (packet: GatewayDispatchPayload) => unknown; - offlineSendQueue: PriorityQueue<(_?: unknown) => void> = new PriorityQueue(); - bucket: DynamicBucket; - logger: Logger; - resolves: Map<"READY" | "RESUMED" | "INVALID_SESSION", (payload: GatewayReceivePayload) => void> = new Map(); - - /** wheter the shard was already identified */ - ready: boolean; - - /** - * a guard of wheter is connected or not - */ - isConnected(): this is Shard { - { - return this.websocket?.readyState === WebSocket.OPEN; - } - } - - /** - * the state of the current shard - */ - get state() { - return this.data.shardState ?? ShardState.Offline; - } - - set state(st: ShardState) { - this.data.shardState = st; - } - - /** - * pushes dead requests for the bot to resolve later on send payload - * this is a remanecent of the old gateway but I (Yuzu) decided to let it untouched - */ - async checkOffline(priority: number) { - if (!this.isConnected()) { - return new Promise((resolve) => this.offlineSendQueue.push(resolve, priority)); - } - return; - } - - /** - * Send a message to Discord Gateway. - * sets up the buckets aswell for every path - * these buckets are dynamic memory however a good practice is to use 'WebSocket.send' directly - * in simpler terms, do not use where we don't want buckets - */ - async send(this: Shard, priority: number, message: GatewaySendPayload) { - // Before acquiring a token from the bucket, check whether the shard is currently offline or not. - // Else bucket and token wait time just get wasted. - await this.checkOffline(priority); - - // pause the function execution for the bucket to be acquired - await this.bucket.acquire(priority); - - // It's possible, that the shard went offline after a token has been acquired from the bucket. - await this.checkOffline(priority); - - // send the payload at last - this.websocket.send(JSON.stringify(message)); - } - - /** starts the ws connection */ - async connect(): Promise> { - if (this.state === ShardState.Resuming) { - this.state = ShardState.Connecting; - } - if (this.state === ShardState.Identifying) { - this.state = ShardState.Connecting; - } - - // set client - const websocket = new WebSocket(this.data.session_id ? this.gatewayURL : this.options.info.url); - this.websocket = websocket as When; - - this.websocket.onmessage = async (event) => { - return await (this as Shard).handleMessage(event); - }; - - this.websocket.onerror = (event) => { - return this.logger.error({ error: event, shardId: this.data.id }); - }; - - this.websocket.onclose = async (event) => { - return await (this as Shard).handleClose(event.code, event.reason); - }; - - return new Promise>((resolve, _reject) => { - this.websocket!.onopen = () => { - if (this.state === ShardState.Resuming) { - this.state = ShardState.Unidentified; - } - if (this.state === ShardState.Identifying) { - this.state = ShardState.Unidentified; - } - resolve(this as Shard); - }; - - // set hearbeater - const heartbeater = new ShardHeartbeater(this as Shard); - this.heartbeater = heartbeater as When; - }); - } - - /** Handle an incoming gateway message. */ - async handleMessage(this: Shard, event: MessageEvent) { - let preProcessMessage = event.data; - - if (preProcessMessage instanceof Buffer) { - preProcessMessage = inflateSync(preProcessMessage); - } - - if (typeof preProcessMessage === "string") { - const packet = JSON.parse(preProcessMessage); - - // emit heartbeat events - this.heartbeater.onpacket(packet); - - // try to identify - if (packet.op === GatewayOpcodes.Hello) { - this.identify(); - } - - // emit other events - this.onpacket(packet); - } - return; - } - - async handleClose(this: Shard, code: number, reason: string) { - this.heartbeater.stopHeartbeating(); - - switch (code) { - case 1000: { - this.logger.info("Uknown error code trying to resume"); - return this.resume(); - } - case GatewayCloseCodes.UnknownOpcode: - case GatewayCloseCodes.NotAuthenticated: - case GatewayCloseCodes.InvalidSeq: - case GatewayCloseCodes.RateLimited: - case GatewayCloseCodes.SessionTimedOut: - this.logger.info(`Gateway connection closing requiring re-identify. Code: ${code}`); - this.state = ShardState.Identifying; - return this.identify(); - case GatewayCloseCodes.AuthenticationFailed: - case GatewayCloseCodes.InvalidShard: - case GatewayCloseCodes.ShardingRequired: - case GatewayCloseCodes.InvalidAPIVersion: - case GatewayCloseCodes.InvalidIntents: - case GatewayCloseCodes.DisallowedIntents: - this.state = ShardState.Offline; - // disconnected event - throw new Error(reason || "Discord gave no reason! GG! You broke Discord!"); - default: - return this.disconnect({ reconnect: () => this.resume() }); - } - } - - async resume(this: Shard) { - this.state = ShardState.Resuming; - - if (!this.ready) return; - - return await this.send(0, { - op: GatewayOpcodes.Resume, - d: { - token: this.options.token, - session_id: this.data.session_id!, - seq: this.data.resumeSeq!, - }, - }); - } - - resetState() { - this.ready = false; - } - - disconnect(options?: { reconnect?: () => unknown }) { - this.logger.error("closing connection"); - - if (!this.websocket) { - return; - } - - if (!options?.reconnect) { - this.websocket.close(1012, "BiscuitWS: close"); - } else { - this.logger.error("trying to reconnect"); - options.reconnect(); - } - - this.websocket.terminate(); - - this.resetState(); - } - - async onpacket(this: Shard, packet: GatewayDispatchPayload | GatewayReceivePayload) { - if (packet.s) this.data.resumeSeq = packet.s; - - switch (packet.op) { - case GatewayOpcodes.InvalidSession: - this.logger.debug("got invalid session, trying to identify back"); - this.data.resumeSeq = null; - this.data.session_id = undefined; - this.data.resume_gateway_url = undefined; - this.resetState(); - await this.identify(); - break; - case GatewayOpcodes.Reconnect: - this.disconnect({ - reconnect: () => { - this.resume(); - }, - }); - break; - } - - switch (packet.t) { - case GatewayDispatchEvents.Resumed: - this.heartbeater.heartbeat(false); - break; - default: - this.handlePayload(packet as any); - } - } - - /** do the handshake with discord */ - async identify() { - if (!this.isConnected()) { - await this.connect().then((shardThis: Shard) => { - this.identify.call(shardThis); - }); - return; - } - - if (!this.ready) { - this.logger.debug(`identifying shard ${this.data.id} with a total of ${this.options.info.shards}`); - this.data.shardState = ShardState.Identifying; - await this.send(0, { - op: GatewayOpcodes.Identify, - d: { - token: this.options.token, - intents: this.options.intents, - properties: properties, - shard: [this.data.id, this.options.info.shards], - compress: COMPRESS, - }, - }).then(() => { - this.logger.debug("finished identifying"); - }); - // ^if we don't get this message start preocupating - - this.ready = true; - } - // ^we make sure we can no longer identify unless invalid session - - return new Promise((resolve) => { - return this.shardIsReady?.().then(() => { - resolve(); - }); - }); - } - - get gatewayURL() { - return this.data.resume_gateway_url ?? this.options.info.url; - } - - /** Calculate the amount of requests which can safely be made per rate limit interval, before the gateway gets disconnected due to an exceeded rate limit. */ - calculateSafeRequests(): number { - // * 2 adds extra safety layer for discords OP 1 requests that we need to respond to - const safeRequests = - this.options.ratelimitOptions!.maxRequestsPerRateLimitTick - - Math.ceil(this.options.ratelimitOptions!.rateLimitResetInterval / this.heartbeater!.heart.interval) * 2; - - if (safeRequests < 0) return 0; - else return safeRequests; - } - - shardIsReady?: () => Promise; -} diff --git a/packages/ws/src/gateway/sharder.ts b/packages/ws/src/gateway/sharder.ts deleted file mode 100644 index c26a794..0000000 --- a/packages/ws/src/gateway/sharder.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { APIGatewayBotInfo } from "@biscuitland/common"; -import { Collection, Logger } from "@biscuitland/common"; -import { GatewayManagerDefaults, Options } from "../constants/index"; -import { SequentialBucket } from "../structures/index"; -import { Shard } from "./shard"; -import type { GatewayManagerOptions } from "./shared"; - -export class GatewayManager extends Collection { - connectQueue: SequentialBucket; - options: Required; - logger: Logger; - - constructor(_options: GatewayManagerOptions) { - super(); - this.options = Options>(GatewayManagerDefaults, { info: { shards: _options.totalShards } }, _options); - this.connectQueue = new SequentialBucket(this.concurrency); - this.logger = new Logger({ - name: "[GatewayManager]", - active: this.options.debug, - logLevel: 0, - }); - } - - get concurrency(): number { - return this.options.info.session_start_limit.max_concurrency; - } - - get remaining(): number { - return this.options.info.session_start_limit.remaining; - } - - /* - * spawns buckets in order - * https://discord.com/developers/docs/topics/gateway#sharding-max-concurrency - */ - spawnBuckets(): Shard[][] { - this.logger.info("Preparing buckets"); - const chunks = SequentialBucket.chunk(new Array(this.options.totalShards), this.concurrency); - - chunks.forEach((arr: any[], index: number) => { - for (let i = 0; i < arr.length; i++) { - const id = i + (index > 0 ? index * this.concurrency : 0); - chunks[index][i] = this.spawn(id); - } - }); - - return chunks; - } - - async spawnShards(): Promise { - const buckets = this.spawnBuckets(); - - this.logger.info("Spawn shards"); - for (const bucket of buckets) { - for (const shard of bucket) { - if (!shard) break; - await this.connectQueue.push(shard.identify.bind(shard)); - } - } - } - - spawn(shardId: number) { - let shard = this.get(shardId); - - shard ??= new Shard(shardId, { - token: this.options.token, - intents: this.options.intents, - info: Options(this.options.info, { shards: this.options.totalShards }), - handlePayload: this.options.handlePayload, - properties: this.options.properties, - debug: this.options.debug, - }); - - this.set(shardId, shard); - - return shard; - } - - async forceIdentify(shardId: number) { - await this.spawn(shardId).identify(); - } - - explode() { - return this.forEach(($) => $.disconnect()); - } -} diff --git a/packages/ws/src/index.ts b/packages/ws/src/index.ts index eb18939..8e02e0b 100644 --- a/packages/ws/src/index.ts +++ b/packages/ws/src/index.ts @@ -1,3 +1,3 @@ -export * from "./SharedTypes"; -export * from "./constants"; -export * from "./gateway"; +export * from './SharedTypes'; +export * from './constants'; +export * from './discord'; diff --git a/packages/ws/src/structures/dynamic_bucket.ts b/packages/ws/src/structures/dynamic_bucket.ts deleted file mode 100644 index d8dfe83..0000000 --- a/packages/ws/src/structures/dynamic_bucket.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Logger, delay } from "@biscuitland/common"; -import { PriorityQueue } from "./lists"; - -/** - * just any kind of request to queue and resolve later - */ -export type QueuedRequest = (value: void | Promise) => Promise | any; - -/** - * options of the dynamic bucket - */ -export interface DynamicBucketOptions { - limit: number; - refillInterval: number; - refillAmount: number; - debug?: boolean; -} - -/** - * generally useless for interaction based bots - * ideally this would only be triggered on certain paths - * example: a huge amount of messages being spammed - * - * a dynamic bucket is just a priority queue implemented using linked lists - * we create an empty bucket for every path - * dynamically allocating memory improves the final memory footprint - */ -export class DynamicBucket { - limit: number; - refillInterval: number; - refillAmount: number; - - /** The queue of requests to acquire an available request. Mapped by */ - queue: PriorityQueue = new PriorityQueue(); - - /** The amount of requests that have been used up already. */ - used = 0; - - /** Whether or not the queue is already processing. */ - processing = false; - - /** The timeout id for the timer to reduce the used amount by the refill amount. */ - timeoutId?: NodeJS.Timeout; - - /** The timestamp in milliseconds when the next refill is scheduled. */ - refillsAt?: number; - - logger: Logger; - - constructor(options: DynamicBucketOptions) { - this.limit = options.limit; - this.refillInterval = options.refillInterval; - this.refillAmount = options.refillAmount; - this.logger = new Logger({ - name: "[Gateway]", - active: options.debug, - logLevel: 0, - }); - } - - get remaining(): number { - if (this.limit < this.used) return 0; - else return this.limit - this.used; - } - - refill(): void { - // Lower the used amount by the refill amount - this.used = this.refillAmount > this.used ? 0 : this.used - this.refillAmount; - - // Reset the refillsAt timestamp since it just got refilled - this.refillsAt = undefined; - - if (this.used > 0) { - if (this.timeoutId) { - clearTimeout(this.timeoutId); - } - this.timeoutId = setTimeout(() => { - this.refill(); - }, this.refillInterval); - this.refillsAt = Date.now() + this.refillInterval; - } - } - - /** Begin processing the queue. */ - async processQueue(): Promise { - // There is already a queue that is processing - if (this.processing) { - return; - } - - // Begin going through the queue. - while (!this.queue.isEmpty()) { - if (this.remaining) { - this.logger.debug(`Processing queue. Remaining: ${this.remaining} Length: ${this.queue.size()}`); - // Resolves the promise allowing the paused execution of this request to resolve and continue. - this.queue.peek()(); - this.queue.pop(); - - // A request can be made - this.used++; - - // Create a new timeout for this request if none exists. - if (!this.timeoutId) { - this.timeoutId = setTimeout(() => { - this.refill(); - }, this.refillInterval); - - // Set the time for when this refill will occur. - this.refillsAt = Date.now() + this.refillInterval; - } - } - - // Check if a refill is scheduled, since we have used up all available requests - else if (this.refillsAt) { - const now = Date.now(); - // If there is time left until next refill, just delay execution. - if (this.refillsAt > now) { - await delay(this.refillsAt - now); - } - } - } - - // Loop has ended mark false so it can restart later when needed - this.processing = false; - } - - /** Pauses the execution until the request is available to be made. */ - async acquire(priority: number): Promise { - return await new Promise((resolve) => { - this.queue.push(resolve, priority); - void this.processQueue(); - }); - } - - toString() { - return [...this.queue].toString(); - } -} diff --git a/packages/ws/src/structures/index.ts b/packages/ws/src/structures/index.ts index 49413a0..c7bda7b 100644 --- a/packages/ws/src/structures/index.ts +++ b/packages/ws/src/structures/index.ts @@ -1,3 +1,337 @@ -export * from "./dynamic_bucket"; -export * from "./lists"; -export * from "./sequential_bucket"; +import { Logger, delay } from '@biscuitland/common'; + +/** + * just any kind of request to queue and resolve later + */ +export type QueuedRequest = (value: void | Promise) => Promise | any; + +/** + * options of the dynamic bucket + */ +export interface DynamicBucketOptions { + limit: number; + refillInterval: number; + refillAmount: number; + logger: Logger; +} + +/** + * generally useless for interaction based bots + * ideally this would only be triggered on certain paths + * example: a huge amount of messages being spammed + * + * a dynamic bucket is just a priority queue implemented using linked lists + * we create an empty bucket for every path + * dynamically allocating memory improves the final memory footprint + */ +export class DynamicBucket { + limit: number; + refillInterval: number; + refillAmount: number; + + /** The queue of requests to acquire an available request. Mapped by */ + queue = new PriorityQueue(); + + /** The amount of requests that have been used up already. */ + used = 0; + + /** Whether or not the queue is already processing. */ + processing = false; + + /** The timeout id for the timer to reduce the used amount by the refill amount. */ + timeoutId?: NodeJS.Timeout; + + /** The timestamp in milliseconds when the next refill is scheduled. */ + refillsAt?: number; + + logger: Logger; + + constructor(options: DynamicBucketOptions) { + this.limit = options.limit; + this.refillInterval = options.refillInterval; + this.refillAmount = options.refillAmount; + this.logger = options.logger; + } + + get remaining(): number { + if (this.limit < this.used) return 0; + return this.limit - this.used; + } + + refill(): void { + // Lower the used amount by the refill amount + this.used = this.refillAmount > this.used ? 0 : this.used - this.refillAmount; + + // Reset the refillsAt timestamp since it just got refilled + this.refillsAt = undefined; + + if (this.used > 0) { + if (this.timeoutId) { + clearTimeout(this.timeoutId); + } + this.timeoutId = setTimeout(() => { + this.refill(); + }, this.refillInterval); + this.refillsAt = Date.now() + this.refillInterval; + } + } + + /** Begin processing the queue. */ + async processQueue(): Promise { + // There is already a queue that is processing + if (this.processing) { + return; + } + + // Begin going through the queue. + while (!this.queue.isEmpty()) { + if (this.remaining) { + this.logger.debug(`Processing queue. Remaining: ${this.remaining} Length: ${this.queue.size()}`); + // Resolves the promise allowing the paused execution of this request to resolve and continue. + this.queue.peek()(); + this.queue.pop(); + + // A request can be made + this.used++; + + // Create a new timeout for this request if none exists. + if (!this.timeoutId) { + this.timeoutId = setTimeout(() => { + this.refill(); + }, this.refillInterval); + + // Set the time for when this refill will occur. + this.refillsAt = Date.now() + this.refillInterval; + } + // Check if a refill is scheduled, since we have used up all available requests + } else if (this.refillsAt) { + const now = Date.now(); + // If there is time left until next refill, just delay execution. + if (this.refillsAt > now) { + await delay(this.refillsAt - now); + } + } + } + + // Loop has ended mark false so it can restart later when needed + this.processing = false; + } + + /** Pauses the execution until the request is available to be made. */ + async acquire(priority: number): Promise { + return await new Promise((resolve) => { + this.queue.push(resolve, priority); + // biome-ignore lint/complexity/noVoid: + void this.processQueue(); + }); + } + + toString() { + return [...this.queue].toString(); + } +} + +/** + * abstract node lol + */ +export interface AbstractNode { + data: T; + next: this | null; +} + +export interface QueuePusher { + push(data: T): NonNullable>; +} + +export interface QueuePusherWithPriority { + push(data: T, priority: number): NonNullable>; +} + +export class TNode implements AbstractNode { + data: T; + next: this | null; + + constructor(data: T) { + this.data = data; + this.next = null; + } + + static null(list: AbstractNode | null): list is null { + return !list; + } +} + +export class PNode extends TNode { + priority: number; + + constructor(data: T, priority: number) { + super(data); + this.priority = priority; + } +} + +export abstract class Queue { + protected abstract head: AbstractNode | null; + + /** + * O(1) + */ + public pop() { + if (TNode.null(this.head)) { + throw new Error('cannot pop a list without elements'); + } + + return (this.head = this.head.next); + } + + /** + * O(1) + */ + public peek(): T { + if (TNode.null(this.head)) { + throw new Error('cannot peek an empty list'); + } + + return this.head.data; + } + + /** + * O(n) + */ + public size(): number { + let aux = this.head; + + if (TNode.null(aux)) { + return 0; + } + + let count = 1; + + while (aux.next !== null) { + count++; + aux = aux.next; + } + + return count; + } + + /** + * O(1) + */ + public isEmpty() { + return TNode.null(this.head); + } + + *[Symbol.iterator](): IterableIterator { + let temp = this.head; + while (temp !== null) { + yield temp.data; + temp = temp.next; + } + } + + public toArray(): T[] { + return Array.from(this); + } + public toString() { + return this.head?.toString() || ''; + } +} + +export class LinkedList extends Queue implements QueuePusher { + protected head: TNode | null = null; + + /** + * O(1) + */ + public push(data: T): NonNullable> { + const temp = new TNode(data); + temp.next = this.head; + this.head = temp; + + return this.head; + } +} + +export class PriorityQueue extends Queue implements QueuePusherWithPriority { + protected head: PNode | null = null; + + /** + * O(#priorities) + */ + public push(data: T, priority: number): NonNullable> { + let start = this.head; + const temp = new PNode(data, priority); + + if (TNode.null(this.head) || TNode.null(start)) { + this.head = temp; + return this.head; + } + + if (this.head.priority > priority) { + temp.next = this.head; + this.head = temp; + return this.head; + } + + while (start.next !== null && start.next.priority < priority) { + start = start.next; + } + + temp.next = start.next as PNode; + start.next = temp; + + return this.head; + } +} + +export class SequentialBucket { + private connections: LinkedList; + private capacity: number; // max_concurrency + private spawnTimeout: number; + + constructor(maxCapacity: number) { + this.connections = new LinkedList(); + this.capacity = maxCapacity; + this.spawnTimeout = 5000; + } + + public async destroy() { + this.connections = new LinkedList(); + } + + public async push(promise: QueuedRequest) { + this.connections.push(promise); + + if (this.capacity <= this.connections.size()) { + await this.acquire(); + await delay(this.spawnTimeout); + } + return; + } + + public async acquire(promises = this.connections) { + while (!promises.isEmpty()) { + const item = promises.peek(); + item().catch((...args: any[]) => { + Promise.reject(...args); + }); + promises.pop(); + } + + return Promise.resolve(true); + } + + public static chunk(array: T[], chunks: number): T[][] { + let index = 0; + let resIndex = 0; + const result = Array(Math.ceil(array.length / chunks)); + + while (index < array.length) { + result[resIndex] = array.slice(index, (index += chunks)); + resIndex++; + } + + return result; + } +} diff --git a/packages/ws/src/structures/lists.ts b/packages/ws/src/structures/lists.ts deleted file mode 100644 index 8d746b5..0000000 --- a/packages/ws/src/structures/lists.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * abstract node lol - */ -export interface AbstractNode { - data: T; - next: this | null; -} - -export interface QueuePusher { - push(data: T): NonNullable>; -} - -export interface QueuePusherWithPriority { - push(data: T, priority: number): NonNullable>; -} - -export class TNode implements AbstractNode { - data: T; - next: this | null; - - constructor(data: T) { - this.data = data; - this.next = null; - } - - static null(list: AbstractNode | null): list is null { - return !list; - } -} - -export class PNode extends TNode { - priority: number; - - constructor(data: T, priority: number) { - super(data); - this.priority = priority; - } -} - -export abstract class Queue { - protected abstract head: AbstractNode | null; - - /** - * O(1) - */ - public pop() { - if (TNode.null(this.head)) { - throw new Error("cannot pop a list without elements"); - } - - return (this.head = this.head.next); - } - - /** - * O(1) - */ - public peek(): T { - if (TNode.null(this.head)) { - throw new Error("cannot peek an empty list"); - } - - return this.head.data; - } - - /** - * O(n) - */ - public size(): number { - let aux = this.head; - - if (TNode.null(aux)) { - return 0; - } - - let count = 1; - - while (aux.next !== null) { - count++; - aux = aux.next; - } - - return count; - } - - /** - * O(1) - */ - public isEmpty() { - return TNode.null(this.head); - } - - *[Symbol.iterator](): IterableIterator { - let temp = this.head; - while (temp !== null) { - yield temp.data; - temp = temp.next; - } - } - - public toArray(): T[] { - return Array.from(this); - } - public toString() { - return this.head?.toString() || ""; - } -} - -export class LinkedList extends Queue implements QueuePusher { - protected head: TNode | null = null; - - /** - * O(1) - */ - public push(data: T): NonNullable> { - const temp = new TNode(data); - temp.next = this.head; - this.head = temp; - - return this.head; - } -} - -export class PriorityQueue extends Queue implements QueuePusherWithPriority { - protected head: PNode | null = null; - - /** - * O(#priorities) - */ - public push(data: T, priority: number): NonNullable> { - let start = this.head; - const temp = new PNode(data, priority); - - if (TNode.null(this.head) || TNode.null(start)) { - this.head = temp; - return this.head; - } - - if (this.head.priority > priority) { - temp.next = this.head; - this.head = temp; - return this.head; - } - - while (start.next !== null && start.next.priority < priority) { - start = start.next; - } - - temp.next = start.next as PNode; - start.next = temp; - - return this.head; - } -} diff --git a/packages/ws/src/structures/sequential_bucket.ts b/packages/ws/src/structures/sequential_bucket.ts deleted file mode 100644 index 7f9a452..0000000 --- a/packages/ws/src/structures/sequential_bucket.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { delay } from "@biscuitland/common"; -import type { QueuedRequest } from "./dynamic_bucket"; -import { LinkedList } from "./lists"; - -export class SequentialBucket { - private connections: LinkedList; - private capacity: number; // max_concurrency - private spawnTimeout: number; - - constructor(maxCapacity: number) { - this.connections = new LinkedList(); - this.capacity = maxCapacity; - this.spawnTimeout = 5000; - } - - public async destroy() { - this.connections = new LinkedList(); - } - - public async push(promise: QueuedRequest) { - this.connections.push(promise); - - if (this.capacity <= this.connections.size()) { - await this.acquire(); - await delay(this.spawnTimeout); - } - return; - } - - public async acquire(promises = this.connections) { - while (!promises.isEmpty()) { - const item = promises.peek(); - item().catch((...args: any[]) => { - Promise.reject(...args); - }); - promises.pop(); - } - - return Promise.resolve(true); - } - - public static chunk(array: T[], chunks: number): T[][] { - let index = 0; - let resIndex = 0; - const result = Array(Math.ceil(array.length / chunks)); - - while (index < array.length) { - result[resIndex] = array.slice(index, (index += chunks)); - resIndex++; - } - - return result; - } -}