Class: Cloudflare::Cache
- Inherits:
-
Object
- Object
- Cloudflare::Cache
- Defined in:
- lib/cloudflare_workers/cache.rb
Overview
Wrapper around a JS Cache object (caches.default or a named cache).
Instance Attribute Summary collapse
-
#js_cache ⇒ Object
readonly
Returns the value of attribute js_cache.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
Class Method Summary collapse
-
.default ⇒ Object
caches.default — the shared cache that every Worker gets for free.
-
.open(name) ⇒ Object
caches.open(name) — named cache partitions.
Instance Method Summary collapse
-
#available? ⇒ Boolean
True when the underlying JS cache is present.
-
#delete(request_or_url) ⇒ Object
Remove a Request/URL from the cache.
-
#initialize(js_cache, name = 'default') ⇒ Cache
constructor
A new instance of Cache.
-
#match(request_or_url) ⇒ Object
Look up a Request (or URL String) in the cache.
-
#put(request_or_url, body, status: 200, headers: {}) ⇒ Object
Store a Response for the given Request/URL.
Constructor Details
#initialize(js_cache, name = 'default') ⇒ Cache
Returns a new instance of Cache.
79 80 81 82 |
# File 'lib/cloudflare_workers/cache.rb', line 79 def initialize(js_cache, name = 'default') @js_cache = js_cache @name = name.to_s end |
Instance Attribute Details
#js_cache ⇒ Object (readonly)
Returns the value of attribute js_cache.
59 60 61 |
# File 'lib/cloudflare_workers/cache.rb', line 59 def js_cache @js_cache end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
59 60 61 |
# File 'lib/cloudflare_workers/cache.rb', line 59 def name @name end |
Class Method Details
.default ⇒ Object
caches.default — the shared cache that every Worker gets for free. Returns a fresh wrapper each call; the underlying JS object is a singleton per isolate.
64 65 66 |
# File 'lib/cloudflare_workers/cache.rb', line 64 def self.default Cache.new(`(typeof caches !== 'undefined' && caches ? caches.default : null)`, 'default') end |
.open(name) ⇒ Object
caches.open(name) — named cache partitions. Returns a JS Promise resolving to a wrapped Cache. Following Workers conventions, the wrapper itself holds the resolved JS Cache so subsequent calls don’t re-open the handle.
72 73 74 75 76 77 |
# File 'lib/cloudflare_workers/cache.rb', line 72 def self.open(name) name_str = name.to_s js_promise = `(typeof caches !== 'undefined' && caches && caches.open ? caches.open(#{name_str}) : Promise.resolve(null))` js_cache = js_promise.__await__ Cache.new(js_cache, name_str) end |
Instance Method Details
#available? ⇒ Boolean
True when the underlying JS cache is present. In unusual runtimes (tests / non-Workers hosts) ‘caches` may be undefined. We check Ruby-nil first because Opal’s ‘nil` marshals to an object (not JS null) so a bare `#js != null` would be truthy — this is the same pitfall `Cloudflare::AI.run` documents for `env.AI`.
89 90 91 92 93 94 95 96 |
# File 'lib/cloudflare_workers/cache.rb', line 89 def available? js = @js_cache # Opal marshals Ruby `nil` to a runtime sentinel (`Opal.nil`), # not JS null / undefined. Compare against the sentinel # explicitly so a Cache built with Ruby `nil` reports itself as # unavailable (which the non-Workers tests rely on). !!`(#{js} !== null && #{js} !== undefined && #{js} !== Opal.nil)` end |
#delete(request_or_url) ⇒ Object
Remove a Request/URL from the cache. Returns a JS Promise resolving to a boolean — true if an entry was removed.
179 180 181 182 183 184 185 |
# File 'lib/cloudflare_workers/cache.rb', line 179 def delete(request_or_url) js = @js_cache err_klass = Cloudflare::CacheError req = request_to_js(request_or_url) # Single-line IIFE — see `put` for the Opal multi-line quirk. `(async function(js, req, Kernel, err_klass) { if (js == null || js === Opal.nil) return false; try { var deleted = await js.delete(req); return deleted ? true : false; } catch (e) { Kernel.$raise(err_klass.$new(e && e.message ? e.message : String(e), Opal.hash({ operation: 'delete' }))); } return false; })(#{js}, #{req}, #{Kernel}, #{err_klass})` end |
#match(request_or_url) ⇒ Object
Look up a Request (or URL String) in the cache. Returns a JS Promise resolving to a Cloudflare::HTTPResponse (populated with body text + headers) or nil.
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/cloudflare_workers/cache.rb', line 101 def match(request_or_url) js = @js_cache response_klass = Cloudflare::HTTPResponse err_klass = Cloudflare::CacheError req = request_to_js(request_or_url) # Copilot review PR #9 (fourth pass): `request_or_url.to_s` is # correct for a String URL but produces "#<Cloudflare::HTTPResponse:...>" # for the HTTPResponse wrapper and "[object Request]" for a raw # JS Request. Derive the URL from the same shapes supported by # `request_to_js` so the `url` field on the returned # HTTPResponse is always a real URL. url_str = if request_or_url.is_a?(String) request_or_url elsif defined?(Cloudflare::HTTPResponse) && request_or_url.is_a?(Cloudflare::HTTPResponse) request_or_url.url.to_s elsif `(#{request_or_url} != null && typeof #{request_or_url} === 'object' && typeof #{request_or_url}.url === 'string')` `String(#{request_or_url}.url)` else request_or_url.to_s end # Single-line backtick IIFE — see `put` for the Opal multi-line # x-string quirk that silently drops the returned Promise. js_promise = `(async function(js, req, Kernel, err_klass) { if (js == null || js === Opal.nil) return null; var cached; try { cached = await js.match(req); } catch (e) { Kernel.$raise(err_klass.$new(e && e.message ? e.message : String(e), Opal.hash({ operation: 'match' }))); } if (cached == null) return null; var text = ''; try { text = await cached.text(); } catch (_) { text = ''; } var hk = []; var hv = []; if (cached.headers && typeof cached.headers.forEach === 'function') { cached.headers.forEach(function(v, k) { hk.push(String(k).toLowerCase()); hv.push(String(v)); }); } return { status: cached.status|0, text: text, hkeys: hk, hvals: hv }; })(#{js}, #{req}, #{Kernel}, #{err_klass})` js_result = js_promise.__await__ return nil if `#{js_result} == null` hkeys = `#{js_result}.hkeys` hvals = `#{js_result}.hvals` h = {} i = 0 len = `#{hkeys}.length` while i < len h[`#{hkeys}[#{i}]`] = `#{hvals}[#{i}]` i += 1 end response_klass.new( status: `#{js_result}.status`, headers: h, body: `#{js_result}.text`, url: url_str ) end |
#put(request_or_url, body, status: 200, headers: {}) ⇒ Object
Store a Response for the given Request/URL.
cache.put(request.url, body_str,
status: 200,
headers: { 'content-type' => 'application/json',
'cache-control' => 'public, max-age=60' }).__await__
Returns a JS Promise that resolves to nil. Workers refuses to store responses without a cacheable status / cache-control; we surface that as a CacheError rather than silently succeeding.
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/cloudflare_workers/cache.rb', line 155 def put(request_or_url, body, status: 200, headers: {}) js = @js_cache err_klass = Cloudflare::CacheError req = request_to_js(request_or_url) hdrs = ruby_headers_to_js(headers) body_str = body.to_s status_int = status.to_i # Single-line backtick IIFE — multi-line form is parsed by Opal # as a statement (not an expression), so the returned Promise # gets dropped and the caller's `__await__` receives `undefined` # instead of waiting for `cache.put` to resolve. That was the # silent bug: the inner `await` ran, but the outer await had # already proceeded. See lib/cloudflare_workers/scheduled.rb for # the same Opal multi-line x-string constraint. # Warn ONCE per isolate on a nil cache. Non-Workers runtimes # hit `Cache.new(nil, ...)` intentionally (tests, safe fall-back # for routes that can run without caching) and repeated warn # output would drown signal in noise — Copilot review PR #9. `(async function(js, req, body_str, status_int, hdrs, Kernel, err_klass) { if (js == null || js === Opal.nil) { try { if (!globalThis.__HOMURA_CACHE_NOOP_WARNED__) { globalThis.__HOMURA_CACHE_NOOP_WARNED__ = true; globalThis.console.warn('[Cloudflare::Cache] caches.default unavailable; skipping put (this is expected in non-Workers runtimes). Further warnings suppressed.'); } } catch (_) {} return null; } try { var resp = new Response(String(body_str), { status: status_int, headers: hdrs }); await js.put(req, resp); } catch (e) { try { globalThis.console.error('[Cloudflare::Cache] put threw:', e && e.stack || e); } catch (_) {} Kernel.$raise(err_klass.$new(e && e.message ? e.message : String(e), Opal.hash({ operation: 'put' }))); } return null; })(#{js}, #{req}, #{body_str}, #{status_int}, #{hdrs}, #{Kernel}, #{err_klass})` end |