Search code examples
javascriptjquerypromisedeferredsequential

How to use when then to send file upload sequentially in a function that is also a deferred promise?


I intend to upload an array of files using jQuery.

This intention is wrapped in a function called uploadFilesUsingAjax();

var uploadFilesPromise = uploadFilesUsingAjax();

$.when(uploadFilesPromise).done(function (uploadFilesAjaxResult) {
        // redirect to success page...

I need to wait for all the files to be uploaded successfully before doing something else.

Inside uploadFilesUsingAjax(),

I wrote my code this way

function uploadFilesUsingAjax() {
    var files = pages; // pages is a global variable which is an array of files
    var url = "/users/" + currentUser.id + "/files.json";
    var type = "POST";

    console.info('files length:' + files.length);
    if (files.length > 0) {

        var promises=[];
        for (var i = 0; i < files.length; i++) {
            var data = new FormData();
            var postData = {};
            var file = files.getByIndex(i);
            var key = i + 1;
            if (typeof (file.id) !== "undefined" && file.id > 0) {
                data.append(key, JSON.stringify(file));
            } else {
                data.append(key, file);
            }
            var request = $.ajax({
                //this is the php file that processes the data 
                url: url,

                //POST method is used
                type: type,

                //pass the data
                data: data,

                //Do not cache the page
                cache: false,

                xhr: function() {
                    // custom xhr
                    myXhr = $.ajaxSettings.xhr();
                    if(myXhr.upload) { // check if upload property exists

                            myXhr.upload.addEventListener('progress',updatePagesProgress, false); // for handling the progress of the upload
                    }
                    return myXhr;
                },

                // DO NOT set the contentType and processData
                // see http://stackoverflow.com/a/5976031/80353
                contentType: false,
                processData: false,

                //success
                success: function (json) {
                    // json is already an object thanks to cakephp code to serialize

                    //if POST is a success expect no errors
                    if (json.error == null && json.result != null) {
                        currentUser = json.result.User;
                    // error
                    } else {
                        alert(json.error);
                    }
                }
            });
            promises.push( request);
        }

        var promise = promises[0];
        for (var i = 1; i < promises.length; i++) {
          promise = promise.then(promises[i]);
        }

        return promise.done(function () { console.log('all!')});

Unfortunately, I was not able to upload a lot of files before I got redirected to success page.

I have tried various StackOverflow solutions on how to do this. So far nothing works. Please advise.

Some code has been truncated to save space.


Solution

  • All your promises are parallel and not sequential.

    A promise represents an already running task. Promises in JavaScript, unlike C# tasks or other abstractions are already started. The way to represent a task that has not started is a function returning a promise.

    Since promises[i] is already a promise - when you do promise.then(object) it does not add a .then handler but rather return immediately. .then ignores any arguments that are not a function.

    This is why it returns early, it returns as soon as the first promise fulfills. You also don't need the .when. Create a function that creates an upload process as such:

    function createUploadTask(file,i){
        return function(){
             var data = new FormData();
             var postData = {};
             var file = files.getByIndex(i);
             var key = i + 1;
             if (typeof (file.id) !== "undefined" && file.id > 0) {
                 data.append(key, JSON.stringify(file));
             } else {
                 data.append(key, file);
             }
             return $.ajax({...}); // return the promise
       }
    }
    

    Now, you can map the files to tasks:

     var tasks = files.map(createUploadTask);
    

    Note, that now the tasks are each functions that return a promise over a file upload. They are not promises.

    Now, you can chain them:

     var p = tasks[0](); // start off the chain
     for(var i = 1; i < tasks.length; i++){
          // chain the next task, note, that we're passing a _function_ here
          // when you return a promise from a `.then` it will fulfill when that promise 
          // fulfills, in our case the $.ajax
          p = p.then(tasks[i]); 
     }
     return p;
    

    You also now don't need to use when, since you return a single promise. I assume you don't need the actual result here (but only to know success/failure).

    You simply do:

     function uploadFilesUsingAjax() {
         // settings
         if(tasks.length === 0){
              // a method MUST always either return either synchronously or asynchronously never
              // both, always return a promise. Otherwise you get API hell.
              var d = $.Deferred();
              d.reject(new Error("Called uploadFiles with no files to upload"));
              return d.promise;
         }
         tasks = pages.map(createUploadTask)
         var p = tasks[0](); // call first one
         for(var i = 1; i < tasks.length; i++) p = p.then(tasks[i]);
         return p; 
     }