使用 Q 进行 Promise 轮询

3
我有一个类似于这篇博客文章所描述的情况:使用 Promises 进行轮询。作者描述了如何使用 Promises 进行轮询,直到返回 JobID。我想使用 Q 来转换它。
我很乐意发布代码作为起点,但我不确定该发布什么。理想情况下,我正在尝试将 Promises 链接在一起。我一直在尝试使用 Q.delay(),但它似乎无法实现我的目标。
var promise = uploadFile();
promise.then(startImageProcessingJob)
.then(saveConvertedImage);

请提供有关如何创建一个promise的建议,该promise将继续轮询直到检索到数据(或达到最大尝试次数)。

以下是作者使用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的评论,我正在添加我已经完成的逻辑。我试图避免添加额外的代码来使问题变得臃肿。

目标:

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

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

(2) 使用ZamZar API启动图像转换(获取JobID);

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

(4) 一旦PNG图像回到我的服务器上,我想将图像返回给客户端浏览器并放入HTML画布中(使用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');
  });
});

这是作者的代码,但该代码存在缺陷,因此不要将其用作起点。 - Jaromanda X
谢谢@Jaromanda,有什么特别明显的缺陷吗? - Gregg
2
1 - retry 永远不会被反馈到轮询中,因此 retry 将始终未定义,并且轮询的第一行将始终将其设置为 5,因此这将永远重试。 2 - 即使 retry 被反馈到轮询中,当它达到 0 时,轮询中的第一行也会将其重置为 5,因此下一行永远不会成立。 - Jaromanda X
术语不清晰。在引用的“poll”函数中,未说明导致延迟调用的data.state的字符串值。你能告诉我们可能是什么吗?您如何设想startImageProcessingJob目标函数会遇到此状态。 - traktor
@Traktor53,我已经更新了问题并添加了评论。我不确定data.state的值是什么。 ZamZar API将返回JSON字符串作为响应。根据API调用,响应将包含JobId(用于处理文件转换)或FileId(当文件准备好下载时)。 - Gregg
2个回答

2

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

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();
};

我感觉这段代码可能可以写得更好...但是今天是周六,所以这是“周末代码”,它至少为OP提供了一个更好的起点。


2

一个可能感兴趣的设计思路:

  1. 为承诺编写onFulfill监听器 (pollZamZarForOurPngFile),该监听器使用returnObj来实现。
  2. 此监听器返回一个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;
}

谢谢你的帮助。它起作用了。我也感觉自己很傻,忽略了API中的示例。不确定我当时在想什么。 - Gregg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接