DramaHarvester/src/rdrama/session/SessionManager.ts

114 lines
4.3 KiB
TypeScript

import axios, { AxiosInstance, AxiosError, AxiosResponse, AxiosRequestConfig } from 'axios';
import axiosRetry from 'axios-retry';
import Bottleneck from 'bottleneck';
import dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
class SessionManager {
private static instance: SessionManager;
public readonly axiosInstance: AxiosInstance;
private limiter: Bottleneck;
private constructor() {
// Initialize the Bottleneck limiter
this.limiter = new Bottleneck({
id: "rDramaAPI-limiter",
datastore: "ioredis",
clearDatastore: false,
clientOptions: {
host: process.env.REDIS_HOST!,
port: Number(process.env.REDIS_PORT),
password: process.env.REDIS_PASSWORD || undefined, // Use undefined if no password is set
enableOfflineQueue: true
},
maxConcurrent: 1, // Maximum number of concurrent requests
minTime: 1000, // Minimum time between dispatches of requests in milliseconds
reservoir: 41, // Initial reservoir value for the first hour
reservoirRefreshAmount: 41, // Reservoir value to reset to every hour
reservoirRefreshInterval: 60 * 60 * 1000, // Interval to reset the reservoir (1 hour in milliseconds)
highWater: 5, // Maximum number of queued jobs.
strategy: Bottleneck.strategy.OVERFLOW_PRIORITY // Strategy to drop the oldest jobs in the queue when highWater is reached
});
this.axiosInstance = axios.create({
baseURL: 'https://rdrama.net/',
headers: {
'Content-Type': 'application/json',
},
});
// Wrap axios requests with the limiter
this.wrapAxiosInstance(this.axiosInstance);
axiosRetry(this.axiosInstance, {
retries: 10,
retryDelay: this.retryDelayStrategy,
retryCondition: this.retryCondition,
});
}
public static getInstance(): SessionManager {
if (!SessionManager.instance) {
SessionManager.instance = new SessionManager();
}
return SessionManager.instance;
}
public async shutdown(): Promise<void> {
await this.limiter.disconnect();
}
private retryDelayStrategy(retryCount: number, error: AxiosError): number {
const retryAfter = error.response?.headers['retry-after'];
if (retryAfter) {
console.log(`429 Retry After: ${retryAfter}`);
return +retryAfter * 1000;
}
return Math.pow(2, retryCount) * 2000;
}
private retryCondition(error: AxiosError): boolean {
const status = error.response?.status ?? 0;
return status === 429 || status >= 500;
}
private wrapAxiosInstance(instance: AxiosInstance): void {
// Wrap the get method
const originalGet = instance.get;
instance.get = <T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R> => {
return this.limiter.schedule(() => originalGet.apply(instance, [url, config])) as Promise<R>;
};
// Wrap the post method
const originalPost = instance.post;
instance.post = <T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R> => {
return this.limiter.schedule({ priority: 1 }, () => originalPost.apply(instance, [url, data, config])) as Promise<R>;
};
// Wrap the put method
const originalPut = instance.put;
instance.put = <T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R> => {
return this.limiter.schedule({ priority: 1 }, () => originalPut.apply(instance, [url, data, config])) as Promise<R>;
};
// Wrap the delete method
const originalDelete = instance.delete;
instance.delete = <T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R> => {
return this.limiter.schedule(() => originalDelete.apply(instance, [url, config])) as Promise<R>;
};
}
public setAuthorizationToken(token: string): void {
this.axiosInstance.defaults.headers.common['Authorization'] = `${token}`;
}
}
export default SessionManager;