nheinrich
nheinrich

Reputation: 1841

node.js promise loop architecture with bluebird

I've recently started learning to write applications with node.js and the async nature of everything is giving me some trouble.

I'm trying to write a script that iterates through an array of objects (example below). Each object includes a couple of urls to files (plist, png) I want to download. Once downloaded I want to get some data from one and then crop the other one based on that data.

I have this working when I create a single object and pass it through the promise chain (which is the example I've provided below). The problem I'm having is when I try to use Promise.each, Promise.all, or Promise.mapSeries. When I use any of them it's obvious (by the order of my console.log statements) that they are all being run right away and not one at a time.

Here is an example of what I'm working. Sorry it's so long, I've tried to keep it pretty tidy so that it's understandable.

// ---------------------------------------------------------------------------

var Promise = require("bluebird"),
  fs = Promise.promisifyAll(require("fs")),
  gm = Promise.promisifyAll(require("gm")),
  plist = require("plist"),
  request = require("request-promise")


// ---------------------------------------------------------------------------
// Test Data
// This is what I'd like to replace with an array which would contain a few hundred of these

var card = {
  slug: "neutral_zurael",
  plist: "https://assets-counterplaygames.netdna-ssl.com/production/resources/units/neutral_zurael.plist",
  sprite: "https://assets-counterplaygames.netdna-ssl.com/production/resources/units/neutral_zurael.png"
}


// ---------------------------------------------------------------------------

var getXML = function() {
  console.log("getXML")
  return request({url: card.plist, gzip: true})
}

var writeXML = function(file){
  console.log("writeXML")
  return fs.writeFile("./lib/card.plist", file)
}

var getSprite = function() {
  console.log("getSprite")
  return request({url: card.sprite, gzip: true, encoding: "binary"})
}

var writeSprite = function(file) {
  console.log("writeSprite")
  return fs.writeFile("./lib/card.png", file, "binary")
}

var parseXML = function() {
  console.log("parseXML")
  var obj = plist.parse(fs.readFileSync("./lib/card.plist", "utf8"))
  var framename = card.slug + "_idle_000.png"
  var frame = obj.frames[framename].frame
  var values = frame.replace(/[{}]/g, "").split(",")
  var data = { x: values[0], y: values[1], width: values[2], height: values[3] }
  return data
}

// Not returning a promise due to chained methods
var cropImage = function(data){
  console.log("cropImage")
  return gm("./lib/card.png")
    .crop(data.width, data.height, data.x, data.y)
    .write("./lib/avatar.png", function(error){
      if (!error) {
        fs.unlink("./lib/card.plist")
        fs.unlink("./lib/card.png")
        console.log("Image Created")
      }
    })
}


// ---------------------------------------------------------------------------

getXML()
  .then(writeXML)
  .then(getSprite)
  .then(writeSprite)
  .then(parseXML)
  .then(cropImage)
  .catch(function(error){
    console.log(error)
  })
  .done()

This actually works as is. I'm looking for some help in transforming it into something that works on an array of objects. I'd need a way to pass them in and have it run sequentially (or be more resilient if they're all going to be run right away).

Any advice would help as I'm new to this but completely struggling to get this to work. Thanks!

Upvotes: 1

Views: 474

Answers (1)

jfriend00
jfriend00

Reputation: 707406

The request-promise module you are using does convert the normal request calls to use promises instead of callbacks. But, Bluebird's Promise.promisifyAll() works differently. It leaves the normal callback version of methods such as fs.writeFile() exactly like they are.

Instead, it adds new versions of those functions that return a promise. By default, the new version has the same name as the original with the addition of "Async" on the end so fs.writeFileAsync() is what returns a promise.

So, you have to use the appropriate method names in order to work with promises:

So change these:

var writeXML = function(file){
  console.log("writeXML")
  return fs.writeFile("./lib/card.plist", file)
}

var writeSprite = function(file) {
  console.log("writeSprite")
  return fs.writeFile("./lib/card.png", file, "binary")
}

to these:

var writeXML = function(file){
  console.log("writeXML")
  return fs.writeFileAsync("./lib/card.plist", file)
}

var writeSprite = function(file) {
  console.log("writeSprite")
  return fs.writeFileAsync("./lib/card.png", file, "binary")
}

Then, you have to convert cropImage() to actually use promise logic and to return a promise.

var cropImage = function(data){
  console.log("cropImage")
  return gm("./lib/card.png")
    .crop(data.width, data.height, data.x, data.y)
    .writeAsync("./lib/avatar.png").then(function() {
        fs.unlink("./lib/card.plist")
        fs.unlink("./lib/card.png")
        console.log("Image Created")
    });
    // Note: You are missing error handling for writeAsync
}

This should allow you to then do:

getXML()
  .then(writeXML)
  .then(getSprite)
  .then(writeSprite)
  .then(parseXML)
  .then(cropImage)
  .then(function() {
      // done successfully here
  }, function(err) {
      // error here
  })

Note: you still have synchronous file I/O in parseXML() which could be converted to use async I/O. Here's what it could be with async file I/O that returns a promise that works in your current scheme:

var parseXML = function() {
  console.log("parseXML")
  return fs.readFileAsync("./lib/card.plist", "utf8").then(function(file) {
      var obj = plist.parse(file);
      var framename = card.slug + "_idle_000.png"
      var frame = obj.frames[framename].frame
      var values = frame.replace(/[{}]/g, "").split(",")
      var data = { x: values[0], y: values[1], width: values[2], height: values[3] }
      return data
  });
}

Upvotes: 2

Related Questions