Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ES6 Promise patterns for exotic control flows

ES6 Promises are great. So far it’s been pretty easy to adjust my thinking from the callback idiom. I’ve found it naturally encourages more modular code, and of course error handling is much clearer.

But a few times I’ve encountered flow situations that don’t seem(?) like they can be readily translated from nodebacks to promises (and perhaps that’s just that, but maybe I’m just blind to the answers). Since promises are agnostic about the next operation (or if there even is one), it seems pretty tough to use Promises with APIs that don’t just take callbacks, but also return them.

The most common example that comes to mind is the ‘done’ callback. It shows up in things like database connections to signify ‘return connection to pool’ but I’ve seen it pop up in plenty of other places, too.

function getSomeStupidConnection(cb) {
    var conn = /* ... */;
    var iNeedToBeToldWhenIAmDone = function() { /* ... */ };

    cb(conn, iNeedToBeToldWhenIAmDone);
}

getSomeStupidConnection(function(conn, done) {
    /* ... */

    conn.doLotsOfStuff(function(soMuchStuff) {

        /* stuff! so much fun! */

        /* okay conn go away I’m tired */

        done();
    });
});

Flow-reversal like this is obviously not something you want to have in your APIs to start with, but it’s out there and you can’t really avoid it sometimes. With callbacks, you can pass the ‘call later’ inner callback to the original ‘outer’ callback. It doesn’t exactly lead to a clean seperation of concerns, but at least it’s quick and simple.

Is there a Promise-based approach suited to situations like this? A way to say, ‘here’s the resolve value -- but when the chain is complete, also do this’? I suspect there’s nothing that perfectly matches what I just described because it isn’t really possible to say a chain is ‘done’, but maybe I’m missing some pattern that gets you close to that without making a mess...


Edit: Based on the feedback so far I've realized that there's simply no way to wrap such an API in true promises, because the promise you return will never be able to tell you anything about any subsequent chained promises that piggyback on it. But you can fake it. The twist is that the result is rather brittle; it must assume that the only then which needs the connection object is the one which immediately follows. The consumer of the promise would need to understand that it’s a one-time-use connection, which isn’t otherwise obvious. Therefore I don't really recommend it in practice, but for the sake of curiosity here is a solution that hides the done while behaving as (and ultimately becoming) a promise chain:

/* jshint node: true, esnext: true */
'use strict';

// Assume this comes from an external library. It returns a connection and a
// callback to signal that you are finished with the connection.

function getConnectionExternal(cb) {
    let connection = 'Received connection.';
    let done = () => console.log('Done was called.');

    cb(null, connection, done);
}

// Our promisey wrapper for the above

function getConnection() {
    let _done;

    let promise = new Promise((resolve, reject) => {
        getConnectionExternal((err, connection, done) => {

            if (err) return reject(err);

            _done = (val) => {
                done();
                return val;
            };

            resolve(connection);
        });
    });

    let _then = promise.then.bind(promise);

    promise.then = (handler) => _then(handler).then(_done, _done);

    return promise;
}

// Test it out

getConnection()
    .then(connection => {
        console.log(connection);

        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('Finished using connection!');
                resolve('This should be after connection closes.');
            }, 200);
        });
    })
    .then(msg => console.log(msg))
    .catch(err => console.error(err));

Console prints:

  • Received connection.
  • Finished using connection!
  • Done was called.
  • This should be after connection closes.
like image 530
Semicolon Avatar asked Mar 17 '23 20:03

Semicolon


2 Answers

Elaborating on Bergi's solution, this is called the disposer pattern. It exists in many forms in many languages - with in Python, using in C# and try(){ with resource in Java. Some languages handle resource in scopes this way natively through destructurs like C#.

The general idea is for a scope to encapsulate the life-time of a value. In your case a database connection. It's a lot tidier than having to call done in a callback since it's much easier to forget to call done which leaves an open connection and a resource leak. Synchronously it'd look like:

function scope(cb){
     try{ 
         var conn = getConnection(...);
         return cb(conn);
     } finally {
         conn.release();
     }
}

The promises version is not too different:

function conn(data){
    var _connection;
    return getConnection().then(function(connection){
        _connection = connection; // keep a reference
        return data(_connection); // pass it to the function
    }).then(function(val){
         // release and forward
         _connection.release(); // if release is async - chain
         return val; 
    }, function(err){
         _connection.release();
         throw err; // forward error
    });
});

Which would use:

conn(function(db){
    return db.query("SELECT * FROM ...");
}).then(function(result){ // handle result
    // connection is released here
});
like image 108
Benjamin Gruenbaum Avatar answered Mar 19 '23 12:03

Benjamin Gruenbaum


A way to say, ‘here’s the resolve value -- but when the chain is complete, also do this’?

No, native promises do not provide such a facility. I would go with a resource function that takes a promise-returning callback, the callback does everything (in a chain) that needs to be done while the connection is open. Instead of passing iNeedToBeTold to the callback, the resource manager function observes the promise and does what needs to be done when it resolves.

function manageConnection(cb) {
    return getSomeConnection(…) // get the connections asynchronously - via a promise of course
    .then(function(conn) {
        function whenDone() {
            … // do what needs to be done
            return result;
        }
        var result = cb(conn);
        return result.then(whenDone, whenDone);
    });
}

manageConnection(function(conn) {
    return conn.doLotsOfStuff(soMuch)
    .then(function(stuff) {
        /* stuff! so much fun! */
    });
}).then(…)
like image 21
Bergi Avatar answered Mar 19 '23 11:03

Bergi