Skip to main content
Glama
HttpCacheWrapper.ts10.2 kB
/** * HTTP-level caching wrapper with ETags and Cache-Control headers * Implements WordPress REST API caching best practices */ import { CacheManager, CachePresets } from "./CacheManager.js"; import * as crypto from "crypto"; import { LoggerFactory } from "@/utils/logger.js"; export interface HttpCacheOptions { ttl?: number; cacheControl?: string; varyHeaders?: string[]; private?: boolean; revalidate?: boolean; } export interface CachedResponse { data: unknown; status: number; headers: Record<string, string>; etag?: string; lastModified?: string; cacheControl?: string; } export interface RequestOptions { method: string; url: string; headers?: Record<string, string>; params?: Record<string, unknown>; data?: unknown; } /** * HTTP caching wrapper that adds intelligent caching to HTTP requests */ export class HttpCacheWrapper { private logger = LoggerFactory.cache(); constructor( private cacheManager: CacheManager, private siteId: string, ) {} /** * Execute request with intelligent caching */ async request<T = unknown>( requestFn: (extraHeaders?: Record<string, string>) => Promise<{ data: T; status: number; headers: Record<string, string>; }>, options: RequestOptions, cacheOptions?: HttpCacheOptions, ): Promise<{ data: T; status: number; headers: Record<string, string>; cached?: boolean; }> { // Only cache GET requests if (options.method.toUpperCase() !== "GET") { return await requestFn(); } const cacheKey = this.generateCacheKey(options); const cachedEntry = this.cacheManager.getEntry(cacheKey); // Check for conditional request support if (cachedEntry && this.cacheManager.supportsConditionalRequest(cacheKey)) { const conditionalHeaders = this.cacheManager.getConditionalHeaders(cacheKey); // Add conditional headers to request const _requestWithHeaders = { ...options, headers: { ...options.headers, ...conditionalHeaders, }, }; try { const response = await requestFn(conditionalHeaders); // 304 Not Modified - return cached data if (response.status === 304) { return { data: (cachedEntry.value as CachedResponse).data as T, status: 200, headers: (cachedEntry.value as CachedResponse).headers, cached: true, }; } // Content changed - update cache return await this.cacheAndReturn( response as { data: T; status: number; headers: Record<string, string> }, cacheKey, cacheOptions, ); } catch (_error) { // If conditional request fails, try without conditions this.logger.warn("Conditional request failed, falling back to regular request", { _error: _error instanceof Error ? _error.message : String(_error), siteId: this.siteId, }); } } // Check for valid cached response const cached = this.cacheManager.get<CachedResponse>(cacheKey); if (cached) { return { data: cached.data as T, status: cached.status, headers: cached.headers, cached: true, }; } // Execute fresh request const response = await requestFn(); return await this.cacheAndReturn(response, cacheKey, cacheOptions); } /** * Invalidate cache for specific endpoint */ invalidate(endpoint: string, params?: Record<string, unknown>): void { const cacheKey = this.cacheManager.generateKey(this.siteId, endpoint, params); this.cacheManager.delete(cacheKey); } /** * Invalidate cache entries matching pattern */ invalidatePattern(pattern: string): number { const regex = new RegExp(`${this.siteId}:${pattern}`); return this.cacheManager.clearPattern(regex); } /** * Invalidate all cache for this site */ invalidateAll(): number { return this.cacheManager.clearSite(this.siteId); } /** * Pre-warm cache with data */ warm<T>(endpoint: string, data: T, params?: Record<string, unknown>, cacheOptions?: HttpCacheOptions): void { const cacheKey = this.cacheManager.generateKey(this.siteId, endpoint, params); const ttl = cacheOptions?.ttl || this.getDefaultTTL(endpoint); const cachedResponse: CachedResponse = { data, status: 200, headers: this.generateCacheHeaders(cacheOptions, endpoint), etag: this.generateETag(data), lastModified: new Date().toUTCString(), cacheControl: cacheOptions?.cacheControl || this.getDefaultCacheControl(endpoint), }; this.cacheManager.set(cacheKey, cachedResponse, ttl, cachedResponse.etag, cachedResponse.lastModified); } /** * Get cache statistics for this site */ getStats() { return this.cacheManager.getStats(); } /** * Generate cache key for request */ private generateCacheKey(options: RequestOptions): string { const endpoint = this.extractEndpoint(options.url); return this.cacheManager.generateKey(this.siteId, endpoint, { ...options.params, // Include relevant headers that affect response ...this.extractCacheableHeaders(options.headers), }); } /** * Extract endpoint from full URL */ private extractEndpoint(url: string): string { // Extract the path after /wp-json/wp/v2/ const match = url.match(/\/wp-json\/wp\/v2\/(.+?)(?:\?|$)/); return match ? match[1] : url; } /** * Extract headers that affect caching */ private extractCacheableHeaders(headers?: Record<string, string>): Record<string, string> { if (!headers) return {}; const cacheableHeaders: Record<string, string> = {}; const relevantHeaders = ["accept", "accept-language", "authorization"]; for (const header of relevantHeaders) { if (headers[header]) { cacheableHeaders[header] = headers[header]; } } return cacheableHeaders; } /** * Execute request with modified headers */ /** * Cache response and return with cache metadata */ private async cacheAndReturn<T>( response: { data: T; status: number; headers: Record<string, string> }, cacheKey: string, cacheOptions?: HttpCacheOptions, ): Promise<{ data: T; status: number; headers: Record<string, string>; cached?: boolean; }> { // Don't cache error responses (unless specifically configured) if (response.status >= 400) { return response; } const endpoint = this.extractEndpointFromKey(cacheKey); const ttl = cacheOptions?.ttl || this.getDefaultTTL(endpoint); // Generate ETags and cache headers const etag = this.generateETag(response.data); const lastModified = new Date().toUTCString(); const cacheControl = cacheOptions?.cacheControl || this.getDefaultCacheControl(endpoint); const cachedResponse: CachedResponse = { data: response.data, status: response.status, headers: { ...response.headers, etag: etag, "last-modified": lastModified, "cache-control": cacheControl, }, etag, lastModified, cacheControl, }; // Store in cache this.cacheManager.set(cacheKey, cachedResponse, ttl, etag, lastModified); return { data: response.data, status: response.status, headers: cachedResponse.headers, cached: false, }; } /** * Generate ETag for response data */ private generateETag(data: unknown): string { const hash = crypto.createHash("md5").update(JSON.stringify(data)).digest("hex"); return `"${hash}"`; } /** * Get default TTL based on endpoint type */ private getDefaultTTL(endpoint: string): number { // Static data endpoints if (this.isStaticEndpoint(endpoint)) { return CachePresets.STATIC.ttl; } // Semi-static data endpoints if (this.isSemiStaticEndpoint(endpoint)) { return CachePresets.SEMI_STATIC.ttl; } // Session/auth endpoints if (this.isSessionEndpoint(endpoint)) { return CachePresets.SESSION.ttl; } // Default to dynamic for posts, comments, etc. return CachePresets.DYNAMIC.ttl; } /** * Get default Cache-Control header based on endpoint */ private getDefaultCacheControl(endpoint: string): string { if (this.isStaticEndpoint(endpoint)) { return CachePresets.STATIC.cacheControl; } if (this.isSemiStaticEndpoint(endpoint)) { return CachePresets.SEMI_STATIC.cacheControl; } if (this.isSessionEndpoint(endpoint)) { return CachePresets.SESSION.cacheControl; } return CachePresets.DYNAMIC.cacheControl; } /** * Generate cache headers */ private generateCacheHeaders(options?: HttpCacheOptions, endpoint?: string): Record<string, string> { const headers: Record<string, string> = {}; if (options?.cacheControl) { headers["cache-control"] = options.cacheControl; } else if (endpoint) { headers["cache-control"] = this.getDefaultCacheControl(endpoint); } if (options?.varyHeaders?.length) { headers["vary"] = options.varyHeaders.join(", "); } return headers; } /** * Check if endpoint contains static data */ private isStaticEndpoint(endpoint: string): boolean { const staticEndpoints = ["settings", "types", "statuses"]; return staticEndpoints.some((pattern) => endpoint.includes(pattern)); } /** * Check if endpoint contains semi-static data */ private isSemiStaticEndpoint(endpoint: string): boolean { const semiStaticEndpoints = ["categories", "tags", "users", "taxonomies"]; return semiStaticEndpoints.some((pattern) => endpoint.includes(pattern)); } /** * Check if endpoint is session-related */ private isSessionEndpoint(endpoint: string): boolean { const sessionEndpoints = ["users/me", "application-passwords"]; return sessionEndpoints.some((pattern) => endpoint.includes(pattern)); } /** * Extract endpoint from cache key */ private extractEndpointFromKey(cacheKey: string): string { const parts = cacheKey.split(":"); return parts[1] || ""; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docdyhr/mcp-wordpress'

If you have feedback or need assistance with the MCP directory API, please join our Discord server