This repository has no description
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}