

  // Firefox < 40 (no "stream" option), IE, Edge
  function TextDecoderPolyfill() {
  }

  //TODO: streaming
  TextDecoderPolyfill.prototype.decode = function (octets) {
    var string = "";
    var i = 0;
    while (i < octets.length) {
      var octet = octets[i];
      var bytesNeeded = 0;
      var codePoint = 0;
      if (octet <= 0x7F) {
        bytesNeeded = 0;
        codePoint = octet & 0xFF;
      } else if (octet <= 0xDF) {
        bytesNeeded = 1;
        codePoint = octet & 0x1F;
      } else if (octet <= 0xEF) {
        bytesNeeded = 2;
        codePoint = octet & 0x0F;
      } else if (octet <= 0xF4) {
        bytesNeeded = 3;
        codePoint = octet & 0x07;
      }
      if (octets.length - i - bytesNeeded > 0) {
        var k = 0;
        while (k < bytesNeeded) {
          octet = octets[i + k + 1];
          codePoint = (codePoint << 6) | (octet & 0x3F);
          k += 1;
        }
      } else {
        codePoint = 0xFFFD;
        bytesNeeded = octets.length - i;
      }
      string += String.fromCodePoint(codePoint);
      i += bytesNeeded + 1;
    }
    return string
  };

  if (Promise.prototype.finally == undefined) {
    Promise.prototype.finally = function (callback) {
      return this.then(function (result) {
        return Promise.resolve(callback()).then(function () {
          return result;
        });
      }, function (error) {
        return Promise.resolve(callback()).then(function () {
          throw error;
        });
      });
    };
  }

  function FetchTransport() {
    this.reader = undefined;
    this.lastRequestId = 1;
  }

  FetchTransport.prototype.open = function (onStartCallback, onProgressCallback, onFinishCallback, url, withCredentials, headers) {
    // cache: "no-store"
    // https://bugs.chromium.org/p/chromium/issues/detail?id=453190
    this.cancel();
    var textDecoder = new TextDecoder();
    var that = this;
    var lastRequestId = this.lastRequestId;
    fetch(url, {
      headers: headers,
      credentials: withCredentials ? "include" : "same-origin"
    }).then(function (response) {
      if (lastRequestId === that.lastRequestId) {
        that.reader = response.body.getReader();
        onStartCallback(response.status, response.statusText, response.headers.get("Content-Type"));
        return new Promise(function (resolve, reject) {
          var readNextChunk = function () {
            if (that.reader != undefined) {
              that.reader.read().then(function (result) {
                if (result.done) {
                  //Note: bytes in textDecoder are ignored
                  resolve(undefined);
                } else {
                  var chunk = textDecoder.decode(result.value, {stream: true});
                  //var chunk = String.fromCharCode.apply(undefined, result.value);
                  onProgressCallback(chunk);
                  readNextChunk();
                }
              })["catch"](reject);
            } else {
              resolve(undefined);
            }
          };
          readNextChunk();
        });
      }
      return undefined;
    })["finally"](function () {
      onFinishCallback();
    });
  };

  FetchTransport.prototype.cancel = function () {
    this.lastRequestId += 1;
    if (this.reader != undefined) {
      this.reader.cancel();
      this.reader = undefined;
    }
  };

  function XHRTransport(xhr) {
    this.xhr = xhr;
  }

  XHRTransport.prototype.open = function (onStartCallback, onProgressCallback, onFinishCallback, url, withCredentials, headers) {
    var xhr = this.xhr;
    xhr.open("GET", url);
    var offset = 0;
    xhr.onprogress = function () {
      var responseText = xhr.responseText;
      var chunk = responseText.slice(offset);
      offset += chunk.length;
      onProgressCallback(chunk);
    };
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 2) {
        var status = xhr.status;
        var statusText = xhr.statusText;
        var contentType = xhr.getResponseHeader("Content-Type");
        onStartCallback(status, statusText, contentType);
      } else if (xhr.readyState === 4) {
        onFinishCallback();
      }
    };
    xhr.withCredentials = withCredentials;
    xhr.responseType = "text";
    for (var name in headers) {
      if (Object.prototype.hasOwnProperty.call(headers, name)) {
        xhr.setRequestHeader(name, headers[name]);
      }
    }
    xhr.send();
  };

  XHRTransport.prototype.cancel = function () {
    var xhr = this.xhr;
    xhr.abort();
  };


  function EventTarget() {
    this._listeners = new Map();
  }

  function throwError(e) {
    setTimeout(function () {
      throw e;
    }, 0);
  }

  EventTarget.prototype.dispatchEvent = function (event) {
    event.target = this;
    var typeListeners = this._listeners.get(event.type);
    if (typeListeners != undefined) {
      typeListeners.forEach(function (listener) {
        try {
          if (typeof listener.handleEvent === "function") {
            listener.handleEvent(event);
          } else {
            listener.call(this, event);
          }
        } catch (e) {
          throwError(e);
        }
      }, this);
    }
  };
  EventTarget.prototype.addEventListener = function (type, listener) {
    var listeners = this._listeners;
    var typeListeners = listeners.get(type);
    if (typeListeners == undefined) {
      typeListeners = new Set();
      listeners.set(type, typeListeners);
    }
    typeListeners.add(listener);
  };
  EventTarget.prototype.removeEventListener = function (type, listener) {
    var listeners = this._listeners;
    var typeListeners = listeners.get(type);
    if (typeListeners != undefined) {
      typeListeners["delete"](listener);
      if (typeListeners.size === 0) {
        listeners["delete"](type);
      }
    }
  };
