Skip to main content
Glama
schedulerService.ts5.72 kB
import { Cron, scheduledJobs } from 'croner' import { dev } from '$app/environment' import { ContentSyncService } from '$lib/server/contentSync' import { CacheDbService } from '$lib/server/cacheDb' import { log, logAlways, logErrorAlways } from '$lib/log' export class SchedulerService { private static instance: SchedulerService | null = null private jobs: Map<string, Cron> = new Map() private isInitialized = false private cacheService: CacheDbService private constructor() { this.cacheService = new CacheDbService() } static getInstance(): SchedulerService { if (!SchedulerService.instance) { SchedulerService.instance = new SchedulerService() } return SchedulerService.instance } private cleanupOrphanedJobs(): void { if (scheduledJobs && Array.isArray(scheduledJobs)) { // More robust cleanup - track our job instances and clean up any unrecognized jobs const knownJobInstances = Array.from(this.jobs.values()) for (let i = scheduledJobs.length - 1; i >= 0; i--) { const job = scheduledJobs[i] if (job && !knownJobInstances.includes(job)) { // This is an orphaned job not in our tracking map job.stop() logAlways(`Cleaned up orphaned cron job`) } } } } async init(): Promise<void> { // Clean up any orphaned jobs from previous runs (e.g., hot module reloading) this.cleanupOrphanedJobs() if (this.isInitialized) { logAlways('SchedulerService already initialized, reinitializing...') await this.stop() } logAlways('Initializing SchedulerService...') // Content sync: update every 12 hours (at 2:00 AM and 2:00 PM) const contentSyncSchedule = process.env.CONTENT_SYNC_SCHEDULE || '0 2,14 * * *' // In development, run every 30 minutes for testing const devSchedule = '*/30 * * * *' this.scheduleContentSync(dev ? devSchedule : contentSyncSchedule) this.scheduleCacheCleanup() this.isInitialized = true logAlways(`SchedulerService initialized with ${this.jobs.size} jobs`) } private scheduleContentSync(schedule: string): void { const existingJob = this.jobs.get('content-sync') if (existingJob) { existingJob.stop() logAlways('Stopped existing content-sync job') } const job = new Cron( schedule, { name: 'content-sync', timezone: 'UTC', catch: (err) => { logErrorAlways('Error in content sync job:', err) } }, () => { this.syncContent() } ) this.jobs.set('content-sync', job) logAlways(`Scheduled content sync: ${schedule}`) } private async syncContent(): Promise<void> { logAlways('Starting scheduled content sync job...') try { const isStale = await ContentSyncService.isRepositoryContentStale() if (!isStale) { logAlways(`Repository content is fresh, skipping sync`) return } logAlways(`Syncing sveltejs/svelte.dev repository using ContentSyncService...`) const result = await ContentSyncService.syncRepository({ returnStats: true }) logAlways('Scheduled content sync completed successfully') logAlways( `Sync details: ${result.sync_details.upserted_files} upserted, ${result.sync_details.deleted_files} deleted, ${result.sync_details.unchanged_files} unchanged` ) logAlways(`Total files in database: ${result.stats.total_files}`) } catch (error) { logErrorAlways('Failed to sync content:', error) } logAlways('Scheduled content sync job completed') } getJobStatus(): Record<string, { running: boolean; nextRun: Date | null }> { const status: Record<string, { running: boolean; nextRun: Date | null }> = {} for (const [name, job] of this.jobs) { status[name] = { running: job.isRunning(), nextRun: job.nextRun() } } return status } async stop(): Promise<void> { logAlways('Stopping SchedulerService...') for (const [name, job] of this.jobs) { job.stop() logAlways(`Stopped job: ${name}`) } // Clean up any remaining orphaned jobs by name if (scheduledJobs && Array.isArray(scheduledJobs)) { const jobNames = ['content-sync', 'cache-cleanup'] for (let i = scheduledJobs.length - 1; i >= 0; i--) { const job = scheduledJobs[i] if (job && job.options && job.options.name && jobNames.includes(job.options.name)) { job.stop() logAlways(`Removed orphaned job from global registry: ${job.options.name}`) } } } this.jobs.clear() this.isInitialized = false logAlways('SchedulerService stopped') } private scheduleCacheCleanup(): void { const existingJob = this.jobs.get('cache-cleanup') if (existingJob) { existingJob.stop() logAlways('Stopped existing cache-cleanup job') } // Run every minute const job = new Cron( '* * * * *', { name: 'cache-cleanup', // Now consistently using names timezone: 'UTC', catch: (err) => { logErrorAlways('Error in cache cleanup job:', err) } }, () => { this.cleanupExpiredCache() } ) this.jobs.set('cache-cleanup', job) logAlways('Scheduled cache cleanup: every minute') } private async cleanupExpiredCache(): Promise<void> { log('Starting cache cleanup job...') try { const deletedCount = await this.cacheService.deleteExpired() if (deletedCount > 0) { logAlways(`Cleaned up ${deletedCount} expired cache entries`) } } catch (error) { logErrorAlways('Failed to clean up expired cache entries:', error) } } async triggerContentSync(): Promise<void> { logAlways('Manually triggering content sync...') await this.syncContent() } async triggerCacheCleanup(): Promise<void> { logAlways('Manually triggering cache cleanup...') await this.cleanupExpiredCache() } } // Export singleton instance export const schedulerService = SchedulerService.getInstance()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/khromov/llmctx'

If you have feedback or need assistance with the MCP directory API, please join our Discord server