使用 Q 进行 promises 轮询

Polling with promises using Q

我遇到了与此博客 post 中所述类似的情况:Polling with promises. The author describes how promises were used to poll until a JobID was returned. I would like to convert this using Q

我很乐意 post 代码作为起点,但我不确定要 post。理想情况下,我正在尝试将承诺链接在一起。我一直在试验 Q.delay() 但它似乎没有实现我的目标。

var promise = uploadFile();
promise.then(startImageProcessingJob)
.then(saveConvertedImage);

能否请您提供有关如何创建将继续轮询直到检索到数据(或达到最大尝试次数)的承诺的建议。

这里是作者使用bluebird的代码。

var getJobStatusAsync = Promise.promisifyAll(api);

function poll(jobId, retry) {  
  if(!retry) retry = 5;
  if(!retry--) throw new Error('Too many retries');

  return getJobStatusAsync(jobId)
  .then(function(data) {
    if(data.state === 'error') throw new Error(data.error);
    if(data.state === 'finished') return data;
    return Promise.delay(jobId, 10000).then(poll);
  });

编辑:

为了回应 Traktor53 的评论,我添加了目前为止的逻辑。我试图避免添加导致问题膨胀的额外代码。

objective:

在我的 Angular 应用程序中,我想使用 ZamZar 第三方服务将图像转换为 PNG 格式。我的设计实现是使用 promises 来:

(1)从客户端上传文件到服务器(Node);

(2) 使用ZamZar API开始图片转换(获取JobID);

(3) 使用 JobID,轮询 ZamZar API 以获取状态更新,直到图像可供下载。图像准备好后,我可以获得 fileId 并将文件下载回节点服务器。

(4) 一旦 PNG 图像回到我的服务器上,我想 return 图像到客户端浏览器并放入 HTML canvas(使用 three.js 和 fabric.js).

/* Dependencies */
var express = require('express');
var request = require('request');
var formidable = require('formidable');
var randomstring = require("randomstring");
var fs = require('fs');
var Q = require('q');

/*
 * Helper functions in Node
 */
convertFileUtil = function() {

  /**
   * Step 1: upload file from client to node server.
   * formidable is used for file upload management. This means the file is
   * automatically uploaded to a temp directory. We are going to move the
   * uploaded file to our own temp directory for processing.
   * Return Type: A Promise is returned with payload containing the directory
   * path for the image file. This path is referenced in subsequent chained methods.
   */
  var uploadFileFromClientToNodeServer = function(req) {
    var q = Q.defer();
    var form = new formidable.IncomingForm();
    var tmpFolder = 'upload/' + randomstring.generate() + '/';

    //Use formidable to parse the file upload.
    form.parse(req, function(err, fields, files) {
      if (err) {
        console.log(err);
        throw err;
      }

      //When upload is successful, create a temp directory and MOVE file there.
      //Again, file is already uploaded. There is no need to use fs.writeFile* methods.
      mkdirp(tmpFolder, function (err) {
        if (err) {
          q.reject(err);
        } else {

          //File will be saved here.
          var tmpFileSavedLocation = tmpFolder + files.file.name;

          //Call fs.rename to MOVE file from formidable temp directory to our temp directory.
          fs.rename(files.file.path, tmpFileSavedLocation, function (err) {
            if (err) {
              q.reject(err);
            }
            console.log('File saved to directory:', tmpFileSavedLocation);
            q.resolve(tmpFileSavedLocation);
          });
        }
      });
    });

    return q.promise;
  };

  /**
   * Step 2: Post the temp file to zam zar. ZamZar is an API service that converts
   * images to a different file format. For example, when a user uploads an Adobe
   * Illustrator EPS file; the file is sent to zamzar for conversion to a PNG. all
   * image formats are returned as PNG which will be added to the canvas.
   * Return: This promise will return the JobID of our submission. The JobID will be
   * used in subsequent promise to retrieve the converted image.
   */
  var postTempFileToZamZar = function(filePath) {
    console.log('FilePath', filePath);
    var q = Q.defer();
    var formData = {
      target_format: 'png',
      source_file: fs.createReadStream(filePath),
    };
    //console.log('OK', formData);

    //Send file to zamzar for conversion.
    request.post({ url: 'https://sandbox.zamzar.com/v1/jobs/', formData: formData }, function (err, response, body) {
      if (err) {
        console.log('An error occurred', err);
        q.reject(err);
      } else {
        var jsonData = JSON.parse(body);
        console.log('SUCCESS! Conversion job started:', jsonData.id);

        //This object will be returned in promise payload.
        var returnObj = {
          filePath: filePath,
          jobId: jsonData.id,
        };

        console.log('Process complete. Returning: ', returnObj);
        q.resolve(returnObj);

        return q.promise;
      }

    }).auth(zamzarApiKey, '', true);
  };

  /*
   * Step 3: Poll for PNG.
   */
  var pollZamZarForOurPngFile = function(dataObj) {

    console.log('pollZamZarForOurPngFile', dataObj);
  }

  //API
  return {
    uploadFileFromClientToNodeServer: uploadFileFromClientToNodeServer,
    postTempFileToZamZar: postTempFileToZamZar,
    pollZamZarForOurPngFile: pollZamZarForOurPngFile,
  };
};

//Call to convert file.
app.post('/convertFile', function (req, res) {
  var util = convertFileUtil();

  //Get file data.
  var promise = util.uploadFileFromClientToNodeServer(req);
  promise
  .then(util.postTempFileToZamZar)
  .then(util.pollZamZarForOurPngFile);
  .then(function(data) {
    console.log('Done processing');
  });
});

poll 函数的工作版本仅使用标准 Promise/A+

function poll(jobId, retry) {
    if(!retry) retry = 5; // default retries = 5
    function delay(timeout) {
        return new Promise(function(fulfill) {
            setTimeout(function() {
                fulfill();
            }, timeout);
        });
    }
    function poller() {
        if(!retry--) throw new Error('Too many retries');

        return getJobStatusAsync(jobId)
        .then(function(data) {
            if (data.state === 'error') throw new Error(data.error);
            if (data.state === 'finished') return data;
            return delay(10000).then(poller);
        });
    }
    return poller();
};

我觉得这段代码可能会写得很远"better" ...但是今天是星期六,所以这是"weekend code",它至少为OP[提供了一个更好的起点

可能感兴趣的设计思路:

  1. 为实现 returnObj 的承诺编写一个 onFulfill 侦听器 (pollZamZarForOurPngFile)。
  2. 这个侦听器 returns 一个 Promise 对象。
  3. 如果 zambar 已完成转换,则返回的承诺将通过 returnObj 实现(将其传递到链中)。
  4. 如果出现 zamzar 错误或重试次数过多,则返回的承诺将被拒绝。

请注意,这会将文件检索留给承诺链中的下一个 (onFulfilled) 侦听器。为了方便起见,我使用了 Promise,因为 node.js 支持它并且它与 Promise/Aplus 兼容。根据需要将其转换为 Q。 投票请求代码直接来自 zamzar 网站,可能来自教程示例 - 请查看。

function pollZamZarForOurPngFile( returnObj)
{   var jobID = returnObj.jobId;
    var resolve, reject;
    function unwrap( r, j) { resolve = r, reject = j};
    var promise = new Promise( unwrap);
    var maxRetry = 5;
    var firstDelay = 500;     // 1/2 second
    var retryDelay = 5000;    // 5 second?

    function checkIfFinished()
    {   // refer to https://developers.zamzar.com/docs under node.js tab for documentation

        request.get ('https://sandbox.zamzar.com/v1/jobs/' + jobID,
        function (err, response, body)
        {   if (err)
            {   reject( new Error("checkIfFinished: unable to get job"));
                return;
            }
            if( JSON.parse(body).status == "successful")
            {   resolve( returnObj);    // fulfill return promise with "returnObj" passed in; 
                return;
            }    
            // has not succeeded, need to retry
            if( maxRetry <= 0)
            {    reject( new Error("checkIfFinished: too many retries"));
            }
            else
            {   --maxRetry;
                setTimeout(checkIfFinished, retryDelay);
            }    
        }
    }).auth(apiKey, '', true);
    setTimeout(checkIfFinished, firstDelay);
    return promise;
}