diff --git a/docs/Config.md b/docs/Config.md index d31b477..525f663 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -1,5 +1,8 @@ # `Config` +- [`Config.apiRequestAttempts`](#configapirequestattempts) +- [`Config.apiRequestRetryDelay`](#configapirequestretrydelay) +- [`Config.apiRequestTimeout`](#configapirequesttimeout) - [`Config.clientId`](#configclientid) - [`Config.clientSecret`](#configclientsecret) - [`Config.console`](#configconsole) @@ -33,6 +36,9 @@ The `Config` object is used to configure a [`Client`](./Client.md). ```ts interface CommonConfig { + apiRequestAttempts?: number; + apiRequestRetryDelay?: number; + apiRequestTimeout?: number; console?: Pick; excludedGroups?: number[]; includedGroups?: number[]; @@ -72,6 +78,27 @@ interface UserConfig extends CommonConfig { type Config = BotConfig | UserConfig; ``` +## `Config.apiRequestAttempts` + +- `` Amount of request attempts. +- Defaults to 3 attempts. + +This option configures the number of times [`Client`](./Client.md) will attempt to send an API request. + +## `Config.apiRequestRetryDelay` + +- `` Time in milliseconds. +- Defaults to 3 seconds. + +This option configures the delay before a failed API request is retried. + +## `Config.apiRequestTimeout` + +- `` Time in milliseconds. +- Defaults to 5 seconds. + +This option configures how API requests are allowed to take. + ## `Config.clientId` - `` A bot account's client ID provided to you by Alta. diff --git a/src/Api/Api.ts b/src/Api/Api.ts index 96029b1..be14402 100644 --- a/src/Api/Api.ts +++ b/src/Api/Api.ts @@ -140,7 +140,8 @@ export class Api { endpoint: TEndpoint, params?: Partial, query?: Parameters, - payload?: ApiRequest + payload?: ApiRequest, + attemptsLeft = this.client.config.apiRequestAttempts ): Promise> { if (typeof this.headers === 'undefined') { this.client.logger.error(`[API] Not authorised. Ordering authorisation now.`); @@ -150,18 +151,43 @@ export class Api { const url = this.createUrl(endpoint, params, query); - this.client.logger.debug(`Requesting ${method} ${url}`, JSON.stringify(payload)); - - const response: ApiResponse = await fetch(url.toString(), { - method, - headers: this.headers, - body: typeof payload === 'undefined' ? null : JSON.stringify(payload) - }); - - if (!response.ok) { - this.client.logger.error(`${method} ${response.url} responded with ${response.status} ${response.statusText}.`); - const body = await response.json(); - throw new Error('message' in body ? body.message : JSON.stringify(body)); + this.client.logger.debug(`[API] ${method} ${url}`, JSON.stringify(payload)); + + let response: ApiResponse; + + try { + response = await Promise.race<[Promise>, Promise]>([ + fetch(url.toString(), { + method, + headers: this.headers, + body: typeof payload === 'undefined' ? null : JSON.stringify(payload) + }), + new Promise((_, reject) => + setTimeout(() => { + reject(new Error(`${method} ${url} request timed out.`)); + }, this.client.config.apiRequestTimeout) + ) + ]); + + if (!response.ok) { + this.client.logger.error( + `[API] ${method} ${response.url} responded with ${response.status} ${response.statusText}.` + ); + const body = await response.json(); + throw new Error('message' in body ? body.message : JSON.stringify(body)); + } + } catch (error) { + this.client.logger.error(`[API] ${method} ${url} error: ${(error as Error).message}`); + + if (attemptsLeft > 0) { + this.client.logger.debug(`[API] ${method} ${url} retrying in ${this.client.config.apiRequestRetryDelay} ms.`); + + await new Promise(resolve => setTimeout(resolve, this.client.config.apiRequestRetryDelay)); + + return await this.request(method, endpoint, params, query, payload, attemptsLeft - 1); + } else { + throw new Error(`[API] ${method} ${url} exhausted request attempts.`); + } } return response; diff --git a/src/Client/Client.ts b/src/Client/Client.ts index e1778fb..65bbd68 100644 --- a/src/Client/Client.ts +++ b/src/Client/Client.ts @@ -125,6 +125,9 @@ export class Client extends TypedEmitter { this.config = { ...credentials, + apiRequestAttempts: config.apiRequestAttempts ?? DEFAULTS.apiRequestAttempts, + apiRequestRetryDelay: config.apiRequestRetryDelay ?? DEFAULTS.apiRequestRetryDelay, + apiRequestTimeout: config.apiRequestTimeout ?? DEFAULTS.apiRequestTimeout, console: configuredConsole, excludedGroups: config.excludedGroups && (typeof config.includedGroups === 'undefined' || config.includedGroups.length === 0) diff --git a/src/Client/Config.ts b/src/Client/Config.ts index a750b7d..7422c38 100644 --- a/src/Client/Config.ts +++ b/src/Client/Config.ts @@ -17,6 +17,9 @@ export type Scope = | 'ws.group_servers'; interface CommonConfig { + apiRequestAttempts?: number; + apiRequestRetryDelay?: number; + apiRequestTimeout?: number; console?: Pick; excludedGroups?: number[]; includedGroups?: number[]; diff --git a/src/constants.ts b/src/constants.ts index 9fdf090..8f88d32 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -10,6 +10,12 @@ export const AGENT = { const SECOND = 1000; const MINUTE = 60 * SECOND; +const API_REQUEST_ATTEMPTS = 3; + +const API_REQUEST_RETRY_DELAY = 3 * SECOND; + +const API_REQUEST_TIMEOUT = 5 * SECOND; + const MAX_MISSED_SERVER_HEARTBEATS = 3; const MAX_SUBSCRIPTIONS_PER_WEBSOCKET = 500; @@ -49,6 +55,9 @@ const WEBSOCKET_URL = 'wss://websocket.townshiptale.com'; const X_API_KEY = '2l6aQGoNes8EHb94qMhqQ5m2iaiOM9666oDTPORf'; export const DEFAULTS: Required> = { + apiRequestAttempts: API_REQUEST_ATTEMPTS, + apiRequestRetryDelay: API_REQUEST_RETRY_DELAY, + apiRequestTimeout: API_REQUEST_TIMEOUT, console: console, excludedGroups: [], includedGroups: [],