Skip to main content
Glama
updater.ts11.6 kB
/** * Auto-update system for documentation * Checks for updates and manages incremental indexing */ import { DocumentStore } from './store.js'; import { DocumentCrawler, getFabricDocumentationUrls } from './crawler.js'; import { DocumentChunker } from './chunker.js'; import { getFabricUrlsFromSitemap } from './sitemap.js'; import type { IndexStats } from './types.js'; export interface UpdateCheckResult { needsUpdate: boolean; reason: string; stats: IndexStats; age: number; // Age in milliseconds staleness: 'fresh' | 'recent' | 'stale' | 'very-stale'; } export interface UpdateOptions { force?: boolean; maxAge?: number; // Maximum age in milliseconds useSitemap?: boolean; verbose?: boolean; } export class DocumentUpdater { private store: DocumentStore; private dbPath: string; constructor(dbPath: string) { this.dbPath = dbPath; this.store = new DocumentStore(dbPath); } /** * Check if documentation needs updating */ // eslint-disable-next-line @typescript-eslint/require-await async checkForUpdates(): Promise<UpdateCheckResult> { const stats = this.store.getStats(); const now = Date.now(); const age = now - stats.lastUpdated.getTime(); // Calculate staleness const ONE_DAY = 24 * 60 * 60 * 1000; let staleness: 'fresh' | 'recent' | 'stale' | 'very-stale'; let needsUpdate = false; let reason = ''; if (age < ONE_DAY) { staleness = 'fresh'; reason = 'Documentation is fresh (< 1 day old)'; } else if (age < 7 * ONE_DAY) { staleness = 'recent'; reason = 'Documentation is recent (< 7 days old)'; } else if (age < 30 * ONE_DAY) { staleness = 'stale'; needsUpdate = true; reason = 'Documentation is stale (> 7 days old)'; } else { staleness = 'very-stale'; needsUpdate = true; reason = 'Documentation is very stale (> 30 days old)'; } // Check if database is empty if (stats.totalDocuments === 0) { needsUpdate = true; reason = 'Database is empty'; staleness = 'very-stale'; } return { needsUpdate, reason, stats, age, staleness, }; } /** * Perform automatic update check and notify */ async autoUpdateCheck(options: UpdateOptions = {}): Promise<void> { const maxAge = options.maxAge || 7 * 24 * 60 * 60 * 1000; // 7 days default const result = await this.checkForUpdates(); if (!result.needsUpdate && result.age < maxAge) { if (options.verbose) { console.error('✅ Documentation is up to date'); console.error(` Last updated: ${result.stats.lastUpdated.toISOString()}`); console.error(` Age: ${this.formatAge(result.age)}`); } return; } // Display update notification console.error(''); console.error('╔════════════════════════════════════════════════════════════╗'); console.error('║ 📢 Documentation Update Available ║'); console.error('╚════════════════════════════════════════════════════════════╝'); console.error(''); console.error(` Status: ${result.staleness}`); console.error(` Reason: ${result.reason}`); console.error(` Last updated: ${result.stats.lastUpdated.toISOString()}`); console.error(` Age: ${this.formatAge(result.age)}`); console.error(''); console.error(' To update documentation, run:'); console.error(' npm run index-docs -- --incremental'); console.error(''); console.error(' Or force a full re-index:'); console.error(' npm run index-docs -- --force'); console.error(''); } /** * Perform incremental update */ async performIncrementalUpdate(options: UpdateOptions = {}): Promise<void> { console.error('🔄 Starting incremental documentation update...\n'); // Get URLs let urls: string[]; if (options.useSitemap) { console.error('📡 Fetching URLs from sitemap...'); urls = await getFabricUrlsFromSitemap(); if (urls.length === 0) { console.error('⚠️ Sitemap fetch failed, using static list'); urls = getFabricDocumentationUrls(); } } else { urls = getFabricDocumentationUrls(); } console.error(`📋 Found ${urls.length} URLs to check\n`); // Get existing URLs const existingUrls = this.store.getAllUrls(); const existingSet = new Set(existingUrls); // Find new URLs const newUrls = urls.filter((url) => !existingSet.has(url)); if (newUrls.length > 0) { console.error(`🆕 Found ${newUrls.length} new pages`); } // Initialize crawler const crawler = new DocumentCrawler({ maxConcurrency: 3, delayMs: 1000, retryAttempts: 3, }); let updatedCount = 0; let skippedCount = 0; let newCount = 0; // Crawl and check for updates console.error('🕷️ Checking for updates...\n'); for (let i = 0; i < urls.length; i++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const url = urls[i]!; try { const doc = await crawler.crawlPage(url); // Check if needs update if (!this.store.needsUpdate(url, doc.hash) && !options.force) { skippedCount++; process.stdout.write( `\r Progress: ${i + 1}/${urls.length} | Updated: ${updatedCount} | New: ${newCount} | Skipped: ${skippedCount} ` ); continue; } // Store document const documentId = this.store.storeDocument(doc); // Create and store chunks const chunker = new DocumentChunker(); const chunks = chunker.chunkDocument(doc); this.store.storeChunks(chunks, documentId); if (existingSet.has(url)) { updatedCount++; } else { newCount++; } process.stdout.write( `\r Progress: ${i + 1}/${urls.length} | Updated: ${updatedCount} | New: ${newCount} | Skipped: ${skippedCount} ` ); // Rate limiting if (i < urls.length - 1) { await new Promise((resolve) => setTimeout(resolve, 1000)); } } catch (error) { console.error(`\n❌ Failed to update ${url}:`, error); } } console.error('\n'); // Update timestamp this.store.updateTimestamp(); // Show results console.error('✅ Incremental update complete!\n'); console.error(` Updated: ${updatedCount} documents`); console.error(` New: ${newCount} documents`); console.error(` Skipped: ${skippedCount} documents (no changes)\n`); const stats = this.store.getStats(); console.error('📊 Current Index Statistics:'); console.error(` Total Documents: ${stats.totalDocuments}`); console.error(` Total Sections: ${stats.totalSections}`); console.error(` Total Code Blocks: ${stats.totalCodeBlocks}`); console.error(` Last Updated: ${stats.lastUpdated.toISOString()}\n`); } /** * Format age in human-readable format */ private formatAge(ms: number): string { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) { return `${days} day${days !== 1 ? 's' : ''}`; } if (hours > 0) { return `${hours} hour${hours !== 1 ? 's' : ''}`; } if (minutes > 0) { return `${minutes} minute${minutes !== 1 ? 's' : ''}`; } return `${seconds} second${seconds !== 1 ? 's' : ''}`; } /** * Close the database */ close(): void { this.store.close(); } } /** * Background update service * Runs periodic checks in the background */ export class BackgroundUpdateService { private updater: DocumentUpdater; private checkInterval: number; private intervalId: NodeJS.Timeout | null = null; private running = false; constructor(dbPath: string, checkInterval: number = 24 * 60 * 60 * 1000) { // Default: check every 24 hours this.updater = new DocumentUpdater(dbPath); this.checkInterval = checkInterval; } /** * Start the background service */ start(): void { if (this.running) return; this.running = true; // Initial check void this.performCheck(); // Schedule periodic checks this.intervalId = setInterval(() => { void this.performCheck(); }, this.checkInterval); console.error('🔄 Background update service started'); console.error(` Check interval: ${this.checkInterval / 1000 / 60 / 60} hours`); } /** * Stop the background service */ stop(): void { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } this.running = false; this.updater.close(); console.error('⏹️ Background update service stopped'); } /** * Perform update check */ private async performCheck(): Promise<void> { try { await this.updater.autoUpdateCheck({ verbose: false }); } catch (error) { console.error('❌ Background update check failed:', error); } } /** * Check if service is running */ isRunning(): boolean { return this.running; } } /** * Update scheduler with configurable strategies */ export interface ScheduleOptions { strategy: 'startup' | 'periodic' | 'manual' | 'hybrid'; checkIntervalHours?: number; autoUpdate?: boolean; maxAgeHours?: number; } export class UpdateScheduler { private updater: DocumentUpdater; private backgroundService: BackgroundUpdateService | null = null; private options: ScheduleOptions; constructor(dbPath: string, options: ScheduleOptions) { this.updater = new DocumentUpdater(dbPath); this.options = options; } /** * Initialize the scheduler */ async initialize(): Promise<void> { switch (this.options.strategy) { case 'startup': await this.handleStartupCheck(); break; case 'periodic': this.startPeriodicChecks(); break; case 'hybrid': await this.handleStartupCheck(); this.startPeriodicChecks(); break; case 'manual': // Do nothing, user controls updates break; } } /** * Handle startup check */ private async handleStartupCheck(): Promise<void> { const maxAge = (this.options.maxAgeHours || 168) * 60 * 60 * 1000; // Default: 7 days const result = await this.updater.checkForUpdates(); if (result.needsUpdate || result.age > maxAge) { if (this.options.autoUpdate) { console.error('🔄 Auto-updating documentation...'); await this.updater.performIncrementalUpdate({ useSitemap: true, verbose: true, }); } else { await this.updater.autoUpdateCheck({ maxAge }); } } } /** * Start periodic background checks */ private startPeriodicChecks(): void { const intervalMs = (this.options.checkIntervalHours || 24) * 60 * 60 * 1000; this.backgroundService = new BackgroundUpdateService(this.updater['dbPath'], intervalMs); this.backgroundService.start(); } /** * Stop the scheduler */ stop(): void { if (this.backgroundService) { this.backgroundService.stop(); } this.updater.close(); } }

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/OGMatrix/mcmodding-mcp'

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