261 lines
6.9 KiB
TypeScript
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();
|