function xhrCallbackWrapper(req, events, blobbed, chunked, isJson) {
  let lastPromise = Promise.resolve();
  let contentType;
  let dataSize;
  let numChunks = 0;
  let chunkSize;
  let getChunk;
  let fullResponse = null;
  let response;
  let responseHeaders;
  let sent = true;
  let sentTextLength = 0;
  let sentReadyState4;
  let tmp;
  const { id, xhr } = req;
  const getResponseHeaders = () => req[kResponseHeaders] || xhr.getAllResponseHeaders();
  const eventQueue = [];
  const sequentialize = async () => {
    const evt = eventQueue.shift();
    const { type } = evt;
    const shouldNotify = events.includes(type);
    const isEnd = type === 'loadend';
    const readyState4 = xhr.readyState === 4 || (sentReadyState4 = false); // reset on redirection
    if (!shouldNotify && !isEnd) {
      return;
    }
    // Firefox duplicates readystatechange for state=4 randomly, #1862
    if (readyState4 && type === 'readystatechange') {
      if (sentReadyState4) return;
      sentReadyState4 = true;
    }
    if (!contentType) {
      contentType = xhr.getResponseHeader('Content-Type') || '';
    }
    if (fullResponse !== xhr[kResponse]) {
      fullResponse = response = xhr[kResponse];
      sent = false;
      if (response) {
        if ((tmp = response.length - sentTextLength)) { // a non-empty text response has `length`
          chunked = tmp > TEXT_CHUNK_SIZE;
          chunkSize = TEXT_CHUNK_SIZE;
          dataSize = tmp;
          getChunk = text2chunk;
          response = sentTextLength ? response.slice(sentTextLength) : response;
          sentTextLength += dataSize;
        } else {
          chunkSize = CHUNK_SIZE;
          dataSize = response.size;
          getChunk = blobbed ? blob2objectUrl : blob2chunk;
        }
        numChunks = chunked ? Math.ceil(dataSize / chunkSize) || 1
          : blobbed ? 1 : 0;
      }
    }
    if (response && isEnd && req[kFileName]) {
      downloadBlob(response, req[kFileName]);
    }
    const shouldSendResponse = shouldNotify && (!isJson || readyState4) && !sent;
    if (shouldSendResponse) {
      sent = true;
      for (let i = 1; i < numChunks; i += 1) {
        await req.cb({
          id,
          i,
          chunk: i * chunkSize,
          data: await getChunk(response, i, chunkSize),
          size: dataSize,
        });
      }
    }
    /* WARNING! We send `null` in the mandatory props because Chrome can't send `undefined`,
     * and for simple destructuring and `prop?.foo` in the receiver without getOwnProp checks. */
    await req.cb({
      blobbed,
      chunked,
      contentType,
      id,
      type,
      /** @type {VMScriptResponseObject} */
      data: shouldNotify ? {
        finalUrl: req.url || xhr.responseURL,
        ...objectPick(xhr, SEND_XHR_PROPS),
        ...objectPick(evt, SEND_PROGRESS_PROPS),
        [kResponse]: shouldSendResponse
          ? (numChunks ? await getChunk(response, 0, chunkSize) : response)
          : null,
        [kResponseHeaders]: responseHeaders !== (tmp = getResponseHeaders())
          ? (responseHeaders = tmp)
          : null,
      } : null,
    });
    if (isEnd) {
      clearRequest(req);
    }
  };
  return (evt) => {
    eventQueue.push(evt);
    lastPromise = lastPromise.then(sequentialize);
  };
}
@param {GMReq.BG} req
@param {GMReq.EventType[]} events
@param {boolean} blobbed
@param {boolean} chunked
@param {boolean} isJson