stevendesu stevendesu - 6 months ago 26
Node.js Question

How to persist data through promise chain in NodeJS (Bluebird)

Follow-up to Swap order of arguments to "then" with Bluebird / NodeJS Promises (the posted answer worked, but immediately revealed a new issue)

This is the first time I've ever used promises in NodeJS so I apologize if some conventions are poorly adhered to or the code is sloppy. I'm trying to aggregate data from multiple APIs, put it in a database, then compute some statistics based on similarities and differences in the data. As a starting point I'm trying to get an API token for a single one of the APIs.

Here is my full code:

var Promise = require('bluebird');
var fs = require('fs');
var request = require('request');
Promise.promisifyAll(fs);
Promise.promisifyAll(request);

// tilde-expansion doesn't follow the callback(err, data) convention
var tilde = function(str) {
var _tilde = require('tilde-expansion');
return new Promise(function(resolve, reject) {
try {
_tilde(str, resolve);
} catch(e) {
reject(e);
}
});
}

var getToken = function() {
return request.getAsync(process.env.token_url, {
headers: {
"Content-Type": "applications/x-www-form-urlencoded"
},
form: {
client_id: process.env.client_id,
client_secret: process.env.client_secret,
grant_type: "client_credentials"
}
})
.then(function(resp) { return resp.body; });
}

var tokenFile = tilde(process.env.token_file)
.catch(function(err) {
console.log("Error parsing path to file... can not recover");
});

var token = tokenFile
.then(fs.readFileAsync) //, "utf8")
.then(function(data) {
console.log("Token (from file): " + data);
return data;
})
.then(JSON.parse)
.catch(function(err) {
console.log("Error reading token from file... getting a new one");
return getToken()
.then(function(data) {
console.log("Token (from API): " + data);
return data;
})
.then(JSON.stringify)
.then(fs.writeFileAsync.bind(null, tokenFile.value()));
});

token.then(function(data) {
console.log("Token (from anywhere): " + token.value);
});


This code is currently logging:

Token: undefined


if I fall back to the API. Assuming I did my promise stuff correctly (
.catch()
can return a promise, right?) then I would assume the issue is occurring because
fs.writeFileAsync
returns void.

I would like to append a
.return()
on the end of this promise, but how would I gain access to the return value of
getToken()
? I tried the following:

.catch(function(err) {
console.log("Error reading token from file... getting a new one");
var token = "nope";
return getToken()
.then(function(data) {
console.log("Token (from API): " + data);
token = data;
return data;
})
.then(JSON.stringify)
.then(fs.writeFileAsync.bind(null, tokenFile.value()))
.return(token);
});


However this logs "nope".

Answer

Over the weekend I continued my research on promises and upon making a pivotal realization I was able to develop the solution to this. Posting here both the realization and the solution:

The Realization

Promises were invented so that asynchronous code could be used in a synchronous manner. Consider the following:

var data = processData(JSON.parse(readFile(getFileName())));

This is the equivalent of:

var filename = getFileName();
var fileData = readFile(filename);
var parsedData = JSON.parse(fileData);
var data = processData(parsedData);

If any one of these functions is asynchronous then it breaks, because the value isn't ready on time. So for those asynchronous bits we used to use callbacks:

var filename = getFileName();
var data = null;
readFile(filename, function(fileData){
    data = processData(JSON.parse(fileData));
});

This is not only ugly, but breaks a lot of things like stack traces, try/catch blocks, etc.

The Promise pattern fixed this, letting you say:

var filename = getFileName();
var fileData = filename.then(readFile);
var parsedData = fileData.then(JSON.parse);
var data = parsedData.then(processData);

This code works regardless of whether these functions are synchronous or asynchronous, and there are zero callbacks. It's actually all synchronous code, but instead of passing values around, we pass promises around.

The led me to the realization that: for every bit of code that can be written with promises, there is a synchronous corollary

The solution

Realizing this, I tried to consider my code if all of the functions were synchronous:

try {
    var tokenFile = tilde(process.env.token_file)
} catch(err) {
    throw new Error("Error parsing path to file... can not recover");
}

var token = null;
try {
    token = JSON.parse(readFile(tokenFile));
} catch(err) {
    token = getToken();
    writeFile(tokenFile, JSON.stringify(token));
}

console.log("Token: " + token.value);

After framing it like this, the promise version follows logically:

var tokenFile = tilde(process.env.token_file)
    .catch(function(err) {
        throw new Error("Error parsing path to file... can not recover");
    });

var token = tokenFile
    .then(readFile)
    .then(JSON.parse)
    .catch(function(err) {
        var _token = getToken();
        _token
            .then(JSON.stringify)
            .then(writeFile.bind(null, tokenFile.value));
        return _token;
    });