image cache on cloudflare r2
at main 9.1 kB view raw
1import { Database } from "bun:sqlite"; 2import { nanoid } from "nanoid"; 3 4const DB_PATH = process.env.STATS_DB_PATH || "./data/stats.db"; 5const db = new Database(DB_PATH, { create: true }); 6 7// Generate realistic fake data 8const _imageKeys: string[] = []; 9const numImages = 500; // More images for variety 10 11// Generate fake image keys with varying introduction dates 12const now = Math.floor(Date.now() / 1000); 13const oneYearAgo = now - 365 * 86400; 14const _thirtyDaysAgo = now - 30 * 86400; 15const oneDayAgo = now - 86400; 16 17interface Image { 18 key: string; 19 introducedAt: number; // When this image first appeared 20 basePopularity: number; // Intrinsic popularity (0-1) 21 trendFactor: number; // How much popularity changes over time (-0.5 to 0.5) 22 viralPeak?: number; // Optional viral spike timestamp 23} 24 25const images: Image[] = []; 26 27// Create images with staggered introduction dates 28for (let i = 0; i < numImages; i++) { 29 const introDate = oneYearAgo + Math.random() * (now - oneYearAgo); 30 const basePopularity = Math.random() ** 1.5; // Skew toward lower popularity 31 const trendFactor = (Math.random() - 0.5) * 0.8; // -0.4 to 0.4 32 33 const image: Image = { 34 key: `${nanoid(12)}.webp`, 35 introducedAt: introDate, 36 basePopularity, 37 trendFactor, 38 }; 39 40 // 10% chance of having a viral spike 41 if (Math.random() < 0.1) { 42 image.viralPeak = introDate + Math.random() * (now - introDate); 43 } 44 45 images.push(image); 46} 47 48console.log("Seeding database with fake data (1 year)..."); 49 50// Seed all three tables 51const hourlyStmt = db.prepare(` 52 INSERT INTO image_stats (image_key, bucket_hour, hits) 53 VALUES (?1, ?2, ?3) 54 ON CONFLICT(image_key, bucket_hour) DO UPDATE SET hits = hits + ?3 55`); 56 57const dailyStmt = db.prepare(` 58 INSERT INTO image_stats_daily (image_key, bucket_day, hits) 59 VALUES (?1, ?2, ?3) 60 ON CONFLICT(image_key, bucket_day) DO UPDATE SET hits = hits + ?3 61`); 62 63console.log("Seeding hourly and daily data (1 year ago to 24 hours ago)..."); 64let _totalHourlyHits = 0; 65let _totalDailyHits = 0; 66 67for (let timestamp = oneYearAgo; timestamp < oneDayAgo; timestamp += 3600) { 68 const bucketHour = timestamp - (timestamp % 3600); 69 const bucketDay = timestamp - (timestamp % 86400); 70 71 // Time-based factors 72 const date = new Date(timestamp * 1000); 73 const dayOfWeek = date.getUTCDay(); 74 const hour = date.getUTCHours(); 75 const month = date.getUTCMonth(); 76 77 // Weekly pattern (weekdays busier) 78 const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; 79 const weekdayMultiplier = isWeekend ? 0.6 : 1.0; 80 81 // Daily pattern (business hours busier, with some randomness) 82 const isBusinessHours = hour >= 9 && hour <= 17; 83 const hourMultiplier = isBusinessHours 84 ? 1.2 + Math.random() * 0.4 // 1.2-1.6 85 : 0.4 + Math.random() * 0.3; // 0.4-0.7 86 87 // Seasonal pattern (summer busier) 88 const isSummer = month >= 5 && month <= 8; // June-Sept 89 const seasonalMultiplier = isSummer ? 1.3 : 0.9; 90 91 // Overall growth trend (traffic increases over time) 92 const timeProgress = (timestamp - oneYearAgo) / (now - oneYearAgo); 93 const growthMultiplier = 0.7 + timeProgress * 0.6; // 0.7 to 1.3 94 95 // Random noise to break perfect cycles 96 const noiseMultiplier = 0.85 + Math.random() * 0.3; // 0.85-1.15 97 98 const baseActivity = 99 0.25 * 100 weekdayMultiplier * 101 hourMultiplier * 102 seasonalMultiplier * 103 growthMultiplier * 104 noiseMultiplier; 105 106 for (const image of images) { 107 // Skip if image doesn't exist yet 108 if (timestamp < image.introducedAt) continue; 109 110 // Calculate image-specific popularity at this time 111 const timeSinceIntro = timestamp - image.introducedAt; 112 const ageInDays = timeSinceIntro / 86400; 113 114 // Popularity changes over time (trend factor) 115 const trendProgress = Math.min(ageInDays / 180, 1); // Over 6 months 116 const trendedPopularity = 117 image.basePopularity + image.trendFactor * trendProgress; 118 119 // Viral spike (if any) 120 let viralBoost = 1; 121 if (image.viralPeak) { 122 const distanceFromPeak = Math.abs(timestamp - image.viralPeak); 123 const peakWindow = 7 * 86400; // 7 day spike 124 if (distanceFromPeak < peakWindow) { 125 viralBoost = 1 + 5 * (1 - distanceFromPeak / peakWindow); // Up to 6x boost 126 } 127 } 128 129 // New images get a temporary boost 130 const newImageBoost = ageInDays < 3 ? 1 + (3 - ageInDays) * 0.5 : 1; 131 132 const finalPopularity = trendedPopularity * viralBoost * newImageBoost; 133 134 if (Math.random() < baseActivity * finalPopularity) { 135 // Power law distribution for hit counts (most hits are small, some are large) 136 const hits = Math.max(1, Math.floor(Math.random() ** 3 * 200)); 137 hourlyStmt.run(image.key, bucketHour, hits); 138 dailyStmt.run(image.key, bucketDay, hits); 139 _totalHourlyHits += hits; 140 _totalDailyHits += hits; 141 } 142 } 143 144 // Progress indicator every 30 days 145 if ((timestamp - oneYearAgo) % (30 * 86400) === 0) { 146 const daysProcessed = Math.floor((timestamp - oneYearAgo) / 86400); 147 console.log(` Processed ${daysProcessed} days...`); 148 } 149} 150 151// Seed 10-minute data for last 24 hours 152const tenMinStmt = db.prepare(` 153 INSERT INTO image_stats_10min (image_key, bucket_10min, hits) 154 VALUES (?1, ?2, ?3) 155 ON CONFLICT(image_key, bucket_10min) DO UPDATE SET hits = hits + ?3 156`); 157 158console.log("Seeding 10-minute, hourly, and daily data (last 24 hours)..."); 159let _total10MinHits = 0; 160 161for (let timestamp = oneDayAgo; timestamp <= now; timestamp += 600) { 162 const bucket10Min = timestamp - (timestamp % 600); 163 const bucketHour = timestamp - (timestamp % 3600); 164 const bucketDay = timestamp - (timestamp % 86400); 165 166 // Recent data gets slightly higher activity 167 const recency = (timestamp - oneDayAgo) / (now - oneDayAgo); 168 const date = new Date(timestamp * 1000); 169 const dayOfWeek = date.getUTCDay(); 170 const hour = date.getUTCHours(); 171 172 const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; 173 const weekdayMultiplier = isWeekend ? 0.6 : 1.0; 174 175 const isBusinessHours = hour >= 9 && hour <= 17; 176 const hourMultiplier = isBusinessHours 177 ? 1.3 + Math.random() * 0.4 178 : 0.5 + Math.random() * 0.3; 179 180 const noiseMultiplier = 0.85 + Math.random() * 0.3; 181 182 const baseActivity = 183 0.35 * 184 (1 + recency * 0.3) * 185 weekdayMultiplier * 186 hourMultiplier * 187 noiseMultiplier; 188 189 for (const image of images) { 190 if (timestamp < image.introducedAt) continue; 191 192 const timeSinceIntro = timestamp - image.introducedAt; 193 const ageInDays = timeSinceIntro / 86400; 194 const trendProgress = Math.min(ageInDays / 180, 1); 195 const trendedPopularity = 196 image.basePopularity + image.trendFactor * trendProgress; 197 198 let viralBoost = 1; 199 if (image.viralPeak) { 200 const distanceFromPeak = Math.abs(timestamp - image.viralPeak); 201 const peakWindow = 7 * 86400; 202 if (distanceFromPeak < peakWindow) { 203 viralBoost = 1 + 5 * (1 - distanceFromPeak / peakWindow); 204 } 205 } 206 207 const newImageBoost = ageInDays < 3 ? 1 + (3 - ageInDays) * 0.5 : 1; 208 const finalPopularity = trendedPopularity * viralBoost * newImageBoost; 209 210 if (Math.random() < baseActivity * finalPopularity) { 211 const hits = Math.max(1, Math.floor(Math.random() ** 3 * 100)); 212 tenMinStmt.run(image.key, bucket10Min, hits); 213 hourlyStmt.run(image.key, bucketHour, hits); 214 dailyStmt.run(image.key, bucketDay, hits); 215 _total10MinHits += hits; 216 } 217 } 218} 219 220// Get summary stats 221const totalHitsHourly = db 222 .prepare(`SELECT SUM(hits) as total FROM image_stats`) 223 .get() as { total: number }; 224const totalHitsDaily = db 225 .prepare(`SELECT SUM(hits) as total FROM image_stats_daily`) 226 .get() as { total: number }; 227const totalHits10Min = db 228 .prepare(`SELECT SUM(hits) as total FROM image_stats_10min`) 229 .get() as { total: number }; 230const uniqueImages = db 231 .prepare(` 232 SELECT COUNT(DISTINCT image_key) as count FROM ( 233 SELECT image_key FROM image_stats 234 UNION 235 SELECT image_key FROM image_stats_10min 236 UNION 237 SELECT image_key FROM image_stats_daily 238 ) 239`) 240 .get() as { count: number }; 241const hourBuckets = db 242 .prepare(`SELECT COUNT(*) as count FROM image_stats`) 243 .get() as { count: number }; 244const dayBuckets = db 245 .prepare(`SELECT COUNT(*) as count FROM image_stats_daily`) 246 .get() as { count: number }; 247const tenMinBuckets = db 248 .prepare(`SELECT COUNT(*) as count FROM image_stats_10min`) 249 .get() as { count: number }; 250const oldestHit = db 251 .prepare(`SELECT MIN(bucket_hour) as min FROM image_stats`) 252 .get() as { min: number }; 253const newestHit = db 254 .prepare(`SELECT MAX(bucket_hour) as max FROM image_stats`) 255 .get() as { max: number }; 256 257console.log("\nSeeding complete!"); 258console.log(`- Total hits (hourly): ${totalHitsHourly.total.toLocaleString()}`); 259console.log(`- Total hits (daily): ${totalHitsDaily.total.toLocaleString()}`); 260console.log(`- Total hits (10-min): ${totalHits10Min.total.toLocaleString()}`); 261console.log(`- Unique images: ${uniqueImages.count}`); 262console.log(`- Hourly buckets: ${hourBuckets.count.toLocaleString()}`); 263console.log(`- Daily buckets: ${dayBuckets.count.toLocaleString()}`); 264console.log(`- 10-minute buckets: ${tenMinBuckets.count.toLocaleString()}`); 265console.log( 266 `- Time range: ${new Date(oldestHit.min * 1000).toISOString()} to ${new Date(newestHit.max * 1000).toISOString()}`, 267); 268console.log( 269 `- Days of data: ${Math.floor((newestHit.max - oldestHit.min) / 86400)}`, 270);