NeahNew/lib/services/refresh-manager.ts
2026-01-06 13:02:07 +01:00

261 lines
6.9 KiB
TypeScript

/**
* Unified Refresh Manager
*
* Centralizes all refresh logic across widgets and notifications.
* Prevents duplicate refreshes, manages intervals, and provides
* a single source of truth for refresh coordination.
*/
export type RefreshableResource =
| 'notifications'
| 'notifications-count'
| 'calendar'
| 'news'
| 'email'
| 'parole'
| 'duties'
| 'navbar-time';
export interface RefreshConfig {
resource: RefreshableResource;
interval: number; // milliseconds
enabled: boolean;
priority: 'high' | 'medium' | 'low';
onRefresh: () => Promise<void>;
}
class RefreshManager {
private intervals: Map<RefreshableResource, NodeJS.Timeout> = new Map();
private configs: Map<RefreshableResource, RefreshConfig> = new Map();
private pendingRequests: Map<string, Promise<any>> = new Map();
private lastRefresh: Map<RefreshableResource, number> = new Map();
private isActive = false;
/**
* Register a refreshable resource
*/
register(config: RefreshConfig): void {
console.log(`[RefreshManager] Registering resource: ${config.resource} (interval: ${config.interval}ms)`);
this.configs.set(config.resource, config);
if (config.enabled && this.isActive) {
this.startRefresh(config.resource);
}
}
/**
* Unregister a resource
*/
unregister(resource: RefreshableResource): void {
console.log(`[RefreshManager] Unregistering resource: ${resource}`);
this.stopRefresh(resource);
this.configs.delete(resource);
this.lastRefresh.delete(resource);
// Clean up pending request
const pendingKey = `${resource}-pending`;
this.pendingRequests.delete(pendingKey);
}
/**
* Start all refresh intervals
*/
start(): void {
if (this.isActive) {
console.log('[RefreshManager] Already active');
return;
}
console.log('[RefreshManager] Starting refresh manager');
this.isActive = true;
// Start all enabled resources
this.configs.forEach((config, resource) => {
if (config.enabled) {
this.startRefresh(resource);
}
});
}
/**
* Stop all refresh intervals
*/
stop(): void {
if (!this.isActive) {
console.log('[RefreshManager] Already stopped');
return;
}
console.log('[RefreshManager] Stopping refresh manager');
this.isActive = false;
// Clear all intervals
this.intervals.forEach((interval, resource) => {
console.log(`[RefreshManager] Stopping refresh for: ${resource}`);
clearInterval(interval);
});
this.intervals.clear();
// Clear pending requests
this.pendingRequests.clear();
}
/**
* Start refresh for a specific resource
*/
private startRefresh(resource: RefreshableResource): void {
// Stop existing interval if any
this.stopRefresh(resource);
const config = this.configs.get(resource);
if (!config || !config.enabled) {
console.log(`[RefreshManager] Cannot start refresh for ${resource}: not configured or disabled`);
return;
}
console.log(`[RefreshManager] Starting refresh for ${resource} (interval: ${config.interval}ms)`);
// Initial refresh
this.executeRefresh(resource);
// Set up interval
const interval = setInterval(() => {
this.executeRefresh(resource);
}, config.interval);
this.intervals.set(resource, interval);
}
/**
* Stop refresh for a specific resource
*/
private stopRefresh(resource: RefreshableResource): void {
const interval = this.intervals.get(resource);
if (interval) {
clearInterval(interval);
this.intervals.delete(resource);
console.log(`[RefreshManager] Stopped refresh for: ${resource}`);
}
}
/**
* Execute refresh with deduplication
*/
private async executeRefresh(resource: RefreshableResource): Promise<void> {
const config = this.configs.get(resource);
if (!config) {
console.warn(`[RefreshManager] No config found for resource: ${resource}`);
return;
}
const now = Date.now();
const lastRefreshTime = this.lastRefresh.get(resource) || 0;
// Prevent too frequent refreshes (minimum 1 second between same resource)
if (now - lastRefreshTime < 1000) {
console.log(`[RefreshManager] Skipping ${resource} - too soon (${now - lastRefreshTime}ms ago)`);
return;
}
// Check if there's already a pending request for this resource
const pendingKey = `${resource}-pending`;
if (this.pendingRequests.has(pendingKey)) {
console.log(`[RefreshManager] Deduplicating ${resource} request - already pending`);
return;
}
// Create and track the request
console.log(`[RefreshManager] Executing refresh for: ${resource}`);
const refreshPromise = config.onRefresh()
.then(() => {
this.lastRefresh.set(resource, Date.now());
console.log(`[RefreshManager] Successfully refreshed: ${resource}`);
})
.catch((error) => {
console.error(`[RefreshManager] Error refreshing ${resource}:`, error);
// Don't update lastRefresh on error to allow retry
})
.finally(() => {
this.pendingRequests.delete(pendingKey);
});
this.pendingRequests.set(pendingKey, refreshPromise);
try {
await refreshPromise;
} catch (error) {
// Error already logged above
}
}
/**
* Manually trigger refresh for a resource
*/
async refresh(resource: RefreshableResource, force = false): Promise<void> {
const config = this.configs.get(resource);
if (!config) {
throw new Error(`Resource ${resource} not registered`);
}
console.log(`[RefreshManager] Manual refresh requested for: ${resource} (force: ${force})`);
if (force) {
// Force refresh: clear last refresh time and pending request
this.lastRefresh.delete(resource);
const pendingKey = `${resource}-pending`;
this.pendingRequests.delete(pendingKey);
}
await this.executeRefresh(resource);
}
/**
* Get refresh status
*/
getStatus(): {
active: boolean;
resources: Array<{
resource: RefreshableResource;
enabled: boolean;
lastRefresh: number | null;
interval: number;
isRunning: boolean;
}>;
} {
const resources = Array.from(this.configs.entries()).map(([resource, config]) => ({
resource,
enabled: config.enabled,
lastRefresh: this.lastRefresh.get(resource) || null,
interval: config.interval,
isRunning: this.intervals.has(resource),
}));
return {
active: this.isActive,
resources,
};
}
/**
* Pause all refreshes (temporary stop)
*/
pause(): void {
console.log('[RefreshManager] Pausing all refreshes');
this.stop();
}
/**
* Resume all refreshes
*/
resume(): void {
console.log('[RefreshManager] Resuming all refreshes');
this.start();
}
}
// Singleton instance
export const refreshManager = new RefreshManager();