Class: Capybara::Simulated::AssetCache
- Inherits:
-
Object
- Object
- Capybara::Simulated::AssetCache
- Defined in:
- lib/capybara/simulated/asset_cache.rb
Overview
HTTP/1.1 (RFC 9111) compliant response cache for ‘Browser#rack_fetch`. Process-wide, GET-only, no Vary support — enough to mirror what a real browser does for Rails-fingerprinted assets (`Cache-Control: public, max-age=31536000, immutable`) which is the request-volume difference behind Cuprite/Selenium being able to skip 80–90 % of repeat asset fetches across a suite.
Not cached:
- Non-GET methods
- Responses with `Cache-Control: no-store`
- Responses with `Cache-Control: private` — per-user responses that a
SHARED cache MUST NOT store (RFC 9111 §5.2.2.7). This cache is
process-wide and shared across test sessions, so it is a shared
cache for that purpose. (Forem's `/async_info/base_data` etc. send
`max-age=0, private`; storing them only wastes memory on data that
is always revalidated.)
- Responses with `Vary` listing anything other than `Accept-Encoding`
(which we ignore because we never send it)
- Responses with no freshness signal at all (no max-age, no Expires,
no ETag/Last-Modified to revalidate against)
Cached but always revalidated:
- Responses with `Cache-Control: no-cache`
- Stale entries with ETag or Last-Modified validators
The cache hands the body bytes back to bridge.js verbatim; downstream ‘__csim_runScript` bytecode caching is content-addressable (sha256(body)), so identical body → identical bytecode falls out naturally.
No Mutex: MRI’s Hash ‘[]`/`[]=` are atomic under the GVL, and Capybara test suites are serial per-process (parallel_tests forks rather than threads). A reader briefly racing a `refresh` may see a partial `stored_at`/`max_age` pair — that just means freshness is computed against a transient mix, never corrupted.
Defined Under Namespace
Classes: Entry
Constant Summary collapse
- CACHEABLE_STATUSES =
RFC 9111 §3: status codes cacheable by default.
[200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501].freeze
- SAFE_VARY_FIELDS =
‘Vary` fields we can safely ignore because Simulated never sends the corresponding request header — the cached “no value” variant is always applicable. `Accept-Encoding` is the common one Rails adds to sprockets-served assets.
%w[accept-encoding].freeze
Instance Method Summary collapse
- #clear ⇒ Object
-
#clear_volatile ⇒ Object
Per-test reset path: keep entries the server marked ‘Cache-Control: immutable` (declared not to change for their freshness lifetime, so a kept entry can’t shadow a later test’s response) and drop everything else, so test-local DB state reaches the app on the next visit.
-
#initialize ⇒ AssetCache
constructor
A new instance of AssetCache.
- #lookup(url) ⇒ Object
-
#refresh(entry, new_headers) ⇒ Object
RFC 9111 §4.3.4: a 304 refreshes the cached entry’s freshness window without replacing the body.
- #revalidation_headers(entry) ⇒ Object
- #store(url, status, headers, body) ⇒ Object
Constructor Details
#initialize ⇒ AssetCache
Returns a new instance of AssetCache.
77 78 79 |
# File 'lib/capybara/simulated/asset_cache.rb', line 77 def initialize @entries = {} end |
Instance Method Details
#clear ⇒ Object
82 |
# File 'lib/capybara/simulated/asset_cache.rb', line 82 def clear = @entries.clear |
#clear_volatile ⇒ Object
Per-test reset path: keep entries the server marked ‘Cache-Control: immutable` (declared not to change for their freshness lifetime, so a kept entry can’t shadow a later test’s response) and drop everything else, so test-local DB state reaches the app on the next visit.
89 90 91 |
# File 'lib/capybara/simulated/asset_cache.rb', line 89 def clear_volatile @entries.reject! {|_, e| e.immutable } end |
#lookup(url) ⇒ Object
81 |
# File 'lib/capybara/simulated/asset_cache.rb', line 81 def lookup(url) = @entries[url] |
#refresh(entry, new_headers) ⇒ Object
RFC 9111 §4.3.4: a 304 refreshes the cached entry’s freshness window without replacing the body. Mutation under the GVL is fine — see class-level Mutex note.
131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/capybara/simulated/asset_cache.rb', line 131 def refresh(entry, new_headers) h = ensure_lowercase(new_headers) cc = parse_cache_control(h['cache-control']) entry.stored_at = Time.now entry.max_age = freshness_seconds(cc, h) || entry.max_age # RFC 9111 §4.3.4: a 304 UPDATES stored header fields with the ones # it carries; it does not delete them. A bare 304 (no Cache-Control — # the common ETag / Last-Modified revalidation) must therefore PRESERVE # the stored no-cache flag, otherwise a `no-cache` resource would stop # revalidating after its first 304 and start serving fresh. Only a 304 # that actually resends Cache-Control re-derives it. entry.no_cache = h['cache-control'] ? cc[:no_cache] : entry.no_cache entry end |
#revalidation_headers(entry) ⇒ Object
121 122 123 124 125 126 |
# File 'lib/capybara/simulated/asset_cache.rb', line 121 def revalidation_headers(entry) h = {} h['If-None-Match'] = entry.headers['etag'] if entry.headers['etag'] h['If-Modified-Since'] = entry.headers['last-modified'] if entry.headers['last-modified'] h end |
#store(url, status, headers, body) ⇒ Object
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/capybara/simulated/asset_cache.rb', line 93 def store(url, status, headers, body) return unless CACHEABLE_STATUSES.include?(status) h = ensure_lowercase(headers) return unless vary_compatible?(h['vary']) cc = parse_cache_control(h['cache-control']) # Honour the server's explicit directives only (RFC 9111) — no # URL-shape heuristic. `no-store` MUST NOT be stored (§5.2.2.5), # full stop; whether a URL "looks fingerprinted" is a guess that # can misfire and serve a genuinely no-store response stale. return if cc[:no_store] # `private` is a per-user response a shared cache MUST NOT store # (§5.2.2.7); this process-wide cache is shared across sessions. return if cc[:private] max_age = freshness_seconds(cc, h) # Nothing useful to cache without a freshness signal or a # validator to revalidate against. return if max_age.nil? && h['etag'].nil? && h['last-modified'].nil? @entries[url] = Entry.new( status: status, headers: h, body: body, stored_at: Time.now, max_age: max_age, no_cache: cc[:no_cache], immutable: cc[:immutable] == true ) end |