This repository has no description
0

Configure Feed

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

semble / src / modules / cards / infrastructure / CachedMetadataService.ts
4.6 kB 138 lines
1import Redis from 'ioredis'; 2import { IMetadataService } from '../domain/services/IMetadataService'; 3import { 4 UrlMetadata, 5 UrlMetadataProps, 6} from '../domain/value-objects/UrlMetadata'; 7import { URL } from '../domain/value-objects/URL'; 8import { Result, ok } from '../../../shared/core/Result'; 9 10export class CachedMetadataService implements IMetadataService { 11 private readonly CACHE_KEY_PREFIX: string; 12 private readonly STALENESS_THRESHOLD_SECONDS: number; 13 14 constructor( 15 private readonly metadataService: IMetadataService, 16 private readonly redis: Redis, 17 private readonly serviceName: string, 18 stalenessThresholdSeconds: number = 3600, // 1 hour default 19 ) { 20 this.CACHE_KEY_PREFIX = `metadata:${serviceName}:`; 21 this.STALENESS_THRESHOLD_SECONDS = stalenessThresholdSeconds; 22 } 23 24 async fetchMetadata( 25 url: URL, 26 refetchStaleMetadata: boolean = false, 27 ): Promise<Result<UrlMetadata>> { 28 const cacheKey = this.getCacheKey(url.value); 29 30 try { 31 // Try cache first 32 const cached = await this.redis.get(cacheKey); 33 if (cached) { 34 try { 35 const metadataResult = UrlMetadata.create( 36 JSON.parse(cached) as UrlMetadataProps, 37 ); 38 if (metadataResult.isErr()) { 39 throw new Error( 40 `Invalid cached metadata for ${url.value} from ${this.serviceName}: ${metadataResult.error.message}`, 41 ); 42 } 43 const metadata = metadataResult.value; 44 45 // If refetchStaleMetadata is true, check if cached data is stale 46 if (refetchStaleMetadata) { 47 // If retrievedAt is missing, treat as stale and refetch 48 if (!metadata.retrievedAt) { 49 console.log( 50 `Cached metadata for ${url.value} from ${this.serviceName} has no retrievedAt, treating as stale...`, 51 ); 52 // Continue to fetch fresh data below 53 } else { 54 const now = new Date(); 55 const retrievedAt = new Date(metadata.retrievedAt); 56 const ageInSeconds = 57 (now.getTime() - retrievedAt.getTime()) / 1000; 58 59 if (ageInSeconds > this.STALENESS_THRESHOLD_SECONDS) { 60 // Data is stale, fetch fresh data 61 console.log( 62 `Cached metadata for ${url.value} from ${this.serviceName} is stale (${ageInSeconds}s old), refetching...`, 63 ); 64 // Continue to fetch fresh data below 65 } else { 66 // Data is fresh enough, return cached 67 return ok(metadata); 68 } 69 } 70 } else { 71 // Not refetching stale data, always return cached if available 72 return ok(metadata); 73 } 74 } catch (parseError) { 75 // If JSON parsing fails, continue to fetch fresh data 76 console.warn( 77 `Failed to parse cached metadata for ${url.value} from ${this.serviceName}:`, 78 parseError, 79 ); 80 } 81 } 82 83 // Cache miss, parse error, or stale data - fetch from underlying service 84 const result = await this.metadataService.fetchMetadata(url); 85 86 if (result.isOk()) { 87 // Cache the successful result with infinite TTL 88 try { 89 await this.redis.set(cacheKey, JSON.stringify(result.value.props)); 90 } catch (cacheError) { 91 // Log cache error but don't fail the request 92 console.warn( 93 `Failed to cache metadata for ${url.value} from ${this.serviceName}:`, 94 cacheError, 95 ); 96 } 97 } 98 99 return result; 100 } catch (redisError) { 101 // If Redis is down, fall back to direct service call 102 console.warn( 103 `Redis error when fetching metadata for ${url.value} from ${this.serviceName}:`, 104 redisError, 105 ); 106 return this.metadataService.fetchMetadata(url); 107 } 108 } 109 110 async isAvailable(): Promise<boolean> { 111 return this.metadataService.isAvailable(); 112 } 113 114 private getCacheKey(url: string): string { 115 return `${this.CACHE_KEY_PREFIX}${url}`; 116 } 117 118 /** 119 * Invalidate cached metadata for a specific URL 120 */ 121 async invalidateMetadata(url: string): Promise<void> { 122 try { 123 await this.redis.del(this.getCacheKey(url)); 124 } catch (error) { 125 console.warn( 126 `Failed to invalidate metadata cache for ${url} from ${this.serviceName}:`, 127 error, 128 ); 129 } 130 } 131 132 /** 133 * Warm the cache by pre-fetching metadata for a URL 134 */ 135 async warmCache(url: URL): Promise<void> { 136 await this.fetchMetadata(url); 137 } 138}