From 6899963893e1aaa0c49fd07aa29411a77e214cd6 Mon Sep 17 00:00:00 2001 From: Dave Gramlich Date: Fri, 14 Oct 2016 11:50:56 -0400 Subject: [PATCH] core: make operations promise friendly (#1689) --- packages/bigquery/src/job.js | 25 +++++++++++++++ packages/bigquery/test/job.js | 42 ++++++++++++++++++++++++- packages/common/src/grpc-operation.js | 17 ++++++++++ packages/common/src/service-object.js | 1 + packages/common/src/service.js | 5 +-- packages/common/src/util.js | 23 +++++--------- packages/common/test/grpc-operation.js | 43 +++++++++++++++++++++++++- packages/common/test/service-object.js | 13 ++++++++ packages/common/test/service.js | 12 +++---- packages/common/test/util.js | 17 ++-------- packages/compute/src/operation.js | 25 +++++++++++++++ packages/compute/test/operation.js | 43 +++++++++++++++++++++++++- 12 files changed, 224 insertions(+), 42 deletions(-) diff --git a/packages/bigquery/src/job.js b/packages/bigquery/src/job.js index 6250b23db9f..6376d8d37eb 100644 --- a/packages/bigquery/src/job.js +++ b/packages/bigquery/src/job.js @@ -312,6 +312,31 @@ Job.prototype.getQueryResultsStream = function(options) { return this.bigQuery.createQueryStream(options); }; +/** + * Convenience method that wraps the `complete` and `error` events in a + * Promise. + * + * @return {promise} + * + * @example + * job.promise().then(function(metadata) { + * // The job is complete. + * }, function(err) { + * // An error occurred during the job. + * }); + */ +Job.prototype.promise = function() { + var self = this; + + return new self.Promise(function(resolve, reject) { + self + .on('error', reject) + .on('complete', function(metadata) { + resolve([metadata]); + }); + }); +}; + /** * Begin listening for events on the job. This method keeps track of how many * "complete" listeners are registered and removed, making sure polling is diff --git a/packages/bigquery/test/job.js b/packages/bigquery/test/job.js index 11e736ec2dc..f2da94dacb6 100644 --- a/packages/bigquery/test/job.js +++ b/packages/bigquery/test/job.js @@ -50,7 +50,8 @@ var fakeUtil = Object.keys(util).reduce(function(fakeUtil, methodName) { describe('BigQuery/Job', function() { var BIGQUERY = { - projectId: 'my-project' + projectId: 'my-project', + Promise: Promise }; var JOB_ID = 'job_XYrk_3z'; var Job; @@ -274,6 +275,45 @@ describe('BigQuery/Job', function() { }); }); + describe('promise', function() { + beforeEach(function() { + job.startPolling_ = util.noop; + }); + + it('should return an instance of the localized Promise', function() { + var FakePromise = job.Promise = function() {}; + var promise = job.promise(); + + assert(promise instanceof FakePromise); + }); + + it('should reject the promise if an error occurs', function() { + var error = new Error('err'); + + setImmediate(function() { + job.emit('error', error); + }); + + return job.promise().then(function() { + throw new Error('Promise should have been rejected.'); + }, function(err) { + assert.strictEqual(err, error); + }); + }); + + it('should resolve the promise on complete', function() { + var metadata = {}; + + setImmediate(function() { + job.emit('complete', metadata); + }); + + return job.promise().then(function(data) { + assert.deepEqual(data, [metadata]); + }); + }); + }); + describe('listenForEvents_', function() { beforeEach(function() { job.startPolling_ = util.noop; diff --git a/packages/common/src/grpc-operation.js b/packages/common/src/grpc-operation.js index ceaf72c59c4..217dcaab04b 100644 --- a/packages/common/src/grpc-operation.js +++ b/packages/common/src/grpc-operation.js @@ -133,6 +133,23 @@ GrpcOperation.prototype.cancel = function(callback) { this.request(protoOpts, reqOpts, callback || util.noop); }; +/** + * Wraps the `complete` and `error` events in a Promise. + * + * @return {promise} + */ +GrpcOperation.prototype.promise = function() { + var self = this; + + return new self.Promise(function(resolve, reject) { + self + .on('error', reject) + .on('complete', function(metadata) { + resolve([metadata]); + }); + }); +}; + /** * Begin listening for events on the operation. This method keeps track of how * many "complete" listeners are registered and removed, making sure polling is diff --git a/packages/common/src/service-object.js b/packages/common/src/service-object.js index 9c3264b8399..bc9a465f885 100644 --- a/packages/common/src/service-object.js +++ b/packages/common/src/service-object.js @@ -71,6 +71,7 @@ function ServiceObject(config) { this.createMethod = config.createMethod; this.methods = config.methods || {}; this.interceptors = []; + this.Promise = this.parent.Promise; if (config.methods) { var allMethodNames = Object.keys(ServiceObject.prototype); diff --git a/packages/common/src/service.js b/packages/common/src/service.js index d508528b691..499f3c79e3b 100644 --- a/packages/common/src/service.js +++ b/packages/common/src/service.js @@ -60,10 +60,7 @@ function Service(config, options) { this.packageJson = config.packageJson; this.projectId = options.projectId; this.projectIdRequired = config.projectIdRequired !== false; - - if (options.promise) { - util.setPromiseOverride(options.promise); - } + this.Promise = options.promise || Promise; } /** diff --git a/packages/common/src/util.js b/packages/common/src/util.js index 485c8ed59c5..3c33b784a1a 100644 --- a/packages/common/src/util.js +++ b/packages/common/src/util.js @@ -51,8 +51,6 @@ var errorMessage = format([ path: '/docs/guides/authentication' }); -var PromiseOverride; - var missingProjectIdError = new Error(errorMessage); util.missingProjectIdError = missingProjectIdError; @@ -656,7 +654,14 @@ function promisify(originalMethod) { return originalMethod.apply(context, args); } - var PromiseCtor = PromiseOverride || Promise; + var PromiseCtor = Promise; + + // Because dedupe will likely create a single install of + // @google-cloud/common to be shared amongst all modules, we need to + // localize it at the Service level. + if (context && context.Promise) { + PromiseCtor = context.Promise; + } return new PromiseCtor(function(resolve, reject) { args.push(function() { @@ -708,15 +713,3 @@ function promisifyAll(Class, options) { } util.promisifyAll = promisifyAll; - -/** - * Allows user to override the Promise constructor without the need to touch - * globals. Override should be ES6 Promise compliant. - * - * @param {promise} override - */ -function setPromiseOverride(override) { - PromiseOverride = override; -} - -module.exports.setPromiseOverride = setPromiseOverride; diff --git a/packages/common/test/grpc-operation.js b/packages/common/test/grpc-operation.js index 20b51a089c4..0f06d166bb8 100644 --- a/packages/common/test/grpc-operation.js +++ b/packages/common/test/grpc-operation.js @@ -47,7 +47,9 @@ var FakeGrpcServiceObject = createFake(GrpcServiceObject); var FakeGrpcService = createFake(GrpcService); describe('GrpcOperation', function() { - var FAKE_SERVICE = {}; + var FAKE_SERVICE = { + Promise: Promise + }; var OPERATION_ID = '/a/b/c/d'; var GrpcOperation; @@ -149,6 +151,45 @@ describe('GrpcOperation', function() { }); }); + describe('promise', function() { + beforeEach(function() { + grpcOperation.startPolling_ = util.noop; + }); + + it('should return an instance of the localized Promise', function() { + var FakePromise = grpcOperation.Promise = function() {}; + var promise = grpcOperation.promise(); + + assert(promise instanceof FakePromise); + }); + + it('should reject the promise if an error occurs', function() { + var error = new Error('err'); + + setImmediate(function() { + grpcOperation.emit('error', error); + }); + + return grpcOperation.promise().then(function() { + throw new Error('Promise should have been rejected.'); + }, function(err) { + assert.strictEqual(err, error); + }); + }); + + it('should resolve the promise on complete', function() { + var metadata = {}; + + setImmediate(function() { + grpcOperation.emit('complete', metadata); + }); + + return grpcOperation.promise().then(function(data) { + assert.deepEqual(data, [metadata]); + }); + }); + }); + describe('listenForEvents_', function() { beforeEach(function() { grpcOperation.startPolling_ = util.noop; diff --git a/packages/common/test/service-object.js b/packages/common/test/service-object.js index c9845393402..ae69cd0639e 100644 --- a/packages/common/test/service-object.js +++ b/packages/common/test/service-object.js @@ -109,6 +109,19 @@ describe('ServiceObject', function() { assert.strictEqual(typeof serviceObject.create, 'function'); assert.strictEqual(serviceObject.delete, undefined); }); + + it('should localize the Promise object', function() { + var FakePromise = function() {}; + var config = extend({}, CONFIG, { + parent: { + Promise: FakePromise + } + }); + + var serviceObject = new ServiceObject(config); + + assert.strictEqual(serviceObject.Promise, FakePromise); + }); }); describe('create', function() { diff --git a/packages/common/test/service.js b/packages/common/test/service.js index 9dc79d160ff..f054e391b31 100644 --- a/packages/common/test/service.js +++ b/packages/common/test/service.js @@ -150,15 +150,15 @@ describe('Service', function() { assert.strictEqual(service.projectIdRequired, true); }); - it('should call setPromiseOverride when promise is set', function(done) { + it('should localize the Promise object', function() { var FakePromise = function() {}; + var service = new Service({}, { promise: FakePromise }); - util.setPromiseOverride = function(override) { - assert.strictEqual(override, FakePromise); - done(); - }; + assert.strictEqual(service.Promise, FakePromise); + }); - new Service({}, { promise: FakePromise }); + it('should localize the native Promise object by default', function() { + assert.strictEqual(service.Promise, global.Promise); }); }); diff --git a/packages/common/test/util.js b/packages/common/test/util.js index a3210e8f3de..d670db02342 100644 --- a/packages/common/test/util.js +++ b/packages/common/test/util.js @@ -1494,21 +1494,10 @@ describe('common/util', function() { assert.strictEqual(err, error); }); }); - }); - - describe('setPromiseOverride', function() { - var FakePromise = function() {}; - - before(function() { - util.setPromiseOverride(FakePromise); - }); - - after(function() { - util.setPromiseOverride(null); - }); - it('should allow the Promise constructor to be specified', function() { - var promise = util.promisify(util.noop)(); + it('should allow the Promise object to be overridden', function() { + var FakePromise = function() {}; + var promise = func.call({ Promise: FakePromise }); assert(promise instanceof FakePromise); }); diff --git a/packages/compute/src/operation.js b/packages/compute/src/operation.js index 5e958f934de..0881f21062d 100644 --- a/packages/compute/src/operation.js +++ b/packages/compute/src/operation.js @@ -230,6 +230,31 @@ Operation.prototype.getMetadata = function(callback) { }); }; +/** + * Convenience method that wraps the `complete` and `error` events in a + * Promise. + * + * @return {promise} + * + * @example + * operation.promise().then(function(metadata) { + * // The operation is complete. + * }, function(err) { + * // An error occurred during the operation. + * }); + */ +Operation.prototype.promise = function() { + var self = this; + + return new self.Promise(function(resolve, reject) { + self + .on('error', reject) + .on('complete', function(metadata) { + resolve([metadata]); + }); + }); +}; + /** * Begin listening for events on the operation. This method keeps track of how * many "complete" listeners are registered and removed, making sure polling is diff --git a/packages/compute/test/operation.js b/packages/compute/test/operation.js index 85bf39cc976..a6e12cce862 100644 --- a/packages/compute/test/operation.js +++ b/packages/compute/test/operation.js @@ -51,7 +51,9 @@ describe('Operation', function() { var Operation; var operation; - var SCOPE = {}; + var SCOPE = { + Promise: Promise + }; var OPERATION_NAME = 'operation-name'; before(function() { @@ -203,6 +205,45 @@ describe('Operation', function() { }); }); + describe('promise', function() { + beforeEach(function() { + operation.startPolling_ = util.noop; + }); + + it('should return an instance of the localized Promise', function() { + var FakePromise = operation.Promise = function() {}; + var promise = operation.promise(); + + assert(promise instanceof FakePromise); + }); + + it('should reject the promise if an error occurs', function() { + var error = new Error('err'); + + setImmediate(function() { + operation.emit('error', error); + }); + + return operation.promise().then(function() { + throw new Error('Promise should have been rejected.'); + }, function(err) { + assert.strictEqual(err, error); + }); + }); + + it('should resolve the promise on complete', function() { + var metadata = {}; + + setImmediate(function() { + operation.emit('complete', metadata); + }); + + return operation.promise().then(function(data) { + assert.deepEqual(data, [metadata]); + }); + }); + }); + describe('listenForEvents_', function() { beforeEach(function() { operation.startPolling_ = util.noop;