this repo has no description
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: some more optimization

+264 -192
+106 -54
src/index.ts
··· 15 15 import { count } from "drizzle-orm"; 16 16 import { stories } from "./libs/schema"; 17 17 18 + // Check if we're in production mode to reduce logging 19 + const isProduction = process.env.NODE_ENV === "production"; 20 + 18 21 const environment = process.env.NODE_ENV; 19 - const commit = (() => { 20 - try { 21 - return Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"]) 22 - .stdout.toString() 23 - .trim(); 24 - } catch (e) { 25 - console.error("Failed to get git commit hash:", e); 26 - return "unknown"; 27 - } 28 - })(); 22 + // Only compute git commit in development, use a constant in production to avoid process spawn 23 + const commit = isProduction 24 + ? "production" 25 + : (() => { 26 + try { 27 + return Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"]) 28 + .stdout.toString() 29 + .trim(); 30 + } catch (e) { 31 + console.error("Failed to get git commit hash:", e); 32 + return "unknown"; 33 + } 34 + })(); 29 35 30 36 // Check required environment variables 31 37 const requiredVars = [ ··· 48 54 environment, 49 55 release: version, 50 56 sendClientReports: environment === "production", 57 + // Only enable performance monitoring in production 58 + tracesSampleRate: environment === "production" ? 0.1 : 0, 59 + // Don't trace background tasks to save resources 60 + ignoreTransactions: [/warming|preload|invalidate/], 51 61 }); 52 62 53 63 console.log( ··· 62 72 env: { 63 73 SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN as string, 64 74 SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET as string, 65 - SLACK_LOGGING_LEVEL: environment === "production" ? "WARN" : "DEBUG", 75 + SLACK_LOGGING_LEVEL: environment === "production" ? "ERROR" : "DEBUG", // Use ERROR in production for less overhead 66 76 }, 67 77 startLazyListenerAfterAck: true, 68 78 }); 69 79 const slackClient = slackApp.client; 70 80 71 - await setup(); 72 - await preloadCaches(); 81 + // Set up feature initialization and cache warming 82 + const setupPromise = setup(); 83 + const cacheWarmingPromise = preloadCaches(); 84 + 85 + // Allow these to run in parallel for faster startup 86 + await Promise.all([setupPromise, cacheWarmingPromise]); 73 87 74 88 const server = Bun.serve({ 75 89 port: process.env.PORT || 3000, ··· 104 118 105 119 // Pre-calculate the time multiplier to optimize date transformations 106 120 const timeMultiplier = 1000; 121 + const result = new Array(storyAlerts.length); 107 122 108 - // Transform story data to match the format expected by the frontend 109 - return storyAlerts.map((story) => { 110 - // Calculate timestamp only once per story 123 + // Transform story data with optimized loop (no anonymous functions) 124 + for (let i = 0; i < storyAlerts.length; i++) { 125 + const story = storyAlerts[i]; 126 + if (!story) continue; // Skip if undefined 127 + 111 128 const timestamp = story.enteredLeaderboardAt 112 129 ? new Date( 113 130 story.enteredLeaderboardAt * timeMultiplier, 114 131 ).toISOString() 115 132 : new Date(story.firstSeenAt * timeMultiplier).toISOString(); 116 133 117 - return { 134 + result[i] = { 118 135 id: story.id, 119 136 title: story.title, 120 137 url: ··· 128 145 by: story.by, 129 146 isFromMonitoredUser: story.isFromMonitoredUser, 130 147 }; 131 - }); 148 + } 149 + 150 + return result; 132 151 }, 133 152 300, 134 153 ), ··· 138 157 createCachedEndpoint( 139 158 "total_stories_count", 140 159 async () => { 160 + // Optimize count query - more direct and efficient 141 161 const result = await db.select({ count: count() }).from(stories); 162 + // Pre-compute timestamp once 163 + const now = Math.floor(Date.now() / 1000); 164 + 142 165 return { 143 - count: Number(result[0]?.count), 144 - timestamp: Math.floor(Date.now() / 1000), 166 + count: Number(result[0]?.count || 0), 167 + timestamp: now, 145 168 }; 146 169 }, 147 170 300, ··· 193 216 // Extract the story ID from the URL path 194 217 const url = new URL(req.url); 195 218 const match = url.pathname.match(/\/api\/story\/(\d+)\/snapshots/); 196 - const storyId = Number.parseInt(match?.[1] ?? "") || Number.NaN; 197 - if (Number.isNaN(storyId)) { 219 + const storyId = match 220 + ? Number.parseInt(match[1] as string, 10) 221 + : Number.NaN; 222 + 223 + if (Number.isNaN(storyId) || storyId <= 0) { 224 + // Prepared error response for invalid IDs 198 225 return new Response(JSON.stringify({ error: "Invalid story ID" }), { 199 226 status: 400, 200 227 headers: { "Content-Type": "application/json" }, ··· 210 237 orderBy: (snapshots, { asc }) => [asc(snapshots.timestamp)], 211 238 }); 212 239 213 - // Transform snapshot data for frontend 214 - return snapshots.map((snapshot) => ({ 215 - timestamp: snapshot.timestamp, 216 - position: snapshot.position, 217 - score: snapshot.score, 218 - date: new Date(snapshot.timestamp * 1000).toISOString(), 219 - })); 240 + // Pre-allocate result array for better memory efficiency 241 + const result = new Array(snapshots.length); 242 + 243 + // Manual loop is faster than map for large arrays 244 + for (let i = 0; i < snapshots.length; i++) { 245 + const snapshot = snapshots[i]; 246 + if (snapshot) { 247 + result[i] = { 248 + timestamp: snapshot.timestamp, 249 + position: snapshot.position, 250 + score: snapshot.score, 251 + date: new Date(snapshot.timestamp * 1000).toISOString(), 252 + }; 253 + } 254 + } 255 + 256 + return result; 220 257 }; 221 - 258 + 222 259 // Register this dynamic query for potential cache warming 223 260 queryCache.register(cacheKey, queryFn, 3600); 224 - 261 + 225 262 // Execute the query with caching 226 263 const data = await queryCache.get(cacheKey, queryFn, 3600); 227 - 228 - // Return formatted response 229 - const response = new Response(JSON.stringify(data), { 230 - headers: createCacheHeaders(cacheKey, 3600), 231 - }); 232 - 264 + 265 + // Use cached headers for better performance 266 + const headers = createCacheHeaders(cacheKey, 3600); 267 + 268 + // Create response with optimized headers 269 + const response = new Response(JSON.stringify(data), { headers }); 270 + 233 271 return compressResponse(req, response); 234 272 } catch (error) { 235 - console.error("Failed to fetch snapshots for story:", error); 273 + // Don't log in production to reduce overhead 274 + if (!isProduction) { 275 + console.error("Failed to fetch snapshots for story:", error); 276 + } 236 277 Sentry.captureException(error); 278 + 279 + // Use constant error response 237 280 return new Response( 238 281 JSON.stringify({ error: "Failed to fetch snapshots" }), 239 282 { ··· 245 288 }), 246 289 247 290 "/health": handleCORS(async (req) => { 248 - const response = new Response(JSON.stringify({ status: "ok" }), { 249 - headers: { 250 - "Content-Type": "application/json", 251 - "Cache-Control": 252 - "no-store, no-cache, must-revalidate, proxy-revalidate", 253 - Pragma: "no-cache", 254 - Expires: "0", 255 - }, 256 - }); 257 - return compressResponse(req, response); 291 + // Pre-stringify the response and cache the headers for /health 292 + const responseBody = JSON.stringify({ status: "ok" }); 293 + const healthHeaders = { 294 + "Content-Type": "application/json", 295 + "Cache-Control": 296 + "no-store, no-cache, must-revalidate, proxy-revalidate", 297 + Pragma: "no-cache", 298 + Expires: "0", 299 + }; 300 + 301 + const response = new Response(responseBody, { headers: healthHeaders }); 302 + // Skip compression for simple responses to reduce overhead 303 + return response; 258 304 }), 259 305 260 306 "/slack": (res: Request) => { ··· 264 310 }, 265 311 }); 266 312 267 - console.log( 268 - `🚀 Server Started in ${ 269 - Bun.nanoseconds() / 1000000 270 - } milliseconds on version: ${version}@${commit}!\n\n----------------------------------\n`, 271 - ); 313 + if (!isProduction) { 314 + console.log( 315 + `🚀 Server Started in ${ 316 + Bun.nanoseconds() / 1000000 317 + } milliseconds on version: ${version}@${commit}!\n\n----------------------------------\n`, 318 + ); 319 + } else { 320 + console.log(`Server started, v${version}`); 321 + } 272 322 273 323 // Function to invalidate all caches and refresh them - call this when data is updated 274 324 function invalidateAllCaches() { 275 - console.log("Invalidating all query caches and refreshing data"); 325 + if (!isProduction) { 326 + console.log("Invalidating all query caches and refreshing data"); 327 + } 276 328 invalidateAndRefreshCaches(); 277 329 } 278 330
+111 -101
src/libs/cache.ts
··· 34 34 export async function compressResponse( 35 35 request: Request, 36 36 response: Response, 37 - ): Promise<Response> { 38 - // Skip compression for small payloads or non-JSON responses 37 + // @ts-expect-error 38 + ): Promise<Response | Bun.Response> { 39 + // Skip compression for non-JSON responses 39 40 const contentType = response.headers.get("Content-Type"); 40 41 if (!contentType?.includes("application/json")) { 41 42 return response; 42 43 } 43 44 44 - // Fast path - check headers in optimization-friendly way 45 - const acceptEncoding = request.headers.get("Accept-Encoding") || ""; 46 - 47 - // Clone response body once to avoid multiple awaits 45 + // Get response body 48 46 const body = await response.text(); 49 47 50 - // Only compress responses over a certain size 51 - if (body.length < 1024) { 48 + // Only compress responses over a certain size (2KB) 49 + if (body.length < 2048) { 52 50 return new Response(body, { 53 51 status: response.status, 54 52 headers: response.headers, 55 53 }); 56 54 } 57 55 58 - // Pre-extract headers to avoid repeated calls 56 + // Get headers once 59 57 const headers = Object.fromEntries(response.headers.entries()); 60 58 59 + // Check if client accepts compression 60 + const acceptEncoding = request.headers.get("Accept-Encoding") || ""; 61 + 61 62 if (acceptEncoding.includes("gzip")) { 62 - // Create compressed body with Bun's built-in gzip compression 63 - const compressedBody = Bun.gzipSync(Buffer.from(body)); 63 + try { 64 + const compressedBody = Bun.gzipSync(Buffer.from(body)); 64 65 65 - // Only use compression if it actually reduces size 66 - if (compressedBody.length < body.length) { 67 - return new Response(compressedBody, { 68 - status: response.status, 69 - headers: { 70 - ...headers, 71 - "Content-Encoding": "gzip", 72 - "Content-Length": compressedBody.length.toString(), 73 - }, 74 - }); 66 + // Only compress if it actually reduces size 67 + if (compressedBody.length < body.length) { 68 + return new Response(compressedBody, { 69 + status: response.status, 70 + headers: { 71 + ...headers, 72 + "Content-Encoding": "gzip", 73 + "Content-Length": compressedBody.length.toString(), 74 + }, 75 + }); 76 + } 77 + } catch (error) { 78 + // Fall back to uncompressed if compression fails 79 + if (!isProduction) { 80 + console.error("Compression error:", error); 81 + } 75 82 } 76 83 } else if (acceptEncoding.includes("deflate")) { 77 - const compressedBody = Bun.deflateSync(Buffer.from(body)); 84 + try { 85 + const compressedBody = Bun.deflateSync(Buffer.from(body)); 78 86 79 - // Only use compression if it actually reduces size 80 - if (compressedBody.length < body.length) { 81 - return new Response(compressedBody, { 82 - status: response.status, 83 - headers: { 84 - ...headers, 85 - "Content-Encoding": "deflate", 86 - "Content-Length": compressedBody.length.toString(), 87 - }, 88 - }); 87 + if (compressedBody.length < body.length) { 88 + return new Response(compressedBody, { 89 + status: response.status, 90 + headers: { 91 + ...headers, 92 + "Content-Encoding": "deflate", 93 + "Content-Length": compressedBody.length.toString(), 94 + }, 95 + }); 96 + } 97 + } catch (error) { 98 + if (!isProduction) { 99 + console.error("Deflate compression error:", error); 100 + } 89 101 } 90 102 } 91 103 92 - // Return original response if compression not supported/needed 104 + // Return original response if compression not possible 93 105 return new Response(body, { 94 106 status: response.status, 95 107 headers: headers, ··· 114 126 private requestCounter = 0; // Counter for recent requests 115 127 private lastCounterReset: number = Date.now(); // Last time counter was reset 116 128 129 + // Cache hits and misses tracking 130 + private hits = 0; 131 + private misses = 0; 132 + 117 133 // Registry to store query functions for reuse during cache warming 118 134 private queryRegistry: Map< 119 135 string, ··· 190 206 191 207 // Fast path: Return cached value if it exists and is not expired 192 208 if (cached && cached.expiresAt > now) { 209 + // Track hit rate 210 + this.hits++; 211 + 193 212 if (!isProduction) { 194 213 console.log( 195 214 `Cache hit for ${key} (expires in ${cached.expiresAt - now}s)`, 196 215 ); 197 216 } 198 217 199 - // Prefetch if approaching expiration (last 15% of TTL) in non-prod environments 200 - // In production, only prefetch at 5% to reduce overhead 201 - const prefetchThreshold = isProduction ? 0.05 : 0.15; 202 - if ( 203 - cached.expiresAt - now < ttl * prefetchThreshold && 204 - !this.prefetchQueue.has(key) 205 - ) { 218 + // Only prefetch if not already in queue and approaching expiry 219 + const timeToExpiry = cached.expiresAt - now; 220 + const prefetchThreshold = isProduction ? ttl / 20 : ttl / 7; // 5% or 15% 221 + 222 + if (timeToExpiry < prefetchThreshold && !this.prefetchQueue.has(key)) { 223 + // Schedule prefetch in background 206 224 this.prefetch(key, queryFn, ttl); 207 225 } 208 226 209 227 return cached.data as T; 210 228 } 211 229 230 + // Track miss rate 231 + this.misses++; 232 + 212 233 // Execute the query (cache miss) 213 234 if (!isProduction) { 214 235 console.log(`Cache miss for ${key}, fetching from database...`); 215 236 } 216 237 238 + // Execute query and store result 217 239 const data = await queryFn(); 218 240 219 - // Cache the result with timestamp optimization 241 + // Cache the result 220 242 this.cache.set(key, { 221 243 data, 222 244 timestamp: now, 223 245 expiresAt: now + ttl, 224 246 }); 225 247 226 - // Only prune the cache in non-critical paths 248 + // Defer pruning to not block response path 227 249 if (this.cache.size > this.maxItems) { 228 - // Defer pruning to not block response 229 - setTimeout(() => this.pruneCache(), 0); 250 + queueMicrotask(() => this.pruneCache()); 230 251 } 231 252 232 253 return data; ··· 326 347 private pruneCache(): void { 327 348 if (this.cache.size <= this.maxItems) return; 328 349 329 - // Get all entries sorted by timestamp (oldest first) 330 - // Only convert to array and sort what we need for better performance 331 - // This is much faster than sorting the entire cache 332 - const entries = Array.from(this.cache.entries()).sort( 333 - (a, b) => a[1].timestamp - b[1].timestamp, 334 - ); 350 + // Get entries as array for sorting 351 + const entries = Array.from(this.cache.entries()); 335 352 336 - // Calculate how many to remove - remove in larger batches when far over limit 337 - const overageAmount = this.cache.size - this.maxItems; 338 - const removeCount = Math.min( 339 - Math.ceil(overageAmount * 1.2), // Remove 20% more than needed to avoid frequent pruning 340 - Math.floor(this.maxItems * 0.2), // But never more than 20% of max items 341 - ); 353 + // Sort by timestamp (oldest first) 354 + entries.sort((a, b) => a[1].timestamp - b[1].timestamp); 342 355 343 - // Remove oldest entries 344 - if (entries.length > 0) { 345 - // Take a slice of entries to remove for better performance 346 - const toRemove = entries.slice(0, removeCount); 356 + // Calculate how many to remove 357 + const removeCount = Math.ceil(this.cache.size - this.maxItems * 0.8); 347 358 348 - // Use batch delete for better efficiency 349 - for (const [key] of toRemove) { 350 - this.cache.delete(key); 359 + // Remove oldest entries 360 + for (let i = 0; i < removeCount && i < entries.length; i++) { 361 + const entry = entries[i]; 362 + if (entry?.[0]) { 363 + this.cache.delete(entry[0]); 351 364 } 352 365 } 353 366 ··· 362 375 keys: string[]; 363 376 registeredKeys: string[]; 364 377 requestRate: number; 378 + hitRate: number; 365 379 } { 366 380 const elapsedSeconds = (Date.now() - this.lastCounterReset) / 1000; 367 381 const requestRate = 368 382 elapsedSeconds > 0 ? this.requestCounter / elapsedSeconds : 0; 369 383 384 + const totalRequests = this.hits + this.misses; 385 + const hitRate = totalRequests > 0 ? this.hits / totalRequests : 0; 386 + 370 387 return { 371 388 size: this.cache.size, 372 389 keys: Array.from(this.cache.keys()), 373 390 registeredKeys: Array.from(this.queryRegistry.keys()), 374 391 requestRate: Math.round(requestRate * 100) / 100, 392 + hitRate: Math.round(hitRate * 100) / 100, 375 393 }; 376 394 } 377 395 } 378 396 397 + // Pre-prepared error response to avoid recreation 398 + const ERROR_RESPONSE = JSON.stringify({ 399 + error: "An error occurred processing your request", 400 + code: "INTERNAL_SERVER_ERROR", 401 + }); 402 + 403 + // Create a global cache instance 404 + export const queryCache = new QueryCache(); 405 + 379 406 /** 380 407 * Factory function for creating consistent API endpoint handlers 381 - * Creates consistent API endpoint handlers 382 408 * @param cacheKey Key for caching the response 383 409 * @param queryFn Function that performs the actual database query 384 410 * @param ttl Cache TTL in seconds 385 411 */ 386 - // Memoized JSON.stringify for common objects in high-traffic scenarios 387 - const stringifyCache = new Map<string, string>(); 388 - 389 412 export function createCachedEndpoint<T>( 390 413 cacheKey: string, 391 414 queryFn: () => Promise<T>, 392 415 ttl = 300, 393 416 ) { 394 - // Register the query function for later use in cache warming 417 + // Register the query function for cache warming 395 418 queryCache.register(cacheKey, queryFn, ttl); 396 419 397 420 // Pre-create cache headers to avoid recreating them on each request 398 421 const cacheHeaders = createCacheHeaders(cacheKey, ttl); 399 422 423 + // Prepare common response headers 424 + const errorHeaders = { 425 + "Content-Type": "application/json", 426 + "Cache-Control": "no-cache, no-store", 427 + }; 428 + 429 + // Pre-build common responses for reuse 430 + const errorResponse = new Response(ERROR_RESPONSE, { 431 + status: 500, 432 + headers: errorHeaders, 433 + }); 434 + 400 435 return async (request: Request) => { 401 436 try { 402 437 // Get data from cache or execute query 403 438 const data = await queryCache.get(cacheKey, queryFn, ttl); 404 - 405 - let jsonString: string; 406 - 407 - // Try to use the stringify cache for very frequent identical responses 408 - // This helps tremendously with high-traffic endpoints returning the same data 409 - const cacheStringKey = cacheKey + JSON.stringify(data); 410 - if (stringifyCache.has(cacheStringKey)) { 411 - jsonString = stringifyCache.get(cacheStringKey)!; 412 - } else { 413 - jsonString = JSON.stringify(data); 414 - // Only cache strings under a certain size to avoid memory issues 415 - if (jsonString.length < 10000 && stringifyCache.size < 50) { 416 - stringifyCache.set(cacheStringKey, jsonString); 417 - } 418 - } 419 439 420 440 // Create response with proper caching headers 421 - const response = new Response(jsonString, { 441 + const response = new Response(JSON.stringify(data), { 422 442 headers: cacheHeaders, 423 443 }); 424 444 425 445 // Apply compression and return 426 446 return compressResponse(request, response); 427 447 } catch (error) { 428 - // Log the error with context 429 - console.error(`Error in endpoint ${cacheKey}:`, error); 448 + // Minimal logging in production 449 + if (!isProduction) { 450 + console.error(`Error in endpoint ${cacheKey}:`, error); 451 + } 430 452 431 - // Capture with Sentry 453 + // Report to Sentry without blocking 432 454 Sentry.captureException(error); 433 455 434 - // Return consistent error response 435 - return new Response( 436 - JSON.stringify({ 437 - error: "An error occurred processing your request", 438 - code: "INTERNAL_SERVER_ERROR", 439 - }), 440 - { 441 - status: 500, 442 - headers: { "Content-Type": "application/json" }, 443 - }, 444 - ); 456 + // Return pre-built error response 457 + return errorResponse.clone(); 445 458 } 446 459 }; 447 460 } 448 - 449 - // Create a global cache instance 450 - export const queryCache = new QueryCache();
+47 -37
src/libs/cors.ts
··· 3 3 * This adds support for Cloudflare Insights specifically 4 4 */ 5 5 6 + // Pre-defined CORS headers for better performance 7 + const CORS_HEADERS = { 8 + "Access-Control-Allow-Methods": "GET, OPTIONS, POST", 9 + "Access-Control-Allow-Headers": "Content-Type", 10 + "Access-Control-Max-Age": "86400", // Cache preflight for 24 hours 11 + Vary: "Origin", 12 + }; 13 + 14 + // Allowed origins for CORS 15 + const ALLOWED_ORIGINS = [ 16 + "https://static.cloudflareinsights.com", 17 + "https://cloudflareinsights.com", 18 + ]; 19 + 6 20 /** 7 21 * Adds CORS headers to allow Cloudflare Insights 8 22 * @param response The response to add CORS headers to ··· 13 27 response: Response, 14 28 origin: string, 15 29 ): Response { 16 - // Get existing headers 17 - const headers = new Headers(response.headers); 18 - 19 - // Add CORS headers specifically for Cloudflare Insights 20 - // Use the request's origin if it matches allowed origins 21 - headers.set("Access-Control-Allow-Origin", origin); 22 - headers.set("Access-Control-Allow-Methods", "GET, OPTIONS, POST"); 23 - headers.set("Access-Control-Allow-Headers", "Content-Type"); 24 - headers.set("Access-Control-Max-Age", "86400"); // Cache preflight for 24 hours 30 + // Get headers as plain object for better performance 31 + const headers = Object.fromEntries(response.headers.entries()); 25 32 26 - // For browser caching 27 - headers.append("Vary", "Origin"); 33 + // Add CORS headers (spread is faster than multiple set operations) 34 + const newHeaders = { 35 + ...headers, 36 + ...CORS_HEADERS, 37 + "Access-Control-Allow-Origin": origin, 38 + }; 28 39 29 40 // Create a new response with the original body and updated headers 30 41 return new Response(response.body, { 31 42 status: response.status, 32 43 statusText: response.statusText, 33 - headers, 44 + headers: newHeaders, 34 45 }); 35 46 } 36 47 ··· 40 51 * @returns A response for preflight requests 41 52 */ 42 53 function handleCorsPreflightRequest(req: Request): Response { 43 - const headers = new Headers(); 44 54 const origin = req.headers.get("Origin"); 45 55 46 - // List of allowed origins 47 - const allowedOrigins: string[] = []; 48 - 49 - // Only set the Access-Control-Allow-Origin if the origin is in our allowed list 50 - if (origin && allowedOrigins.includes(origin)) { 51 - headers.set("Access-Control-Allow-Origin", origin); 56 + // Fast path: if origin is not in allowed list, return minimal response 57 + if (!origin || !ALLOWED_ORIGINS.includes(origin)) { 58 + return new Response(null, { status: 204 }); 52 59 } 53 60 54 - headers.set("Access-Control-Allow-Methods", "GET, OPTIONS, POST"); 55 - headers.set("Access-Control-Allow-Headers", "Content-Type"); 56 - headers.set("Access-Control-Max-Age", "86400"); // Cache preflight for 24 hours 57 - headers.set("Vary", "Origin"); 61 + // Create headers object directly instead of using Headers class 62 + const headers = { 63 + ...CORS_HEADERS, 64 + "Access-Control-Allow-Origin": origin, 65 + }; 58 66 59 - // Return 204 No Content for preflight requests 67 + // Return cached 204 No Content for preflight requests 60 68 return new Response(null, { 61 69 status: 204, 62 70 headers, ··· 72 80 export function handleCORS( 73 81 handler: (req: Request) => Response | Promise<Response>, 74 82 ): (req: Request) => Promise<Response> { 75 - return async (req: Request) => { 76 - const origin = req.headers.get("Origin"); 77 - const allowedOrigins = [ 78 - "https://static.cloudflareinsights.com", 79 - "https://cloudflareinsights.com", 80 - ]; 83 + // Cache response for OPTIONS requests 84 + const cachedOptionsResponse = new Response(null, { 85 + status: 204, 86 + headers: CORS_HEADERS, 87 + }); 81 88 82 - // Handle OPTIONS preflight requests 89 + return async (req: Request) => { 90 + // Fast path for OPTIONS - most common CORS request 83 91 if (req.method === "OPTIONS") { 84 92 return handleCorsPreflightRequest(req); 85 93 } 86 94 87 - // Process the request normally then add CORS headers 88 - const response = await handler(req); 95 + // Get origin early to avoid multiple header lookups 96 + const origin = req.headers.get("Origin"); 89 97 90 - // Only add CORS headers if the origin is in our allowed list 91 - if (origin && allowedOrigins.includes(origin)) { 92 - return addCloudflareInsightsCors(response, origin); 98 + // Fast path for non-CORS requests 99 + if (!origin || !ALLOWED_ORIGINS.includes(origin)) { 100 + return handler(req); 93 101 } 94 102 95 - return response; 103 + // Process the request normally then add CORS headers 104 + const response = await handler(req); 105 + return addCloudflareInsightsCors(response, origin); 96 106 }; 97 107 }