/** * Instances Route Handler * * Endpoint for querying instance types with filtering, sorting, and pagination. * Integrates with cache service for performance optimization. */ import type { Env, InstanceQueryParams } from '../types'; import { QueryService } from '../services/query'; import { getGlobalCacheService } from '../services/cache'; import { logger } from '../utils/logger'; import { SUPPORTED_PROVIDERS, type SupportedProvider, VALID_SORT_FIELDS, INSTANCE_FAMILIES, PAGINATION, CACHE_TTL, HTTP_STATUS, } from '../constants'; /** * Worker-level service singleton cache * Performance optimization: Reuse service instances across requests within same Worker instance * * Benefits: * - Reduces GC pressure by avoiding object creation per request * - Maintains service state (e.g., logger initialization) across requests * - Safe for Cloudflare Workers as Worker instances are isolated and stateless * * Note: Worker instances are recreated periodically, preventing memory leaks */ let cachedQueryService: QueryService | null = null; let cachedDb: D1Database | null = null; /** * Get or create QueryService singleton * Lazy initialization on first request, then reused for subsequent requests * Invalidates cache if database binding changes (rolling deploy scenario) */ function getQueryService(db: D1Database, env: Env): QueryService { // Invalidate cache if db binding changed (rolling deploy scenario) if (!cachedQueryService || cachedDb !== db) { cachedQueryService = new QueryService(db, env); cachedDb = db; logger.debug('[Instances] QueryService singleton initialized/refreshed'); } return cachedQueryService; } /** * Parsed and validated query parameters */ interface ParsedQueryParams { provider?: string; region?: string; min_vcpu?: number; max_vcpu?: number; min_memory_gb?: number; max_memory_gb?: number; max_price?: number; instance_family?: string; has_gpu?: boolean; sort_by?: string; order?: 'asc' | 'desc'; limit: number; offset: number; } /** * Validate provider name */ function isSupportedProvider(provider: string): provider is SupportedProvider { return SUPPORTED_PROVIDERS.includes(provider as SupportedProvider); } /** * Validate sort field */ function isValidSortField(field: string): boolean { return VALID_SORT_FIELDS.includes(field as typeof VALID_SORT_FIELDS[number]); } /** * Validate instance family */ function isValidFamily(family: string): boolean { return INSTANCE_FAMILIES.includes(family as typeof INSTANCE_FAMILIES[number]); } /** * Parse and validate query parameters */ function parseQueryParams(url: URL): { params?: ParsedQueryParams; error?: { code: string; message: string; parameter?: string }; } { const searchParams = url.searchParams; const params: ParsedQueryParams = { limit: PAGINATION.DEFAULT_LIMIT, offset: PAGINATION.DEFAULT_OFFSET, }; // Provider validation const provider = searchParams.get('provider'); if (provider !== null) { if (!isSupportedProvider(provider)) { return { error: { code: 'INVALID_PARAMETER', message: `Invalid provider: ${provider}. Supported providers: ${SUPPORTED_PROVIDERS.join(', ')}`, parameter: 'provider', }, }; } params.provider = provider; } // Region (no validation, passed as-is) const region = searchParams.get('region'); if (region !== null) { params.region = region; } // Numeric parameter validation helper function parsePositiveNumber( name: string, value: string | null ): number | undefined | { error: { code: string; message: string; parameter: string } } { if (value === null) return undefined; const parsed = Number(value); if (isNaN(parsed) || parsed < 0) { return { error: { code: 'INVALID_PARAMETER', message: `Invalid value for ${name}: must be a positive number`, parameter: name, }, }; } return parsed; } // Parse min_vcpu const minVcpuResult = parsePositiveNumber('min_vcpu', searchParams.get('min_vcpu')); if (minVcpuResult && typeof minVcpuResult === 'object' && 'error' in minVcpuResult) { return minVcpuResult; } if (typeof minVcpuResult === 'number') { params.min_vcpu = minVcpuResult; } // Parse max_vcpu const maxVcpuResult = parsePositiveNumber('max_vcpu', searchParams.get('max_vcpu')); if (maxVcpuResult && typeof maxVcpuResult === 'object' && 'error' in maxVcpuResult) { return maxVcpuResult; } if (typeof maxVcpuResult === 'number') { params.max_vcpu = maxVcpuResult; } // Parse min_memory_gb const minMemoryResult = parsePositiveNumber('min_memory_gb', searchParams.get('min_memory_gb')); if (minMemoryResult && typeof minMemoryResult === 'object' && 'error' in minMemoryResult) { return minMemoryResult; } if (typeof minMemoryResult === 'number') { params.min_memory_gb = minMemoryResult; } // Parse max_memory_gb const maxMemoryResult = parsePositiveNumber('max_memory_gb', searchParams.get('max_memory_gb')); if (maxMemoryResult && typeof maxMemoryResult === 'object' && 'error' in maxMemoryResult) { return maxMemoryResult; } if (typeof maxMemoryResult === 'number') { params.max_memory_gb = maxMemoryResult; } // Parse max_price const maxPriceResult = parsePositiveNumber('max_price', searchParams.get('max_price')); if (maxPriceResult && typeof maxPriceResult === 'object' && 'error' in maxPriceResult) { return maxPriceResult; } if (typeof maxPriceResult === 'number') { params.max_price = maxPriceResult; } // Instance family validation const family = searchParams.get('instance_family'); if (family !== null) { if (!isValidFamily(family)) { return { error: { code: 'INVALID_PARAMETER', message: `Invalid instance_family: ${family}. Valid values: ${INSTANCE_FAMILIES.join(', ')}`, parameter: 'instance_family', }, }; } params.instance_family = family; } // GPU filter (boolean) const hasGpu = searchParams.get('has_gpu'); if (hasGpu !== null) { if (hasGpu !== 'true' && hasGpu !== 'false') { return { error: { code: 'INVALID_PARAMETER', message: 'Invalid value for has_gpu: must be "true" or "false"', parameter: 'has_gpu', }, }; } params.has_gpu = hasGpu === 'true'; } // Sort by validation const sortBy = searchParams.get('sort_by'); if (sortBy !== null) { if (!isValidSortField(sortBy)) { return { error: { code: 'INVALID_PARAMETER', message: `Invalid sort_by: ${sortBy}. Valid values: ${VALID_SORT_FIELDS.join(', ')}`, parameter: 'sort_by', }, }; } params.sort_by = sortBy; } // Sort order validation const order = searchParams.get('order'); if (order !== null) { if (order !== 'asc' && order !== 'desc') { return { error: { code: 'INVALID_PARAMETER', message: 'Invalid order: must be "asc" or "desc"', parameter: 'order', }, }; } params.order = order; } // Limit validation const limitStr = searchParams.get('limit'); if (limitStr !== null) { const limit = Number(limitStr); if (isNaN(limit) || limit < 1 || limit > PAGINATION.MAX_LIMIT) { return { error: { code: 'INVALID_PARAMETER', message: `Invalid limit: must be between 1 and ${PAGINATION.MAX_LIMIT}`, parameter: 'limit', }, }; } params.limit = limit; } // Offset validation const offsetStr = searchParams.get('offset'); if (offsetStr !== null) { const offset = Number(offsetStr); if (isNaN(offset) || offset < 0) { return { error: { code: 'INVALID_PARAMETER', message: 'Invalid offset: must be a non-negative number', parameter: 'offset', }, }; } params.offset = offset; } // Range consistency validation if (params.min_vcpu !== undefined && params.max_vcpu !== undefined) { if (params.min_vcpu > params.max_vcpu) { return { error: { code: 'INVALID_RANGE', message: 'min_vcpu cannot be greater than max_vcpu', parameter: 'min_vcpu', }, }; } } if (params.min_memory_gb !== undefined && params.max_memory_gb !== undefined) { if (params.min_memory_gb > params.max_memory_gb) { return { error: { code: 'INVALID_RANGE', message: 'min_memory_gb cannot be greater than max_memory_gb', parameter: 'min_memory_gb', }, }; } } return { params }; } /** * Handle GET /instances endpoint * * @param request - HTTP request object * @param env - Cloudflare Worker environment bindings * @returns JSON response with instance query results * * @example * GET /instances?provider=linode&min_vcpu=2&max_price=20&sort_by=price&order=asc&limit=50 */ export async function handleInstances( request: Request, env: Env ): Promise { const startTime = Date.now(); logger.info('[Instances] Request received', { url: request.url }); try { // Parse URL and query parameters const url = new URL(request.url); const parseResult = parseQueryParams(url); // Handle validation errors if (parseResult.error) { logger.error('[Instances] Validation error', parseResult.error); return Response.json( { success: false, error: parseResult.error, }, { status: HTTP_STATUS.BAD_REQUEST } ); } const params = parseResult.params!; logger.info('[Instances] Query params validated', params as unknown as Record); // Get global cache service singleton (shared across all routes) const cacheService = getGlobalCacheService(CACHE_TTL.INSTANCES, env.RATE_LIMIT_KV); // Generate cache key from query parameters const cacheKey = cacheService.generateKey(params as unknown as Record); logger.info('[Instances] Cache key generated', { cacheKey }); // Check cache first interface CachedData { instances: unknown[]; pagination: { total: number; limit: number; offset: number; has_more: boolean; }; metadata: { cached: boolean; last_sync: string; query_time_ms: number; filters_applied: unknown; }; } const cached = await cacheService.get(cacheKey); if (cached) { logger.info('[Instances] Cache hit', { cacheKey, age: cached.cache_age_seconds, }); return Response.json( { success: true, data: { ...cached.data, metadata: { ...cached.data.metadata, cached: true, cache_age_seconds: cached.cache_age_seconds, cached_at: cached.cached_at, }, }, }, { status: HTTP_STATUS.OK, headers: { 'Cache-Control': `public, max-age=${CACHE_TTL.INSTANCES}`, }, } ); } logger.info('[Instances] Cache miss'); // Map route parameters to QueryService parameters const queryParams: InstanceQueryParams = { provider: params.provider, region_code: params.region, family: params.instance_family as 'general' | 'compute' | 'memory' | 'storage' | 'gpu' | undefined, min_vcpu: params.min_vcpu, max_vcpu: params.max_vcpu, min_memory: params.min_memory_gb ? params.min_memory_gb * 1024 : undefined, // Convert GB to MB max_memory: params.max_memory_gb ? params.max_memory_gb * 1024 : undefined, // Convert GB to MB min_price: undefined, // Route doesn't expose min_price max_price: params.max_price, has_gpu: params.has_gpu, sort_by: params.sort_by, sort_order: params.order, page: Math.floor(params.offset / params.limit) + 1, // Convert offset to page limit: params.limit, }; // Get QueryService singleton (reused across requests) const queryService = getQueryService(env.DB, env); const result = await queryService.queryInstances(queryParams); const queryTime = Date.now() - startTime; logger.info('[Instances] Query executed', { queryTime, results: result.data.length, total: result.pagination.total_results, }); // Prepare response data const responseData = { instances: result.data, pagination: { total: result.pagination.total_results, limit: params.limit, offset: params.offset, has_more: result.pagination.has_next, }, metadata: { cached: false, last_sync: new Date().toISOString(), query_time_ms: queryTime, filters_applied: result.meta.filters_applied, }, }; // Store result in cache try { await cacheService.set(cacheKey, responseData, CACHE_TTL.INSTANCES); } catch (error) { // Graceful degradation: log error but don't fail the request logger.error('[Instances] Cache write failed', error instanceof Error ? { message: error.message } : { error: String(error) }); } return Response.json( { success: true, data: responseData, }, { status: HTTP_STATUS.OK, headers: { 'Cache-Control': `public, max-age=${CACHE_TTL.INSTANCES}`, }, } ); } catch (error) { logger.error('[Instances] Unexpected error', { error }); return Response.json( { success: false, error: { code: 'QUERY_FAILED', message: 'Instance query failed. Please try again later.', request_id: crypto.randomUUID(), }, }, { status: HTTP_STATUS.INTERNAL_ERROR } ); } }