aliyun-sdk
Version:
Aliyun SDK for JavaScript
500 lines (444 loc) • 15.9 kB
JavaScript
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;
}
});