Class: Cloudflare::Cache

Inherits:
Object
  • Object
show all
Defined in:
lib/cloudflare_workers/cache.rb

Overview

Wrapper around a JS Cache object (caches.default or a named cache).

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

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_cacheObject (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

#nameObject (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

.defaultObject

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`.

Returns:

  • (Boolean)


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