|
|
|
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<string, unknown>;
|
|
|
|
rateLimit?: {
|
|
|
|
maxRPS: number;
|
|
|
|
maxRequests: number;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
class ExternalAPI {
|
|
|
|
protected axios: AxiosInstance;
|
|
|
|
private baseUrl: string;
|
|
|
|
private cache?: NodeCache;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
baseUrl: string,
|
|
|
|
params: Record<string, unknown>,
|
|
|
|
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<T>(
|
|
|
|
endpoint: string,
|
|
|
|
config?: AxiosRequestConfig,
|
|
|
|
ttl?: number
|
|
|
|
): Promise<T> {
|
|
|
|
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
|
|
|
const cachedItem = this.cache?.get<T>(cacheKey);
|
|
|
|
if (cachedItem) {
|
|
|
|
return cachedItem;
|
|
|
|
}
|
|
|
|
|
|
|
|
const response = await this.axios.get<T>(endpoint, config);
|
|
|
|
|
|
|
|
if (this.cache) {
|
|
|
|
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
|
|
|
}
|
|
|
|
|
|
|
|
return response.data;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async post<T>(
|
|
|
|
endpoint: string,
|
|
|
|
data: Record<string, unknown>,
|
|
|
|
config?: AxiosRequestConfig,
|
|
|
|
ttl?: number
|
|
|
|
): Promise<T> {
|
|
|
|
const cacheKey = this.serializeCacheKey(endpoint, {
|
|
|
|
config: config?.params,
|
|
|
|
data,
|
|
|
|
});
|
|
|
|
const cachedItem = this.cache?.get<T>(cacheKey);
|
|
|
|
if (cachedItem) {
|
|
|
|
return cachedItem;
|
|
|
|
}
|
|
|
|
|
|
|
|
const response = await this.axios.post<T>(endpoint, data, config);
|
|
|
|
|
|
|
|
if (this.cache) {
|
|
|
|
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
|
|
|
}
|
|
|
|
|
|
|
|
return response.data;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async getRolling<T>(
|
|
|
|
endpoint: string,
|
|
|
|
config?: AxiosRequestConfig,
|
|
|
|
ttl?: number
|
|
|
|
): Promise<T> {
|
|
|
|
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
|
|
|
const cachedItem = this.cache?.get<T>(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<T>(endpoint, config).then((response) => {
|
|
|
|
this.cache?.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return cachedItem;
|
|
|
|
}
|
|
|
|
|
|
|
|
const response = await this.axios.get<T>(endpoint, config);
|
|
|
|
|
|
|
|
if (this.cache) {
|
|
|
|
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
|
|
|
}
|
|
|
|
|
|
|
|
return response.data;
|
|
|
|
}
|
|
|
|
|
|
|
|
private serializeCacheKey(
|
|
|
|
endpoint: string,
|
|
|
|
params?: Record<string, unknown>
|
|
|
|
) {
|
|
|
|
if (!params) {
|
|
|
|
return `${this.baseUrl}${endpoint}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default ExternalAPI;
|