diff --git a/src/core/PatternDecoder.ts b/src/core/PatternDecoder.ts index 1311aec44..c749b1615 100644 --- a/src/core/PatternDecoder.ts +++ b/src/core/PatternDecoder.ts @@ -5,10 +5,7 @@ export class PatternDecoder { readonly width: number; readonly scale: number; - constructor( - base64: string, - base64urlDecode: (input: Uint8Array | string) => Uint8Array, - ) { + constructor(base64: string, base64urlDecode: (input: string) => Uint8Array) { this.bytes = base64urlDecode(base64); if (this.bytes.length < 3) { diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 5c50d07c3..88babd8a2 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -84,6 +84,8 @@ export enum GameMapType { Italia = "Italia", } +export type GameMapName = keyof typeof GameMapType; + export const mapCategories: Record = { continental: [ GameMapType.World, diff --git a/src/server/Archive.ts b/src/server/Archive.ts index 9f5cfc6b5..6b3675d94 100644 --- a/src/server/Archive.ts +++ b/src/server/Archive.ts @@ -35,11 +35,20 @@ export async function archive(gameRecord: GameRecord) { ); await archiveFullGameToR2(gameRecord); } - } catch (error) { + } catch (error: unknown) { + // If the error is not an instance of Error, log it as a string + if (!(error instanceof Error)) { + log.error( + `${gameRecord.info.gameID}: Final archive error. Non-Error type: ${String(error)}`, + ); + return; + } + + const { message, stack, name } = error; log.error(`${gameRecord.info.gameID}: Final archive error: ${error}`, { - message: error?.message ?? error, - stack: error?.stack, - name: error?.name, + message: message, + stack: stack, + name: name, ...(error && typeof error === "object" ? error : {}), }); } @@ -68,11 +77,20 @@ async function archiveAnalyticsToR2(gameRecord: GameRecord) { }); log.info(`${info.gameID}: successfully wrote game analytics to R2`); - } catch (error) { + } catch (error: unknown) { + // If the error is not an instance of Error, log it as a string + if (!(error instanceof Error)) { + log.error( + `${gameRecord.info.gameID}: Error writing game analytics to R2. Non-Error type: ${String(error)}`, + ); + return; + } + + const { message, stack, name } = error; log.error(`${info.gameID}: Error writing game analytics to R2: ${error}`, { - message: error?.message ?? error, - stack: error?.stack, - name: error?.name, + message: message, + stack: stack, + name: name, ...(error && typeof error === "object" ? error : {}), }); throw error; @@ -116,12 +134,20 @@ export async function readGameRecord( if (response.Body === undefined) return null; const bodyContents = await response.Body.transformToString(); return JSON.parse(bodyContents) as GameRecord; - } catch (error) { + } catch (error: unknown) { + // If the error is not an instance of Error, log it as a string + if (!(error instanceof Error)) { + log.error( + `${gameId}: Error reading game record from R2. Non-Error type: ${String(error)}`, + ); + return null; + } + const { message, stack, name } = error; // Log the error for monitoring purposes log.error(`${gameId}: Error reading game record from R2: ${error}`, { - message: error?.message ?? error, - stack: error?.stack, - name: error?.name, + message: message, + stack: stack, + name: name, ...(error && typeof error === "object" ? error : {}), }); @@ -137,14 +163,22 @@ export async function gameRecordExists(gameId: GameID): Promise { Key: `${gameFolder}/${gameId}`, // Fixed - needed to include gameFolder }); return true; - } catch (error) { - if (error.name === "NotFound") { + } catch (error: unknown) { + // If the error is not an instance of Error, log it as a string + if (!(error instanceof Error)) { + log.error( + `${gameId}: Error checking archive existence. Non-Error type: ${String(error)}`, + ); + return false; + } + const { message, stack, name } = error; + if (name === "NotFound") { return false; } log.error(`${gameId}: Error checking archive existence: ${error}`, { - message: error?.message ?? error, - stack: error?.stack, - name: error?.name, + message: message, + stack: stack, + name: name, ...(error && typeof error === "object" ? error : {}), }); return false; diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 76e7615b0..1f4956b19 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -46,13 +46,14 @@ export class GameServer { private _hasStarted = false; private _startTime: number | null = null; - private endTurnIntervalID; + private endTurnIntervalID: ReturnType | undefined; private lastPingUpdate = 0; private winner: ClientSendWinnerMessage | null = null; - private gameStartInfo: GameStartInfo; + // Note: This can be undefined if accessed before the game starts. + private gameStartInfo!: GameStartInfo; private log: Logger; @@ -402,7 +403,9 @@ export class GameServer { async end() { // Close all WebSocket connections - clearInterval(this.endTurnIntervalID); + if (this.endTurnIntervalID) { + clearInterval(this.endTurnIntervalID); + } this.allClients.forEach((client) => { client.ws.removeAllListeners("message"); if (client.ws.readyState === WebSocket.OPEN) { diff --git a/src/server/Logger.ts b/src/server/Logger.ts index 94e508fbb..16c3496b2 100644 --- a/src/server/Logger.ts +++ b/src/server/Logger.ts @@ -24,7 +24,7 @@ const loggerProvider = new LoggerProvider({ if (config.env() === GameEnv.Prod && config.otelEnabled()) { console.log("OTEL enabled"); // Configure OpenTelemetry endpoint with basic auth (if provided) - const headers = {}; + const headers: Record = {}; if (config.otelUsername() && config.otelPassword()) { headers["Authorization"] = "Basic " + diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index 49c832f9c..66ab8cacb 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -2,6 +2,7 @@ import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; import { Difficulty, Duos, + GameMapName, GameMapType, GameMode, GameType, @@ -17,7 +18,9 @@ const log = logger.child({}); const config = getServerConfigFromServer(); -const frequency = { +// How many times each map should appear in the playlist. +// Note: The Partial should eventually be removed for better type safety. +const frequency: Partial> = { World: 3, Europe: 2, Africa: 2, @@ -27,7 +30,6 @@ const frequency = { GatewayToTheAtlantic: 1, Iceland: 1, SouthAmerica: 1, - KnownWorld: 1, DeglaciatedAntarctica: 1, EuropeClassic: 1, Mena: 1, @@ -109,8 +111,8 @@ export class MapPlaylist { private shuffleMapsPlaylist(): boolean { const maps: GameMapType[] = []; - Object.keys(GameMapType).forEach((key) => { - for (let i = 0; i < parseInt(frequency[key]); i++) { + (Object.keys(GameMapType) as GameMapName[]).forEach((key) => { + for (let i = 0; i < (frequency[key] ?? 0); i++) { maps.push(GameMapType[key]); } }); diff --git a/src/server/WorkerMetrics.ts b/src/server/WorkerMetrics.ts index 1576b0f73..9c2071bb9 100644 --- a/src/server/WorkerMetrics.ts +++ b/src/server/WorkerMetrics.ts @@ -18,7 +18,7 @@ export function initWorkerMetrics(gameManager: GameManager): void { const resource = getOtelResource(); // Configure auth headers - const headers = {}; + const headers: Record = {}; if (config.otelEnabled()) { headers["Authorization"] = "Basic " +