0

I have the following functions which I would prefer to be handled synchronously but somehow it doesnt work properly.

function upload_to_aws(data) {
  return new Promise(function(resolve, reject) {
    loan_application_id = $('#loan_application_id').val();

    var s3BucketName = data.bucket_name;
    var s3RegionName = data.region;
    AWS.config.update({accessKeyId: data.key, secretAccessKey: data.secret_key, region: s3RegionName});
    var s3 = new AWS.S3({params: {Bucket: s3BucketName, Region: s3RegionName}});

    aws_url= []
    $('.attached_image').each(function() {
      if($(this).attr('src') != "/assets/upload_bg.png" && $(this).attr('src') != '' ) {
        var timestamp = (new Date()).getTime();
        var randomInteger = Math.floor((Math.random() * 1000000) + 1);
        filename = 'self_evaluation_images/'+ loan_application_id + '_self_eval_ic_' + timestamp  + '.png';
        var u = $(this).attr('src').split(',')[1],
          binary = atob(u),
          array = [];

        for (var i = 0; i < binary.length; i++) {
            array.push(binary.charCodeAt(i));
        }

        var typedArray = new Uint8Array(array);
        s3_upload(s3, filename, typedArray).then(function(url_aws) {
          aws_url.push(url_aws);
          console.log(aws_url)
          console.log(aws_url.length)
        })
      }
    })
    resolve(aws_url);
  })
}

function s3_upload(s3, filename, typedArray) {
  return new Promise(function(resolve, reject) {
    s3.putObject({Key: filename, ContentType: 'image/png', Body: typedArray.buffer, ContentEncoding: 'base64', ACL: 'public-read'},
    function(err, data) {
        if (data !== null) {
          url_aws = s3.endpoint.href + filename;
          resolve(url_aws)
        }
        else {
          reject(err);
        }
    });
  })
}

When this function is called,it calls the upload_to_aws function and i want everything to be executed in that function before it returns to me the array of aws_uploaded url.

$.when(upload_to_aws(data.data)).then(function(aws_uploaded_url) {
   console.log(aws_uploaded_url);
})

But what basically happens at the moment is that during when the image is being uploaded to s3, this gets called resolve(aws_url) even before the images are uploaded to s3 hence this prints console.log(aws_uploaded_url) as an empty array [] because the function hasnt executed completely.

Is there any other way to handle callbacks and synchronous functions in javascript?

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
Kingsley Simon
  • 2,090
  • 5
  • 38
  • 84

2 Answers2

1

The major problem is that the code doesn't wait for promises returned by s3_update to be settled and populate the aws_url array before resolving the promise returned by upload_to_aws with that array (it's still empty).

So far this is a frequently asked question addressed in How do I return the response from an asynchronous call?, but substituting the s3.putObject method for an AJAX call.

You also want to wait on multiple (zero or more promises) since the number of requests is determined by the data. Waiting for multiple promises to finish involves the use of Promise.all.

The fulfilled value of a Promise.all call is an array of the fulfilled values of the promises provided as arguments, in the order of arguments provided. In other words, the fulfilled value is the aws_url array used in the post.

An (untested) approach to try, with slight modifications to declare all variables and simplify s3_upload could be:

function upload_to_aws(data) {
    var loan_application_id = $('#loan_application_id').val();
    var s3BucketName = data.bucket_name;
    var s3RegionName = data.region;
    AWS.config.update({accessKeyId: data.key, secretAccessKey: data.secret_key, region: s3RegionName});
    var s3 = new AWS.S3({params: {Bucket: s3BucketName, Region: s3RegionName}});

    var urlPromises = [];
    $('.attached_image').each(function() {
      if($(this).attr('src') != "/assets/upload_bg.png" && $(this).attr('src') != '' ) {
        var timestamp = (new Date()).getTime();
        var randomInteger = Math.floor((Math.random() * 1000000) + 1);
        var filename = 'self_evaluation_images/'+ loan_application_id + '_self_eval_ic_' + timestamp  + '.png';
        var u = $(this).attr('src').split(',')[1];
        var binary = atob(u);
        var array = [];
        for (var i = 0; i < binary.length; i++) {
            array.push(binary.charCodeAt(i));
        }
        var typedArray = new Uint8Array(array);
        urlPromises.push( s3_upload(s3, filename, typedArray))
      }
    });
    return Promise.all( urlPromises);
}

