Class: Dommy::XMLHttpRequest

Inherits:
Object
  • Object
show all
Includes:
Bridge::Methods, EventTarget
Defined in:
lib/dommy/xml_http_request.rb

Overview

XMLHttpRequest polyfill. Consults the same stub maps that FetchFn reads (__fetchy_stub__ / __resource_fetch_stub__ / __inject_fetch_stub__) so a single set of fixtures drives both fetch(...) and new XMLHttpRequest() style code.

State transitions match the spec:

UNSENT(0) → OPENED(1) → HEADERS_RECEIVED(2) → LOADING(3) → DONE(4)

Each transition fires readystatechange. load / loadend fire on completion; error / timeout / abort fire on the respective failure paths.

Async requests resolve via the scheduler (a microtask, or a setTimeout for stubs with delay:); sync requests (open(..., false)) deliver inline so tests can read xhr.responseText immediately.

Spec: https://xhr.spec.whatwg.org/

Defined Under Namespace

Classes: Error

Constant Summary collapse

UNSENT =
0
OPENED =
1
HEADERS_RECEIVED =
2
LOADING =
3
DONE =
4
INLINE_HANDLERS =
%w[
  readystatechange
  loadstart
  load
  loadend
  progress
  error
  timeout
  abort
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Bridge::Methods

included

Methods included from EventTarget

#__dommy_dump_event_failure__, #__internal_deliver_event__, #__internal_process_event_handler_return__, #add_event_listener, capture_flag, #deliver_at, #dispatch_event, #event_name_from_on, #invoke_listener_isolated, js_truthy?, #on_handler, #remove_event_listener, #set_on_handler

Constructor Details

#initialize(window) ⇒ XMLHttpRequest

Returns a new instance of XMLHttpRequest.



57
58
59
60
61
62
63
64
65
66
# File 'lib/dommy/xml_http_request.rb', line 57

def initialize(window)
  @window = window
  @timeout = 0
  @with_credentials = false
  @response_type = ""
  @generation = 0
  reset_state
  @inline_handlers = {}
  @upload = XMLHttpRequestUpload.new
end

Instance Attribute Details

#ready_stateObject (readonly)

Returns the value of attribute ready_state.



44
45
46
# File 'lib/dommy/xml_http_request.rb', line 44

def ready_state
  @ready_state
end

#responseObject (readonly)

Returns the value of attribute response.



44
45
46
# File 'lib/dommy/xml_http_request.rb', line 44

def response
  @response
end

#response_textObject (readonly)

Returns the value of attribute response_text.



44
45
46
# File 'lib/dommy/xml_http_request.rb', line 44

def response_text
  @response_text
end

#response_typeObject

Returns the value of attribute response_type.



55
56
57
# File 'lib/dommy/xml_http_request.rb', line 55

def response_type
  @response_type
end

#response_urlObject (readonly)

Returns the value of attribute response_url.



44
45
46
# File 'lib/dommy/xml_http_request.rb', line 44

def response_url
  @response_url
end

#response_xmlObject (readonly)

Returns the value of attribute response_xml.



44
45
46
# File 'lib/dommy/xml_http_request.rb', line 44

def response_xml
  @response_xml
end

#statusObject (readonly)

Returns the value of attribute status.



44
45
46
# File 'lib/dommy/xml_http_request.rb', line 44

def status
  @status
end

#status_textObject (readonly)

Returns the value of attribute status_text.



44
45
46
# File 'lib/dommy/xml_http_request.rb', line 44

def status_text
  @status_text
end

#timeoutObject

Returns the value of attribute timeout.



55
56
57
# File 'lib/dommy/xml_http_request.rb', line 55

def timeout
  @timeout
end

#uploadObject (readonly)

Returns the value of attribute upload.



44
45
46
# File 'lib/dommy/xml_http_request.rb', line 44

def upload
  @upload
end

#with_credentialsObject

Returns the value of attribute with_credentials.



55
56
57
# File 'lib/dommy/xml_http_request.rb', line 55

def with_credentials
  @with_credentials
end

Instance Method Details

#__internal_event_parent__Object



293
294
295
# File 'lib/dommy/xml_http_request.rb', line 293

def __internal_event_parent__
  nil
end

#__js_call__(method, args) ⇒ Object



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/dommy/xml_http_request.rb', line 268

def __js_call__(method, args)
  case method
  when "open"
    open(args[0], args[1], args[2].nil? ? true : args[2], args[3], args[4])
  when "send"
    send(args[0])
  when "setRequestHeader"
    set_request_header(args[0], args[1])
  when "abort"
    abort
  when "getResponseHeader"
    get_response_header(args[0])
  when "getAllResponseHeaders"
    get_all_response_headers
  when "overrideMimeType"
    override_mime_type(args[0])
  when "addEventListener"
    add_event_listener(args[0], args[1], args[2])
  when "removeEventListener"
    remove_event_listener(args[0], args[1], args[2])
  when "dispatchEvent"
    dispatch_event(args[0])
  end
end

#__js_get__(key) ⇒ Object

--- JS bridge ---------------------------------------------------



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/dommy/xml_http_request.rb', line 196

def __js_get__(key)
  case key
  when "readyState"
    @ready_state
  when "status"
    @status
  when "statusText"
    @status_text
  when "responseURL"
    @response_url
  when "response"
    @response
  when "responseText"
    @response_text
  when "responseXML"
    @response_xml
  when "responseType"
    @response_type
  when "timeout"
    @timeout
  when "withCredentials"
    @with_credentials
  when "upload"
    @upload
  when "UNSENT"
    UNSENT
  when "OPENED"
    OPENED
  when "HEADERS_RECEIVED"
    HEADERS_RECEIVED
  when "LOADING"
    LOADING
  when "DONE"
    DONE
  else
    @inline_handlers[inline_event_for(key)]
  end
end

#__js_set__(key, value) ⇒ Object



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/dommy/xml_http_request.rb', line 235

def __js_set__(key, value)
  case key
  when "responseType"
    # WHATWG: setting responseType on a synchronous request in a Window, or
    # once the request is loading/done, is an InvalidStateError.
    if @window && !@async
      raise DOMException::InvalidStateError,
            "responseType cannot be set on a synchronous XMLHttpRequest in a document"
    end
    if @ready_state == LOADING || @ready_state == DONE
      raise DOMException::InvalidStateError,
            "responseType cannot be set when the request state is loading or done"
    end
    @response_type = value.to_s
  when "timeout"
    @timeout = value.to_i
  when "withCredentials"
    @with_credentials = !!value
  else
    event = inline_event_for(key)
    return Bridge::UNHANDLED unless event

    set_inline_handler(event, value)
  end

  nil
end

#abortObject



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/dommy/xml_http_request.rb', line 150

def abort
  return if @ready_state == UNSENT || @ready_state == DONE
  # WHATWG: abort() is a no-op when in OPENED with the send()
  # flag unset. Without this guard, `xhr.open(); xhr.abort()`
  # would fire abort + loadend even though no request is
  # in flight.
  return if @ready_state == OPENED && !@sent

  @aborted = true
  @generation += 1
  @status = 0
  @status_text = ""
  transition(DONE)
  dispatch_event(ProgressEvent.new("abort"))
  dispatch_event(ProgressEvent.new("loadend"))
  reset_state(keep_handlers: true, keep_generation: true)
  nil
end

#deliver_resolved(entry, gen) ⇒ Object

Deliver a resolved response entry (nil -> 404), honoring abort/reopen (the generation may have changed) and a simulated delay. Shared by the synchronous path and an async deferred completion.



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/dommy/xml_http_request.rb', line 129

def deliver_resolved(entry, gen)
  return unless active?(gen)

  if entry.nil?
    # WHATWG: an async request's response is delivered by a network task, not
    # inline during send() — so readystatechange fires after send() returns.
    miss = proc { deliver(body: "not found", status: 404, status_text: "Not Found", headers: {}) if active?(gen) }
    @async ? @window.scheduler.queue_microtask(miss) : miss.call
    return
  end

  delay = entry["delay"]
  if delay && @async
    schedule_delivery_with_delay(entry, delay.to_i, gen)
  elsif @async
    @window.scheduler.queue_microtask(proc { deliver_entry(entry) if active?(gen) })
  else
    deliver_entry(entry)
  end
end

#get_all_response_headersObject Also known as: getAllResponseHeaders



179
180
181
182
183
# File 'lib/dommy/xml_http_request.rb', line 179

def get_all_response_headers
  return "" if @ready_state < HEADERS_RECEIVED

  @response_headers.map { |k, v| "#{k}: #{v}\r\n" }.join
end

#get_response_header(name) ⇒ Object Also known as: getResponseHeader



169
170
171
172
173
174
175
# File 'lib/dommy/xml_http_request.rb', line 169

def get_response_header(name)
  return nil if @ready_state < HEADERS_RECEIVED

  key = name.to_s.downcase
  hit = @response_headers.find { |k, _| k.to_s.downcase == key }
  hit ? hit.last : nil
end

#open(method, url, async = true, _user = nil, _password = nil) ⇒ Object

XHR §open. method is uppercased; async defaults to true.



69
70
71
72
73
74
75
76
77
78
# File 'lib/dommy/xml_http_request.rb', line 69

def open(method, url, async = true, _user = nil, _password = nil)
  reset_state
  @method = method.to_s.upcase
  # XHR resolves the request URL against the document base URL, like fetch.
  @url = @window.__internal_resolve_url__(url.to_s)
  @async = async.nil? ? true : !!async
  @request_headers = {}
  transition(OPENED)
  nil
end

#override_mime_type(mime) ⇒ Object Also known as: overrideMimeType



187
188
189
190
# File 'lib/dommy/xml_http_request.rb', line 187

def override_mime_type(mime)
  @override_mime = mime.to_s
  nil
end

#send(body = nil) ⇒ Object



91
92
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
120
121
122
123
124
# File 'lib/dommy/xml_http_request.rb', line 91

def send(body = nil)
  # WHATWG: send() throws InvalidStateError unless the state is OPENED and the
  # send() flag is unset (so a second send() before the request settles fails).
  if @ready_state != OPENED || @sent
    raise DOMException::InvalidStateError, "send() requires the OPENED state and an unsent request"
  end

  @request_body = body
  # WHATWG "extract a body": normalize the body (string / ArrayBuffer /
  # TypedArray / Blob / URLSearchParams / FormData) to bytes once, and default
  # the Content-Type from that extraction unless the author set one.
  @request_body_bytes, default_ct = body.nil? ? [nil, nil] : Response.extract_body(body)
  if default_ct && @request_headers.none? { |k, _| k.to_s.casecmp?("content-type") }
    @request_headers["Content-Type"] = default_ct
  end
  @sent = true
  @generation += 1
  gen = @generation
  dispatch_event(ProgressEvent.new("loadstart"))

  entry = data_uri_entry || lookup_stub
  track_globals

  # Async (live network): the handler returns a deferred whose response
  # arrives off-thread and is delivered on the page thread. The request stays
  # open until then. The sync path (stubs / cache / data:) delivers inline.
  if entry.respond_to?(:on_complete)
    entry.on_complete { |resolved| deliver_resolved(resolved, gen) }
    return nil
  end

  deliver_resolved(entry, gen)
  nil
end

#set_request_header(name, value) ⇒ Object Also known as: setRequestHeader

Raises:



80
81
82
83
84
85
86
87
# File 'lib/dommy/xml_http_request.rb', line 80

def set_request_header(name, value)
  raise Error, "setRequestHeader called before open" if @ready_state != OPENED

  key = name.to_s
  existing = @request_headers[key]
  @request_headers[key] = existing ? "#{existing}, #{value}" : value.to_s
  nil
end