Class: Dommy::FetchFn

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

Overview

fetch polyfill. No real network — instead resolves a response entry and synthesizes a Response from it. Entries come from, in order:

1. `window.globals["__fetch_handler__"]` — a callable
 `call(url, init) -> entry-or-nil`. This is the seam host
 environments use to serve real requests (e.g. dommy-rack's
 NetworkBridge routes same-origin URLs to the Rack app).
 Returning nil falls through to the stub maps.
2. `JS.global[:__fetchy_stub__]` (a Hash{url => entry})
 installed by the test. Mirrors the same fixture protocol that
 `test_fetchy.rb`'s JavaScript installer uses, so tests don't
 need a JS engine to drive the stub.

Each entry supports:

"status" / "statusText" / "body" / "contentType" /
"headers" (Hash) / "url" / "redirected" / "delay" (ms)

plus AbortController signal propagation when init[:signal] is passed.

Instance Method Summary collapse

Constructor Details

#initialize(window) ⇒ FetchFn

Returns a new instance of FetchFn.



28
29
30
# File 'lib/dommy/fetch.rb', line 28

def initialize(window)
  @window = window
end

Instance Method Details

#__js_call__(_method, args) ⇒ Object

JS calls fetch(url, init) end up here via either Window-level __js_call__("fetch", ...) or as a callable handle. Both routes delegate to call(args) so behavior is identical.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/dommy/fetch.rb', line 35

def __js_call__(_method, args)
  # Per Fetch, the request URL is resolved against the document base URL up
  # front; the handler, stub maps, and response.url all see the absolute
  # URL (no per-handler resolution).
  url = @window.__internal_resolve_url__(args[0].to_s)
  init = normalize_init(args[1] || {})
  # A cross-origin request always carries an Origin header (even for GET,
  # which normalize_init omits it for same-origin GETs).
  if cross_origin?(url) && !header?(init["headers"], "origin")
    init["headers"]["Origin"] = request_origin
  end

  # `js_eval`'s JS installer increments these globals; mirror so
  # specs that probe `__fetch_count__` / `__last_url__` / etc.
  # observe the same state shape they'd see from a real injector.
  @window.globals["__fetch_count__"] = (@window.globals["__fetch_count__"] || 0).to_i + 1
  @window.globals["__last_url__"] = url
  @window.globals["__last_init__"] = init
  @window.globals["__last_body__"] = init["body"] if init.is_a?(Hash)

  promise = PromiseValue.new(@window)
  # `mode: "same-origin"` forbids a cross-origin request — it is a network
  # error before any fetch happens.
  if (init["mode"] || "cors").to_s == "same-origin" && cross_origin?(url)
    deliver_task { promise.reject(fetch_type_error) }
    return promise
  end
  # A non-simple cross-origin cors request is preceded by a CORS preflight
  # (OPTIONS); a failed preflight is a network error before the real request.
  if needs_preflight?(url, init) && !preflight_ok?(url, init)
    deliver_task { promise.reject(fetch_type_error) }
    return promise
  end
  result = resolve_entry(url, init)
  # A handler may answer asynchronously (live network off-thread): it returns
  # a deferred whose response arrives later and is applied on the page thread
  # (via the scheduler inbox). The sync path (stubs / cache / data:) resolves
  # the promise inline, exactly as before.
  if result.respond_to?(:on_complete)
    result.on_complete { |entry| fulfill_from_entry(promise, entry, url, init) }
  else
    settle_with_redirects(promise, url, init, result)
  end
  promise
end