You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
455 lines
12 KiB
455 lines
12 KiB
/**
|
|
* TimeSafari Community Features Integration
|
|
*
|
|
* Implements community features integration with rate limiting, backoff policies,
|
|
* and polling optimization for TimeSafari's community-driven platform.
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 1.0.0
|
|
*/
|
|
|
|
import {
|
|
TimeSafariIntegrationService,
|
|
TimeSafariUserConfig,
|
|
TimeSafariNotificationBundle
|
|
} from './timesafari-integration';
|
|
|
|
/**
|
|
* TimeSafari Community Integration Service
|
|
*
|
|
* Handles community features integration with intelligent rate limiting,
|
|
* backoff policies, and polling optimization.
|
|
*/
|
|
export class TimeSafariCommunityIntegrationService {
|
|
private static instance: TimeSafariCommunityIntegrationService;
|
|
private integrationService: TimeSafariIntegrationService;
|
|
private rateLimiter: RateLimiter;
|
|
private backoffManager: BackoffManager;
|
|
private pollingManager: PollingManager;
|
|
|
|
private constructor() {
|
|
this.integrationService = TimeSafariIntegrationService.getInstance();
|
|
this.rateLimiter = new RateLimiter();
|
|
this.backoffManager = new BackoffManager();
|
|
this.pollingManager = new PollingManager();
|
|
}
|
|
|
|
/**
|
|
* Get singleton instance
|
|
*/
|
|
static getInstance(): TimeSafariCommunityIntegrationService {
|
|
if (!TimeSafariCommunityIntegrationService.instance) {
|
|
TimeSafariCommunityIntegrationService.instance = new TimeSafariCommunityIntegrationService();
|
|
}
|
|
return TimeSafariCommunityIntegrationService.instance;
|
|
}
|
|
|
|
/**
|
|
* Initialize community integration
|
|
*
|
|
* @param config - Community integration configuration
|
|
*/
|
|
async initialize(config: CommunityIntegrationConfig): Promise<void> {
|
|
try {
|
|
// Initializing TimeSafari community integration
|
|
|
|
// Initialize rate limiter
|
|
this.rateLimiter.configure({
|
|
maxRequestsPerMinute: config.maxRequestsPerMinute || 30,
|
|
maxRequestsPerHour: config.maxRequestsPerHour || 1000,
|
|
burstLimit: config.burstLimit || 10
|
|
});
|
|
|
|
// Initialize backoff manager
|
|
this.backoffManager.configure({
|
|
initialDelayMs: config.initialBackoffMs || 1000,
|
|
maxDelayMs: config.maxBackoffMs || 30000,
|
|
backoffMultiplier: config.backoffMultiplier || 2,
|
|
jitterEnabled: config.jitterEnabled !== false
|
|
});
|
|
|
|
// Initialize polling manager
|
|
this.pollingManager.configure({
|
|
baseIntervalMs: config.basePollingIntervalMs || 300000, // 5 minutes
|
|
maxIntervalMs: config.maxPollingIntervalMs || 1800000, // 30 minutes
|
|
adaptivePolling: config.adaptivePolling !== false
|
|
});
|
|
|
|
// TimeSafari community integration initialization complete
|
|
} catch (error) {
|
|
throw new Error(`TimeSafari community integration initialization failed: ${error}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch community data with rate limiting and backoff
|
|
*
|
|
* @param config - User configuration
|
|
* @returns Community data bundle
|
|
*/
|
|
async fetchCommunityDataWithRateLimit(config: TimeSafariUserConfig): Promise<TimeSafariNotificationBundle> {
|
|
try {
|
|
// Fetching community data with rate limiting
|
|
|
|
// Check rate limits
|
|
if (!this.rateLimiter.canMakeRequest()) {
|
|
const waitTime = this.rateLimiter.getWaitTime();
|
|
// Rate limit exceeded, waiting
|
|
await this.sleep(waitTime);
|
|
}
|
|
|
|
// Check backoff status
|
|
if (this.backoffManager.isInBackoff()) {
|
|
const backoffTime = this.backoffManager.getRemainingBackoffTime();
|
|
// In backoff, waiting
|
|
await this.sleep(backoffTime);
|
|
}
|
|
|
|
// Record request attempt
|
|
this.rateLimiter.recordRequest();
|
|
|
|
try {
|
|
// Fetch data
|
|
const bundle = await this.integrationService.fetchCommunityData(config);
|
|
|
|
// Reset backoff on success
|
|
this.backoffManager.recordSuccess();
|
|
|
|
// Update polling interval based on success
|
|
this.pollingManager.recordSuccess();
|
|
|
|
// Community data fetch successful
|
|
return bundle;
|
|
|
|
} catch (error) {
|
|
// Record failure and apply backoff
|
|
this.backoffManager.recordFailure();
|
|
this.pollingManager.recordFailure();
|
|
|
|
throw new Error(`Community data fetch failed: ${error}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
throw new Error(`Rate-limited fetch failed: ${error}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get optimized polling interval
|
|
*
|
|
* @returns Polling interval in milliseconds
|
|
*/
|
|
getOptimizedPollingInterval(): number {
|
|
return this.pollingManager.getCurrentInterval();
|
|
}
|
|
|
|
/**
|
|
* Get rate limit status
|
|
*
|
|
* @returns Rate limit status
|
|
*/
|
|
getRateLimitStatus(): RateLimitStatus {
|
|
return this.rateLimiter.getStatus();
|
|
}
|
|
|
|
/**
|
|
* Get backoff status
|
|
*
|
|
* @returns Backoff status
|
|
*/
|
|
getBackoffStatus(): BackoffStatus {
|
|
return this.backoffManager.getStatus();
|
|
}
|
|
|
|
/**
|
|
* Get polling status
|
|
*
|
|
* @returns Polling status
|
|
*/
|
|
getPollingStatus(): PollingStatus {
|
|
return this.pollingManager.getStatus();
|
|
}
|
|
|
|
/**
|
|
* Sleep utility
|
|
*
|
|
* @param ms - Milliseconds to sleep
|
|
*/
|
|
private sleep(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rate Limiter
|
|
*/
|
|
class RateLimiter {
|
|
private config: RateLimitConfig = {
|
|
maxRequestsPerMinute: 30,
|
|
maxRequestsPerHour: 1000,
|
|
burstLimit: 10
|
|
};
|
|
private requests: number[] = [];
|
|
private burstRequests: number[] = [];
|
|
|
|
configure(config: RateLimitConfig): void {
|
|
this.config = { ...this.config, ...config };
|
|
}
|
|
|
|
canMakeRequest(): boolean {
|
|
const now = Date.now();
|
|
const oneMinuteAgo = now - 60000;
|
|
const oneHourAgo = now - 3600000;
|
|
const oneSecondAgo = now - 1000;
|
|
|
|
// Clean old requests
|
|
this.requests = this.requests.filter(time => time > oneHourAgo);
|
|
this.burstRequests = this.burstRequests.filter(time => time > oneSecondAgo);
|
|
|
|
// Check limits
|
|
const requestsLastMinute = this.requests.filter(time => time > oneMinuteAgo).length;
|
|
const requestsLastHour = this.requests.length;
|
|
const burstRequests = this.burstRequests.length;
|
|
|
|
return requestsLastMinute < this.config.maxRequestsPerMinute &&
|
|
requestsLastHour < this.config.maxRequestsPerHour &&
|
|
burstRequests < this.config.burstLimit;
|
|
}
|
|
|
|
recordRequest(): void {
|
|
const now = Date.now();
|
|
this.requests.push(now);
|
|
this.burstRequests.push(now);
|
|
}
|
|
|
|
getWaitTime(): number {
|
|
const now = Date.now();
|
|
const oneMinuteAgo = now - 60000;
|
|
const oneHourAgo = now - 3600000;
|
|
const oneSecondAgo = now - 1000;
|
|
|
|
// Find oldest request in each window
|
|
const requestsLastMinute = this.requests.filter(time => time > oneMinuteAgo);
|
|
const requestsLastHour = this.requests.filter(time => time > oneHourAgo);
|
|
const burstRequests = this.burstRequests.filter(time => time > oneSecondAgo);
|
|
|
|
let waitTime = 0;
|
|
|
|
// Check minute limit
|
|
if (requestsLastMinute.length >= this.config.maxRequestsPerMinute) {
|
|
const oldestInMinute = Math.min(...requestsLastMinute);
|
|
waitTime = Math.max(waitTime, oldestInMinute + 60000 - now);
|
|
}
|
|
|
|
// Check hour limit
|
|
if (requestsLastHour.length >= this.config.maxRequestsPerHour) {
|
|
const oldestInHour = Math.min(...requestsLastHour);
|
|
waitTime = Math.max(waitTime, oldestInHour + 3600000 - now);
|
|
}
|
|
|
|
// Check burst limit
|
|
if (burstRequests.length >= this.config.burstLimit) {
|
|
const oldestBurst = Math.min(...burstRequests);
|
|
waitTime = Math.max(waitTime, oldestBurst + 1000 - now);
|
|
}
|
|
|
|
return Math.max(0, waitTime);
|
|
}
|
|
|
|
getStatus(): RateLimitStatus {
|
|
const now = Date.now();
|
|
const oneMinuteAgo = now - 60000;
|
|
const oneHourAgo = now - 3600000;
|
|
const oneSecondAgo = now - 1000;
|
|
|
|
return {
|
|
requestsLastMinute: this.requests.filter(time => time > oneMinuteAgo).length,
|
|
requestsLastHour: this.requests.filter(time => time > oneHourAgo).length,
|
|
burstRequests: this.burstRequests.filter(time => time > oneSecondAgo).length,
|
|
canMakeRequest: this.canMakeRequest(),
|
|
waitTime: this.getWaitTime()
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Backoff Manager
|
|
*/
|
|
class BackoffManager {
|
|
private config: BackoffConfig = {
|
|
initialDelayMs: 1000,
|
|
maxDelayMs: 30000,
|
|
backoffMultiplier: 2,
|
|
jitterEnabled: true
|
|
};
|
|
private failureCount = 0;
|
|
private lastFailureTime = 0;
|
|
private currentDelay = 0;
|
|
|
|
configure(config: BackoffConfig): void {
|
|
this.config = { ...this.config, ...config };
|
|
}
|
|
|
|
recordSuccess(): void {
|
|
this.failureCount = 0;
|
|
this.currentDelay = 0;
|
|
}
|
|
|
|
recordFailure(): void {
|
|
this.failureCount++;
|
|
this.lastFailureTime = Date.now();
|
|
|
|
// Calculate exponential backoff
|
|
this.currentDelay = Math.min(
|
|
this.config.initialDelayMs * Math.pow(this.config.backoffMultiplier, this.failureCount - 1),
|
|
this.config.maxDelayMs
|
|
);
|
|
|
|
// Add jitter if enabled
|
|
if (this.config.jitterEnabled) {
|
|
const jitter = Math.random() * 0.1 * this.currentDelay;
|
|
this.currentDelay += jitter;
|
|
}
|
|
}
|
|
|
|
isInBackoff(): boolean {
|
|
if (this.failureCount === 0) return false;
|
|
|
|
const timeSinceLastFailure = Date.now() - this.lastFailureTime;
|
|
return timeSinceLastFailure < this.currentDelay;
|
|
}
|
|
|
|
getRemainingBackoffTime(): number {
|
|
if (!this.isInBackoff()) return 0;
|
|
|
|
const timeSinceLastFailure = Date.now() - this.lastFailureTime;
|
|
return this.currentDelay - timeSinceLastFailure;
|
|
}
|
|
|
|
getStatus(): BackoffStatus {
|
|
return {
|
|
failureCount: this.failureCount,
|
|
currentDelay: this.currentDelay,
|
|
isInBackoff: this.isInBackoff(),
|
|
remainingBackoffTime: this.getRemainingBackoffTime()
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Polling Manager
|
|
*/
|
|
class PollingManager {
|
|
private config: PollingConfig = {
|
|
baseIntervalMs: 300000, // 5 minutes
|
|
maxIntervalMs: 1800000, // 30 minutes
|
|
adaptivePolling: true
|
|
};
|
|
private currentInterval = 300000;
|
|
private consecutiveFailures = 0;
|
|
private consecutiveSuccesses = 0;
|
|
|
|
configure(config: PollingConfig): void {
|
|
this.config = { ...this.config, ...config };
|
|
this.currentInterval = this.config.baseIntervalMs;
|
|
}
|
|
|
|
recordSuccess(): void {
|
|
this.consecutiveSuccesses++;
|
|
this.consecutiveFailures = 0;
|
|
|
|
if (this.config.adaptivePolling && this.consecutiveSuccesses >= 3) {
|
|
// Decrease interval on sustained success
|
|
this.currentInterval = Math.max(
|
|
this.config.baseIntervalMs,
|
|
this.currentInterval * 0.8
|
|
);
|
|
}
|
|
}
|
|
|
|
recordFailure(): void {
|
|
this.consecutiveFailures++;
|
|
this.consecutiveSuccesses = 0;
|
|
|
|
if (this.config.adaptivePolling) {
|
|
// Increase interval on failure
|
|
this.currentInterval = Math.min(
|
|
this.config.maxIntervalMs,
|
|
this.currentInterval * 1.5
|
|
);
|
|
}
|
|
}
|
|
|
|
getCurrentInterval(): number {
|
|
return this.currentInterval;
|
|
}
|
|
|
|
getStatus(): PollingStatus {
|
|
return {
|
|
currentInterval: this.currentInterval,
|
|
consecutiveFailures: this.consecutiveFailures,
|
|
consecutiveSuccesses: this.consecutiveSuccesses,
|
|
adaptivePolling: this.config.adaptivePolling
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configuration Interfaces
|
|
*/
|
|
export interface CommunityIntegrationConfig {
|
|
maxRequestsPerMinute?: number;
|
|
maxRequestsPerHour?: number;
|
|
burstLimit?: number;
|
|
initialBackoffMs?: number;
|
|
maxBackoffMs?: number;
|
|
backoffMultiplier?: number;
|
|
jitterEnabled?: boolean;
|
|
basePollingIntervalMs?: number;
|
|
maxPollingIntervalMs?: number;
|
|
adaptivePolling?: boolean;
|
|
}
|
|
|
|
interface RateLimitConfig {
|
|
maxRequestsPerMinute: number;
|
|
maxRequestsPerHour: number;
|
|
burstLimit: number;
|
|
}
|
|
|
|
interface BackoffConfig {
|
|
initialDelayMs: number;
|
|
maxDelayMs: number;
|
|
backoffMultiplier: number;
|
|
jitterEnabled: boolean;
|
|
}
|
|
|
|
interface PollingConfig {
|
|
baseIntervalMs: number;
|
|
maxIntervalMs: number;
|
|
adaptivePolling: boolean;
|
|
}
|
|
|
|
/**
|
|
* Status Interfaces
|
|
*/
|
|
export interface RateLimitStatus {
|
|
requestsLastMinute: number;
|
|
requestsLastHour: number;
|
|
burstRequests: number;
|
|
canMakeRequest: boolean;
|
|
waitTime: number;
|
|
}
|
|
|
|
export interface BackoffStatus {
|
|
failureCount: number;
|
|
currentDelay: number;
|
|
isInBackoff: boolean;
|
|
remainingBackoffTime: number;
|
|
}
|
|
|
|
export interface PollingStatus {
|
|
currentInterval: number;
|
|
consecutiveFailures: number;
|
|
consecutiveSuccesses: number;
|
|
adaptivePolling: boolean;
|
|
}
|
|
|