smartsuite-typescript-api
Version:
Typescript type generator and wrapper for the REST API provided by SmartSuite. Currently in pre 1.0 so no semver guarantees are given
109 lines (108 loc) • 4.61 kB
JavaScript
import { RateLimiter } from './ratelimit.js';
/**
* A simple class for handling all https logic when querying the API.
*
* Regarding {@link https://help.smartsuite.com/en/articles/4856710-api-limits API limits}:
*
* - Optionally, if {@link RateLimiterOptions} are provided in the options the httpClient will try to
* limit itself to prevent spamming over the requests per second limit as described in the SmartSuite
* {@link https://help.smartsuite.com/en/articles/4856710-api-limits documentation}
* - If a 429 is replied by the API, the httpClient will retry 3 times staggered 30 sec each retry, if all
* retries fail the httpClient will throw an Error, all future attempts will also throw Errors until month change
*/
export class HttpsClient {
baseUrl = "https://app.smartsuite.com/api/v1/";
headers;
logger;
rateLimiter;
// If this is non-null, it identifies the month the quota was reached
monthQuotaReached = null;
constructor(options) {
this.logger = options.logger;
this.headers = {
'Authorization': `Token ${options.apiKey}`,
'Account-ID': `${options.workspaceId}`,
'Content-Type': 'application/json'
};
if (options.rateLimitOptions) {
this.rateLimiter = new RateLimiter(options.rateLimitOptions);
}
}
async request(method, route, body) {
if (this.monthQuotaReached) {
if (new Date().getMonth() !== this.monthQuotaReached) {
// Wow! A month change, time to go ham again...
this.monthQuotaReached = null;
}
else {
if (this.logger) {
this.logger("Request aborted to avoid ban, amount of consecutive 429s received from the API hints to request limit for this month being reached");
}
//return null
throw new Error();
}
}
if (this.rateLimiter) {
await this.rateLimiter.throttle();
}
const url = `${this.baseUrl}${route}`;
const options = {
method,
headers: this.headers,
body: JSON.stringify(body),
};
if (this.rateLimiter) {
// I don't think requests resulting in a 429 will count towards the total count,
// so we can safely count a single time here
this.rateLimiter.trackRequest();
}
let retries = 3;
while (retries > 0) {
const start = performance.now();
const response = await fetch(url, options);
const end = performance.now();
if (this.logger) {
this.logger(`[HTTPS ${method}] ${url} -> ${response.status} in ${end - start}ms`);
}
if (response.status === 429 || (response.status <= 500 && response.status > 600)) {
if (--retries > 0) {
const delay = 1000 * 30; // TODO maybe randomize a little to spread out?
if (this.logger) {
this.logger(`${response.status} received with error message: \n${await response.text()}`);
this.logger(`Retrying in ${delay.toFixed(0)}ms...`);
}
await new Promise((res) => setTimeout(res, delay));
continue;
}
if (response.status === 429) {
this.monthQuotaReached = new Date().getMonth();
if (this.logger) {
this.logger('Rate limit seemingly exceeded. Retries exhausted... :(');
}
//return null
throw new Error();
}
else {
console.error(await response.text());
throw new Error("Responding has repeatedly thrown a 5xx error, see logs for details");
}
}
if (!response.ok) {
const errorText = await response.text();
const errorMessage = `HTTPS error ${response.status}: ${errorText}`;
if (this.logger) {
this.logger(errorMessage);
}
//return null
throw new Error(errorMessage, { cause: response.status });
}
const data = await response.json();
return data;
}
if (this.logger) {
this.logger('Unexpected error in HTTPS request logic.');
}
//return null
throw new Error();
}
}