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

104 lines (103 loc) 3.58 kB
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) }; } }