feat: implement TimeSafari integration services and improve code quality
- Add DailyNotificationService with circuit breaker and rate limiting - Add DatabaseIntegrationService with watermark management - Add TimeSafariIntegrationService with DID/VC support - Add TimeSafariCommunityIntegrationService with rate limiting - Add PlatformServiceMixin for Vue component integration - Add comprehensive TimeSafari integration example - Fix all linting issues (133 → 0 warnings) - Add .eslintignore to exclude dist/ from linting - Replace console statements with proper error handling - Replace 'any' types with 'unknown' for better type safety - Add explicit return types to all functions - Replace non-null assertions with proper null checks All tests passing (115 tests across 8 suites)
This commit is contained in:
455
src/timesafari-community-integration.ts
Normal file
455
src/timesafari-community-integration.ts
Normal file
@@ -0,0 +1,455 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user