Q is the dominant promise implementation in node.js. I also have my own super light weight promises library Promise. My library doesn't implement all the features I've used in these examples, but it could be made to work with minor adaptation. The underpinning specification for how promises work and ineroperate is Promises/A+. It defines the behavior for a .then
method and is pretty readable, so definitely give it a look at some point (not necessarily straight away).
The idea behind promises is that they encapsulate an asynchronous value. This makes it easier to reason about how to convert synchronous code into asynchronous code because there are usually nice parallels. As an introduction to these concepts I would recommend my talk on Promises and Generators or one of Domenic Denicola's talks (such as Promises, Promises or Callbacks, Promises, and Coroutines (oh my!)).
The first thing to decide is whether you want to make your requests in parallel, or one at a time sequenctially. From the question I'm going to guess that you want to do them in parallel. I'm also going to assume you're using Q which means you'll have to install it with:
npm install q
and require it at the top of each file in which you use it:
var Q = require('q');
Thinking about the ideal data structure to be using to print out that report, I think you'd have an array of brands, with an array of devices which would be objects with properties stage
and prod
, something like:
[
{
brand: 'A',
devices: [
{
device: 'phone',
stage: TAPE,
prod: TAPE
},
{
device: 'tablet',
stage: TAPE,
prod: TAPE
}
...
]
},
{
brand: 'B',
devices: [
{
device: 'phone',
stage: TAPE,
prod: TAPE
},
{
device: 'tablet',
stage: TAPE,
prod: TAPE
}
...
]
}
...
]
I'm going to assume that if you had that then you would have no trouble printing out the desired report.
Promised HTTP Request
Lets start by looking at the getTape
function. Are you expecting it to return a node.js stream or a buffer/string containing the entire downloaded file? Either way, you're going to find it a lot easier with the help of a library. If you're new to node.js I'd recommend request as a library that just does what you'd expect. If you're feeling more confident, substack's hyperquest is a much smaller library and arguably neater, but it requires you to handle things like redirects manually, which you probably don't want to get in to.
Streaming (difficult)
The streaming approach is tricky. It can be done and will be needed if your tapes are 100s of MB long, but promises are then probably not the right way to go. I'm happy to look into this in more detail if it's an issue you actually have.
Buffering with request (easy)
To create a function that does a buffering HTTP request using request and returns a promise, it's fairly simple.
var Q = require('q')
var request = Q.denodeify(require('request'))
Q.denodeify
is just a shortcut for saying: "take this function that normally expects a callback and give me a function that takes a promise".
To write getTape
based off of that we do something like:
function getTape(env, brand, device) {
var response = request({
uri: 'http://example.com/' + env + '/' + brand + '/' + device,
method: 'GET'
})
return response.then(function (res) {
if (res.statusCode >= 300) {
throw new Error('Server responded with status code ' + res.statusCode)
} else {
return res.body.toString() //assuming tapes are strings and not binary data
}
})
}
What's happening there is that request
(via Q.denodeify
) is returning a promise. We're calling .then(onFulfilled, onRejected)
on that promise. This returns a new transformed promise. If the response promise was rejected (equivalent to throw
in synchronous code) then so is the transformed promise (because we didn't attach an onRejected
handler).
If you throw in one of the handlers, the transformed promise is rejected. If you return a value from one of the handlers then the transformed promise is "fulfilled" (also sometimes referred to as "resolved") with that value. We can then chain more .then
calls on the end of our transformed promise.
We return the transformed promise as the result of our function.
Making the requests
JavaScript has a really helpful function called .map
. It's like .forEach
but returns a transformed array. I'm going to use that to stay as close as possible to the original synchronous code.
var data = brands.map(function (brand) {
var b = {brand: brand}
b.devices = devices.map(function (device) {
var d = {device: device}
d.tapeS = getTape('stage',brand,device); // bad example...tapeS never set
d.tapeP = getTape('prod' ,brand,device);
return d
})
})
Now we have code that gives us the data structure I proposed at the start, except we have Promise<TAPE>
instead of TAPE
.
Waiting for the requests
Q has a really helpful method called Q.all
. It takes an array of promises and waits for them all to complete, so lets turn our data structure into an array of promises to pass to Q.all.
One way to do this is at the end, we can go through each item and wait for the promises to resolve.
var updated = Q.all(data.map(function (brand) {
return Q.all(brand.devices.map(function (device) {
return Q.all([device.tapeS, device.tapeP])
.spread(function (tapeS, tapeP) {
//update the values with the returned promises
device.tapeS = tapeS
device.tapeP = tapeP
})
})
}))
//if you add a line that reads `updated = updated.thenResolve(data)`,
//updated would become a promise for the data structure (after being resolved)
updated.then(function () {
// `data` structure now has no promises in it and is ready to be printed
})
Another aproach would be to do it as we go, so that the "making the requests" code gets replaced with:
var data = Q.all(brands.map(function (brand) {
var b = {brand: brand}
Q.all(devices.map(function (device) {
var d = {device: device}
var tapeSPromise = getTape('stage',brand,device);
var tapePPromise = getTape('prod' ,brand,device);
return Q.all([tapeSPromise, tapePPromise])
.spread(function (tapeS, tapeP) { //now these are the actual tapes
d.tapeS = tapeS
d.tapeP = tapeP
return d
})
}))
.then(function (devices) {
b.devices = devices
return b
})
}))
data.then(function (data) {
// `data` structure now has no promises in it and is ready to be printed
})
Still another approach would be to use a small utility library that does a recursive deep-resolve of an object. I haven't got round to publishing it, but this utility function (borrowed from work by Kriskowal) does a deep resolve, which would let you use:
var data = deep(brands.map(function (brand) {
var b = {brand: brand}
b.devices = devices.map(function (device) {
var d = {device: device}
d.tapeS = getTape('stage',brand,device); // bad example...tapeS never set
d.tapeP = getTape('prod' ,brand,device);
return d
})
}))
data.then(function (data) {
// `data` structure now has no promises in it and is ready to be printed
})
To get a promise for the final data.