/*! * Copyright 2016 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /*! * @module common/operation */ 'use strict'; const events = require('events'); const extend = require('extend'); const modelo = require('modelo'); /** * @type {module:common/serviceObject} * @private */ const ServiceObject = require('./service-object.js'); // jscs:disable maximumLineLength /** * An Operation object allows you to interact with APIs that take longer to * process things. * * @constructor * @alias module:common/operation * * @param {object} config - Configuration object. * @param {module:common/service|module:common/serviceObject|module:common/grpcService|module:common/grpcServiceObject} config.parent - The * parent object. * @param {string} id - The operation ID. */ // jscs:enable maximumLineLength function Operation(config) { const methods = { /** * Checks to see if an operation exists. */ exists: true, /** * Retrieves the operation. */ get: true, /** * Retrieves metadata for the operation. */ getMetadata: { reqOpts: { name: config.id, }, }, }; config = extend( { baseUrl: '', }, config ); config.methods = config.methods || methods; ServiceObject.call(this, config); events.EventEmitter.call(this); this.completeListeners = 0; this.hasActiveListeners = false; this.listenForEvents_(); } modelo.inherits(Operation, ServiceObject, events.EventEmitter); /** * Wraps the `complete` and `error` events in a Promise. * * @return {promise} */ Operation.prototype.promise = function() { const 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 * handled automatically. * * As long as there is one active "complete" listener, the connection is open. * When there are no more listeners, the polling stops. * * @private */ Operation.prototype.listenForEvents_ = function() { const self = this; this.on('newListener', function(event) { if (event === 'complete') { self.completeListeners++; if (!self.hasActiveListeners) { self.hasActiveListeners = true; self.startPolling_(); } } }); this.on('removeListener', function(event) { if (event === 'complete' && --self.completeListeners === 0) { self.hasActiveListeners = false; } }); }; /** * Poll for a status update. Execute the callback: * * - callback(err): Operation failed * - callback(): Operation incomplete * - callback(null, metadata): Operation complete * * @private * * @param {function} callback */ Operation.prototype.poll_ = function(callback) { this.getMetadata(function(err, resp) { if (err || resp.error) { callback(err || resp.error); return; } if (!resp.done) { callback(); return; } callback(null, resp); }); }; /** * Poll `getMetadata` to check the operation's status. This runs a loop to ping * the API on an interval. * * Note: This method is automatically called once a "complete" event handler is * registered on the operation. * * @private */ Operation.prototype.startPolling_ = function() { const self = this; if (!this.hasActiveListeners) { return; } this.poll_(function(err, metadata) { if (err) { self.emit('error', err); return; } if (!metadata) { setTimeout(self.startPolling_.bind(self), 500); return; } self.emit('complete', metadata); }); }; module.exports = Operation;