UNPKG

jsdom

Version:

A JavaScript implementation of many web standards

117 lines (96 loc) 3.95 kB
"use strict"; const idlUtils = require("../../../generated/idl/utils"); const { fireAnEvent } = require("../../living/helpers/events"); const { fetchCollected } = require("./jsdom-dispatcher"); module.exports = class PerDocumentResourceLoader { constructor(document) { this._document = document; this._defaultEncoding = document._encoding; const defaultView = document._defaultView; this._dispatcher = defaultView ? defaultView._dispatcher : null; this._loadSubresources = defaultView ? defaultView._loadSubresources : false; this._requestManager = document._requestManager; this._queue = document._queue; this._deferQueue = document._deferQueue; this._asyncQueue = document._asyncQueue; } fetch(url, { element, onLoad, onError }) { if (!this._loadSubresources) { return null; } const abortController = new AbortController(); // Add it to the request manager. The request manager is very old code, but it works with the contract of "has an // `abort()` method". One day this whole subsystem will be refactored to use `AbortController`s natively, but for // now this just happens to work. // // Note that we add the controller now, before calling `fetchCollected()`, so that if any interceptors or other code // calls `window.close()`, the abort controller is already registered and will see the abort. this._requestManager.add(abortController); const fetchPromise = fetchCollected(this._dispatcher, { url, headers: { Referer: this._document.URL }, signal: abortController.signal, element: idlUtils.wrapperForImpl(element) }); const onErrorWrapped = cause => { this._requestManager.remove(abortController); // If the request was aborted, don't fire error events if (cause && cause.name === "AbortError") { return Promise.resolve(); } if (onError) { onError(cause); } fireAnEvent("error", element); const jsomError = new Error(`Could not load ${element.localName}: "${url}"`, { cause }); jsomError.type = "resource-loading"; jsomError.url = url; this._document._defaultView._virtualConsole.emit("jsdomError", jsomError); return Promise.resolve(); }; const onLoadWrapped = response => { this._requestManager.remove(abortController); // Extract data and create a response-like object for compatibility const { body: data, status, headers, url: responseURL } = response; const responseObj = { ok: status >= 200 && status < 300, status, headers: { get(name) { return headers[name.toLowerCase()] ?? null; } }, url: responseURL }; try { const result = onLoad ? onLoad(data, responseObj) : undefined; return Promise.resolve(result) .then(() => { fireAnEvent("load", element); return Promise.resolve(); }) .catch(err => { return onErrorWrapped(err); }); } catch (err) { return onErrorWrapped(err); } }; // Create a wrapper object that can be used by the queue system const request = { then: (onFulfilled, onRejected) => fetchPromise.then(onFulfilled, onRejected), catch: onRejected => fetchPromise.catch(onRejected) }; if (element.localName === "script" && element.hasAttributeNS(null, "async")) { this._asyncQueue.push(request, onLoadWrapped, onErrorWrapped, this._queue.getLastScript()); } else if ( element.localName === "script" && element.hasAttributeNS(null, "defer") && this._document.readyState !== "interactive") { this._deferQueue.push(request, onLoadWrapped, onErrorWrapped, false, element); } else { this._queue.push(request, onLoadWrapped, onErrorWrapped, false, element); } return request; } };