diff --git a/.vscode/settings.json b/.vscode/settings.json index c04fd7c61..f9d65679c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,7 @@ "kranvagn", "Leaderboard", "leaderboards", + "Oopsy", "outdir", "outfile", "ownedtanks", diff --git a/docs/changelogs/1.14.1.md b/docs/changelogs/1.14.1.md new file mode 100644 index 000000000..14100392b --- /dev/null +++ b/docs/changelogs/1.14.1.md @@ -0,0 +1,17 @@ +# Blitzkrieg 1.14.1 + +Oopsy daisy, a memory leak! + +## Changes + +- Simplified message for users to use `/link` + +## Fixes + +- Fixed a memory leak in the render thread manager + +## Technical changes + +- Added `UserError` extending `Error` + - This lets the bot avoid spamming the debug console for mistakes users make +- Reduced render threads to 2 diff --git a/src/commands/ratings.tsx b/src/commands/ratings.tsx index 647507ed0..18c4f4504 100644 --- a/src/commands/ratings.tsx +++ b/src/commands/ratings.tsx @@ -18,7 +18,7 @@ import getMidnightLeaderboard, { DATABASE_REPO, } from '../core/blitzkrieg/getMidnightLeaderboard'; import { octokit } from '../core/blitzkrieg/octokit'; -import throwError from '../core/blitzkrieg/throwError'; +import { UserError } from '../core/blitzkrieg/userError'; import addRegionChoices from '../core/discord/addRegionChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices'; import autocompleteUsername from '../core/discord/autocompleteUsername'; @@ -185,7 +185,7 @@ export const ratingsCommand = new Promise( }) .then(({ data }) => { if (!Array.isArray(data)) { - throw throwError('Archived ratings data is malformed'); + throw new Error('Archived ratings data is malformed'); } return data.map( (folder) => @@ -292,7 +292,7 @@ export const ratingsCommand = new Promise( }) .then(async ({ data }) => { if (Array.isArray(data) || data.type !== 'file') { - throw throwError('Archived ratings data is malformed'); + throw new Error('Archived ratings data is malformed'); } const players = (await fetch(data.download_url!).then( @@ -361,7 +361,7 @@ export const ratingsCommand = new Promise( }) .then(async ({ data }) => { if (Array.isArray(data) || data.type !== 'file') { - throw throwError('Archived ratings info is malformed'); + throw new Error('Archived ratings info is malformed'); } const response = fetch(data.download_url!); @@ -404,7 +404,7 @@ export const ratingsCommand = new Promise( }) .then(async ({ data }) => { if (Array.isArray(data) || data.type !== 'file') { - throw throwError( + throw new Error( 'Archived ratings latest data is malformed', ); } @@ -418,9 +418,12 @@ export const ratingsCommand = new Promise( ); if (playerIndex === -1) { - throw throwError( + throw new UserError( `${accountInfo.nickname} didn't player ratings in season ${season}`, - 'This player did not participate in this season or did not get past calibration.', + { + cause: + 'This player did not participate in this season or did not get past calibration.', + }, ); } diff --git a/src/core/blitz/fetchWargaming.ts b/src/core/blitz/fetchWargaming.ts index 86f99ac71..a17c98d5c 100644 --- a/src/core/blitz/fetchWargaming.ts +++ b/src/core/blitz/fetchWargaming.ts @@ -1,5 +1,3 @@ -import throwError from '../blitzkrieg/throwError'; - export type BlitzResponse = | { status: 'error'; @@ -18,9 +16,8 @@ export default async function fetchBlitz(url: string) { if (data.status === 'ok') { return data.data; } else { - throw throwError( - `Wargaming response error status:"${data.status}"`, - `Message: "${data.error.message}"\nURL: "${url}"`, - ); + throw new Error(`Wargaming response error status:"${data.status}"`, { + cause: `Message: "${data.error.message}"\nURL: "${url}"`, + }); } } diff --git a/src/core/blitz/getTankStats.ts b/src/core/blitz/getTankStats.ts index 3d435143c..acb9fcc39 100644 --- a/src/core/blitz/getTankStats.ts +++ b/src/core/blitz/getTankStats.ts @@ -1,7 +1,7 @@ import { Region } from '../../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { TanksStats } from '../../types/tanksStats'; -import throwError from '../blitzkrieg/throwError'; +import { UserError } from '../blitzkrieg/userError'; import fetchBlitz from './fetchWargaming'; export default async function getTankStats(region: Region, id: number) { @@ -10,10 +10,10 @@ export default async function getTankStats(region: Region, id: number) { ); if (tankStats[id] === null) { - throw throwError( - 'No tank stats available', - "This player doesn't have any stats for tanks available. This may not be the player you are looking for.", - ); + throw new UserError('No tank stats available', { + cause: + "This player doesn't have any stats for tanks available. This may not be the player you are looking for.", + }); } return tankStats[id]; diff --git a/src/core/blitz/resolveTankId.ts b/src/core/blitz/resolveTankId.ts index 391785fbe..a3748942f 100644 --- a/src/core/blitz/resolveTankId.ts +++ b/src/core/blitz/resolveTankId.ts @@ -1,5 +1,5 @@ import { go } from 'fuzzysort'; -import throwError from '../blitzkrieg/throwError'; +import { UserError } from '../blitzkrieg/userError'; import { TANKS, tankopedia } from '../blitzstars/tankopedia'; export default async function resolveTankId(tank: string | number) { @@ -12,10 +12,9 @@ export default async function resolveTankId(tank: string | number) { }); if (searchResult.length === 0) { - throw throwError( - 'Tank not found', - `Could not find tank by the name "${tank}".`, - ); + throw new UserError('Tank not found', { + cause: `Could not find tank by the name "${tank}".`, + }); } else { return searchResult[0].obj.tank_id; } @@ -23,10 +22,9 @@ export default async function resolveTankId(tank: string | number) { if ((await tankopedia)[number]) { return number; } else { - throw throwError( - 'Tank not found', - `Could not find tank by the ID "${number}".`, - ); + throw new UserError('Tank not found', { + cause: `Could not find tank by the ID "${number}".`, + }); } } } diff --git a/src/core/blitzkrieg/getArchivedRatingsInfo.ts b/src/core/blitzkrieg/getArchivedRatingsInfo.ts index 6a97afcef..3a1e3157a 100644 --- a/src/core/blitzkrieg/getArchivedRatingsInfo.ts +++ b/src/core/blitzkrieg/getArchivedRatingsInfo.ts @@ -2,7 +2,6 @@ import { RatingsInfo } from '../../commands/ratings'; import { Region } from '../../constants/regions'; import { DATABASE_REPO } from './getMidnightLeaderboard'; import { octokit } from './octokit'; -import throwError from './throwError'; const ARCHIVED_RATINGS_CACHE: Record< Region, @@ -32,7 +31,7 @@ export default async function getArchivedRatingsInfo( }); if (Array.isArray(data) || data.type !== 'file') - throw throwError('Archived ratings info is malformed'); + throw new Error('Archived ratings info is malformed'); const content = Buffer.from(data.content, 'base64').toString(); const jsonContent = JSON.parse(content) as RatingsInfo & { diff --git a/src/core/blitzkrieg/svgToPngThreaded.ts b/src/core/blitzkrieg/svgToPngThreaded.ts index dbba997b4..96afc52b5 100644 --- a/src/core/blitzkrieg/svgToPngThreaded.ts +++ b/src/core/blitzkrieg/svgToPngThreaded.ts @@ -1,6 +1,6 @@ import { Worker } from 'worker_threads'; -const MAX_WORKERS = 3; +const MAX_WORKERS = 2; const workers = [...Array(MAX_WORKERS)].map(() => { const worker = new Worker(`${__dirname}/workers/render.cjs`); @@ -19,18 +19,22 @@ function manageQueue() { function cleanup() { workers.push(worker); + worker.off('message', handleMessage); + worker.off('error', handleError); manageQueue(); } + function handleMessage(png: Uint8Array) { + cleanup(); + resolve(Buffer.from(png)); + } + function handleError(error: Error) { + cleanup(); + throw error; + } worker.postMessage(svg); - worker.on('message', (png: Uint8Array) => { - cleanup(); - resolve(Buffer.from(png)); - }); - worker.on('error', (error) => { - cleanup(); - throw error; - }); + worker.on('message', handleMessage); + worker.on('error', handleError); } } diff --git a/src/core/blitzkrieg/throwError.ts b/src/core/blitzkrieg/throwError.ts deleted file mode 100644 index 2a8a8585f..000000000 --- a/src/core/blitzkrieg/throwError.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function throwError(message: string, cause?: string) { - return new Error(message, { cause }); -} diff --git a/src/core/blitzkrieg/userError.ts b/src/core/blitzkrieg/userError.ts new file mode 100644 index 000000000..06b0ee65c --- /dev/null +++ b/src/core/blitzkrieg/userError.ts @@ -0,0 +1,4 @@ +/** + * Discriminator for user caused errors to not report in logs + */ +export class UserError extends Error {} diff --git a/src/core/discord/resolveClanFromCommand.ts b/src/core/discord/resolveClanFromCommand.ts index f9828d0c0..a43bb78fc 100644 --- a/src/core/discord/resolveClanFromCommand.ts +++ b/src/core/discord/resolveClanFromCommand.ts @@ -1,7 +1,7 @@ import { ChatInputCommandInteraction } from 'discord.js'; import { Region } from '../../constants/regions'; import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions'; -import throwError from '../blitzkrieg/throwError'; +import { UserError } from '../blitzkrieg/userError'; import { serverAndIdPattern } from './resolvePlayerFromCommand'; export default async function resolveClanFromCommand( @@ -18,10 +18,9 @@ export default async function resolveClanFromCommand( if (accounts[0]) { return { region: accounts[0].region, id: accounts[0].clan_id }; } else { - throw throwError( - 'Could not find clan', - `I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`, - ); + throw new UserError('Could not find clan', { + cause: `I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`, + }); } } } diff --git a/src/core/discord/resolvePlayerFromCommand.ts b/src/core/discord/resolvePlayerFromCommand.ts index 5a53615c0..a766f2161 100644 --- a/src/core/discord/resolvePlayerFromCommand.ts +++ b/src/core/discord/resolvePlayerFromCommand.ts @@ -3,7 +3,7 @@ import markdownEscape from 'markdown-escape'; import { Region } from '../../constants/regions'; import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions'; import { getBlitzFromDiscord } from '../blitzkrieg/discordBlitz'; -import throwError from '../blitzkrieg/throwError'; +import { UserError } from '../blitzkrieg/userError'; export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/; @@ -34,12 +34,11 @@ export default async function resolvePlayerFromCommand( id: accounts[0].account_id, } satisfies ResolvedPlayer; } else { - throw throwError( - 'Could not find user', - `I couldn't find user "${markdownEscape( + throw new UserError('Could not find user', { + cause: `I couldn't find user "${markdownEscape( commandUsername, )}". Try picking an user from the search result, typing in a valid username, or using the \`/link\` command.`, - ); + }); } } } else { @@ -48,10 +47,9 @@ export default async function resolvePlayerFromCommand( if (account) { return { region: account.region, id: account.blitz }; } else { - throw throwError( - "You're not linked", - 'Use the `/link` command to link your Discord and Blitz accounts.', - ); + throw new UserError('Use the `/link` command', { + cause: 'Link your Blitz and Discord accounts to get started.', + }); } } } diff --git a/src/events/interactionCreate/handlers/button.tsx b/src/events/interactionCreate/handlers/button.tsx index 4394a54ce..9a0be8196 100644 --- a/src/events/interactionCreate/handlers/button.tsx +++ b/src/events/interactionCreate/handlers/button.tsx @@ -1,6 +1,5 @@ import { ButtonInteraction, CacheType } from 'discord.js'; import { commands } from '..'; -import throwError from '../../../core/blitzkrieg/throwError'; import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable'; export default async function handleButton( @@ -13,8 +12,7 @@ export default async function handleButton( const button = (await commands)[commandName]?.button; if (!button) { - throwError(`Button handler not found for "${commandName}"`); - return; + throw new Error(`Button handler not found for "${commandName}"`); } if (interaction.message.interaction?.user.id !== interaction.user.id) { diff --git a/src/events/interactionCreate/handlers/chatInputCommand.ts b/src/events/interactionCreate/handlers/chatInputCommand.ts index 039e4fd0e..dbe76ff67 100644 --- a/src/events/interactionCreate/handlers/chatInputCommand.ts +++ b/src/events/interactionCreate/handlers/chatInputCommand.ts @@ -7,10 +7,11 @@ import { } from 'discord.js'; import { commands } from '..'; import discord from '../../../../discord.json' assert { type: 'json' }; -import { psa } from '../../../core/discord/psa'; +import { UserError } from '../../../core/blitzkrieg/userError'; import embedNegative from '../../../core/discord/embedNegative'; import embedWarning from '../../../core/discord/embedWarning'; import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable'; +import { psa } from '../../../core/discord/psa'; export default async function handleChatInputCommand( interaction: ChatInputCommandInteraction, @@ -53,7 +54,9 @@ export default async function handleChatInputCommand( .setStyle(ButtonStyle.Link), ); - console.error(interaction.commandName, error); + if (!(error instanceof UserError)) { + console.error(interaction.commandName, error); + } await interaction.editReply({ embeds: [