luciotato / waitfor

Sequential programming for node.js, end of callback hell / pyramid of doom
MIT License
531 stars 29 forks source link

Suggestion: user-customized callback form #10

Closed lych77 closed 10 years ago

lych77 commented 10 years ago

This example will explain well: I wanted to fit an http.request operation into a fiber context, but soon found it hardly possible with the current wait.for API: if the request succeeds, the "response" event is fired, but if some error occurs before it, the "error" event is fired instead and "response" will never get fired, i.e. there is a number of APIs that doesn't respect the "callback(err, res)" convention. What even worse is after 'response' and 'error' are hooked, I have to do extra work (to feed the POST body into the req object, or at least call "req.end()") to enter next phase in that the callbacks are really possible to work. These are all beyond the capability of wait.for.

So, according to some attemptations in my work, I'd like to suggest providing some open API enabling the users to wrap some complex async operations into the fiber context by themselves. like the form shown in following code (though considering less of various situations than the current wait.for code, just showing the idea):

Library:

function wrapIntoCurrentContext(func /* ,args... */) {
    var fib = fibers.current;
    var ctx = {
        return: function (val) { fib.run(val);  },
        throw: function (err) { fib.throwInto(err); },
    };
    arguments[0] = ctx;
    func.apply(null, arguments);
    return fibers.yield();
}

User:

// wrapper provided by the user
// first arg always being a "fiber context" it can inject return values or errors into

function requestHttp(ctx, options, body) {
    var req = http.request(options);

    req.on('response', function (res) {
        var buf = new Buffer(0);
        res.on('data', function (data) { buf = Buffer.concat([buf, data]); });
        res.on('end', function() { ctx.return(buf); });
    });

    req.on('error', function (err) {
        ctx.throw(err);
    });

    req.end(body);
}

function main() {
    try {
        var res = wrapIntoCurrentContext(requestHttp, {hostname: 'some.host.com'}, 'body_data');
        console.log(res.toString('utf8'));
    } catch (err) {
        console.log(err);
    }
}

launchFiber(main);
luciotato commented 10 years ago

I want to keep it simple, and also compatible with wait.for-ES6 (based on generators). In order to do that, I can't expose node-fibers internals.

Why not just normalize the callbacks? like this (warning: I've just write this here, based on your code, not tested, not even fed to node once)

//normalized request
function httpRequest(options, body, callback) {
try{
    var req = http.request(options);

    var buf = new Buffer(0);

    req.on('response', function (res) {
              console.log('STATUS: ' + res.statusCode);
              if (res.statusCode!==200) return callback(new Error('statuscode '+res.statusCode));
              console.log('HEADERS: ' + JSON.stringify(res.headers));
              res.setEncoding('utf8');        
              res.on('data', function (data) { buf = Buffer.concat([buf, data]); });
              res.on('end', function() { return callback(null,buf); });
    });

    req.on('error', function (err) {
        return callback(err);
    });

    req.end(body);
}
catch (err)
  return callback(err);
}
}

//normalized get
function httpGet(options, callback) {
    options.method = 'get';
    httpRequest(options, null, callback) 
}
//normalized post
function httpPost(options, body, callback) {
    options.method = 'post';
    httpRequest(options, body, callback) 
}

function main() {
    try {

        var getResult = wait.for(httpGet, {hostname: 'some.host.com'});
        console.log(getResult.toString('utf8'));

        var postResponseBody = wait.for(httpPost, {hostname: 'some.host.com'}, 'body_data');
        console.log(postResponseBody.toString('utf8'));

    } catch (err) {
        console.log(err);
    }
}

launchFiber(main);
lych77 commented 10 years ago

Thanks a lot, your approach does make sense while I haven't thought that far. Really this is the better way to end callback hell than any other.