UNPKG

aliyun-sdk

Version:
500 lines (444 loc) 15.9 kB
var ALY = require('./core'); var inherit = ALY.util.inherit; function AcceptorStateMachine(states, state) { this.currentState = state || null; this.states = states || {}; } AcceptorStateMachine.prototype.runTo = function runTo(finalState, done, bindObject, inputError) { if (typeof finalState === 'function') { inputError = bindObject; bindObject = done; done = finalState; finalState = null; } var self = this; var state = self.states[self.currentState]; state.fn.call(bindObject || self, inputError, function(err) { if (err) { if (bindObject.logger) bindObject.logger.log(self.currentState, '->', state.fail, err); if (state.fail) self.currentState = state.fail; else return done ? done(err) : null; } else { if (bindObject.logger) bindObject.logger.log(self.currentState, '->', state.accept); if (state.accept) self.currentState = state.accept; else return done ? done() : null; } if (self.currentState === finalState) return done ? done(err) : null; self.runTo(finalState, done, bindObject, err); }); }; AcceptorStateMachine.prototype.addState = function addState(name, acceptState, failState, fn) { if (typeof acceptState === 'function') { fn = acceptState; acceptState = null; failState = null; } else if (typeof failState === 'function') { fn = failState; failState = null; } if (!this.currentState) this.currentState = name; this.states[name] = { accept: acceptState, fail: failState, fn: fn }; return this; }; var fsm = new AcceptorStateMachine(); fsm.setupStates = function() { var hardErrorStates = ['success', 'error', 'complete']; var transition = function transition(err, done) { try { var self = this; var origError = self.response.error; self.emit(self._asm.currentState, function() { if (self.response.error && origError != self.response.error) { if (hardErrorStates.indexOf(this._asm.currentState) >= 0) { this._hardError = true; } } done(self.response.error); }); } catch (e) { this.response.error = e; if (hardErrorStates.indexOf(this._asm.currentState) >= 0) { this._hardError = true; } done(e); } }; this.addState('validate', 'build', 'error', transition); this.addState('restart', 'build', 'error', function(err, done) { err = this.response.error; if (!err) return done(); if (!err.retryable) return done(err); if (this.response.retryCount < this.service.config.maxRetries) { this.response.retryCount++; done(); } else { done(err); } }); this.addState('build', 'afterBuild', 'restart', transition); this.addState('afterBuild', 'sign', 'restart', transition); this.addState('sign', 'send', 'retry', transition); this.addState('retry', 'afterRetry', 'afterRetry', transition); this.addState('afterRetry', 'sign', 'error', transition); this.addState('send', 'validateResponse', 'retry', transition); this.addState('validateResponse', 'extractData', 'extractError', transition); this.addState('extractError', 'extractData', 'retry', transition); this.addState('extractData', 'success', 'retry', transition); this.addState('success', 'complete', 'complete', transition); this.addState('error', 'complete', 'complete', transition); this.addState('complete', null, 'uncaughtException', transition); this.addState('uncaughtException', function(err, done) { try { ALY.SequentialExecutor.prototype.unhandledErrorCallback.call(this, err); } catch (e) { if (this._hardError) throw err; } done(err); }); }; fsm.setupStates(); ALY.Request = inherit({ /** * Creates a request for an operation on a given service with * a set of input parameters. * * @param service [ALY.Service] the service to perform the operation on * @param operation [String] the operation to perform on the service * @param params [Object] parameters to send to the operation. * See the operation's documentation for the format of the * parameters. */ constructor: function Request(service, operation, params) { var endpoint = new ALY.Endpoint(service.config.endpoint); var region = service.config.region; this.service = service; this.operation = operation; this.params = params || {}; this.httpRequest = new ALY.HttpRequest(endpoint, region); this.startTime = ALY.util.date.getDate(); this.response = new ALY.Response(this); this.restartCount = 0; this._asm = new AcceptorStateMachine(fsm.states, 'validate'); ALY.SequentialExecutor.call(this); this.emit = this.emitEvent; }, /** * @!group Sending a Request */ /** * @overload send(callback = null) * Sends the request object. * * @callback callback function(err, data) * If a callback is supplied, it is called when a response is returned * from the service. * @param err [Error] the error object returned from the request. * Set to `null` if the request is successful. * @param data [Object] the de-serialized data returned from * the request. Set to `null` if a request error occurs. * @example Sending a request with a callback * request = s3.putObject({Bucket: 'bucket', Key: 'key'}); * request.send(function(err, data) { console.log(err, data); }); * @example Sending a request with no callback (using event handlers) * request = s3.putObject({Bucket: 'bucket', Key: 'key'}); * request.on('complete', function(response) { ... }); // register a callback * request.send(); */ send: function send(callback) { if (callback) { this.on('complete', function (resp) { try { callback.call(resp, resp.error, resp.data); } catch (e) { resp.request._hardError = true; throw e; } }); } this.runTo(); return this.response; }, build: function build(callback) { this._hardError = callback ? false : true; return this.runTo('send', callback); }, runTo: function runTo(state, done) { this._asm.runTo(state, done, this); return this; }, /** * Aborts a request, emitting the error and complete events. * * @!macro nobrowser * @example Aborting a request after sending * var params = { * Bucket: 'bucket', Key: 'key', * Body: new Buffer(1024 * 1024 * 5) // 5MB payload * }; * var request = s3.putObject(params); * request.send(function (err, data) { * if (err) console.log("Error:", err.code, err.message); * else console.log(data); * }); * * // abort request in 1 second * setTimeout(request.abort.bind(request), 1000); * * // prints "Error: RequestAbortedError Request aborted by user" * @return [ALY.Request] the same request object, for chaining. * @since v1.4.0 */ abort: function abort() { this.removeAllListeners('validateResponse'); this.removeAllListeners('extractError'); this.on('validateResponse', function addAbortedError(resp) { resp.error = ALY.util.error(new Error('Request aborted by user'), { code: 'RequestAbortedError', retryable: false }); }); if (this.httpRequest.stream) { // abort HTTP stream this.httpRequest.stream.abort(); this.httpRequest._abortCallback(); } return this; }, /** * Iterates over each page of results given a pageable request, calling * the provided callback with each page of data. After all pages have been * retrieved, the callback is called with `null` data. * * @note This operation can generate multiple requests to a service. * @example Iterating over multiple pages of objects in an S3 bucket * var pages = 1; * s3.listObjects().eachPage(function(err, data) { * if (err) return; * console.log("Page", pages++); * console.log(data); * }); * @callback callback function(err, data) * Called with each page of resulting data from the request. * * @param err [Error] an error object, if an error occurred. * @param data [Object] a single page of response data. If there is no * more data, this object will be `null`. * @return [Boolean] if the callback returns `false`, pagination will * stop. * * @api experimental * @see ALY.Request.eachItem * @see ALY.Response.nextPage * @since v1.4.0 */ eachPage: function eachPage(callback) { function wrappedCallback(response) { var result = callback.call(response, response.error, response.data); if (result === false) return; if (response.hasNextPage()) { response.nextPage().on('complete', wrappedCallback).send(); } else { callback.call(response, null, null); } } this.on('complete', wrappedCallback).send(); }, /** * Enumerates over individual items of a request, paging the responses if * necessary. * * @api experimental * @since v1.4.0 */ eachItem: function eachItem(callback) { function wrappedCallback(err, data) { if (err) return callback(err, null); if (data === null) return callback(null, null); var config = this.request.service.paginationConfig(this.request.operation); var resultKey = config.resultKey; if (Array.isArray(resultKey)) resultKey = resultKey[0]; var results = ALY.util.jamespath.query(resultKey, data); ALY.util.arrayEach(results, function(result) { ALY.util.arrayEach(result, function(item) { callback(null, item); }); }); } this.eachPage(wrappedCallback); }, /** * @return [Boolean] whether the operation can return multiple pages of * response data. * @api experimental * @see ALY.Response.eachPage * @since v1.4.0 */ isPageable: function isPageable() { return this.service.paginationConfig(this.operation) ? true : false; }, /** * Converts the request object into a readable stream that * can be read from or piped into a writable stream. * * @note The data read from a readable stream contains only * the raw HTTP body contents. * @example Manually reading from a stream * request.createReadStream().on('data', function(data) { * console.log("Got data:", data.toString()); * }); * @example Piping a request body into a file * var out = fs.createWriteStream('/path/to/outfile.jpg'); * s3.service.getObject(params).createReadStream().pipe(out); * @return [Stream] the readable stream object that can be piped * or read from (by registering 'data' event listeners). */ createReadStream: function createReadStream() { var streams = require('stream'); var req = this; var stream = null; var legacyStreams = false; if (ALY.HttpClient.streamsApiVersion === 2) { stream = new streams.Readable(); stream._read = function() { stream.push(''); }; } else { stream = new streams.Stream(); stream.readable = true; } stream.sent = false; stream.on('newListener', function(event) { if (!stream.sent && (event === 'data' || event === 'readable')) { if (event === 'data') legacyStreams = true; stream.sent = true; process.nextTick(function() { req.send(function() { }); }); } }); this.on('httpHeaders', function streamHeaders(statusCode, headers, resp) { if (statusCode < 300) { this.httpRequest._streaming = true; req.removeListener('httpData', ALY.EventListeners.Core.HTTP_DATA); req.removeListener('httpError', ALY.EventListeners.Core.HTTP_ERROR); req.on('httpError', function streamHttpError(error, resp) { resp.error = error; resp.error.retryable = false; }); var httpStream = resp.httpResponse.stream; stream.response = resp; stream._read = function() { var data; /*jshint boss:true*/ while (data = httpStream.read()) { stream.push(data); } stream.push(''); }; var events = ['end', 'error', (legacyStreams ? 'data' : 'readable')]; ALY.util.arrayEach(events, function(event) { httpStream.on(event, function(arg) { stream.emit(event, arg); }); }); } }); this.on('error', function(err) { stream.emit('error', err); }); return stream; }, /** * @param [Array,Response] args This should be the response object, * or an array of args to send to the event. * @api private */ emitEvent: function emit(eventName, args, done) { if (typeof args === 'function') { done = args; args = null; } if (!done) done = this.unhandledErrorCallback; if (!args) args = this.eventParameters(eventName, this.response); var origEmit = ALY.SequentialExecutor.prototype.emit; origEmit.call(this, eventName, args, function (err) { if (err) this.response.error = err; done.call(this, err); }); }, /** * @api private */ eventParameters: function eventParameters(eventName) { switch (eventName) { case 'validate': case 'sign': case 'build': case 'afterBuild': return [this]; case 'error': return [this.response.error, this.response]; default: return [this.response]; } } }); ALY.util.mixin(ALY.Request, ALY.SequentialExecutor); ALY.Response = inherit({ /** * @api private */ constructor: function Response(request) { this.request = request; this.data = null; this.error = null; this.retryCount = 0; this.redirectCount = 0; this.httpResponse = new ALY.HttpResponse(); }, nextPage: function nextPage(callback) { var config; var service = this.request.service; var operation = this.request.operation; try { config = service.paginationConfig(operation, true); } catch (e) { this.error = e; } if (!this.hasNextPage()) { if (callback) callback(this.error, null); else if (this.error) throw this.error; return null; } var params = ALY.util.copy(this.request.params); if (!this.nextPageTokens) { return callback ? callback(null, null) : null; } else { var inputTokens = config.inputToken; if (typeof inputTokens === 'string') inputTokens = [inputTokens]; for (var i = 0; i < inputTokens.length; i++) { params[inputTokens[i]] = this.nextPageTokens[i]; } return service.makeRequest(this.request.operation, params, callback); } }, /** * @return [Boolean] whether more pages of data can be returned by further * requests * @api experimental * @since v1.4.0 */ hasNextPage: function hasNextPage() { this.cacheNextPageTokens(); if (this.nextPageTokens) return true; if (this.nextPageTokens === undefined) return undefined; else return false; }, /** * @api private */ cacheNextPageTokens: function cacheNextPageTokens() { if (this.hasOwnProperty('nextPageTokens')) return this.nextPageTokens; this.nextPageTokens = undefined; var config = this.request.service.paginationConfig(this.request.operation); if (!config) return this.nextPageTokens; this.nextPageTokens = null; if (config.moreResults) { if (!ALY.util.jamespath.find(config.moreResults, this.data)) { return this.nextPageTokens; } } var exprs = config.outputToken; if (typeof exprs === 'string') exprs = [exprs]; ALY.util.arrayEach.call(this, exprs, function (expr) { var output = ALY.util.jamespath.find(expr, this.data); if (output) { this.nextPageTokens = this.nextPageTokens || []; this.nextPageTokens.push(output); } }); return this.nextPageTokens; } });