Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Promisify imported class (constructor) with bluebird in ES6 + babel

Suppose I created or have a node.js library lib.js

export class C {
    constructor(value, callback) {
        callback(false, `Hello ${value}`);
    }

    task(value, callback) {
        callback(false, "returned " + value);
    }
}

The important part is that the classes' constructor needs to accept a callback as it does database connections and file I/O. If I now import and use the library callback-style, everything is fine (see c1 below).

I would really like to promisify the library where I use it to make object construction more convenient (in reality it's a whole bunch of classes and methods).

However, I can't find a way to new the class properly in a promise-safe nicely.

import Promise from 'bluebird';
import * as lib from './lib';


Promise.promisifyAll(lib);


// old style -- works as expected
const c1 = new lib.C("c1", (e, v) => {
    console.log(c1, e, v); 
});


// assuming c1 got initialized, .task() also works
c1.task("t1", console.log);
c1.taskAsync("t2").then(() => console.log("also works"));


// But how to do this properly with promises?
const c2 = new lib.C("c2"); c2.then(console.log); // clearly doesn't work, lack of callback
const c3 = new lib.CAsync("c3"); c3.then(console.log); // "cannot read property apply of undefined"
const c4 = ???

How would I do this best? Changing library signature isn't a good option, creating factory methods also seems to be ugly.

like image 989
left4bread Avatar asked Jul 13 '15 19:07

left4bread


2 Answers

I feel strongly about this so I'll start with it: Don't do IO in constructors it's a bad idea to bind io and construction together.

That said, if you must do this because the library is out of your control and are OK with losing the ability to build objects in a sync way, you can:

export class C {
    constructor(value, callback) {
        callback(false, `Hello ${value}`);
    }

    task(value, callback) {
        callback(false, "returned " + value);
    }
}

And when promisifying:

import Promise from 'bluebird';
import * as lib from './lib';


Promise.promisifyAll(lib);

var old = lib.C; // reference the constructor
lib.C = function(value){ // override it
  o; // object we'll later return, populate in promise constructor
  var p = new Promise(function(resolve, reject){ 
    // the promise constructor is always sync, so the following works
    o = new old(value, function(err, data) {
      if(err) return reject(err);
      resolve(data);   
    });
  }); 
  // THIS IS THE IMPORTANT PART
  o.then = p.then.bind(p); // make the object a thenable, 
  return o
};

Which would let you both use the return value and the promise, the promise will only have a then so you might want to Promise.resolve it to get a "real" promise rather than an object with properties and a promise.

var o = new lib.C(); // get object
o.then(function(data){
    // access data
});

This can be extracted to a pattern:

 function promisifyConstructor(cons){
   return function(...args) => { // new constructor function
     let o;
     let p = new Promise((resolve, reject) => {
         // delegate arguments
        o = new cons(...args, (err, data) => err ? reject(err) : resolve(data));
     });
     o.then = p.then.bind(p);
     return o;
   }
 }
like image 185
Benjamin Gruenbaum Avatar answered Nov 13 '22 15:11

Benjamin Gruenbaum


You can't directly promisify a constructor (that I'm aware of), but you can trivially work around that with a factory method:

function createC(value) {
  return new Promise(function (res, rej) {
    var c = new C(value, function (err, val) {
      if (err) {
        rej(err);
      } else {
        res(val); // or res(c) if you prefer
      }
    });
  });
}

I don't think there's a prettier way, and a well-constructed factory shouldn't be too ugly. You could generalize the factory to take any constructor of that form, but then you're approaching full DI and it may be worth finding a promise-friendly DI library.

like image 45
ssube Avatar answered Nov 13 '22 15:11

ssube