function s3_upload(s3, filename, typedArray) { // promisify a call back
  return new Promise(function(resolve, reject) {
    s3.putObject(
        {Key: filename, ContentType: 'image/png', Body: typedArray.buffer, ContentEncoding: 'base64', ACL: 'public-read'},
        function(err, data) { err ? reject(err) : resolve( data);}
    );
  });
}

(If any of the declared variables should be global then remove the var in front of them).

The promise returned by calling upload_to_aws should be fulfilled with an array of zero or more uploaded urls:

$.when(upload_to_aws(data.data)).then(function(aws_uploaded_urls {
    console.log(aws_uploaded_urls);
})


JQuery compatability (update)

Prior to version 3, jQuery did not implement promises that complied with the Aplus promise specificaion or the later ECMAScript version 6 standard. Older versions of JQuery are capable of not recognizing ES6 promises as promises at all and fail to wait for them to be settled.

Check you are using JQuery 3 or later with code that uses native Promises. If you need to support IE browsers you will also need to include a polyfill for ES6 style promises that supports Promise.all.

If you need to support browsers that are no longer supported in JQuery 3 consider removing Promise usage altogether and, say, refactoring code around the use of Deferred objects (outside the scope of this answer). This would also remove the need for a polyfill in older browsers that lack native Promise support.

If the .when method gives problems in conjunction with ES6 promise usage, consider calling the code in plain JavaScript:

upload_to_aws(data.data)
.then(function(aws_uploaded_urls) {
    console.log(aws_uploaded_urls);
})
.catch( function( err) {
   console.log( "upload_to_aws failed: ", err);
}
traktor
  • 17,588
  • 4
  • 32
  • 53
0

Because each iteration is running something asynchronous, you need to use Promise.all instead, mapping each iteration to a Promise and finally resolving the promise returned by upload_to_aws once all iterations' promises are finished:

function upload_to_aws(data) {
    loan_application_id = $('#loan_application_id').val();

    var s3BucketName = data.bucket_name;
    var s3RegionName = data.region;
    AWS.config.update({accessKeyId: data.key, secretAccessKey: data.secret_key, region: s3RegionName});
    var s3 = new AWS.S3({params: {Bucket: s3BucketName, Region: s3RegionName}});

    return Promise.all(
      [...document.querySelectorAll('.attached_image')]
      .map(image => new Promise((resolve, reject) => {
        const { src } = image;
        if(src != "/assets/upload_bg.png" && src != '' ) {
          var timestamp = (new Date()).getTime();
          var randomInteger = Math.floor((Math.random() * 1000000) + 1);
          filename = 'self_evaluation_images/'+ loan_application_id + '_self_eval_ic_' + timestamp  + '.png';
          var u = src.split(',')[1],
              binary = atob(u),
              array = [];

          for (var i = 0; i < binary.length; i++) {
            array.push(binary.charCodeAt(i));
          }

          var typedArray = new Uint8Array(array);
          s3_upload(s3, filename, typedArray).then(resolve, reject);
        }
      }))
  );
}
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • 1
    You may prefer that last line as `s3_upload(s3, filename, typedArray).then(resolve, reject);` to pass on errors – traktor Jun 12 '18 at 04:55
  • @CertainPerformance, this looks great but still it prints `Promise ` and the resolve part gets executed before the function has finished executing for this part. $.when(upload_to_aws(data.data)).then(function(aws_uploaded_url) { console.log(aws_uploaded_url); }) – Kingsley Simon Jun 12 '18 at 05:04
  • @KingsleySimon Are you sure you copied the code exactly? There is no explicit `Promise` being constructed at the beginning of the function with mine. You might consider using the native Promises alone, rather than bringing jQuery into the mix - eg `upload_to_aws(data.data).then(aws_uploaded_url => console.log(aws_uploaded_url))` – CertainPerformance Jun 12 '18 at 05:14
  • @KingsleySimon I can't reproduce the problem unfortunately. Is it that `aws_uploaded_url` itself looks to be a `Promise`, or is it an array of `Promise`s? – CertainPerformance Jun 12 '18 at 06:07