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
JavaScript
;
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