From a7f5d12c0129932236cfcc7dc6fb824edf40d8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 13 Feb 2015 08:21:22 -0800 Subject: [PATCH] Support Promise-returning method and hooks If a shared method or a hook handler returns a duck-typed promise, i.e. an object with a `then` method, then the invocation waits until the promise is fulfilled or rejected. --- .jshintrc | 1 + lib/remote-objects.js | 5 +++- lib/shared-method.js | 16 ++++++++--- package.json | 1 + test/rest.test.js | 29 ++++++++++++++++++++ test/shared-method.test.js | 54 +++++++++++++++++++++++++++++++++----- 6 files changed, 96 insertions(+), 10 deletions(-) diff --git a/.jshintrc b/.jshintrc index 6d3b91b..1f55544 100644 --- a/.jshintrc +++ b/.jshintrc @@ -10,6 +10,7 @@ "undef": true, "laxcomma" : true, "globals" : { + "Promise": true, "it": false, "describe": false, "before": false, diff --git a/lib/remote-objects.js b/lib/remote-objects.js index 3213132..1168a04 100644 --- a/lib/remote-objects.js +++ b/lib/remote-objects.js @@ -360,7 +360,10 @@ RemoteObjects.prototype.execHooks = function(when, method, scope, ctx, next) { if (cur) { try { - cur.call(scope, ctx, execStack, method); + var result = cur.call(scope, ctx, execStack, method); + if (result && typeof result.then === 'function') { + result.then(function() { next(); }, next); + } } catch (err) { next(err); } diff --git a/lib/shared-method.js b/lib/shared-method.js index 54b47eb..67e189f 100644 --- a/lib/shared-method.js +++ b/lib/shared-method.js @@ -151,7 +151,6 @@ SharedMethod.prototype.invoke = function(scope, args, remotingOptions, cb) { var method = this.getFunction(); var sharedMethod = this; var formattedArgs = []; - var result; if (cb === undefined && typeof remotingOptions === 'function') { cb = remotingOptions; @@ -229,7 +228,7 @@ SharedMethod.prototype.invoke = function(scope, args, remotingOptions, cb) { return cb(err); } - result = SharedMethod.toResult(returns, [].slice.call(arguments, 1)); + var result = SharedMethod.toResult(returns, [].slice.call(arguments, 1)); debug('- %s - result %j', sharedMethod.name, result); @@ -243,7 +242,18 @@ SharedMethod.prototype.invoke = function(scope, args, remotingOptions, cb) { // invoke try { - return method.apply(scope, formattedArgs); + var retval = method.apply(scope, formattedArgs); + if (retval && typeof retval.then === 'function') { + return retval.then( + function(args) { + var result = SharedMethod.toResult(returns, args); + debug('- %s - promise result %j', sharedMethod.name, result); + cb(null, result); + }, + cb // error handler + ); + } + return retval; } catch (err) { debug('error caught during the invocation of %s', this.name); return cb(err); diff --git a/package.json b/package.json index 50e8d6a..e45abcf 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "xml2js": "^0.4.4" }, "devDependencies": { + "bluebird": "^2.9.6", "browserify": "~5.11.1", "chai": "^1.10.0", "grunt": "~0.4.5", diff --git a/test/rest.test.js b/test/rest.test.js index cad338d..e9419f4 100644 --- a/test/rest.test.js +++ b/test/rest.test.js @@ -6,6 +6,7 @@ var express = require('express'); var request = require('supertest'); var expect = require('chai').expect; var factory = require('./helpers/shared-objects-factory.js'); +var Promise = global.Promise || require('bluebird'); var ACCEPT_XML_OR_ANY = 'application/xml,*/*;q=0.8'; @@ -1514,6 +1515,34 @@ describe('strong-remoting-rest', function() { .expect(500) .end(expectErrorResponseContaining({message: 'test-error'}, done)); }); + + it('should resolve promise returned by a hook', function(done) { + var method = givenSharedPrototypeMethod(); + objects.before('**', function(ctx) { + return new Promise(function(resolve, reject) { + resolve('value-to-ignore'); + }); + }); + + json(method.url).expect(204).end(done); + }); + + it('should handle rejected promise returned by a hook', function(done) { + var testError = new Error('expected test error'); + var method = givenSharedPrototypeMethod(); + objects.after('**', function(ctx) { + return new Promise(function(resolve, reject) { + reject(testError); + }); + }); + + json(method.url).expect(500).end(function(err, res) { + if (err) return done(err); + expect(res.body) + .to.have.deep.property('error.message', testError.message); + done(); + }); + }); }); it('returns 404 for unknown method of a shared class', function(done) { diff --git a/test/shared-method.test.js b/test/shared-method.test.js index 9bc5f11..5335602 100644 --- a/test/shared-method.test.js +++ b/test/shared-method.test.js @@ -3,6 +3,7 @@ var extend = require('util')._extend; var expect = require('chai').expect; var SharedMethod = require('../lib/shared-method'); var factory = require('./helpers/shared-objects-factory.js'); +var Promise = global.Promise || require('bluebird'); describe('SharedMethod', function() { describe('sharedMethod.isDelegateFor(suspect, [isStatic])', function() { @@ -83,13 +84,54 @@ describe('SharedMethod', function() { }); }); - function givenSharedMethod(options) { - var aFn = function() { - arguments[arguments.length - 1](); - }; + it('resolves promise returned from the method', function(done) { + var method = givenSharedMethod( + function() { + return new Promise(function(resolve, reject) { + resolve(['one', 'two']); + }); + }, + { + returns: [ + { arg: 'first', type: 'string' }, + { arg: 'second', type: 'string' } + ] + }); + + method.invoke('ctx', {}, function(err, result) { + setImmediate(function() { + expect(result).to.eql({ first: 'one', second: 'two' }); + done(); + }); + }); + }); + + it('handles rejected promise returned from the method', function(done) { + var testError = new Error('expected test error'); + var method = givenSharedMethod(function() { + return new Promise(function(resolve, reject) { + reject(testError); + }); + }); + + method.invoke('ctx', {}, function(err, result) { + setImmediate(function() { + expect(err).to.equal(testError); + done(); + }); + }); + }); + + function givenSharedMethod(fn, options) { + if (options === undefined && typeof fn === 'object') { + options = fn; + fn = function() { + arguments[arguments.length - 1](); + }; + } - var mockSharedClass = { fn: aFn }; - return new SharedMethod(aFn, 'fn', mockSharedClass, options); + var mockSharedClass = { fn: fn }; + return new SharedMethod(fn, 'fn', mockSharedClass, options); } }); });