foxer
foxer

Reputation: 901

adding onerror function to XMLHttpRequest

Here is a code to preload files and save them to browser's cache using HTTP requests in JavaScript. The problem is I can't find a solution to track errors in preloadMe function without your kind help:)

I know we can add onerror to preloadOne and it works fine:

xhr.onerror = function() {
    console.log('onerror happend');
};

But I wonder how can we do this in preloadMe function like oncomplete , onfetched and onprogress. something like this:

preload.onerror = items => {
   console.log('onerror happend'); // onerror here
}

Any comments or modifications are extremely appreciated...

Here is my code so far:

(function(global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    (global.Preload = factory());
}(this, (function() {
  'use strict';

  function preloadOne(url, done) {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.responseType = 'blob';
    xhr.onprogress = event => {
      if (!event.lengthComputable) return false
      let item = this.getItemByUrl(event.target.responseURL);
      item.completion = parseInt((event.loaded / event.total) * 100);
      item.downloaded = event.loaded;
      item.total = event.total;
      this.updateProgressBar(item);
    };
    xhr.onload = event => {
      let type = event.target.response.type;
      let blob = new Blob([event.target.response], {
        type: type
      });
      let url = URL.createObjectURL(blob);
      let responseURL = event.target.responseURL;
      let item = this.getItemByUrl(responseURL);
      item.blobUrl = url;
      item.fileName = responseURL.substring(responseURL.lastIndexOf('/') + 1);
      item.type = type;
      item.size = blob.size;
      done(item);
    };
    xhr.send();
  }

  function updateProgressBar(item) {
    var sumCompletion = 0;
    var maxCompletion = this.status.length * 100;

    for (var itemStatus of this.status) {
      if (itemStatus.completion) {
        sumCompletion += itemStatus.completion;
      }
    }
    var totalCompletion = parseInt((sumCompletion / maxCompletion) * 100);

    if (!isNaN(totalCompletion)) {
      this.onprogress({
        progress: totalCompletion,
        item: item
      });
    }
  }

  function getItemByUrl(rawUrl) {
    for (var item of this.status) {
      if (item.url == rawUrl) return item
    }
  }

  function fetch(list) {
    return new Promise((resolve, reject) => {
      this.loaded = list.length;
      for (let item of list) {
        this.onerror(item); // onerror here
        this.status.push({
          url: item
        });
        this.preloadOne(item, item => {
          this.onfetched(item);
          this.loaded--;
          if (this.loaded == 0) {
            this.oncomplete(this.status);
            resolve(this.status);
          }
        });
      }
    });
  }

  function Preload() {
    return {
      status: [],
      loaded: false,
      onprogress: () => {},
      oncomplete: () => {},
      onfetched: () => {},
      onerror: () => {}, // onerror here
      fetch,
      updateProgressBar,
      preloadOne,
      getItemByUrl
    }
  }

  return Preload;

})));

//preload here
preloadMe();

function preloadMe() {

  const preload = Preload();

  preload.fetch([
    'https://round-arm-authority.000webhostapp.com/Ultimate%20Video%20Hack/videos/vid1.mp4'

  ]).then(items => {
    // use either a promise or 'oncomplete'
    console.log(items);
  });

  preload.oncomplete = items => {
    console.log(items);
  }

  preload.onerror = items => {
    console.log('onerror happend'); // onerror here
  }

  preload.onprogress = event => {
    console.log(event.progress + '%');
  }

  preload.onfetched = item => {
    console.log(item);
  }

};

Upvotes: 1

Views: 798

Answers (1)

David784
David784

Reputation: 7464

There are a couple of options. You could add the error callback as an argument, much as you've done with the done callback.

If you wanted to make the error callback optional, you'd do something like this:

function preloadOne(url, done, error) {
  const xhr = new XML HttpRequest();
  // ...
  if(typeof error === 'function') xhr.onerror = error;
  // ...
}

In some ways this might be preferable because you could register the error handler before calling xhr.send(). (Although I don't know if it matters, I'm not sure how the event loop works in this regard.)

The other way would be to return xhr at the end of your preloadOne function, like this:

  function preloadOne(url, done) {
    const xhr = new XMLHttpRequest();
    // ...
    xhr.send();
    return xhr;
  }

Then you could easily add your own event handler after the fact:

var preload = preloadOne('https://my.site.com/path', x=> {
  console.log('done');
});
preload.onerror = function(event) {
  console.log('error');
};

Edit: Event loop considerations of return xhr solution.

After doing some research, I believe it is possible to lose error events if you add the event listener after the xhr.send(). This is based on the MDN event loop page, specifically the Adding messages section, which says:

In web browsers, messages are added anytime an event occurs and there is an event listener attached to it. If there is no listener, the event is lost.

So while the messages don't get handled until the JavaScript stack is empty, the queuing of a message for an XHR error could happen at just about any time. Or attempted queuing, if the handler isn't attached yet.

However if you really want to return the xhr and ensure the opportunity for event handlers to be attached, I believe there is a way to do it, by changing preoloadOne to put the xhr.send inside of a setTimeout.

setTimeout(()=> xhr.send(), 0);

This will throw the send into the event queue, which means it has to wait for the JavaScript call stack to empty and then the next cycle of the event loop. And by the time the call stack empties, any error event handlers should be attached.

Upvotes: 1

Related Questions