Fix type errors in /server related to typescript strict mode (#1468)

## Description:

Task: #1075 Enable typescript strict mode


First (out of many) PR in order to enable typescript Strict mode in the
project.

This PR fixes all type issues present under the `/server` path.

## Specifics

Most changes are just basic type fixes, however here are some further
explanation for some of the more complex changes:

1. `PatternDecoder.ts` used to accept `Uint8Array` as an input, however
this was never used, so to simplify the typing and avoid casting in
various places, I just removed support for it.
2. `MapPlaylist.ts` has a `frequency` object with map names, used to
specify how often each map should appear in the playlist. However this
list is not in sync with the actual map list, so some maps were missing
from that list. I fixed that while adding stronger typing for the
future. Also removed an un-necessary call to `parseInt`.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I have read and accepted the CLA aggreement (only required once).

## Please put your Discord username so you can be contacted if a bug or
regression is found:

azlod
This commit is contained in:
Antoine
2025-07-18 20:10:07 +02:00
committed by GitHub
parent b5c1ae1bbf
commit c7ca457c45
7 changed files with 68 additions and 30 deletions
+1 -4
View File
@@ -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) {
+2
View File
@@ -84,6 +84,8 @@ export enum GameMapType {
Italia = "Italia",
}
export type GameMapName = keyof typeof GameMapType;
export const mapCategories: Record<string, GameMapType[]> = {
continental: [
GameMapType.World,
+51 -17
View File
@@ -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<boolean> {
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;
+6 -3
View File
@@ -46,13 +46,14 @@ export class GameServer {
private _hasStarted = false;
private _startTime: number | null = null;
private endTurnIntervalID;
private endTurnIntervalID: ReturnType<typeof setInterval> | 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) {
+1 -1
View File
@@ -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<string, string> = {};
if (config.otelUsername() && config.otelPassword()) {
headers["Authorization"] =
"Basic " +
+6 -4
View File
@@ -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<Record<GameMapName, number>> = {
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]);
}
});
+1 -1
View File
@@ -18,7 +18,7 @@ export function initWorkerMetrics(gameManager: GameManager): void {
const resource = getOtelResource();
// Configure auth headers
const headers = {};
const headers: Record<string, string> = {};
if (config.otelEnabled()) {
headers["Authorization"] =
"Basic " +