Reputation: 3428
The Q library (https://github.com/kriskowal/q) provides very helpful adaptors for functions that follow Node's standard callback layout, i.e. last argument is function(err, result)
.
return Q.nfcall(FS.readFile, "foo.txt", "utf-8");
return Q.nfapply(FS.readFile, ["foo.txt", "utf-8"]);
That's discussed further in the "Adapting Node" section of the README.
When using native ES6 Promises to accomplish the same, one often ends up with this unwieldy kind of trainwreck:
const fs = require('fs');
const http = require('http');
const server = http.createServer((req, res) => {
new Promise((resolve, reject) => {
fs.readFile('/etc/motd', (err, data) => {
if(err) {
reject(err.toString());
return;
}
resolve(data);
});
}).then(data => {
res.writeHead(200);
res.end(data);
}).catch(e => {
res.writeHead(500);
res.end(e);
});
}).listen(8000);
While this does flatten worst-case callback hell, it's still visually cluttered and hard to follow.
Obviously, one could decompose this into functions and inline less code to make it more readable, but that solution works fairly well for rectifying the very callback hell that promises are supposed to help solve in the first place. :-)
Is there anything I'm missing about the standard ES2015/6 Promise feature set that could allow one to save some mess here? Failing that, suggestions for low-calorie polyfills would be appreciated.
Upvotes: 0
Views: 460
Reputation: 7745
You can avoid promises altogether and just execute your code synchronously via nsynjs. Your code will transform as follows:
Step 1. Wrap slow functions with callbacks into nsynjs-aware wrappers:
// wrappers.js
var fs=require('fs');
exports.readFile = function (ctx,name) {
console.log("reading config");
var res={};
fs.readFile( name, "utf8", function( error , data){
if( error ) res.error = error;
res.data = data;
ctx.resume(error);
} );
return res;
};
exports.readFile.nsynjsHasCallback = true;
Step 2: Write your logic as if it was synchronous, and put it into function:
const synchronousCode = function(req,res,wrappers) {
try {
var data = wrappers.readFile(nsynjsCtx,'/etc/motd').data;
res.writeHead(200);
res.end(data);
}
catch(e) {
res.writeHead(500);
res.end(e);
};
}
Step 3. Execute that function via nsynjs:
// index.js
const fs = require('fs');
const http = require('http');
const nsynjs = require('nsynjs');
const wrappers = require('./wrappers');
const synchronousCode = function(req,res,wrappers) {
...
};
const server = http.createServer(function(req, res){
nsynjs.run(synchronousCode,{},req,res,wrappers,function(){
console.log('synchronousCode is done');
})
}).listen(8000);
Please see similar example here https://github.com/amaksr/nsynjs/tree/master/examples/node-module-loading
Upvotes: 0
Reputation: 222989
Most popular callback-based packages have their promisified counterparts, e.g. fs-extra
and mz/fs
for fs
.
pify
is widely known solution in for promisification that uses native Promise
by default. Other promise-related packages from this maintainer can be helpful as well, for example p-event
to promisify one-time event listeners.
http
example involves a callback that is triggered multiple times, something that cannot be replaced with a promise-based function. But it's obviously possible to promisify callback-based things beforehand like fs
(as shown in pify-fs
package):
const pify = require('pify');
const fs = pify(require('fs'), {
exclude: [/^exists/, /.+(Stream|Sync)$/, /watch/],
excludeMain: true
});
...
http.createServer((req, res) => {
let code;
let body;
fs.readFile('/etc/motd')
.then(
data => {
body = data;
code = 200;
},
err => {
body = String(err);
code = 500;
}
)
.then(() => {
res.writeHead(code);
res.end(body);
});
})
Without third-party promisification solution the developer is forced to reinvent the wheel, this involves promise construction with new Promise
, like shown in original example.
It should be noticed that Bluebird is a popular alternative to ES6 promises particularly because it provides demanded features out of the box, including promisification.
As of 8.0.0, Node has built-in util.promisify
to promisify Node-style callbacks. A recipe for batch promisification of a plain object like fs
is
const util = require('util');
const fs = Object.assign({}, require('fs'),
Object.entries(require('fs'))
.filter(([, val]) => typeof val === 'function')
.filter(([key]) => !/^[A-Z_]|^exists|.+(Stream|Sync)$|watch/.test(key))
.reduce((fs, [key, val]) => Object.assign(fs, { [key]: util.promisify(val) }), {})
);
Upvotes: 3
Reputation: 3428
I guess one can always invent one's own ...
const fs = require('fs');
const http = require('http');
function promiseAdapt(func, ...args) {
return new Promise((resolve, reject) => {
func.apply(this, args.concat((err, res) => {
if(err) {
reject(err.toString());
return;
}
resolve(res || undefined);
}));
});
}
const server = http.createServer((req, res) => {
let data = undefined;
promiseAdapt(fs.readFile, '/etc/motd', {
encoding: 'utf8'
}).then(d => {
data = d;
let str = `${req.method} ${req.url} (${req.headers['user-agent'] || '?'}) from ${req.connection.remoteAddress}` + "\n";
return promiseAdapt(fs.writeFile, 'access.log', str, {
encoding: 'utf8',
flag: 'a',
mode: '0755'
});
}).then(() => {
res.writeHead(200);
res.end(data);
}).catch(e => {
res.writeHead(500);
res.end(e);
});
}).listen(8000);
Upvotes: 0