Class: Dommy::Rack::Resources

Inherits:
Object
  • Object
show all
Defined in:
lib/dommy/rack/resources.rb

Overview

Dommy::Resources adapter backed by a Rack session: serves same-origin requests from the real app (sharing the session's cookie jar). Cross-origin requests are declined (return nil, so callers fall through to stubs) UNLESS the session's subresource allowlist permits the host — letting an embedding browser opt a host in (after prompting) so a SPA's cross-origin bundle can load. Declined cross-origin hosts are recorded on the session for that UI. This is the Rack arm of the single resources interface used for both <script src> loads and fetch / XHR; NetworkBridge.install wires it as the window's fetch handler.

Constant Summary collapse

PREFETCH_CONCURRENCY =

Number of bundles prefetched at once: enough to saturate the link without spawning a thread per script on a huge page.

8

Instance Method Summary collapse

Constructor Details

#initialize(session) ⇒ Resources

Returns a new instance of Resources.



17
18
19
20
# File 'lib/dommy/rack/resources.rb', line 17

def initialize(session)
  @session = session
  @prefetched = {} # absolute GET url => Resources::Response, warmed by #prefetch
end

Instance Method Details

#get(url, headers: {}) ⇒ Object



22
# File 'lib/dommy/rack/resources.rb', line 22

def get(url, headers: {}) = request(method: "GET", url: url, headers: headers)

#prefetch(urls) ⇒ Object

Warm the GET cache for urls by downloading them CONCURRENTLY, so the synchronous script boot that follows reads them instantly instead of fetching a dozen big bundles one after another (the dominant cost of a heavy SPA's first paint). Gated on browser mode (open cross-origin) so plain test / Rails sessions stay sequential and deterministic. Uses its own bounded thread set (independent of any fetch/XHR executor) and blocks until warmed (the wait is the slowest single download, not their sum). The worker-safe jobs touch only thread-safe state (cookie jar, frozen config).



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/dommy/rack/resources.rb', line 54

def prefetch(urls)
  return unless @session.open_subresources?

  jobs = warmable_jobs(urls)
  return if jobs.empty?

  pending = Thread::Queue.new
  jobs.each { |job| pending << job }
  done = Thread::Queue.new
  threads = Array.new([PREFETCH_CONCURRENCY, jobs.size].min) do
    Thread.new do
      while (item = (pending.pop(true) rescue nil))
        target, job = item
        done << [target, (job.call rescue nil)]
      end
    end
  end
  jobs.size.times do
    target, rack_response = done.pop
    @prefetched[target] = to_resources_response(rack_response) if rack_response
  end
  threads.each(&:join)
end

#request(method:, url:, headers: {}, body: nil) ⇒ Object



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/dommy/rack/resources.rb', line 24

def request(method:, url:, headers: {}, body: nil)
  target = served_target(method: method, url: url, headers: headers, body: body)
  return nil unless target

  # A warmed GET (the page's <script src> bundles, fetched concurrently
  # before the synchronous boot) is served from the cache with no network.
  if method.to_s.upcase == "GET" && @prefetched.key?(target)
    return @prefetched[target]
  end

  to_resources_response(@session.fetch(
    target,
    method: method.to_s.upcase,
    headers: headers.is_a?(Hash) ? headers : {},
    body: body&.to_s
  ))
end

#request_job(method:, url:, headers: {}, body: nil) ⇒ Object

The async-network counterpart of #request: makes the same page-thread serve/decline decision (origin gate, blocked-host recording), then — for a served URL — returns a worker-safe thunk that runs the request off the page thread and yields a Resources::Response. Returns nil for a URL we do not serve, exactly like #request, so the fetch handler falls through to stubs. The fetch handler submits the thunk to the network executor.



84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/dommy/rack/resources.rb', line 84

def request_job(method:, url:, headers: {}, body: nil)
  target = served_target(method: method, url: url, headers: headers, body: body)
  return nil unless target

  job = @session.build_subresource_fetch_job(
    target,
    method: method.to_s.upcase,
    headers: headers.is_a?(Hash) ? headers : {},
    body: body&.to_s
  )
  -> { to_resources_response(job.call) }
end