UNPKG

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
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(); } }