1+ # frozen_string_literal: true
2+
3+ module Prefab
4+ class CachingHttpConnection
5+ CACHE_SIZE = 2 . freeze
6+ CacheEntry = Struct . new ( :data , :etag , :expires_at )
7+
8+ class << self
9+ def cache
10+ @cache ||= FixedSizeHash . new ( CACHE_SIZE )
11+ end
12+
13+ def reset_cache!
14+ @cache = FixedSizeHash . new ( CACHE_SIZE )
15+ end
16+ end
17+
18+ def initialize ( uri , api_key )
19+ @connection = HttpConnection . new ( uri , api_key )
20+ end
21+
22+ def get ( path )
23+ now = Time . now . to_i
24+ cache_key = "#{ @connection . uri } #{ path } "
25+ cached = self . class . cache [ cache_key ]
26+
27+ # Check if we have a valid cached response
28+ if cached &.data && cached . expires_at && now < cached . expires_at
29+ return Faraday ::Response . new (
30+ status : 200 ,
31+ body : cached . data ,
32+ response_headers : {
33+ 'ETag' => cached . etag ,
34+ 'X-Cache' => 'HIT' ,
35+ 'X-Cache-Expires-At' => cached . expires_at . to_s
36+ }
37+ )
38+ end
39+
40+ # Make request with conditional GET if we have an ETag
41+ response = if cached &.etag
42+ @connection . get ( path , { 'If-None-Match' => cached . etag } )
43+ else
44+ @connection . get ( path )
45+ end
46+
47+ # Handle 304 Not Modified
48+ if response . status == 304 && cached &.data
49+ return Faraday ::Response . new (
50+ status : 200 ,
51+ body : cached . data ,
52+ response_headers : {
53+ 'ETag' => cached . etag ,
54+ 'X-Cache' => 'HIT' ,
55+ 'X-Cache-Expires-At' => cached . expires_at . to_s
56+ }
57+ )
58+ end
59+
60+ # Parse caching headers
61+ cache_control = response . headers [ 'Cache-Control' ] . to_s
62+ etag = response . headers [ 'ETag' ]
63+
64+ # Always add X-Cache header
65+ response . headers [ 'X-Cache' ] = 'MISS'
66+
67+ # Don't cache if no-store is present
68+ return response if cache_control . include? ( 'no-store' )
69+
70+ # Calculate expiration
71+ max_age = cache_control . match ( /max-age=(\d +)/ ) &.captures &.first &.to_i
72+ expires_at = max_age ? now + max_age : nil
73+
74+ # Cache the response if we have caching headers
75+ if etag || expires_at
76+ self . class . cache [ cache_key ] = CacheEntry . new (
77+ response . body ,
78+ etag ,
79+ expires_at
80+ )
81+ end
82+
83+ response
84+ end
85+
86+ # Delegate other methods to the underlying connection
87+ def post ( path , body )
88+ @connection . post ( path , body )
89+ end
90+
91+ def uri
92+ @connection . uri
93+ end
94+ end
95+ end
0 commit comments