import type { AxiosInstance, AxiosRequestConfig } from 'axios'; import axios from 'axios'; import rateLimit from 'axios-rate-limit'; import type NodeCache from 'node-cache'; // 5 minute default TTL (in seconds) const DEFAULT_TTL = 300; // 10 seconds default rolling buffer (in ms) const DEFAULT_ROLLING_BUFFER = 10000; interface ExternalAPIOptions { nodeCache?: NodeCache; headers?: Record; rateLimit?: { maxRPS: number; maxRequests: number; }; } class ExternalAPI { protected axios: AxiosInstance; private baseUrl: string; private cache?: NodeCache; constructor( baseUrl: string, params: Record, options: ExternalAPIOptions = {} ) { this.axios = axios.create({ baseURL: baseUrl, params, headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...options.headers, }, }); if (options.rateLimit) { this.axios = rateLimit(this.axios, { maxRequests: options.rateLimit.maxRequests, maxRPS: options.rateLimit.maxRPS, }); } this.baseUrl = baseUrl; this.cache = options.nodeCache; } protected async get( endpoint: string, config?: AxiosRequestConfig, ttl?: number ): Promise { const cacheKey = this.serializeCacheKey(endpoint, config?.params); const cachedItem = this.cache?.get(cacheKey); if (cachedItem) { return cachedItem; } const response = await this.axios.get(endpoint, config); if (this.cache) { this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); } return response.data; } protected async post( endpoint: string, data: Record, config?: AxiosRequestConfig, ttl?: number ): Promise { const cacheKey = this.serializeCacheKey(endpoint, { config: config?.params, data, }); const cachedItem = this.cache?.get(cacheKey); if (cachedItem) { return cachedItem; } const response = await this.axios.post(endpoint, data, config); if (this.cache) { this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); } return response.data; } protected async getRolling( endpoint: string, config?: AxiosRequestConfig, ttl?: number ): Promise { const cacheKey = this.serializeCacheKey(endpoint, config?.params); const cachedItem = this.cache?.get(cacheKey); if (cachedItem) { const keyTtl = this.cache?.getTtl(cacheKey) ?? 0; // If the item has passed our rolling check, fetch again in background if ( keyTtl - (ttl ?? DEFAULT_TTL) * 1000 < Date.now() - DEFAULT_ROLLING_BUFFER ) { this.axios.get(endpoint, config).then((response) => { this.cache?.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); }); } return cachedItem; } const response = await this.axios.get(endpoint, config); if (this.cache) { this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); } return response.data; } private serializeCacheKey( endpoint: string, params?: Record ) { if (!params) { return `${this.baseUrl}${endpoint}`; } return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`; } } export default ExternalAPI;