Sweet Patterns for Promises 

Promises/A+

ok?

One Choice

Bluebird

/petkaantonov/bluebird

It's the FASTEST

and robust

Look it up!

What are Promises?

Manage Asynchronicity

  • Single callback guarantee
  • Robust error handling
  • Composability

Single Callback Guarantee

entity.create()
  .then(function(udo) {
    // once for either outcome
  })
  .catch(function(error) {
    // once for either outcome
  });

Robust Error Handling

entity.create()
  .then(function(udo) {
    // any error that happens in here
    // will get caught in ".catch()"
    throw new Error('ops');
  })
  .catch(function(error) {});

Composability

entity.create()
  .then(entity.read)
  .then(entity.parseForApi)
  .then(entity.process)
  .catch(entity.onError);

Creating Promises

2 ways

Creating a Novel Promise

Typically interfacing with vanilla callbacks

var Promise = require('bluebird');

/* ... */

ClipEntity.prototype.fetchVideos = function() {
    return new Promise(function(resolve, reject) {
        fs.read(file, function(err, res) {
            if (err) {
                reject(err);
            } else {
                resolve(res);
            }
        });
    });
};

Interfacing with Promises

When your interfaces are all returning promises

var Promise = require('bluebird');

/* ... */

ClipEntity.prototype.showVideos = Promise.method(function() {
    return this.fetchVideos();
});

Error Handling with new

Always use reject()

return new Promise(function(resolve, reject) {
    if (someCondition) {
        var err = new Error('Not good');
        reject(err);
    }
});

Error Handling with method()

Throw away

ClipEntity.prototype.showVideos = Promise.method(function() {
    if(someCondition) {
        throw new Error('ops');
    }
});

REMEMBER

returning a Promise is CRITICAL!

  • But you may return any value as well

Promises will only

return one argument

Using method()

Anything you return will be the resolving value

Foo.prototype.one = Promise.method(function() {
    return this.two();
});

Foo.prototype.two = Promise.method(function() {
    return 5;
});

/* ... */

foo.one()
    .then(function(res) {
        console.log(res); // 5
    });

Working with Promises

Consuming a Promise

entity.create({name: 'thanasis'})
  .then(function(udo) {
    udo.name === 'thanasis'; // true
  })
  .catch(function(error) {
    // deal with error.
  });

Composing

ClipEntity.prototype._process = function(data) {
  var clipProcessEnt = new ClipProcessEnt(data);

  return clipProcessEnt.parseFiles()
    .bind(clipProcessEnt)
    .then(clipProcessEnt.isFileVideo)
    .then(clipProcessEnt.storeClip)
    .then(clipProcessEnt.storeThumb)
    .then(clipProcessEnt.updateDatabase)
    .then(clipProcessEnt.removeSourceFiles)
    .catch(clipProcessEnt.rollback);
};

Create master methods

<--- Notice the .bind() !!

Error Chaining

Process.prototype.rollback = Promise.method(function(err) {
  log.finer('rollback() :: Removing Clip from DB, clipId:', this.clipId);

  return this.clipEnt.delete(this.clipId)
    .bind(this)
    .catch(function (error) {
      log.warn('rollback() :: NOT GOOD ERROR. File:', this.sourceFilePath,
        ' Failed to remove record for clip with ID:', this.clipId, 'Error:', error);
      throw error;
    })
    .then(function() {
      throw err;
    });
});

The Rollback

<--- From clipEnt.delete()

<--- From Parent

Returning Custom Value

ClipEntity.prototype._process = function(data) {
  var clipProcessEnt = new ClipProcessEnt(data);

  return clipProcessEnt.parseFiles()
    .bind(clipProcessEnt)
    .then(clipProcessEnt.isFileVideo)
    .then(clipProcessEnt.storeClip)
    .then(clipProcessEnt.storeThumb)
    .then(clipProcessEnt.updateDatabase)
    .then(clipProcessEnt.removeSourceFiles)
    .return(data)
    .catch(clipProcessEnt.rollback);
};

<-- Here it is!

Running in Parallel

ClipEntity.prototype._process = function(data) {
  var clipProcessEnt = new ClipProcessEnt(data);

  var promises = [];

  promises.push(clipProcessEnt.isFileVideo());
  promises.push(clipProcessEnt.storeClip());
  promises.push(clipProcessEnt.storeThumb());
  promises.push(clipProcessEnt.updateDatabase());

  return Promise.all(promises)
    .catch(clipProcessEnt.rollback);
};

Running in Parallel

ClipEntity.prototype._process = function(data) {
  var clipProcessEnt = new ClipProcessEnt(data);

  var promises = [];

  promises.push(clipProcessEnt.isFileVideo());
  promises.push(clipProcessEnt.storeClip());
  promises.push(clipProcessEnt.storeThumb());
  promises.push(clipProcessEnt.updateDatabase());

  return Promise.all(promises, {concurrency: 10})
    .catch(clipProcessEnt.rollback);
};

... with a throttle

Applying an Array to a Promise

// Compact
ClipExport.prototype._copyClips = Promise.method(function (filenames) {

  return Promise.map(filenames, this._copyClip.bind(this));

});



// Expanded, better
ClipExport.prototype._copyClips = Promise.method(function (filenames) {

  return Promise.resolve(filenames)
    .map(this._copyClip);
    // can chain easier now, error catch, etc

});

Multiple items in an Array

// Compact
ClipExport.prototype._copyClips = Promise.method(function (filenames) {

  return Promise.map(filenames, this._copyClip.bind(this), {concurrency: 10});

});



// Expanded, better
ClipExport.prototype._copyClips = Promise.method(function (filenames) {

  return Promise.resolve(filenames)
    .map(this._copyClip, {concurrency: 10});
    // can chain easier now, error catch, etc

});

... with a throttle

Promisifying

Promisify

var Promise = require('bluebird');

// Promisify all methods of a module
var fs = Promise.promisifyAll(require('fs'));

fs.readFileAsync('one').then().catch();

// Promisify a single method
var readFileAsync = Promise.promisify(fs.readFile);

readFileAsync.then().catch();

Spreading Arguments

The spread()

Foo.prototype.one = Promise.method(function () {
  return Promise.resolve([
      'one',
      'two'
    ])
    map(function(val) {
      return val + '-lol';
    })
    .spread(function(arg1, arg2) {
      console.log(arg1, arg2);
      // prints: "one-lol two-lol"
    });
});

All Together

All Together

Foo.prototype.one = Promise.method(function (data) {
  this.readAllFilesFor(data)
    .bind(this)
    .map(this.copyFile)
    .then(function(filenames) {
      return Promise.all([
          this.process(filenames),
          this.save(filenames),
          this.notify(filenames),
        ]);
    })
    .spread(function(fromProcess, fromSave, fromNotify) {
      return this.massup(fromProcess, fromSave, fromNotify)
    })
    .then(this.compress)
    .then(this.sign)
    .then(this.publish)
    .catch(this.onError);
});

<-- filenames is the result of copyfiles()

<-- Parallel Async OPs

<-- spread() is one to one with the array

Thank you

(here is where you applaud)

Thanasis Polychronakis

@thanpolas

Questions

Thanasis Polychronakis

@thanpolas

Promises patterns

By thanpolas

Promises patterns

Useful and practical Promises patterns

  • 957