Skip to content

Commit

Permalink
Support API request retries
Browse files Browse the repository at this point in the history
  • Loading branch information
mdingena committed Apr 8, 2024
1 parent 04d8a3d commit 3a01334
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 13 deletions.
27 changes: 27 additions & 0 deletions docs/Config.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# `Config`

- [`Config.apiRequestAttempts`](#configapirequestattempts)
- [`Config.apiRequestRetryDelay`](#configapirequestretrydelay)
- [`Config.apiRequestTimeout`](#configapirequesttimeout)
- [`Config.clientId`](#configclientid)
- [`Config.clientSecret`](#configclientsecret)
- [`Config.console`](#configconsole)
Expand Down Expand Up @@ -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<Console, 'error' | 'warn' | 'info' | 'debug'>;
excludedGroups?: number[];
includedGroups?: number[];
Expand Down Expand Up @@ -72,6 +78,27 @@ interface UserConfig extends CommonConfig {
type Config = BotConfig | UserConfig;
```

## `Config.apiRequestAttempts`

- `<number>` 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`

- `<number>` Time in milliseconds.
- Defaults to 3 seconds.

This option configures the delay before a failed API request is retried.

## `Config.apiRequestTimeout`

- `<number>` Time in milliseconds.
- Defaults to 5 seconds.

This option configures how API requests are allowed to take.

## `Config.clientId`

- `<string>` A bot account's client ID provided to you by Alta.
Expand Down
52 changes: 39 additions & 13 deletions src/Api/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ export class Api {
endpoint: TEndpoint,
params?: Partial<Parameters>,
query?: Parameters,
payload?: ApiRequest
payload?: ApiRequest,
attemptsLeft = this.client.config.apiRequestAttempts
): Promise<ApiResponse<TMethod, TEndpoint>> {
if (typeof this.headers === 'undefined') {
this.client.logger.error(`[API] Not authorised. Ordering authorisation now.`);
Expand All @@ -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<TMethod, TEndpoint> = 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<TMethod, TEndpoint>;

try {
response = await Promise.race<[Promise<ApiResponse<TMethod, TEndpoint>>, Promise<never>]>([
fetch(url.toString(), {
method,
headers: this.headers,
body: typeof payload === 'undefined' ? null : JSON.stringify(payload)
}),
new Promise<never>((_, 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;
Expand Down
3 changes: 3 additions & 0 deletions src/Client/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ export class Client extends TypedEmitter<Events> {

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)
Expand Down
3 changes: 3 additions & 0 deletions src/Client/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export type Scope =
| 'ws.group_servers';

interface CommonConfig {
apiRequestAttempts?: number;
apiRequestRetryDelay?: number;
apiRequestTimeout?: number;
console?: Pick<Console, 'error' | 'warn' | 'info' | 'debug'>;
excludedGroups?: number[];
includedGroups?: number[];
Expand Down
9 changes: 9 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -49,6 +55,9 @@ const WEBSOCKET_URL = 'wss://websocket.townshiptale.com';
const X_API_KEY = '2l6aQGoNes8EHb94qMhqQ5m2iaiOM9666oDTPORf';

export const DEFAULTS: Required<Omit<Config, 'clientId' | 'clientSecret' | 'scope' | 'username' | 'password'>> = {
apiRequestAttempts: API_REQUEST_ATTEMPTS,
apiRequestRetryDelay: API_REQUEST_RETRY_DELAY,
apiRequestTimeout: API_REQUEST_TIMEOUT,
console: console,
excludedGroups: [],
includedGroups: [],
Expand Down

0 comments on commit 3a01334

Please sign in to comment.