import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn";
import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";
import _inherits from "@babel/runtime/helpers/inherits";
import _createClass from "@babel/runtime/helpers/createClass";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _regeneratorRuntime from "@babel/runtime/regenerator";
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
/* global $SolidTestEnvironment */
/**
 *
 * Project: rdflib.js
 *
 * @file: fetcher.js
 *
 * Description: contains functions for requesting/fetching/retracting
 *  This implements quite a lot of the web architecture.
 * A fetcher is bound to a specific quad store, into which
 * it loads stuff and into which it writes its metadata
 * @@ The metadata could be optionally a separate graph
 *
 * - implements semantics of HTTP headers, Internet Content Types
 * - selects parsers for rdf/xml, n3, rdfa, grddl
 *
 * TO do:
 * - Implement a runtime registry for parsers and serializers
 * -
 */

/**
 * Things to test: callbacks on request, refresh, retract
 *   loading from HTTP, HTTPS, FTP, FILE, others?
 * To do:
 * Firing up a mail client for mid:  (message:) URLs
 */
import IndexedFormula from './store';
import log from './log';
import N3Parser from './n3parser';
import RDFlibNamedNode from './named-node';
import Namespace from './namespace';
import rdfParse from './parse';
import { parseRDFaDOM } from './rdfaparser';
import RDFParser from './rdfxmlparser';
import * as Uri from './uri';
import { isCollection, isNamedNode } from './utils/terms';
import * as Util from './utils-js';
import serialize from './serialize';
import crossFetch, { Headers } from 'cross-fetch';
import { TurtleContentType, RDFXMLContentType, XHTMLContentType } from './types';
import { termValue } from './utils/termValue';
import jsonldParser from './jsonldparser';
var Parsable = {
  'text/n3': true,
  'text/turtle': true,
  'application/rdf+xml': true,
  'application/xhtml+xml': true,
  'text/html': true,
  'application/ld+json': true
};

// This is a minimal set to allow the use of damaged servers if necessary
var CONTENT_TYPE_BY_EXT = {
  'rdf': RDFXMLContentType,
  'owl': RDFXMLContentType,
  'n3': 'text/n3',
  'ttl': 'text/turtle',
  'nt': 'text/n3',
  'acl': 'text/n3',
  'html': 'text/html',
  'xml': 'text/xml'
};

// Convenience namespaces needed in this module.
// These are deliberately not exported as the user application should
// make its own list and not rely on the prefixes used here,
// and not be tempted to add to them, and them clash with those of another
// application.
var getNS = function getNS(factory) {
  return {
    link: Namespace('http://www.w3.org/2007/ont/link#', factory),
    http: Namespace('http://www.w3.org/2007/ont/http#', factory),
    httph: Namespace('http://www.w3.org/2007/ont/httph#', factory),
    // headers
    rdf: Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#', factory),
    rdfs: Namespace('http://www.w3.org/2000/01/rdf-schema#', factory),
    dc: Namespace('http://purl.org/dc/elements/1.1/', factory),
    ldp: Namespace('http://www.w3.org/ns/ldp#', factory)
  };
};
var ns = getNS();

/** An extended interface of Response, since RDFlib.js adds some properties. */

/** tell typescript that a 'panes' child may exist on Window */

/** All valid inputs for initFetchOptions */

