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
104 lines (103 loc) • 3.58 kB
JavaScript
import { existsSync, readFileSync, writeFileSync } from 'fs';
/**
* Default implementation of StorageHook using a local JSON file.
* Bad error handling, if any error occurs on save its logged but ignored, if any error
* occurs on load 0 is returned and the error logged,
*
* This class is currently responsible for monthly reset?? TODO
*/
class FileStorage {
path;
constructor(path) {
this.path = path;
}
save(count) {
const data = { month: new Date().toISOString().slice(0, 7), count, lastUpdate: new Date().toISOString() };
writeFileSync(this.path, JSON.stringify(data, null, 2), 'utf-8');
}
load() {
if (existsSync(this.path)) {
try {
const file = JSON.parse(readFileSync(this.path, 'utf-8'));
if (file.month === new Date().toISOString().slice(0, 7)) {
return file.count;
}
}
catch (e) {
console.error("Oops! Error when trying to load API usage number");
console.error(e);
return 0;
}
}
return 0;
}
}
/**
* @see https://help.smartsuite.com/en/articles/4856710-api-limits#h_bcb27ff03a
*/
const PLAN_LIMITS = {
Free: { fullSpeedLimit: 100, monthlyLimit: 100 },
Team: { fullSpeedLimit: 5000, monthlyLimit: 6250 },
Professional: { fullSpeedLimit: 50000, monthlyLimit: 62500 },
Enterprise: { fullSpeedLimit: 250000, monthlyLimit: 312500 },
};
export class RateLimiter {
fullSpeedLimit;
monthlyLimit;
useApiAddOns;
logger;
storageHook;
requestTimestamps = [];
requestCount;
constructor(options) {
this.fullSpeedLimit = PLAN_LIMITS[options.plan].fullSpeedLimit;
this.monthlyLimit = PLAN_LIMITS[options.plan].monthlyLimit;
this.useApiAddOns = options.enableApiAddOns ?? false;
this.logger = options.logger;
this.storageHook = options.storageHook ?? new FileStorage("api-usage.json");
this.requestCount = this.storageHook.load();
}
log(msg) {
if (this.logger)
this.logger(`[RateLimiter] ${msg}`);
}
saveCount() {
this.storageHook.save(this.requestCount);
}
async throttle() {
if (!this.checkQuota()) {
throw new Error('Monthly quota exceeded. Aborting request.');
}
const now = Date.now();
this.requestTimestamps = this.requestTimestamps.filter(ts => now - ts < 1000);
const fullSpeedAvailable = this.fullSpeedLimit - this.requestCount > 0;
// throttle at either 5/sec or 2/sec depending on plan/used requests
while (this.requestTimestamps.length >= (fullSpeedAvailable ? 5 : 2)) {
await new Promise(res => setTimeout(res, 200));
this.requestTimestamps = this.requestTimestamps.filter(ts => Date.now() - ts < 1000);
}
}
trackRequest() {
this.requestTimestamps.push(Date.now());
this.requestCount++;
this.saveCount();
}
undoRequest() {
this.requestCount--;
this.saveCount();
}
checkQuota() {
if (this.useApiAddOns)
return true;
if (this.requestCount < this.monthlyLimit)
return true;
this.log(`Quota exceeded: ${this.requestCount}/${this.monthlyLimit}`);
return false;
}
getUsage() {
return {
used: this.requestCount,
remaining: this.useApiAddOns ? '∞' : Math.max(this.monthlyLimit - this.requestCount, 0)
};
}
}