UNPKG

nostr-deploy-server

Version:

Node.js server for hosting static websites under npub subdomains using Nostr protocol and Blossom servers

538 lines (528 loc) โ€ข 22.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CacheWarmer = void 0; exports.runCacheWarmerCLI = runCacheWarmerCLI; const nostr_tools_1 = require("nostr-tools"); const blossom_1 = require("../helpers/blossom"); const nostr_1 = require("../helpers/nostr"); const cache_1 = require("./cache"); const config_1 = require("./config"); const logger_1 = require("./logger"); class CacheWarmer { constructor() { this.config = config_1.ConfigManager.getInstance().getConfig(); this.nostrHelper = new nostr_1.NostrHelper(); this.blossomHelper = new blossom_1.BlossomHelper(); this.stats = this.initializeStats(); } initializeStats() { return { startTime: new Date(), endTime: new Date(), duration: 0, timePeriod: '', pubkeysProcessed: 0, eventsProcessed: 0, filesWarmed: 0, relayListsWarmed: 0, blossomServersWarmed: 0, domainsWarmed: 0, blobUrlsWarmed: 0, fileContentWarmed: 0, errors: 0, cacheHits: 0, cacheMisses: 0, totalOperations: 0, averageOperationTime: 0, operationDetails: { pubkeyDiscovery: 0, relayQueries: 0, blossomQueries: 0, cacheOperations: 0, }, }; } /** * Parse time period string to Unix timestamp * Supports formats like: '1d', '7d', '1w', '2w', '1m', '3m', '1y' */ parseTimePeriod(timePeriod) { const now = Math.floor(Date.now() / 1000); const match = timePeriod.match(/^(\d+)([hdwmy])$/); if (!match) { throw new Error(`Invalid time period format: ${timePeriod}. Use formats like '1d', '1w', '1m'`); } const [, numStr, unit] = match; const num = parseInt(numStr, 10); const multipliers = { h: 3600, // hours d: 86400, // days w: 604800, // weeks m: 2592000, // months (30 days) y: 31536000, // years (365 days) }; const multiplier = multipliers[unit]; if (!multiplier) { throw new Error(`Invalid time unit: ${unit}. Use h, d, w, m, or y`); } return now - num * multiplier; } /** * Discover pubkeys from recent events if not provided */ async discoverPubkeys(since, limit = 100) { const startTime = Date.now(); logger_1.logger.info(`๐Ÿ” Discovering pubkeys from events since ${new Date(since * 1000).toISOString()}`); try { // Query for recent deployment events (kind 30019) and profile events (kind 0) const relays = this.config.defaultRelays; const filters = [ { kinds: [30019], since, limit }, // Static site deployment events { kinds: [0], since, limit: Math.floor(limit / 2) }, // Profile events { kinds: [10002], since, limit: Math.floor(limit / 4) }, // Relay list events { kinds: [10063], since, limit: Math.floor(limit / 4) }, // Blossom server events ]; const pubkeysSet = new Set(); for (const filter of filters) { try { const events = await this.nostrHelper['queryRelays'](relays, filter); events.forEach((event) => pubkeysSet.add(event.pubkey)); this.stats.eventsProcessed += events.length; } catch (error) { logger_1.logger.warn(`Failed to query events with filter:`, { filter, error: error instanceof Error ? error.message : 'Unknown error', }); this.stats.errors++; } } const pubkeys = Array.from(pubkeysSet); this.stats.operationDetails.pubkeyDiscovery = Date.now() - startTime; logger_1.logger.info(`โœ… Discovered ${pubkeys.length} unique pubkeys`); return pubkeys; } catch (error) { logger_1.logger.error('Failed to discover pubkeys', { error: error instanceof Error ? error.message : 'Unknown error', }); this.stats.errors++; return []; } } /** * Warm cache for a single pubkey */ async warmPubkeyCache(pubkey, options) { const npub = nostr_tools_1.nip19.npubEncode(pubkey); logger_1.logger.debug(`๐Ÿ”ฅ Warming cache for pubkey: ${pubkey.substring(0, 8)}... (${npub})`); try { // 1. Warm relay list cache await this.warmRelayList(pubkey, options.dryRun); // 2. Warm blossom servers cache await this.warmBlossomServers(pubkey, options.dryRun); // 3. Warm static file mappings await this.warmStaticFiles(pubkey, options, this.parseTimePeriod(options.timePeriod)); this.stats.pubkeysProcessed++; } catch (error) { logger_1.logger.error(`Failed to warm cache for pubkey ${pubkey.substring(0, 8)}...`, { pubkey, error: error instanceof Error ? error.message : 'Unknown error', }); this.stats.errors++; } } /** * Warm relay list cache for a pubkey */ async warmRelayList(pubkey, dryRun = false) { try { const cached = await cache_1.CacheService.getRelaysForPubkey(pubkey); if (cached) { this.stats.cacheHits++; logger_1.logger.debug(`๐Ÿ“ก Relay list cache HIT for ${pubkey.substring(0, 8)}...`); return; } this.stats.cacheMisses++; if (dryRun) { logger_1.logger.info(`[DRY RUN] Would fetch relay list for ${pubkey.substring(0, 8)}...`); return; } const relays = await this.nostrHelper.getRelayList(pubkey); if (relays.length > 0) { await cache_1.CacheService.setRelaysForPubkey(pubkey, relays); this.stats.relayListsWarmed++; logger_1.logger.debug(`๐Ÿ“ก Warmed relay list cache for ${pubkey.substring(0, 8)}... (${relays.length} relays)`); } } catch (error) { logger_1.logger.warn(`Failed to warm relay list for ${pubkey.substring(0, 8)}...`, { pubkey, error: error instanceof Error ? error.message : 'Unknown error', }); this.stats.errors++; } } /** * Warm blossom servers cache for a pubkey */ async warmBlossomServers(pubkey, dryRun = false) { try { const cached = await cache_1.CacheService.getBlossomServersForPubkey(pubkey); if (cached) { this.stats.cacheHits++; logger_1.logger.debug(`๐ŸŒธ Blossom servers cache HIT for ${pubkey.substring(0, 8)}...`); return; } this.stats.cacheMisses++; if (dryRun) { logger_1.logger.info(`[DRY RUN] Would fetch blossom servers for ${pubkey.substring(0, 8)}...`); return; } const servers = await this.nostrHelper.getBlossomServers(pubkey); if (servers.length > 0) { await cache_1.CacheService.setBlossomServersForPubkey(pubkey, servers); this.stats.blossomServersWarmed++; logger_1.logger.debug(`๐ŸŒธ Warmed blossom servers cache for ${pubkey.substring(0, 8)}... (${servers.length} servers)`); } } catch (error) { logger_1.logger.warn(`Failed to warm blossom servers for ${pubkey.substring(0, 8)}...`, { pubkey, error: error instanceof Error ? error.message : 'Unknown error', }); this.stats.errors++; } } /** * Warm static file mappings for a pubkey */ async warmStaticFiles(pubkey, options, since) { try { // Get relay list for this pubkey to query for static file events const relays = (await cache_1.CacheService.getRelaysForPubkey(pubkey)) || this.config.defaultRelays; // Query for static file deployment events since the specified time const filter = { kinds: [30019], // Static site deployment events authors: [pubkey], since, limit: options.maxEventsPerPubkey || 100, }; if (options.dryRun) { logger_1.logger.info(`[DRY RUN] Would query static files for ${pubkey.substring(0, 8)}... since ${new Date(since * 1000).toISOString()}`); return; } const events = await this.nostrHelper['queryRelays'](relays, filter); for (const event of events) { try { // Parse the event to extract file mappings const tags = event.tags; const dTag = tags.find((tag) => tag[0] === 'd')?.[1]; // Domain if (dTag) { // Warm domain -> pubkey mapping await cache_1.CacheService.setPubkeyForDomain(dTag, pubkey); this.stats.domainsWarmed++; } // Process file tags (assuming standard format) const fileTags = tags.filter((tag) => tag[0] === 'f'); for (const fileTag of fileTags) { const [, path, sha256] = fileTag; if (path && sha256) { // Create parsed event for caching const parsedEvent = { id: event.id, pubkey: event.pubkey, created_at: event.created_at, kind: event.kind, tags: event.tags, content: event.content, sig: event.sig, sha256, path, size: 0, // Will be determined when content is fetched }; await cache_1.CacheService.setBlobForPath(pubkey, path, parsedEvent); this.stats.filesWarmed++; // Optionally warm file content if (options.warmFileContent) { await this.warmFileContent(sha256, pubkey, options.dryRun); } } } this.stats.eventsProcessed++; } catch (error) { logger_1.logger.warn(`Failed to process static file event ${event.id}:`, error); this.stats.errors++; } } } catch (error) { logger_1.logger.warn(`Failed to warm static files for ${pubkey.substring(0, 8)}...`, { pubkey, error: error instanceof Error ? error.message : 'Unknown error', }); this.stats.errors++; } } /** * Warm file content cache */ async warmFileContent(sha256, pubkey, dryRun = false) { try { const cached = await cache_1.CacheService.getFileContent(sha256); if (cached) { this.stats.cacheHits++; return; } this.stats.cacheMisses++; if (dryRun) { logger_1.logger.info(`[DRY RUN] Would fetch file content for ${sha256}`); return; } // Get blossom servers for this pubkey const servers = (await cache_1.CacheService.getBlossomServersForPubkey(pubkey)) || this.config.defaultBlossomServers; // Use fetchFile method instead of getFileContent const fileResponse = await this.blossomHelper.fetchFile(sha256, servers); if (fileResponse && fileResponse.content) { // The fetchFile method automatically caches the content, so we just need to count it this.stats.fileContentWarmed++; logger_1.logger.debug(`๐Ÿ“„ Warmed file content for ${sha256}`); } } catch (error) { logger_1.logger.warn(`Failed to warm file content for ${sha256}`, { sha256, error: error instanceof Error ? error.message : 'Unknown error', }); this.stats.errors++; } } /** * Run cache warming with specified options */ async run(options) { this.stats = this.initializeStats(); this.stats.startTime = new Date(); this.stats.timePeriod = options.timePeriod; logger_1.logger.info(`๐Ÿš€ Starting cache warmer for time period: ${options.timePeriod}`); if (options.dryRun) { logger_1.logger.info(`๐Ÿƒ Running in DRY RUN mode - no actual caching will occur`); } try { const since = this.parseTimePeriod(options.timePeriod); logger_1.logger.info(`๐Ÿ“… Warming cache for events since: ${new Date(since * 1000).toISOString()}`); // Get pubkeys to process let pubkeys = options.pubkeys || []; if (pubkeys.length === 0) { pubkeys = await this.discoverPubkeys(since, options.maxEventsPerPubkey || 100); } if (pubkeys.length === 0) { logger_1.logger.warn('โš ๏ธ No pubkeys found to warm cache for'); return this.finalizeStats(); } logger_1.logger.info(`๐Ÿ‘ฅ Processing ${pubkeys.length} pubkeys with concurrency: ${options.concurrency || 5}`); // Process pubkeys with controlled concurrency const concurrency = options.concurrency || 5; const chunks = this.chunkArray(pubkeys, concurrency); for (const chunk of chunks) { const promises = chunk.map((pubkey) => this.warmPubkeyCache(pubkey, options)); await Promise.allSettled(promises); } return this.finalizeStats(); } catch (error) { logger_1.logger.error('Cache warming failed', { error: error instanceof Error ? error.message : 'Unknown error', }); this.stats.errors++; return this.finalizeStats(); } } /** * Split array into chunks for controlled concurrency */ chunkArray(array, chunkSize) { const chunks = []; for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)); } return chunks; } /** * Finalize stats and return results */ finalizeStats() { this.stats.endTime = new Date(); this.stats.duration = this.stats.endTime.getTime() - this.stats.startTime.getTime(); this.stats.totalOperations = this.stats.cacheHits + this.stats.cacheMisses; this.stats.averageOperationTime = this.stats.totalOperations > 0 ? this.stats.duration / this.stats.totalOperations : 0; return this.stats; } /** * Print formatted statistics */ printStats(stats) { const duration = (stats.duration / 1000).toFixed(2); const avgOpTime = stats.averageOperationTime.toFixed(2); const hitRate = stats.totalOperations > 0 ? ((stats.cacheHits / stats.totalOperations) * 100).toFixed(1) : '0.0'; console.log('\n' + '='.repeat(80)); console.log('๐Ÿ”ฅ CACHE WARMER STATISTICS'); console.log('='.repeat(80)); console.log(`๐Ÿ“… Time Period: ${stats.timePeriod}`); console.log(`โฑ๏ธ Duration: ${duration}s (${stats.startTime.toISOString()} โ†’ ${stats.endTime.toISOString()})`); console.log(`๐Ÿ‘ฅ Pubkeys Processed: ${stats.pubkeysProcessed}`); console.log(`๐Ÿ“ Events Processed: ${stats.eventsProcessed}`); console.log(''); console.log('๐Ÿ“Š CACHE WARMING RESULTS:'); console.log(` ๐Ÿ“ก Relay Lists: ${stats.relayListsWarmed}`); console.log(` ๐ŸŒธ Blossom Servers: ${stats.blossomServersWarmed}`); console.log(` ๐ŸŒ Domains: ${stats.domainsWarmed}`); console.log(` ๐Ÿ“„ Files: ${stats.filesWarmed}`); console.log(` ๐Ÿ’พ File Content: ${stats.fileContentWarmed}`); console.log(` ๐Ÿ”— Blob URLs: ${stats.blobUrlsWarmed}`); console.log(''); console.log('โšก PERFORMANCE METRICS:'); console.log(` ๐ŸŽฏ Cache Hit Rate: ${hitRate}% (${stats.cacheHits}/${stats.totalOperations})`); console.log(` ๐Ÿƒ Avg Operation Time: ${avgOpTime}ms`); console.log(` โŒ Errors: ${stats.errors}`); console.log(''); console.log('๐Ÿ” OPERATION BREAKDOWN:'); console.log(` ๐Ÿ”Ž Pubkey Discovery: ${stats.operationDetails.pubkeyDiscovery}ms`); console.log(` ๐Ÿ“ก Relay Queries: ${stats.operationDetails.relayQueries}ms`); console.log(` ๐ŸŒธ Blossom Queries: ${stats.operationDetails.blossomQueries}ms`); console.log(` ๐Ÿ’พ Cache Operations: ${stats.operationDetails.cacheOperations}ms`); console.log('='.repeat(80)); if (stats.errors > 0) { console.log(`โš ๏ธ Warning: ${stats.errors} errors occurred during cache warming`); } console.log(`โœ… Cache warming completed successfully!\n`); } /** * Clean up resources */ cleanup() { try { this.nostrHelper.closeAllConnections(); logger_1.logger.info('๐Ÿงน Cache warmer cleanup completed'); } catch (error) { logger_1.logger.error('Error during cache warmer cleanup', { error: error instanceof Error ? error.message : 'Unknown error', }); } } } exports.CacheWarmer = CacheWarmer; /** * CLI interface for cache warming */ async function runCacheWarmerCLI() { const args = process.argv.slice(2); // Parse command line arguments const options = { timePeriod: '1d', // default concurrency: 5, maxEventsPerPubkey: 100, warmFileContent: false, dryRun: false, }; for (let i = 0; i < args.length; i++) { const arg = args[i]; const nextArg = args[i + 1]; switch (arg) { case '--time-period': case '-t': if (nextArg) options.timePeriod = nextArg; i++; break; case '--pubkeys': case '-p': if (nextArg) options.pubkeys = nextArg.split(','); i++; break; case '--max-events': case '-m': if (nextArg) options.maxEventsPerPubkey = parseInt(nextArg, 10); i++; break; case '--concurrency': case '-c': if (nextArg) options.concurrency = parseInt(nextArg, 10); i++; break; case '--warm-files': case '-f': options.warmFileContent = true; break; case '--dry-run': case '-n': options.dryRun = true; break; case '--help': case '-h': printUsage(); process.exit(0); break; } } console.log('๐Ÿ”ฅ Nostr Deploy Cache Warmer'); console.log('============================\n'); const warmer = new CacheWarmer(); try { const stats = await warmer.run(options); warmer.printStats(stats); process.exit(0); } catch (error) { console.error('โŒ Cache warming failed:', error); process.exit(1); } finally { warmer.cleanup(); } } function printUsage() { console.log(` ๐Ÿ”ฅ Nostr Deploy Cache Warmer USAGE: npm run cache-warm [OPTIONS] OPTIONS: -t, --time-period <period> Time period to warm cache for (e.g., '1d', '1w', '1m') Default: '1d' -p, --pubkeys <pubkeys> Comma-separated list of pubkeys to warm cache for If not provided, discovers from recent events -m, --max-events <number> Maximum number of events to process per pubkey Default: 100 -c, --concurrency <number> Maximum number of concurrent operations Default: 5 -f, --warm-files Also warm file content cache (slower but more complete) Default: false -n, --dry-run Dry run mode - don't actually cache, just log what would be done Default: false -h, --help Show this help message EXAMPLES: npm run cache-warm # Warm cache for past day npm run cache-warm -t 1w # Warm cache for past week npm run cache-warm -t 1m -f # Warm cache for past month including file content npm run cache-warm -t 7d -c 10 -m 200 # Past week with higher concurrency and more events npm run cache-warm -p npub1abc...,npub1def... # Warm cache for specific pubkeys npm run cache-warm -n -t 1w # Dry run for past week TIME PERIOD FORMATS: 1h, 2h, 24h Hours 1d, 7d Days 1w, 2w Weeks 1m, 3m, 12m Months 1y Years `); } //# sourceMappingURL=cache-warmer.js.map