/** Initiated by initFetchOptions, which runs on load */
var Handler = /*#__PURE__*/_createClass(function Handler(response, dom) {
  _classCallCheck(this, Handler);
  // TODO: Document, type
  _defineProperty(this, "response", void 0);
  // TODO: Document, type
  _defineProperty(this, "dom", void 0);
  this.response = response;
  // The type assertion operator here might need to be removed.
  this.dom = dom;
});
_defineProperty(Handler, "pattern", void 0);
var RDFXMLHandler = /*#__PURE__*/function (_Handler2) {
  function RDFXMLHandler() {
    _classCallCheck(this, RDFXMLHandler);
    return _callSuper(this, RDFXMLHandler, arguments);
  }
  _inherits(RDFXMLHandler, _Handler2);
  return _createClass(RDFXMLHandler, [{
    key: "parse",
    value: function parse(fetcher, /** An XML String */
    responseText, /** Requires .original */
    options) {
      var kb = fetcher.store;
      if (!this.dom) {
        this.dom = Util.parseXML(responseText);
      }
      var root = this.dom.documentElement;
      if (root.nodeName === 'parsererror') {
        // Mozilla only See issue/issue110
        // have to fail the request
        return fetcher.failFetch(options, 'Badly formed XML in ' + options.resource.value, 'parse_error');
      }
      var parser = new RDFParser(kb);
      try {
        parser.parse(this.dom, options.original.value, options.original);
      } catch (err) {
        return fetcher.failFetch(options, 'Syntax error parsing RDF/XML! ' + err, 'parse_error');
      }
      if (!options.noMeta) {
        kb.add(options.original, ns.rdf('type'), ns.link('RDFDocument'), fetcher.appNode);
      }
      return fetcher.doneFetch(options, this.response);
    }
  }], [{
    key: "toString",
    value: function toString() {
      return 'RDFXMLHandler';
    }
  }, {
    key: "register",
    value: function register(fetcher) {
      fetcher.mediatypes[RDFXMLContentType] = {
        'q': 0.9
      };
    }
  }]);
}(Handler);
RDFXMLHandler.pattern = new RegExp('application/rdf\\+xml');
var XHTMLHandler = /*#__PURE__*/function (_Handler3) {
  function XHTMLHandler() {
    _classCallCheck(this, XHTMLHandler);
    return _callSuper(this, XHTMLHandler, arguments);
  }
  _inherits(XHTMLHandler, _Handler3);
  return _createClass(XHTMLHandler, [{
    key: "parse",
    value: function parse(fetcher, responseText, options) {
      var relation, reverse;
      if (!this.dom) {
        this.dom = Util.parseXML(responseText);
      }
      var kb = fetcher.store;

      // dc:title
      var title = this.dom.getElementsByTagName('title');
      if (title.length > 0) {
        kb.add(options.resource, ns.dc('title'), kb.rdfFactory.literal(title[0].textContent), options.resource);
        // log.info("Inferring title of " + xhr.resource)
      }

      // link rel
      var links = this.dom.getElementsByTagName('link');
      for (var x = links.length - 1; x >= 0; x--) {
        // @@ rev
        relation = links[x].getAttribute('rel');
        reverse = false;
        if (!relation) {
          relation = links[x].getAttribute('rev');
          reverse = true;
        }
        if (relation) {
          fetcher.linkData(options.original, relation, links[x].getAttribute('href'), options.resource, reverse);
        }
      }

      // Data Islands
      var scripts = this.dom.getElementsByTagName('script');
      for (var i = 0; i < scripts.length; i++) {
        var contentType = scripts[i].getAttribute('type');
        if (Parsable[contentType]) {
          // @ts-ignore incompatibility between Store.add and Formula.add
          rdfParse(scripts[i].textContent, kb, options.original.value, contentType);
          // @ts-ignore incompatibility between Store.add and Formula.add
          rdfParse(scripts[i].textContent, kb, options.original.value, contentType);
        }
      }
      if (!options.noMeta) {
        kb.add(options.resource, ns.rdf('type'), ns.link('WebPage'), fetcher.appNode);
      }
      if (!options.noRDFa && parseRDFaDOM) {
        // enable by default
        try {
          parseRDFaDOM(this.dom, kb, options.original.value);
        } catch (err) {
          // @ts-ignore
          var msg = 'Error trying to parse ' + options.resource + ' as RDFa:\n' + err + ':\n' + err.stack;
          return fetcher.failFetch(options, msg, 'parse_error');
        }
      }
      return fetcher.doneFetch(options, this.response);
    }
  }], [{
    key: "toString",
    value: function toString() {
      return 'XHTMLHandler';
    }
  }, {
    key: "register",
    value: function register(fetcher) {
      fetcher.mediatypes[XHTMLContentType] = {};
    }
  }]);
}(Handler);
XHTMLHandler.pattern = new RegExp('application/xhtml');
var XMLHandler = /*#__PURE__*/function (_Handler4) {
  function XMLHandler() {
    _classCallCheck(this, XMLHandler);
    return _callSuper(this, XMLHandler, arguments);
  }
  _inherits(XMLHandler, _Handler4);
  return _createClass(XMLHandler, [{
    key: "parse",
    value: function parse(fetcher, responseText, options) {
      var dom = Util.parseXML(responseText);

      // XML Semantics defined by root element namespace
      // figure out the root element
      for (var c = 0; c < dom.childNodes.length; c++) {
        var node = dom.childNodes[c];
        // is this node an element?
        if (XMLHandler.isElement(node)) {
          // We've found the first element, it's the root
          var _ns = node.namespaceURI;

          // Is it RDF/XML?
          if (_ns && _ns === _ns['rdf']) {
            fetcher.addStatus(options.req, 'Has XML root element in the RDF namespace, so assume RDF/XML.');
            var rdfHandler = new RDFXMLHandler(this.response, dom);
            return rdfHandler.parse(fetcher, responseText, options);
          }
          break;
        }
      }

      // Or it could be XHTML?
      // Maybe it has an XHTML DOCTYPE?
      if (dom.doctype) {
        // log.info("We found a DOCTYPE in " + xhr.resource)
        if (dom.doctype.name === 'html' && dom.doctype.publicId.match(/^-\/\/W3C\/\/DTD XHTML/) && dom.doctype.systemId.match(/http:\/\/www.w3.org\/TR\/xhtml/)) {
          fetcher.addStatus(options.req, 'Has XHTML DOCTYPE. Switching to XHTML Handler.\n');
          var xhtmlHandler = new XHTMLHandler(this.response, dom);
          return xhtmlHandler.parse(fetcher, responseText, options);
        }
      }

      // Or what about an XHTML namespace?
      var html = dom.getElementsByTagName('html')[0];
      if (html) {
        var xmlns = html.getAttribute('xmlns');
        if (xmlns && xmlns.match(/^http:\/\/www.w3.org\/1999\/xhtml/)) {
          fetcher.addStatus(options.req, 'Has a default namespace for ' + 'XHTML. Switching to XHTMLHandler.\n');
          var _xhtmlHandler = new XHTMLHandler(this.response, dom);
          return _xhtmlHandler.parse(fetcher, responseText, options);
        }
      }

      // At this point we should check the namespace document (cache it!) and
      // look for a GRDDL transform
      // @@  Get namespace document <n>, parse it, look for  <n> grddl:namespaceTransform ?y
      // Apply ?y to   dom
      // We give up. What dialect is this?
      return fetcher.failFetch(options, 'Unsupported dialect of XML: not RDF or XHTML namespace, etc.\n' + responseText.slice(0, 80), 901);
    }
  }], [{
    key: "toString",
    value: function toString() {
      return 'XMLHandler';
    }
  }, {
    key: "register",
    value: function register(fetcher) {
      fetcher.mediatypes['text/xml'] = {
        'q': 0.5
      };
      fetcher.mediatypes['application/xml'] = {
        'q': 0.5
      };
    }
  }, {
    key: "isElement",
    value: function isElement(node) {
      return node.nodeType === Node.ELEMENT_NODE;
    }
  }]);
}(Handler);
XMLHandler.pattern = new RegExp('(text|application)/(.*)xml');
var HTMLHandler = /*#__PURE__*/function (_Handler5) {
  function HTMLHandler() {
    _classCallCheck(this, HTMLHandler);
    return _callSuper(this, HTMLHandler, arguments);
  }
  _inherits(HTMLHandler, _Handler5);
  return _createClass(HTMLHandler, [{
    key: "parse",
    value: function parse(fetcher, responseText, options) {
      var kb = fetcher.store;

      // We only handle XHTML so we have to figure out if this is XML
      // log.info("Sniffing HTML " + xhr.resource + " for XHTML.")
      if (isXML(responseText)) {
        fetcher.addStatus(options.req, "Has an XML declaration. We'll assume " + "it's XHTML as the content-type was text/html.\n");
        var xhtmlHandler = new XHTMLHandler(this.response);
        return xhtmlHandler.parse(fetcher, responseText, options);
      }

      // DOCTYPE html
      if (isXHTML(responseText)) {
        fetcher.addStatus(options.req, 'Has XHTML DOCTYPE. Switching to XHTMLHandler.\n');
        var _xhtmlHandler2 = new XHTMLHandler(this.response);
        return _xhtmlHandler2.parse(fetcher, responseText, options);
      }

      // xmlns
      if (isXMLNS(responseText)) {
        fetcher.addStatus(options.req, 'Has default namespace for XHTML, so switching to XHTMLHandler.\n');
        var _xhtmlHandler3 = new XHTMLHandler(this.response);
        return _xhtmlHandler3.parse(fetcher, responseText, options);
      }

      // dc:title
      // no need to escape '/' here
      var titleMatch = new RegExp('<title>([\\s\\S]+?)</title>', 'im').exec(responseText);
      if (titleMatch) {
        kb.add(options.resource, ns.dc('title'), kb.rdfFactory.literal(titleMatch[1]), options.resource); // think about xml:lang later
      }
      kb.add(options.resource, ns.rdf('type'), ns.link('WebPage'), fetcher.appNode);
      fetcher.addStatus(options.req, 'non-XML HTML document, not parsed for data.');
      return fetcher.doneFetch(options, this.response);
    }
  }], [{
    key: "toString",
    value: function toString() {
      return 'HTMLHandler';
    }
  }, {
    key: "register",
    value: function register(fetcher) {
      fetcher.mediatypes['text/html'] = {
        'q': 0.9
      };
    }
  }]);
}(Handler);
HTMLHandler.pattern = new RegExp('text/html');
var JsonLdHandler = /*#__PURE__*/function (_Handler6) {
  function JsonLdHandler() {
    _classCallCheck(this, JsonLdHandler);
    return _callSuper(this, JsonLdHandler, arguments);
  }
  _inherits(JsonLdHandler, _Handler6);
  return _createClass(JsonLdHandler, [{
    key: "parse",
    value: function parse(fetcher, responseText, options, response) {
      var kb = fetcher.store;
      return new Promise(function (resolve, reject) {
        try {
          jsonldParser(responseText, kb, options.original.value, function () {
            resolve(fetcher.doneFetch(options, response));
          });
        } catch (err) {
          var msg = 'Error trying to parse ' + options.resource + ' as JSON-LD:\n' + err; // not err.stack -- irrelevant
          resolve(fetcher.failFetch(options, msg, 'parse_error', response));
        }
      });
    }
  }], [{
    key: "toString",
    value: function toString() {
      return 'JsonLdHandler';
    }
  }, {
    key: "register",
    value: function register(fetcher) {
      fetcher.mediatypes['application/ld+json'] = {
        'q': 0.9
      };
    }
  }]);
}(Handler);
JsonLdHandler.pattern = /application\/ld\+json/;
var TextHandler = /*#__PURE__*/function (_Handler7) {
  function TextHandler() {
    _classCallCheck(this, TextHandler);
    return _callSuper(this, TextHandler, arguments);
  }
  _inherits(TextHandler, _Handler7);
  return _createClass(TextHandler, [{
    key: "parse",
    value: function parse(fetcher, responseText, options) {
      // We only speak dialects of XML right now. Is this XML?

      // Look for an XML declaration
      if (isXML(responseText)) {
        fetcher.addStatus(options.req, 'Warning: ' + options.resource + " has an XML declaration. We'll assume " + "it's XML but its content-type wasn't XML.\n");
        var xmlHandler = new XMLHandler(this.response);
        return xmlHandler.parse(fetcher, responseText, options);
      }

      // Look for an XML declaration
      if (responseText.slice(0, 500).match(/xmlns:/)) {
        fetcher.addStatus(options.req, "May have an XML namespace. We'll assume " + "it's XML but its content-type wasn't XML.\n");
        var _xmlHandler = new XMLHandler(this.response);
        return _xmlHandler.parse(fetcher, responseText, options);
      }

      // We give up finding semantics - this is not an error, just no data
      fetcher.addStatus(options.req, 'Plain text document, no known RDF semantics.');
      return fetcher.doneFetch(options, this.response);
    }
  }], [{
    key: "toString",
    value: function toString() {
      return 'TextHandler';
    }
  }, {
    key: "register",
    value: function register(fetcher) {
      fetcher.mediatypes['text/plain'] = {
        'q': 0.5
      };
    }
  }]);
}(Handler);
TextHandler.pattern = new RegExp('text/plain');
var N3Handler = /*#__PURE__*/function (_Handler8) {
  function N3Handler() {
    _classCallCheck(this, N3Handler);
    return _callSuper(this, N3Handler, arguments);
  }
  _inherits(N3Handler, _Handler8);
  return _createClass(N3Handler, [{
    key: "parse",
    value: function parse(fetcher, responseText, options, response) {
      // Parse the text of this N3 file
      var kb = fetcher.store;
      var p = N3Parser(kb, kb, options.original.value, options.original.value, null, null, '', null);
      //                p.loadBuf(xhr.responseText)
      try {
        p.loadBuf(responseText);
      } catch (err) {
        var msg = 'Error trying to parse ' + options.resource + ' as Notation3:\n' + err; // not err.stack -- irrelevant
        return fetcher.failFetch(options, msg, 'parse_error', response);
      }
      fetcher.addStatus(options.req, 'N3 parsed: ' + p.statementCount + ' triples in ' + p.lines + ' lines.');
      fetcher.store.add(options.original, ns.rdf('type'), ns.link('RDFDocument'), fetcher.appNode);
      return fetcher.doneFetch(options, this.response);
    }
  }], [{
    key: "toString",
    value: function toString() {
      return 'N3Handler';
    }
  }, {
    key: "register",
    value: function register(fetcher) {
      fetcher.mediatypes['text/n3'] = {
        'q': '1.0'
      }; // as per 2008 spec
      /*
       fetcher.mediatypes['application/x-turtle'] = {
       'q': 1.0
       } // pre 2008
       */
      fetcher.mediatypes['text/turtle'] = {
        'q': 1.0
      }; // post 2008
    }
  }]);
}(Handler);
N3Handler.pattern = new RegExp('(application|text)/(x-)?(rdf\\+)?(n3|turtle)');
var defaultHandlers = {
  RDFXMLHandler: RDFXMLHandler,
  XHTMLHandler: XHTMLHandler,
  XMLHandler: XMLHandler,
  HTMLHandler: HTMLHandler,
  TextHandler: TextHandler,
  N3Handler: N3Handler,
  JsonLdHandler: JsonLdHandler
};
function isXHTML(responseText) {
  var docTypeStart = responseText.indexOf('<!DOCTYPE html');
  var docTypeEnd = responseText.indexOf('>');
  if (docTypeStart === -1 || docTypeEnd === -1 || docTypeStart > docTypeEnd) {
    return false;
  }
  return responseText.substr(docTypeStart, docTypeEnd - docTypeStart).indexOf('XHTML') !== -1;
}
function isXML(responseText) {
  var match = responseText.match(/\s*<\?xml\s+version\s*=[^<>]+\?>/);
  return !!match;
}
function isXMLNS(responseText) {
  var match = responseText.match(/[^(<html)]*<html\s+[^<]*xmlns=['"]http:\/\/www.w3.org\/1999\/xhtml["'][^<]*>/);
  return !!match;
}

// Not sure about the shapes of this. Response? FetchError?

/** Differs from normal Fetch, has an extended Response type */
/** Fetcher
 *
 * The Fetcher object is a helper object for a quadstore
 * which turns it from an offline store to an online store.
 * The fetcher deals with loading data files rom the web,
  * figuring how to parse them.  It will also refresh, remove, the data
  * and put back the data to the web.
 */
var Fetcher = /*#__PURE__*/function () {
  function Fetcher(store) {
    var _this = this;
    var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    _classCallCheck(this, Fetcher);
    _defineProperty(this, "store", void 0);
    _defineProperty(this, "timeout", void 0);
    _defineProperty(this, "_fetch", void 0);
    _defineProperty(this, "mediatypes", void 0);
    /** Denoting this session */
    _defineProperty(this, "appNode", void 0);
    /**
     * this.requested[uri] states:
     * undefined     no record of web access or records reset
     * true          has been requested, fetch in progress
     * 'done'        received, Ok
     * 401           Not logged in
     * 403           HTTP status unauthorized
     * 404           Resource does not exist. Can be created etc.
     * 'redirected'  In attempt to counter CORS problems retried.
     * 'parse_error' Parse error
     * 'unsupported_protocol'  URI is not a protocol Fetcher can deal with
     * other strings mean various other errors.
     */
    _defineProperty(this, "requested", void 0);
    /** List of timeouts associated with a requested URL */
    _defineProperty(this, "timeouts", void 0);
    /** Redirected from *key uri* to *value uri* */
    _defineProperty(this, "redirectedTo", void 0);
    _defineProperty(this, "fetchQueue", void 0);
    /** fetchCallbacks[uri].push(callback) */
    _defineProperty(this, "fetchCallbacks", void 0);
    /** Keep track of explicit 404s -> we can overwrite etc */
    _defineProperty(this, "nonexistent", void 0);
    _defineProperty(this, "lookedUp", void 0);
    _defineProperty(this, "handlers", void 0);
    _defineProperty(this, "ns", void 0);
    /** Methods added by calling Util.callbackify in the constructor*/
    _defineProperty(this, "fireCallbacks", void 0);
    this.store = store || new IndexedFormula();
    this.ns = getNS(this.store.rdfFactory);
    this.timeout = options.timeout || 30000;

    // solidFetcher is deprecated
    this._fetch = options.fetch || typeof global !== 'undefined' && (global.solidFetcher || global.solidFetch) || typeof window !== 'undefined' && (window.solidFetcher || window.solidFetch) || crossFetch;
    if (!this._fetch) {
      throw new Error('No _fetch function available for Fetcher');
    }
    // This is the name of the graph we store all the HTTP metadata in
    this.appNode = this.store.sym('chrome://TheCurrentSession');
    // this.appNode = this.store.rdfFactory.blankNode() // Needs to have a URI in tests
    this.store.fetcher = this; // Bi-linked
    this.requested = {};
    this.timeouts = {};
    this.redirectedTo = {};
    this.fetchQueue = {};
    this.fetchCallbacks = {};
    this.nonexistent = {};
    this.lookedUp = {};
    this.handlers = [];
    this.mediatypes = {
      'image/*': {
        'q': 0.9
      },
      '*/*': {
        'q': 0.1
      } // Must allow access to random content
    };

    // Util.callbackify(this, ['request', 'recv', 'headers', 'load', 'fail',
    //   'refresh', 'retract', 'done'])
    // In switching to fetch(), 'recv', 'headers' and 'load' do not make sense
    Util.callbackify(this, ['request', 'fail', 'refresh', 'retract', 'done']);
    Object.keys(options.handlers || defaultHandlers).map(function (key) {
      return _this.addHandler(defaultHandlers[key]);
    });
  }
  return _createClass(Fetcher, [{
    key: "load",
    value:
    /**
     * Promise-based load function
     *
     * Loads a web resource or resources into the store.
     *
     * A resource may be given as NamedNode object, or as a plain URI.
     * an array of resources will be given, in which they will be fetched in parallel.
     * By default, the HTTP headers are recorded also, in the same store, in a separate graph.
     * This allows code like editable() for example to test things about the resource.
     *
     * @param uri {Array<RDFlibNamedNode>|Array<string>|RDFlibNamedNode|string}
     *
     * @param [options={}] {Object}
     *
     * @param [options.fetch] {Function}
     *
     * @param [options.referringTerm] {RDFlibNamedNode} Referring term, the resource which
     *   referred to this (for tracking bad links)
     *
     * @param [options.contentType] {string} Provided content type (for writes)
     *
     * @param [options.forceContentType] {string} Override the incoming header to
     *   force the data to be treated as this content-type (for reads)
     *
     * @param [options.force] {boolean} Load the data even if loaded before.
     *   Also sets the `Cache-Control:` header to `no-cache`
     *
     * @param [options.baseURI=docuri] {Node|string} Original uri to preserve
     *   through proxying etc (`xhr.original`).
     *
     * @param [options.proxyUsed] {boolean} Whether this request is a retry via
     *   a proxy (generally done from an error handler)
     *
     * @param [options.withCredentials] {boolean} flag for XHR/CORS etc
     *
     * @param [options.clearPreviousData] {boolean} Before we parse new data,
     *   clear old, but only on status 200 responses
     *
     * @param [options.noMeta] {boolean} Prevents the addition of various metadata
     *   triples (about the fetch request) to the store
     *
     * @param [options.noRDFa] {boolean}
     *
     * @returns {Promise<Result>}
     */
    function load(uri) {
      var _this2 = this;
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
      options = Object.assign({}, options); // Take a copy as we add stuff to the options!!
      if (uri instanceof Array) {
        return Promise.all(uri.map(function (x) {
          return _this2.load(x, Object.assign({}, options));
        }));
      }
      var uriIn = uri;
      var docuri = termValue(uriIn);
      docuri = docuri.split('#')[0];
      options = this.initFetchOptions(docuri, options);
      // if metadata flaged clear cache and removeDocument
      var meta = this.appNode;
      var kb = this.store;
      var requests = kb.statementsMatching(undefined, this.ns.link('requestedURI'), kb.sym(docuri), meta).map(function (st) {
        return st.subject;
      });
      var _iterator = _createForOfIteratorHelper(requests),
        _step;
      try {
        for (_iterator.s(); !(_step = _iterator.n()).done;) {
          var request = _step.value;
          var _response = kb.any(request, this.ns.link('response'), null, meta);
          if (_response != undefined) {
            // ts
            var quad = kb.statementsMatching(_response, this.ns.link('outOfDate'), true, meta);
            kb.remove(quad);
            options.force = true;
            options.clearPreviousData = true;
          }
        }
      } catch (err) {
        _iterator.e(err);
      } finally {
        _iterator.f();
      }
      var initialisedOptions = this.initFetchOptions(docuri, options);
      return this.pendingFetchPromise(docuri, initialisedOptions.baseURI, initialisedOptions);
    }
  }, {
    key: "pendingFetchPromise",
    value: function () {
      var _pendingFetchPromise = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(uri, originalUri, options) {
        var _this3 = this;
        var pendingPromise;
        return _regeneratorRuntime.wrap(function _callee$(_context) {
          while (1) switch (_context.prev = _context.next) {
            case 0:
              _context.t0 = !options.force;
              if (!_context.t0) {
                _context.next = 5;
                break;
              }
              _context.next = 4;
              return this.fetchQueue[originalUri];
            case 4:
              _context.t0 = _context.sent;
            case 5:
              if (!_context.t0) {
                _context.next = 9;
                break;
              }
              pendingPromise = this.fetchQueue[originalUri];
              _context.next = 12;
              break;
            case 9:
              pendingPromise = Promise.race([this.setRequestTimeout(uri, options), this.fetchUri(uri, options)]);
              this.fetchQueue[originalUri] = pendingPromise;

              // Clean up the queued promise after a time, if it's resolved
              this.cleanupFetchRequest(originalUri, undefined, this.timeout);
            case 12:
              return _context.abrupt("return", pendingPromise.then(function (x) {
                if (uri in _this3.timeouts) {
                  _this3.timeouts[uri].forEach(clearTimeout);
                  delete _this3.timeouts[uri];
                }
                return x;
              }));
            case 13:
            case "end":
              return _context.stop();
          }
        }, _callee, this);
      }));
      function pendingFetchPromise(_x, _x2, _x3) {
        return _pendingFetchPromise.apply(this, arguments);
      }
      return pendingFetchPromise;
    }()
    /**
     * @param _options - DEPRECATED
     */
  }, {
    key: "cleanupFetchRequest",
    value: function cleanupFetchRequest(originalUri, _options, timeout) {
      var _this4 = this;
      if (_options !== undefined) {
        console.warn("_options is deprecated");
      }
      this.timeouts[originalUri] = (this.timeouts[originalUri] || []).concat(setTimeout(function () {
        if (!_this4.isPending(originalUri)) {
          delete _this4.fetchQueue[originalUri];
        }
      }, timeout));
    }
  }, {
    key: "initFetchOptions",
    value: function initFetchOptions(uri, options) {
      var kb = this.store;
      var isGet = !options.method || options.method.toUpperCase() === 'GET';
      if (!isGet) {
        options.force = true;
      }
      options.resource = kb.rdfFactory.namedNode(uri); // This might be proxified
      options.baseURI = options.baseURI || uri; // Preserve though proxying etc
      options.original = kb.rdfFactory.namedNode(options.baseURI);
      options.req = kb.bnode();
      options.headers = options.headers || new Headers();
      if (options.contentType) {
        // @ts-ignore
        options.headers['content-type'] = options.contentType;
      }
      if (options.force) {
        options.cache = 'no-cache';
      }
      var acceptString = this.acceptString();
      // @ts-ignore
      options.headers['accept'] = acceptString;
      var requestedURI = Fetcher.offlineOverride(uri);
      options.requestedURI = requestedURI;
      Fetcher.setCredentials(requestedURI, options);
      var actualProxyURI = Fetcher.proxyIfNecessary(requestedURI);
      if (requestedURI !== actualProxyURI) {
        options.proxyUsed = true;
      }
      options.actualProxyURI = actualProxyURI;
      return options;
    }

    /**
     * (The promise chain ends in either a `failFetch()` or a `doneFetch()`)
     *
     * @param docuri {string}
     * @param options {Object}
     *
     * @returns {Promise<Object>} fetch() result or an { error, status } object
     */
  }, {
    key: "fetchUri",
    value: function fetchUri(docuri, options) {
      var _this5 = this;
      if (!docuri) {
        return Promise.reject(new Error('Cannot fetch an empty uri'));
      }
      if (Fetcher.unsupportedProtocol(docuri)) {
        return this.failFetch(options, 'fetcher: Unsupported protocol', 'unsupported_protocol');
      }
      var state = this.getState(docuri);
      if (!options.force) {
        if (state === 'fetched') {
          // URI already fetched and added to store
          return Promise.resolve(
          // @ts-ignore This is not a valid response object
          this.doneFetch(options, {
            status: 200,
            ok: true,
            statusText: 'Already loaded into quadstore.'
          }));
        }
        if (state === 'failed' && this.requested[docuri] === 404) {
          // Remember nonexistence
          var _message = 'Previously failed: ' + this.requested[docuri];
          // @ts-ignore This is not a valid response object
          var dummyResponse = {
            url: docuri,
            // This does not comply to Fetch spec, it can be a string value in rdflib
            status: this.requested[docuri],
            statusText: _message,
            responseText: _message,
            headers: new Headers(),
            // Headers() ???
            ok: false,
            body: null,
            bodyUsed: false,
            size: 0,
            timeout: 0
          };
          return this.failFetch(options, _message, this.requested[docuri], dummyResponse);
        }
      } else {
        // options.force == true
        delete this.nonexistent[docuri];
      }
      this.fireCallbacks('request', [docuri]);
      this.requested[docuri] = true; // mark this uri as 'requested'

      if (!options.noMeta) {
        this.saveRequestMetadata(docuri, options);
      }
      var actualProxyURI = options.actualProxyURI;

      // Map might get mistakenly added into headers
      // error TS2339: Property 'map' does not exist on type 'Headers'.
      /* let map
      if (options.headers && map in options.headers) {
        delete options.headers.map
      } */

      return this._fetch(actualProxyURI, options).then(function (response) {
        return _this5.handleResponse(response, docuri, options);
      }, function (error) {
        // @@ handleError?
        // @ts-ignore Invalid response object
        var dummyResponse = {
          url: actualProxyURI,
          status: 999,
          // @@ what number/string should fetch failures report?
          statusText: (error.name || 'network failure') + ': ' + (error.errno || error.code || error.type),
          responseText: error.message,
          headers: new Headers(),
          // Headers() ???
          ok: false,
          body: null,
          bodyUsed: false,
          size: 0,
          timeout: 0
        };
        // console.log('Fetcher: <' + actualProxyURI + '> Non-HTTP fetch exception: ' + error)
        return _this5.handleError(dummyResponse, docuri, options); // possible credentials retry
        // return this.failFetch(options, 'fetch failed: ' + error, 999, dummyResponse) // Fake status code: fetch exception

        // handleError expects a response so we fake some important bits.
        /*
        this.handleError(, docuri, options)
        */
      });
    }

    /**
     * Asks for a doc to be loaded if necessary then calls back
     *
     * Calling methods:
     *   nowOrWhenFetched (uri, userCallback)
     *   nowOrWhenFetched (uri, options, userCallback)
     *   nowOrWhenFetched (uri, referringTerm, userCallback, options)  <-- old
     *   nowOrWhenFetched (uri, referringTerm, userCallback) <-- old
     *
     *  Options include:
     *   referringTerm    The document in which this link was found.
     *                    this is valuable when finding the source of bad URIs
     *   force            boolean.  Never mind whether you have tried before,
     *                    load this from scratch.
     *   forceContentType Override the incoming header to force the data to be
     *                    treated as this content-type.
     *
     *  Callback function takes:
     *
     *    ok               True if the fetch worked, and got a 200 response.
     *                     False if any error happened
     *
     *    errmessage       Text error message if not OK.
     *
     *    response         The fetch Response object (was: XHR) if there was was one
     *                     includes response.status as the HTTP status if any.
     */
  }, {
    key: "nowOrWhenFetched",
    value: function nowOrWhenFetched(uriIn, p2, userCallback) {
      var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
      var uri = termValue(uriIn);
      if (typeof p2 === 'function') {
        // nowOrWhenFetched (uri, userCallback)
        userCallback = p2;
      } else if (typeof p2 === 'undefined') {// original calling signature
        // referringTerm = undefined
      } else if (isNamedNode(p2)) {
        // referringTerm = p2
        options.referringTerm = p2;
      } else {
        // nowOrWhenFetched (uri, options, userCallback)
        options = p2;
      }
      this.load(uri, options).then(function (fetchResponse) {
        if (userCallback) {
          if (fetchResponse) {
            if (fetchResponse.ok) {
              userCallback(true, 'OK', fetchResponse);
            } else {
              var oops = 'HTTP error: Status ' + fetchResponse.status + ' (' + fetchResponse.statusText + ')';
              if (fetchResponse.responseText) {
                oops += ' ' + fetchResponse.responseText; // not in 404, dns error, nock failure
              }
              userCallback(false, oops, fetchResponse);
            }
          } else {
            var _oops = '@@ nowOrWhenFetched:  no response object!';
            userCallback(false, _oops);
          }
        }
      }, function (err) {
        var message = err.message || err.statusText;
        message = 'Failed to load  <' + uri + '> ' + message;
        if (err.response && err.response.status) {
          message += ' status: ' + err.response.status;
        }
        userCallback(false, message, err.response);
      });
    }

    /**
     * Records a status message (as a literal node) by appending it to the
     * request's metadata status collection.
     *
     */
  }, {
    key: "addStatus",
    value: function addStatus(req, statusMessage) {
      // <Debug about="parsePerformance">
      var now = new Date();
      statusMessage = '[' + now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds() + '.' + now.getMilliseconds() + '] ' + statusMessage;
      // </Debug>
      var kb = this.store;
      var statusNode = kb.the(req, this.ns.link('status'));
      if (isCollection(statusNode)) {
        statusNode.append(kb.rdfFactory.literal(statusMessage));
      } else {
        log.warn('web.js: No list to add to: ' + statusNode + ',' + statusMessage);
      }
    }

    /**
     * Records errors in the system on failure:
     *
     *  - Adds an entry to the request status collection
     *  - Adds an error triple with the fail message to the metadata
     *  - Fires the 'fail' callback
     *  - Rejects with an error result object, which has a response object if any
     */
  }, {
    key: "failFetch",
    value: function failFetch(options, errorMessage, statusCode, response) {
      this.addStatus(options.req, errorMessage);
      if (!options.noMeta) {
        this.store.add(options.original, this.ns.link('error'), this.store.rdfFactory.literal(errorMessage));
      }
      var meth = (options.method || 'GET').toUpperCase();
      var isGet = meth === 'GET' || meth === 'HEAD';
      if (isGet) {
        // only cache the status code on GET or HEAD
        if (!options.resource.equals(options.original)) {
          // console.log('@@ Recording failure  ' + meth + '  original ' + options.original +option '( as ' + options.resource + ') : ' + statusCode)
        } else {
          // console.log('@@ Recording ' + meth + ' failure for ' + options.original + ': ' + statusCode)
        }
        this.requested[Uri.docpart(options.original.value)] = statusCode;
        this.fireCallbacks('fail', [options.original.value, errorMessage]);
      }
      var err = new Error('Fetcher: ' + errorMessage);

      // err.ok = false // Is taken as a response, will work too @@ phase out?
      err.status = statusCode;
      err.statusText = errorMessage;
      err.response = response;
      return Promise.reject(err);
    }

    // in the why part of the quad distinguish between HTML and HTTP header
    // Reverse is set iif the link was rev= as opposed to rel=
  }, {
    key: "linkData",
    value: function linkData(originalUri, rel, uri, why, reverse) {
      if (!uri) return;
      var kb = this.store;
      var predicate;
      // See http://www.w3.org/TR/powder-dr/#httplink for describedby 2008-12-10
      var obj = kb.rdfFactory.namedNode(Uri.join(uri, originalUri.value));
      if (rel === 'alternate' || rel === 'seeAlso' || rel === 'meta' || rel === 'describedby') {
        if (obj.value === originalUri.value) {
          return;
        }
        predicate = this.ns.rdfs('seeAlso');
      } else if (rel === 'type') {
        predicate = kb.rdfFactory.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type');
      } else {
        // See https://www.iana.org/assignments/link-relations/link-relations.xml
        // Alas not yet in RDF yet for each predicate
        // encode space in e.g. rel="shortcut icon"
        predicate = kb.rdfFactory.namedNode(Uri.join(encodeURIComponent(rel), 'http://www.iana.org/assignments/link-relations/'));
      }
      if (reverse) {
        kb.add(obj, predicate, originalUri, why);
      } else {
        kb.add(originalUri, predicate, obj, why);
      }
    }
  }, {
    key: "parseLinkHeader",
    value: function parseLinkHeader(linkHeader, originalUri, reqNode) {
      if (!linkHeader) {
        return;
      }

      // const linkexp = /<[^>]*>\s*(\s*;\s*[^()<>@,;:"/[\]?={} \t]+=(([^()<>@,;:"/[]?={} \t]+)|("[^"]*")))*(,|$)/g
      // const paramexp = /[^()<>@,;:"/[]?={} \t]+=(([^()<>@,;:"/[]?={} \t]+)|("[^"]*"))/g

      // From https://www.dcode.fr/regular-expression-simplificator:
      // const linkexp = /<[^>]*>\s*(\s*;\s*[^()<>@,;:"/[\]?={} t]+=["]))*[,$]/g
      // const paramexp = /[^\\<>@,;:"\/\[\]?={} \t]+=["])/g
      // Original:
      var linkexp = /<[^>]*>\s*(\s*;\s*[^()<>@,;:"/[\]?={} \t]+=(([^\(\)<>@,;:"\/\[\]\?={} \t]+)|("[^"]*")))*(,|$)/g;
      var paramexp = /[^\(\)<>@,;:"\/\[\]\?={} \t]+=(([^\(\)<>@,;:"\/\[\]\?={} \t]+)|("[^"]*"))/g;
      var matches = linkHeader.match(linkexp);
      if (matches == null) return;
      for (var i = 0; i < matches.length; i++) {
        var split = matches[i].split('>');
        var href = split[0].substring(1);
        var ps = split[1];
        var s = ps.match(paramexp);
        if (s == null) return;
        for (var j = 0; j < s.length; j++) {
          var p = s[j];
          var paramsplit = p.split('=');
          // var name = paramsplit[0]
          var rel = paramsplit[1].replace(/["']/g, ''); // '"
          this.linkData(originalUri, rel, href, reqNode);
        }
      }
    }
  }, {
    key: "doneFetch",
    value: function doneFetch(options, response) {
      this.addStatus(options.req, 'Done.');
      this.requested[options.original.value] = 'done';
      this.fireCallbacks('done', [options.original.value]);
      response.req = options.req; // Set the request meta blank node

      return response;
    }

    /**
     * Note two nodes are now smushed
     * If only one was flagged as looked up, then the new node is looked up again,
     * which will make sure all the URIs are dereferenced
     */
  }, {
    key: "nowKnownAs",
    value: function nowKnownAs(was, now) {
      if (this.lookedUp[was.value]) {
        // Transfer userCallback
        if (!this.lookedUp[now.value]) {
          this.lookUpThing(now, was);
        }
      } else if (this.lookedUp[now.value]) {
        if (!this.lookedUp[was.value]) {
          this.lookUpThing(was, now);
        }
      }
    }

    /**
     * Writes back to the web what we have in the store for this uri
     */
  }, {
    key: "putBack",
    value: function putBack(uri) {
      var _this6 = this;
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
      var uriSting = termValue(uri);
      var doc = new RDFlibNamedNode(uriSting).doc(); // strip off #
      options.contentType = options["content-type"] || options["Content-Type"] || options.contentType || TurtleContentType;
      if (options.contentType === 'application/ld+json') {
        return new Promise(function (resolve, reject) {
          serialize(doc, _this6.store, doc.uri, options.contentType, function (err, jsonString) {
            if (err) {
              reject(err);
            } else {
              // @ts-ignore
              options.data = jsonString;
              _this6.webOperation('PUT', uri, options).then(function (res) {
                return resolve(res);
              }).catch(function (error) {
                return reject(error);
              });
            }
          });
        });
      }
      options.data = serialize(doc, this.store, doc.value, options.contentType);
      return this.webOperation('PUT', uriSting, options);
    }
  }, {
    key: "webCopy",
    value: function webCopy(here, there, contentType) {
      var _this7 = this;
      return this.webOperation('GET', here).then(function (result) {
        return _this7.webOperation('PUT',
        // change to binary from text
        there, {
          data: result.responseText,
          contentType: contentType
        });
      });
    }
  }, {
    key: "delete",
    value: function _delete(uri, options) {
      var _this8 = this;
      return this.webOperation('DELETE', uri, options).then(function (response) {
        _this8.requested[uri] = 404;
        _this8.nonexistent[uri] = true;
        _this8.unload(_this8.store.rdfFactory.namedNode(uri));
        return response;
      });
    }

    /** Create an empty resource if it really does not exist
     *  Be absolutely sure something does not exist before creating a new empty file
     * as otherwise existing could  be deleted.
     * @param doc - The resource
    */
  }, {
    key: "createIfNotExists",
    value: (function () {
      var _createIfNotExists = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(doc) {
        var contentType,
          data,
          fetcher,
          response,
          _args2 = arguments;
        return _regeneratorRuntime.wrap(function _callee2$(_context2) {
          while (1) switch (_context2.prev = _context2.next) {
            case 0:
              contentType = _args2.length > 1 && _args2[1] !== undefined ? _args2[1] : TurtleContentType;
              data = _args2.length > 2 && _args2[2] !== undefined ? _args2[2] : '';
              fetcher = this;
              _context2.prev = 3;
              _context2.next = 6;
              return fetcher.load(doc);
            case 6:
              response = _context2.sent;
              _context2.next = 26;
              break;
            case 9:
              _context2.prev = 9;
              _context2.t0 = _context2["catch"](3);
              if (!(_context2.t0.response.status === 404)) {
                _context2.next = 25;
                break;
              }
              _context2.prev = 12;
              _context2.next = 15;
              return fetcher.webOperation('PUT', doc.value, {
                data: data,
                contentType: contentType
              });
            case 15:
              response = _context2.sent;
              _context2.next = 21;
              break;
            case 18:
              _context2.prev = 18;
              _context2.t1 = _context2["catch"](12);
              throw _context2.t1;
            case 21:
              delete fetcher.requested[doc.value]; // delete cached 404 error
              // console.log('createIfNotExists doc created ok ' + doc)
              return _context2.abrupt("return", response);
            case 25:
              throw _context2.t0;
            case 26:
              return _context2.abrupt("return", response);
            case 27:
            case "end":
              return _context2.stop();
          }
        }, _callee2, this, [[3, 9], [12, 18]]);
      }));
      function createIfNotExists(_x4) {
        return _createIfNotExists.apply(this, arguments);
      }
      return createIfNotExists;
    }()
    /**
     * @param parentURI URI of parent container
     * @param folderName - Optional folder name (slug)
     * @param data - Optional folder metadata
     */
    )
  }, {
    key: "createContainer",
    value: function createContainer(parentURI, folderName, data) {
      var headers = {
        // Force the right mime type for containers
        'content-type': TurtleContentType,
        'link': this.ns.ldp('BasicContainer') + '; rel="type"'
      };
      if (folderName) {
        headers['slug'] = folderName;
      }

      // @ts-ignore These headers lack some of the required operators.
      var options = {
        headers: headers
      };
      if (data) {
        options.body = data;
      }
      return this.webOperation('POST', parentURI, options);
    }
  }, {
    key: "invalidateCache",
    value: function invalidateCache(iri) {
      var uri = termValue(iri);
      var fetcher = this;
      // @ts-ignore
      if (fetcher.fetchQueue && fetcher.fetchQueue[uri]) {
        // console.log('Internal error - fetchQueue exists ' + uri)
        var promise = fetcher.fetchQueue[uri];
        if (promise['PromiseStatus'] === 'resolved') {
          delete fetcher.fetchQueue[uri];
        } else {
          // pending
          delete fetcher.fetchQueue[uri];
          // console.log('*** Fetcher: pending fetchQueue deleted ' + uri)
        }
      }
      if (fetcher.requested[uri] && fetcher.requested[uri] !== 'done' && fetcher.requested[uri] !== 'failed' && fetcher.requested[uri] !== 404) {
        var msg = "Rdflib: fetcher: Destructive operation on <".concat(fetcher.requested[uri], "> file being fetched! ") + uri;
        console.error(msg);
        // alert(msg)
      } else {
        delete fetcher.requested[uri]; // invalidate read cache -- @@ messes up logic if request in progress ??
        delete fetcher.nonexistent[uri];
      }
    }

    /**
     * A generic web operation, at the fetch() level.
     * does not involve the quad store.
     *
     *  Returns promise of Response
     *  If data is returned, copies it to response.responseText before returning
     */
  }, {
    key: "webOperation",
    value: function webOperation(method, uriIn) {
      var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
      var uri = termValue(uriIn);
      options.method = method;
      options.body = options.data || options.body;
      options.force = true;
      var fetcher = this;
      if (options.body && !options.contentType) {
        throw new Error('Web operation sending data must have a defined contentType.');
      }
      if (options.contentType) {
        options.headers = options.headers || {};
        options.headers['content-type'] = options.contentType;
      }
      Fetcher.setCredentials(uri, options);
      return new Promise(function (resolve, reject) {
        fetcher._fetch(uri, options).then(function (response) {
          if (response.ok) {
            if (method === 'PUT' || method === 'PATCH' || method === 'POST' || method === 'DELETE') {
              fetcher.invalidateCache(uri);
            } // response.body with Chrome can't be relied on
            if (response.text) {
              // Was: response.body https://github.com/linkeddata/rdflib.js/issues/506
              response.text().then(function (data) {
                response.responseText = data;
                resolve(response);
              });
            } else {
              resolve(response);
            }
          } else {
            var msg = 'Web error: ' + response.status;
            if (response.statusText) msg += ' (' + response.statusText + ')';
            msg += ' on ' + method + ' of <' + uri + '>';
            if (response.responseText) msg += ': ' + response.responseText;
            var e2 = new Error(msg);
            e2.response = response;
            reject(e2);
          }
        }, function (err) {
          var msg = 'Fetch error for ' + method + ' of <' + uri + '>:' + err;
          reject(new Error(msg));
        });
      });
    }

    /**
     * Looks up something.
     * Looks up all the URIs a things has.
     *
     * @param term - canonical term for the thing whose URI is
     *   to be dereferenced
     * @param rterm - the resource which referred to this
     *   (for tracking bad links)
     */
  }, {
    key: "lookUpThing",
    value: function lookUpThing(term, rterm) {
      var _this9 = this;
      var uris = this.store.uris(term); // Get all URIs
      uris = uris.map(function (u) {
        return Uri.docpart(u);
      }); // Drop hash fragments

      uris.forEach(function (u) {
        _this9.lookedUp[u] = true;
      });

      // @ts-ignore Recursive type
      return this.load(uris, {
        referringTerm: rterm
      });
    }

    /**
     * Looks up response header.
     *
     * @returns {Array|undefined} a list of header values found in a stored HTTP
     *   response, or [] if response was found but no header found,
     *   or undefined if no response is available.
     * Looks for { [] link:requestedURI ?uri; link:response [ httph:header-name  ?value ] }
     */
  }, {
    key: "getHeader",
    value: function getHeader(doc, header) {
      var kb = this.store; // look for the URI (AS A STRING NOT A NODE) for a stored request
      var docuri = doc.value;
      var requests = kb.each(undefined, this.ns.link('requestedURI'), kb.rdfFactory.literal(docuri));
      for (var r = 0; r < requests.length; r++) {
        var request = requests[r];
        if (request !== undefined) {
          var _response2 = kb.any(request, this.ns.link('response'));
          if (_response2 !== undefined && kb.anyValue(_response2, this.ns.http('status')) && kb.anyValue(_response2, this.ns.http('status')).startsWith('2')) {
            // Only look at success returns - not 401 error messagess etc
            var results = kb.each(_response2, this.ns.httph(header.toLowerCase()));
            if (results.length) {
              return results.map(function (v) {
                return v.value;
              });
            }
            return [];
          }
        }
      }
      return undefined;
    }
  }, {
    key: "saveRequestMetadata",
    value: function saveRequestMetadata(docuri, options) {
      var req = options.req;
      var kb = this.store;
      var rterm = options.referringTerm;
      this.addStatus(options.req, 'Accept: ' + options.headers['accept']);
      if (isNamedNode(rterm)) {
        kb.add(kb.rdfFactory.namedNode(docuri), this.ns.link('requestedBy'), rterm, this.appNode);
      }
      if (options.original && options.original.value !== docuri) {
        kb.add(req, this.ns.link('orginalURI'), kb.rdfFactory.literal(options.original.value), this.appNode);
      }
      var now = new Date();
      var timeNow = '[' + now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds() + '] ';
      kb.add(req, this.ns.rdfs('label'), kb.rdfFactory.literal(timeNow + ' Request for ' + docuri), this.appNode);
      // We store the docuri as a string, not as a node,
      // see https://github.com/linkeddata/rdflib.js/pull/427#pullrequestreview-447910061
      kb.add(req, this.ns.link('requestedURI'), kb.rdfFactory.literal(docuri), this.appNode);
      kb.add(req, this.ns.link('status'), kb.collection(), this.appNode);
    }
  }, {
    key: "saveResponseMetadata",
    value: function saveResponseMetadata(response, options) {
      var _this10 = this;
      var kb = this.store;
      var responseNode = kb.bnode();
      kb.add(options.req, this.ns.link('response'), responseNode, this.appNode);
      kb.add(responseNode, this.ns.http('status'), kb.rdfFactory.literal(response.status), this.appNode);
      kb.add(responseNode, this.ns.http('statusText'), kb.rdfFactory.literal(response.statusText), this.appNode);

      // Save the response headers
      response.headers.forEach(function (value, header) {
        kb.add(responseNode, _this10.ns.httph(header), _this10.store.rdfFactory.literal(value), _this10.appNode);
        if (header === 'content-type') {
          kb.add(options.resource, _this10.ns.rdf('type'), kb.rdfFactory.namedNode(Util.mediaTypeClass(value).value), _this10.appNode // responseNode
          );
        }
      });
      return responseNode;
    }
  }, {
    key: "objectRefresh",
    value: function objectRefresh(term) {
      var uris = this.store.uris(term); // Get all URIs
      if (typeof uris !== 'undefined') {
        for (var i = 0; i < uris.length; i++) {
          this.refresh(this.store.rdfFactory.namedNode(Uri.docpart(uris[i])));
          // what about rterm?
        }
      }
    }

    /* refresh  Reload data from a given document
    **
    ** @param term - An RDF Named Node for the eodcument in question
    ** @param userCallback - A function userCallback(ok, message, response)
    */
  }, {
    key: "refresh",
    value: function refresh(term, userCallback) {
      // sources_refresh
      this.fireCallbacks('refresh', arguments);
      this.nowOrWhenFetched(term, {
        force: true,
        clearPreviousData: true
      }, userCallback);
    }

    /* refreshIfExpired   Conditional refresh if Expired
    **
    ** @param term - An RDF Named Node for the eodcument in question
    ** @param userCallback - A function userCallback(ok, message, response)
    */
  }, {
    key: "refreshIfExpired",
    value: function refreshIfExpired(term, userCallback) {
      var exp = this.getHeader(term, 'Expires');
      if (!exp || new Date(exp[0]).getTime() <= new Date().getTime()) {
        this.refresh(term, userCallback);
      } else {
        userCallback(true, 'Not expired', {});
      }
    }
  }, {
    key: "retract",
    value: function retract(term) {
      // sources_retract
      this.store.removeMany(undefined, undefined, undefined, term);
      if (term.value) {
        delete this.requested[Uri.docpart(term.value)];
      }
      this.fireCallbacks('retract', arguments);
    }
  }, {
    key: "getState",
    value: function getState(docuri) {
      if (typeof this.requested[docuri] === 'undefined') {
        return 'unrequested';
      } else if (this.requested[docuri] === true) {
        return 'requested';
      } else if (this.requested[docuri] === 'done') {
        return 'fetched';
      } else if (this.requested[docuri] === 'redirected') {
        return this.getState(this.redirectedTo[docuri]);
      } else {
        // An non-200 HTTP error status
        return 'failed';
      }
    }
  }, {
    key: "isPending",
    value: function isPending(docuri) {
      // sources_pending
      // doing anyStatementMatching is wasting time
      // if it's not pending: false -> flailed
      //   'done' -> done 'redirected' -> redirected
      return this.requested[docuri] === true;
    }
  }, {
    key: "unload",
    value: function unload(term) {
      this.store.removeDocument(term);
      delete this.requested[term.value]; // So it can be load2ed again
    }
  }, {
    key: "addHandler",
    value: function addHandler(handler) {
      this.handlers.push(handler);
      handler.register(this);
    }
  }, {
    key: "retryNoCredentials",
    value: function retryNoCredentials(docuri, options) {
      // console.log('Fetcher: CORS: RETRYING with NO CREDENTIALS for ' + options.resource)

      options.retriedWithNoCredentials = true; // protect against being called twice

      delete this.requested[docuri]; // forget the original request happened
      delete this.fetchQueue[docuri];
      // Note: XHR property was withCredentials, but fetch property is just credentials
      var newOptions = Object.assign({}, options, {
        credentials: 'omit'
      });
      this.addStatus(options.req, 'Abort: Will retry with credentials SUPPRESSED to see if that helps');
      return this.load(docuri, newOptions);
    }

    /**
     * Tests whether a request is being made to a cross-site URI (for purposes
     * of retrying with a proxy)
     */
  }, {
    key: "isCrossSite",
    value: function isCrossSite(uri) {
      // Mashup situation, not node etc
      if (typeof document === 'undefined' || !document.location) {
        return false;
      }
      var hostpart = Uri.hostpart;
      var here = '' + document.location;
      return (hostpart(here) && hostpart(uri) && hostpart(here)) !== hostpart(uri);
    }

    /**
     * Called when there's a network error in fetch(), or a response
     * with status of 0.
     */
  }, {
    key: "handleError",
    value: function handleError(response, docuri, options) {
      if (this.isCrossSite(docuri)) {
        // Make sure we haven't retried already
        if (options.credentials && options.credentials === 'include' && !options.retriedWithNoCredentials) {
          return this.retryNoCredentials(docuri, options);
        }

        // Now attempt retry via proxy
        var proxyUri = Fetcher.crossSiteProxy(docuri);
        if (proxyUri && !options.proxyUsed) {
          // console.log('web: Direct failed so trying proxy ' + proxyUri)
          return this.redirectToProxy(proxyUri, options);
        }
      }
      var message;
      if (response instanceof Error) {
        message = 'Fetch error: ' + response.message;
      } else {
        message = response.statusText;
        if (response.responseText) {
          message += " ".concat(response.responseText);
        }
      }

      // This is either not a CORS error, or retries have been made
      return this.failFetch(options, message, response.status || 998, response);
    }

    // deduce some things from the HTTP transaction
  }, {
    key: "addType",
    value: function addType(rdfType, req, kb, locURI) {
      // add type to all redirected resources too
      var prev = req;
      if (locURI) {
        var reqURI = kb.any(prev, this.ns.link('requestedURI'));
        if (reqURI && reqURI.value !== locURI) {
          kb.add(kb.rdfFactory.namedNode(locURI), this.ns.rdf('type'), rdfType, this.appNode);
        }
      }
      for (;;) {
        var doc = kb.any(prev, this.ns.link('requestedURI'));
        if (doc && doc.value) {
          kb.add(kb.rdfFactory.namedNode(doc.value), this.ns.rdf('type'), rdfType, this.appNode);
        } // convert Literal
        prev = kb.any(undefined, kb.rdfFactory.namedNode('http://www.w3.org/2007/ont/link#redirectedRequest'), prev);
        if (!prev) {
          break;
        }
        var response = kb.any(prev, kb.rdfFactory.namedNode('http://www.w3.org/2007/ont/link#response'));
        if (!response) {
          break;
        }
        var redirection = kb.any(response, kb.rdfFactory.namedNode('http://www.w3.org/2007/ont/http#status'));
        if (!redirection) {
          break;
        }
        // @ts-ignore always true?
        if (redirection !== '301' && redirection !== '302') {
          break;
        }
      }
    }

    /**
     * Handle fetch() response
     */
  }, {
    key: "handleResponse",
    value: function handleResponse(response, docuri, options) {
      var _this11 = this;
      var kb = this.store;
      var headers = response.headers;
      var reqNode = options.req;
      var responseNode = this.saveResponseMetadata(response, options);
      var contentType = this.normalizedContentType(options, headers) || '';
      var contentLocation = headers.get('content-location');

      // this.fireCallbacks('recv', xhr.args)
      // this.fireCallbacks('headers', [{uri: docuri, headers: xhr.headers}])

      // Check for masked errors (CORS, etc)
      if (response.status === 0) {
        // console.log('Masked error - status 0 for ' + docuri)
        return this.handleError(response, docuri, options);
      }
      if (response.status >= 400) {
        if (response.status === 404) {
          this.nonexistent[options.original.value] = true;
          this.nonexistent[docuri] = true;
        }
        return this.saveErrorResponse(response, responseNode).then(function () {
          var errorMessage = options.resource + ' ' + response.statusText;
          return _this11.failFetch(options, errorMessage, response.status, response);
        });
      }
      var diffLocation = null;
      var absContentLocation = null;
      if (contentLocation) {
        absContentLocation = Uri.join(contentLocation, docuri);
        if (absContentLocation !== docuri) {
          diffLocation = absContentLocation;
        }
      }
      if (response.status === 200) {
        this.addType(this.ns.link('Document'), reqNode, kb, docuri);
        if (diffLocation) {
          this.addType(this.ns.link('Document'), reqNode, kb, diffLocation);
        }

        // Before we parse new data clear old but only on 200
        if (options.clearPreviousData) {
          // kb.removeDocument(options.resource)
          // only remove content, keep metatdata
          var sts = kb.statementsMatching(undefined, undefined, undefined, options.resource).slice(); // Take a copy as this is the actual index
          for (var i = 0; i < sts.length; i++) {
            kb.removeStatement(sts[i]);
          }
        }
        var isImage = contentType.includes('image/') || contentType.includes('application/pdf');
        if (contentType && isImage) {
          this.addType(kb.rdfFactory.namedNode('http://purl.org/dc/terms/Image'), reqNode, kb, docuri);
          if (diffLocation) {
            this.addType(kb.rdfFactory.namedNode('http://purl.org/dc/terms/Image'), reqNode, kb, diffLocation);
          }
        }
      }

      // If we have already got the thing at this location, abort
      if (contentLocation) {
        if (!options.force && diffLocation && this.requested[absContentLocation] === 'done') {
          // we have already fetched this
          // should we smush too?
          // log.info("HTTP headers indicate we have already" + " retrieved " +
          // xhr.resource + " as " + absContentLocation + ". Aborting.")
          return this.doneFetch(options, response);
        }
        this.requested[absContentLocation] = true;
      }
      this.parseLinkHeader(headers.get('link'), options.original, reqNode);
      var handler = this.handlerForContentType(contentType, response);
      if (!handler) {
        //  Not a problem, we just don't extract data
        this.addStatus(reqNode, 'Fetch over. No data handled.');
        return this.doneFetch(options, response);
      }
      return response.text()
      // @ts-ignore Types seem right
      .then(function (responseText) {
        response.responseText = responseText;
        return handler.parse(_this11, responseText, options, response);
      });
    }
  }, {
    key: "saveErrorResponse",
    value: function saveErrorResponse(response, responseNode) {
      var _this12 = this;
      var kb = this.store;
      return response.text().then(function (content) {
        if (content.length > 10) {
          kb.add(responseNode, _this12.ns.http('content'), kb.rdfFactory.literal(content), responseNode);
        }
      });
    }
  }, {
    key: "handlerForContentType",
    value: function handlerForContentType(contentType, response) {
      if (!contentType) {
        return null;
      }
      var Handler = this.handlers.find(function (handler) {
        return contentType.match(handler.pattern);
      });

      // @ts-ignore in practice all Handlers have constructors.
      return Handler ? new Handler(response) : null;
    }
  }, {
    key: "guessContentType",
    value: function guessContentType(uri) {
      return CONTENT_TYPE_BY_EXT[uri.split('.').pop()];
    }
  }, {
    key: "normalizedContentType",
    value: function normalizedContentType(options, headers) {
      if (options.forceContentType) {
        return options.forceContentType;
      }
      var contentType = headers.get('content-type');
      if (!contentType || contentType.includes('application/octet-stream')) {
        var guess = this.guessContentType(options.resource.value);
        if (guess) {
          return guess;
        }
      }
      var protocol = Uri.protocol(options.resource.value);
      if (!contentType && ['file', 'chrome'].includes(protocol)) {
        return 'text/xml';
      }
      return contentType;
    }

    /**
     * Sends a new request to the specified uri. (Extracted from `onerrorFactory()`)
     */
  }, {
    key: "redirectToProxy",
    value: function redirectToProxy(newURI, options) {
      var _this13 = this;
      this.addStatus(options.req, 'BLOCKED -> Cross-site Proxy to <' + newURI + '>');
      options.proxyUsed = true;
      var kb = this.store;
      var oldReq = options.req; // request metadata blank node

      if (!options.noMeta) {
        kb.add(oldReq, this.ns.link('redirectedTo'), kb.rdfFactory.namedNode(newURI), oldReq);
        this.addStatus(oldReq, 'redirected to new request'); // why
      }
      this.requested[options.resource.value] = 'redirected';
      this.redirectedTo[options.resource.value] = newURI;
      var newOptions = Object.assign({}, options);
      newOptions.baseURI = options.resource.value;
      return this.fetchUri(newURI, newOptions).then(function (response) {
        if (!newOptions.noMeta) {
          kb.add(oldReq, _this13.ns.link('redirectedRequest'), newOptions.req, _this13.appNode);
        }
        return response;
      });
    }
  }, {
    key: "setRequestTimeout",
    value: function setRequestTimeout(uri, options) {
      var _this14 = this;
      return new Promise(function (resolve) {
        _this14.timeouts[uri] = (_this14.timeouts[uri] || []).concat(setTimeout(function () {
          if (_this14.isPending(uri) && !options.retriedWithNoCredentials && !options.proxyUsed) {
            resolve(_this14.failFetch(options, "Request to ".concat(uri, " timed out"), 'timeout'));
          }
        }, _this14.timeout));
      });
    }
  }, {
    key: "addFetchCallback",
    value: function addFetchCallback(uri, callback) {
      if (!this.fetchCallbacks[uri]) {
        this.fetchCallbacks[uri] = [callback];
      } else {
        this.fetchCallbacks[uri].push(callback);
      }
    }
  }, {
    key: "acceptString",
    value: function acceptString() {
      var acceptstring = '';
      for (var mediaType in this.mediatypes) {
        if (acceptstring !== '') {
          acceptstring += ', ';
        }
        acceptstring += mediaType;
        for (var property in this.mediatypes[mediaType]) {
          acceptstring += ';' + property + '=' + this.mediatypes[mediaType][property];
        }
      }
      return acceptstring;
    }
    // var updatesVia = new $rdf.UpdatesVia(this) // Subscribe to headers
    // @@@@@@@@ This is turned off because it causes a websocket to be set up for ANY fetch
    // whether we want to track it ot not. including ontologies loaed though the XSSproxy
  }], [{
    key: "crossSiteProxy",
    value: function crossSiteProxy(uri) {
      if (Fetcher.crossSiteProxyTemplate) {
        return Fetcher.crossSiteProxyTemplate.replace('{uri}', encodeURIComponent(uri));
      } else {
        return undefined;
      }
    }
  }, {
    key: "offlineOverride",
    value: function offlineOverride(uri) {
      // Map the URI to a localhost proxy if we are running on localhost
      // This is used for working offline, e.g. on planes.
      // Is the script itself is running in localhost, then access all
      //   data in a localhost mirror.
      // Do not remove without checking with TimBL
      var requestedURI = uri;
      var UI;
      if (typeof window !== 'undefined' && window.panes && (UI = window.panes.UI) && UI.preferences && UI.preferences.get('offlineModeUsingLocalhost')) {
        if (requestedURI.slice(0, 7) === 'http://' && requestedURI.slice(7, 17) !== 'localhost/') {
          requestedURI = 'http://localhost/' + requestedURI.slice(7);
          log.warn('Localhost kludge for offline use: actually getting <' + requestedURI + '>');
        } else {
          // log.warn("Localhost kludge NOT USED <" + requestedURI + ">")
        }
      } else {
        // log.warn("Localhost kludge OFF offline use: actually getting <" +
        //   requestedURI + ">")
      }
      return requestedURI;
    }
  }, {
    key: "proxyIfNecessary",
    value: function proxyIfNecessary(uri) {
      var UI;
      if (typeof window !== 'undefined' && window.panes && (UI = window.panes.UI) && UI.isExtension) {
        return uri;
      } // Extension does not need proxy

      if (typeof $SolidTestEnvironment !== 'undefined' && $SolidTestEnvironment.localSiteMap) {
        // nested dictionaries of URI parts from origin down
        var hostpath = uri.split('/').slice(2); // the bit after the //

        var lookup = function lookup(parts, index) {
          var z = index[parts.shift()];
          if (!z) {
            return null;
          }
          if (typeof z === 'string') {
            return z + parts.join('/');
          }
          if (!parts) {
            return null;
          }
          return lookup(parts, z);
        };
        var y = lookup(hostpath, $SolidTestEnvironment.localSiteMap);
        if (y) {
          return y;
        }
      }

      // browser does 2014 on as https browser script not trusted
      // If the web app origin is https: then the mixed content rules
      // prevent it loading insecure http: stuff so we need proxy.
      if (Fetcher.crossSiteProxyTemplate && typeof document !== 'undefined' && document.location && ('' + document.location).slice(0, 6) === 'https:' &&
      // origin is secure
      uri.slice(0, 5) === 'http:') {
        // requested data is not
        return Fetcher.crossSiteProxyTemplate.replace('{uri}', encodeURIComponent(uri));
      }
      return uri;
    }

    /**
     * Tests whether the uri's protocol is supported by the Fetcher.
     * @param uri
     */
  }, {
    key: "unsupportedProtocol",
    value: function unsupportedProtocol(uri) {
      var pcol = Uri.protocol(uri);
      return pcol === 'tel' || pcol === 'mailto' || pcol === 'urn';
    }

    /** Decide on credentials using old XXHR api or new fetch()  one
     * @param requestedURI
     * @param options
     */
  }, {
    key: "setCredentials",
    value: function setCredentials(requestedURI) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
      // 2014 CORS problem:
      // XMLHttpRequest cannot load http://www.w3.org/People/Berners-Lee/card.
      // A wildcard '*' cannot be used in the 'Access-Control-Allow-Origin'
      //   header when the credentials flag is true.
      // @ Many ontology files under http: and need CORS wildcard ->
      //   can't have credentials
      if (options.credentials === undefined) {
        // Caller using new fetch convention
        if (options.withCredentials !== undefined) {
          // XHR style is what Fetcher specified before
          options.credentials = options.withCredentials ? 'include' : 'omit';
        } else {
          options.credentials = 'include'; // default is to be logged on
        }
      }
    }
  }]);
}();
_defineProperty(Fetcher, "HANDLERS", void 0);
_defineProperty(Fetcher, "CONTENT_TYPE_BY_EXT", void 0);
// TODO: Document this
_defineProperty(Fetcher, "crossSiteProxyTemplate", void 0);
export { Fetcher as default };
Fetcher.HANDLERS = defaultHandlers;
Fetcher.CONTENT_TYPE_BY_EXT = CONTENT_TYPE_BY_EXT;