async-coord
Version:
Asynchronous coordination primitives for JavaScript and TypeScript
826 lines (824 loc) • 31.1 kB
JavaScript
/*! *****************************************************************************
Copyright (C) Ron A. Buckton. 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.
***************************************************************************** */
var promise = require('./promise');
var cancellation = require('./cancellation');
var Promise = promise.Promise;
var CancellationTokenSource = cancellation.CancellationTokenSource;
var hasMsNonUserCodeExceptions = typeof Debug !== "undefined" &&
typeof Debug.setNonUserCodeExceptions === "boolean";
/**
* A Uri
*/
var Uri = (function () {
function Uri() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i - 0] = arguments[_i];
}
/**
* The protocol for the Uri (e.g. 'http:')
* @type {String}
*/
this.protocol = "";
/**
* The hostname for the Uri
* @type {String}
*/
this.hostname = "";
/**
* The port number for the Uri
* @type {Number}
*/
this.port = null;
/**
* The path name for the Uri
* @type {String}
*/
this.pathname = "";
/**
* The search portion of the path, also known as the querystring
* @type {String}
*/
this.search = "";
/**
* The fragment portion of the path
* @type {String}
*/
this.hash = "";
/**
* A value indicating whether the Url is an absolute url
* @type {Boolean}
*/
this.absolute = false;
if (args.length === 0)
throw new Error("Argument missing");
if (args.length === 1) {
var m = UriParser.exec(args[0]);
if (!m)
throw new URIError();
for (var name in UriParts) {
var index = UriParts[name];
var part = m[index];
if (part) {
if (index < 5)
part = part.toLowerCase();
else if (index === 5)
part = parseInt(part);
}
else {
if (index === 5)
part = m[1] ? UriPorts[this.protocol] : null;
}
this[name] = part;
}
this.absolute = !!m[1];
}
else {
var baseUri = args[0] instanceof Uri ? args[0] : Uri.parse(args[0]);
var uri = args[0] instanceof Uri ? args[1] : Uri.parse(args[1]);
this.hash = uri.hash;
if (uri.protocol) {
this.protocol = uri.protocol;
this.hostname = uri.hostname;
this.port = uri.port;
this.pathname = uri.pathname;
this.search = uri.search;
this.hash = uri.hash;
this.absolute = uri.absolute;
}
else {
this.protocol = baseUri.protocol;
if (uri.hostname) {
this.hostname = uri.hostname;
this.port = uri.port;
this.pathname = uri.pathname;
this.search = uri.search;
this.hash = uri.hash;
this.absolute = uri.absolute;
}
else {
this.hostname = baseUri.hostname;
this.port = baseUri.port;
if (uri.pathname) {
if (uri.pathname.charAt(0) === '/') {
this.pathname = uri.pathname;
}
else {
if ((baseUri.absolute && !baseUri.pathname) || baseUri.pathname === "/") {
this.pathname = '/' + uri.pathname;
}
else if (baseUri.pathname) {
var parts = baseUri.pathname.split('/');
parts[parts.length - 1] = uri.pathname;
this.pathname = parts.join('/');
}
}
}
else {
this.pathname = baseUri.pathname;
if (uri.search) {
this.search = uri.search;
}
else {
this.search = baseUri.search;
}
}
}
}
}
Object.freeze(this);
}
Object.defineProperty(Uri.prototype, "origin", {
/**
* Gets the origin of the Uri
*/
get: function () {
return this.toString("origin");
},
enumerable: true,
configurable: true
});
Object.defineProperty(Uri.prototype, "host", {
/**
* Gets the host for the uri, including the hostname and port
*/
get: function () {
return this.toString("host");
},
enumerable: true,
configurable: true
});
Object.defineProperty(Uri.prototype, "scheme", {
/**
* Gets the scheme for the uri (e.g. 'http://'')
*/
get: function () {
return this.toString("scheme");
},
enumerable: true,
configurable: true
});
/**
* Tests whether the provided uri has the same origin as this uri
* @param uri The uri to compare against
* @returns True if the uri's have the same origin; otherwise, false
*/
Uri.prototype.isSameOrigin = function (uri) {
var other;
if (typeof uri === "string") {
other = Uri.parse(uri);
}
else if (uri instanceof Uri) {
other = uri;
}
else {
throw new TypeError("Argument not optional.");
}
if (this.absolute) {
return this.origin === other.origin;
}
return !other.absolute;
};
/**
* Gets the string representation of the Uri
* @param format {String} A format specifier.
* @returns {String} The string content of the Uri
*/
Uri.prototype.toString = function (format) {
switch (format) {
case "origin":
if (this.protocol && this.hostname) {
return String(this.protocol) + "//" + this.toString("host");
}
return "";
case "authority":
case "host":
if (this.hostname) {
if (this.port !== UriPorts[this.protocol]) {
return String(this.hostname) + ":" + this.toString("port");
}
return String(this.hostname);
}
return "";
case "path+search":
return String(this.pathname) + String(this.search);
case "scheme": return this.toString("protocol") + "//";
case "protocol": return String(this.protocol || "");
case "hostname": return String(this.hostname || "");
case "port":
if (this.port) {
return String(this.port);
}
if (this.protocol && UriPorts[this.protocol]) {
return String(UriPorts[this.protocol]);
}
return "";
case "file":
if (this.pathname) {
var i = this.pathname.lastIndexOf("/") + 1;
if (i > 0) {
return this.pathname.substr(i);
}
}
return "";
case "dir":
if (this.pathname) {
var i = this.pathname.lastIndexOf("/") + 1;
if (i > 0) {
return this.pathname.substr(0, i);
}
}
return "";
case "ext":
if (this.pathname) {
var i = this.pathname.lastIndexOf("/") + 1;
i = this.pathname.lastIndexOf(".", i);
if (i > 0) {
return this.pathname.substr(i);
}
}
return "";
case "file-ext":
if (this.pathname) {
var i = this.pathname.lastIndexOf("/") + 1;
if (i) {
var j = this.pathname.lastIndexOf(".", i);
if (j > 0) {
return this.pathname.substring(i, j);
}
return this.pathname.substr(i);
}
}
return "";
case "fragment":
case "hash":
var hash = String(this.hash || "");
if (hash.length > 0 && hash.charAt(0) != "#") {
return "#" + hash;
}
return hash;
case "path":
case "pathname":
return String(this.pathname || "");
case "search":
case "query":
var search = String(this.search || "");
if (search.length > 0 && search.charAt(0) != "?") {
return "?" + search;
}
return search;
default:
return this.toString("origin") + this.toString("pathname") + this.toString("search") + this.toString("hash");
}
};
/**
* Parses the provided uri string
* @param uri {String} The uri string to parse
* @returns {Uri} The parsed uri
*/
Uri.parse = function (uri) {
return new Uri(uri);
};
/**
* Combines two uris
* @param baseUri The base uri
* @param uri The relative uri
* @returns The combined uri
*/
Uri.combine = function (baseUri, uri) {
return new Uri(baseUri, uri);
};
return Uri;
})();
exports.Uri = Uri;
var QueryString;
(function (QueryString) {
var hasOwn = Object.prototype.hasOwnProperty;
var QueryStringParser = /(?:\?|&|^)([^=&]*)(?:=([^&]*))?/g;
function stringify(obj) {
if (!obj) {
return "";
}
var qs = [];
Object.getOwnPropertyNames(obj).forEach(function (name) {
var value = obj[name];
switch (typeof value) {
case "string":
case "number":
case "boolean": {
qs.push(encodeURIComponent(name) + "=" + encodeURIComponent(String(value)));
return;
}
default: {
if (Array.isArray(value)) {
var ar = value;
for (var i = 0, n = ar.length; i < n; i++) {
switch (typeof ar[i]) {
case "string":
case "number":
case "boolean":
qs.push(encodeURIComponent(name) + "=" + encodeURIComponent(String(value)));
break;
default:
qs.push(encodeURIComponent(name) + "=");
break;
}
}
}
else {
qs.push(encodeURIComponent(name) + "=");
}
}
}
});
if (qs.length) {
return "?" + qs.join("&");
}
return "";
}
QueryString.stringify = stringify;
function parse(text) {
var obj = {};
var part;
while (part = QueryStringParser.exec(text)) {
var key = decodeURIComponent(part[1]);
if (key.length && key !== "__proto__") {
var value = decodeURIComponent(part[2]);
if (hasOwn.call(obj, key)) {
var previous = obj[key];
if (Array.isArray(previous)) {
var ar = previous;
ar.push(value);
}
else {
obj[key] = [previous, value];
}
}
else {
obj[key] = value;
}
}
}
return obj;
}
QueryString.parse = parse;
})(QueryString = exports.QueryString || (exports.QueryString = {}));
/**
* An HTTP request for an HttpClient
*/
var HttpRequest = (function () {
/**
* Creates an HTTP request for an HttpClient
* @param method The HTTP method for the request
* @param url The url for the request
*/
function HttpRequest(method, url) {
if (method === void 0) { method = "GET"; }
this._headers = Object.create(null);
this.method = method;
if (typeof url === "string") {
this.url = Uri.parse(url);
}
else if (url instanceof Uri) {
this.url = url;
}
}
/**
* Sets the named request header
* @param key {String} The header name
* @param value {String} The header value
*/
HttpRequest.prototype.setRequestHeader = function (key, value) {
if (key !== "__proto__") {
this._headers[key] = value;
}
};
return HttpRequest;
})();
exports.HttpRequest = HttpRequest;
/**
* A response from an HttpClient
*/
var HttpResponse = (function () {
/**
* A response from an HttpClient
*/
function HttpResponse(request, xhr) {
this._request = request;
this._xhr = xhr;
}
Object.defineProperty(HttpResponse.prototype, "request", {
/**
* Gets the request for this response
*/
get: function () {
return this._request;
},
enumerable: true,
configurable: true
});
Object.defineProperty(HttpResponse.prototype, "status", {
/**
* Gets the status code of the response
*/
get: function () {
return this._xhr.status;
},
enumerable: true,
configurable: true
});
Object.defineProperty(HttpResponse.prototype, "statusText", {
/**
* Gets the status text of the response
*/
get: function () {
return this._xhr.statusText;
},
enumerable: true,
configurable: true
});
Object.defineProperty(HttpResponse.prototype, "responseText", {
/**
* Gets the response text of the response
*/
get: function () {
return this._xhr.responseText;
},
enumerable: true,
configurable: true
});
/**
* Gets all of the response heades in a single string
* @returns {String} A string containing all of the response headers
*/
HttpResponse.prototype.getAllResponseHeaders = function () {
return this._xhr.getAllResponseHeaders();
};
/**
* Gets the value for the named response header
* @param header {String} The name of the header
* @returns {String} The value for the named header
*/
HttpResponse.prototype.getResponseHeader = function (header) {
return this._xhr.getResponseHeader(header);
};
return HttpResponse;
})();
exports.HttpResponse = HttpResponse;
/**
* A client for HTTP requests
*/
var HttpClient = (function () {
/**
* Creates a client for HTTP requests
* @param baseUrl The base url for the client
*/
function HttpClient(baseUrl) {
this._headers = Object.create(null);
this._cts = new CancellationTokenSource();
this._closed = false;
if (baseUrl) {
if (typeof baseUrl === "string") {
this.baseUrl = Uri.parse(baseUrl);
}
else if (baseUrl instanceof Uri) {
this.baseUrl = baseUrl;
}
}
}
/**
* Closes the client and cancels all pending requests
*/
HttpClient.prototype.close = function () {
if (this._closed)
throw new Error("Object doesn't support this action");
this._closed = true;
this._cts.cancel();
this._cts.close();
};
/**
* Sets a value for a default request header
* @param key The request header key
* @param value The request header value
*/
HttpClient.prototype.setRequestHeader = function (key, value) {
if (this._closed)
throw new Error("Object doesn't support this action");
if (key !== "__proto__") {
this._headers[key] = value;
}
};
/**
* Gets the response text from the requested url
* @param url The url for the request
* @returns A future result for the string
*/
HttpClient.prototype.getStringAsync = function (url) {
return this.getAsync(url).then(function (r) { return r.responseText; });
};
/**
* Gets the response from issuing an HTTP GET to the requested url
* @param url The url for the request
* @param token A token that can be used to cancel the request
* @returns A future result for the response
*/
HttpClient.prototype.getAsync = function (url, token) {
return this.sendAsync(new HttpRequest("GET", url), token);
};
/**
* Gets the response from issuing an HTTP POST to the requested url
* @param url The url for the request
* @param body The body of the request
* @param token A token that can be used to cancel the request
* @returns A future result for the response
*/
HttpClient.prototype.postAsync = function (url, body, token) {
var request = new HttpRequest("POST", url);
request.body = body;
return this.sendAsync(request, token);
};
/**
* Gets the response from issuing an HTTP POST of a JSON serialized value to the requested url
* @param url The url for the request
* @param value The value to serialize
* @param jsonReplacer An array or callback used to replace values during serialization
* @param token A token that can be used to cancel the request
* @returns A future result for the response
*/
HttpClient.prototype.postJsonAsync = function (url, value, jsonReplacer, token) {
var request = new HttpRequest("POST", url);
request.body = JSON.stringify(value, jsonReplacer);
request.setRequestHeader("Content-Type", "application/json");
return this.sendAsync(request, token);
};
/**
* Gets the response from issuing an HTTP PUT to the requested url
* @param url The url for the request
* @param body The body of the request
* @param token A token that can be used to cancel the request
* @returns A future result for the response
*/
HttpClient.prototype.putAsync = function (url, body, token) {
var request = new HttpRequest("PUT", url);
request.body = body;
return this.sendAsync(request, token);
};
/**
* Gets the response from issuing an HTTP PUT of a JSON serialized value to the requested url
* @param url The url for the request
* @param value The value to serialize
* @param jsonReplacer An array or callback used to replace values during serialization
* @param token A token that can be used to cancel the request
* @returns A future result for the response
*/
HttpClient.prototype.putJsonAsync = function (url, value, jsonReplacer, token) {
var request = new HttpRequest("PUT", url);
request.body = JSON.stringify(value, jsonReplacer);
request.setRequestHeader("Content-Type", "application/json");
return this.sendAsync(request, token);
};
/**
* Gets the response from issuing an HTTP DELETE to the requested url
* @param url The url for the request
* @param token A token that can be used to cancel the request
* @returns A future result for the response
*/
HttpClient.prototype.deleteAsync = function (url, token) {
return this.sendAsync(new HttpRequest("DELETE", url), token);
};
/**
* Sends the provided request and returns the response
* @param request {HttpRequest} An HTTP request to send
* @param token {futures.CancellationToken} A token that can be used to cancel the request
* @returns {futures.Promise<HttpResponse>} A future result for the response
*/
HttpClient.prototype.sendAsync = function (request, token) {
var _this = this;
if (this._closed)
throw new Error("Object doesn't support this action");
return new Promise(function (resolve, reject) {
// create a linked token
var cts = new CancellationTokenSource([_this._cts.token, token]);
// throw if we're already canceled, the promise will be rejected
cts.token.throwIfCanceled();
// normalize the uri
var url = null;
if (!request.url) {
url = _this.baseUrl;
}
else if (!request.url.absolute) {
if (!_this.baseUrl)
throw new Error("Invalid argument: request");
url = new Uri(_this.baseUrl, request.url);
}
if (url) {
request.url = url;
}
var xhr = new XMLHttpRequest();
var response = new HttpResponse(request, xhr);
var requestHeaders = request._headers;
var clientHeaders = _this._headers;
// create the onload callback
var onload = function (ev) {
if (hasMsNonUserCodeExceptions)
Debug.setNonUserCodeExceptions = true;
cleanup();
// catch a cancellation and reject the promise
try {
cts.token.throwIfCanceled();
}
catch (e) {
reject(e);
return;
}
if (xhr.status >= 200 && xhr.status < 300) {
resolve(response);
}
else {
var error = createHttpError(_this, response);
reject(error);
}
};
// create the onerror callback
var onerror = function (ev) {
if (hasMsNonUserCodeExceptions)
Debug.setNonUserCodeExceptions = true;
cleanup();
// catch a cancellation and reject the promise
try {
cts.token.throwIfCanceled();
}
catch (e) {
reject(e);
return;
}
var error = createHttpError(_this, response);
reject(error);
};
// register a cleanup phase
var registration = cts.token.register(function () {
if (hasMsNonUserCodeExceptions)
Debug.setNonUserCodeExceptions = true;
cleanup();
// abort the xhr
xhr.abort();
// catch a cancellation and reject the promise
try {
cts.token.throwIfCanceled();
}
catch (e) {
reject(e);
}
});
var cleanup = function () {
xhr.removeEventListener("load", onload, false);
xhr.removeEventListener("error", onerror, false);
registration.unregister();
registration = undefined;
};
// add the headers from the client
Object.getOwnPropertyNames(clientHeaders).forEach(function (key) {
xhr.setRequestHeader(key, clientHeaders[key]);
});
// add the headers from the request
Object.getOwnPropertyNames(requestHeaders).forEach(function (key) {
xhr.setRequestHeader(key, requestHeaders[key]);
});
// wire up the events
xhr.addEventListener("load", onload, false);
xhr.addEventListener("error", onerror, false);
// enable credentials if requested
if (_this.withCredentials) {
xhr.withCredentials = true;
}
// attach a timeout
if (_this.timeout > 0) {
setTimeout(function () { return cts.cancel(new Error("Operation timed out.")); }, _this.timeout);
xhr.timeout = _this.timeout;
}
// send the request
xhr.open(request.method, request.url.toString(), true, _this.username, _this.password);
xhr.send(request.body);
});
};
HttpClient.prototype.getJsonpAsync = function (url, callbackArg, noCache, token) {
var _this = this;
if (callbackArg === void 0) { callbackArg = "callback"; }
if (noCache === void 0) { noCache = false; }
if (this._closed)
throw new Error("Object doesn't support this action");
if (typeof document === "undefined")
throw new Error("JSON-P is not supported in this host.");
return new Promise(function (resolve, reject) {
// create a linked token
var cts = new CancellationTokenSource([_this._cts.token, token]);
// throw if we're already canceled, the promise will be rejected
cts.token.throwIfCanceled();
// normalize the uri
var requestUrl = null;
if (!url) {
requestUrl = _this.baseUrl;
}
else {
if (typeof url === "string") {
requestUrl = new Uri(url);
}
else if (url instanceof Uri) {
requestUrl = url;
}
if (!requestUrl.absolute) {
if (!_this.baseUrl)
throw new Error("Invalid argument: url");
requestUrl = new Uri(_this.baseUrl, requestUrl);
}
}
var index = jsonpRequestIndex++;
var name = "__Promise__jsonp__" + index;
var query = QueryString.parse(requestUrl.search);
query[callbackArg] = name;
if (noCache) {
query["_t"] = Date.now();
}
requestUrl.search = QueryString.stringify(query);
var pending = true;
var head = document.getElementsByTagName("head")[0];
var script = document.createElement("script");
script.type = "text/javascript";
script.async = true;
script.src = requestUrl.toString();
// checks whether the request has been canceled
var checkCanceled = function () {
if (hasMsNonUserCodeExceptions)
Debug.setNonUserCodeExceptions = true;
try {
cts.token.throwIfCanceled();
}
catch (e) {
reject(e);
return true;
}
return false;
};
// waits for the result
var onload = function (result) {
ignore();
registration.unregister();
registration = undefined;
if (!checkCanceled()) {
resolve(result);
}
};
// ignores further calls to fulfill the result
var ignore = function () {
pending = false;
delete window[name];
disconnect();
};
// disconnects the script node
var disconnect = function () {
if (script.parentNode) {
head.removeChild(script);
}
};
// register a cleanup phase
var registration = cts.token.register(function () {
if (pending) {
window[name] = ignore;
}
disconnect();
checkCanceled();
});
// set a timeout before we no longer care about the result.
if (_this.timeout) {
setTimeout(function () { return cts.cancel(new Error("Operation timed out.")); }, _this.timeout);
}
window[name] = onload;
head.appendChild(script);
});
};
return HttpClient;
})();
exports.HttpClient = HttpClient;
function createHttpError(httpClient, response, message) {
if (message === void 0) { message = "An error occurred while processing your request"; }
var error = new Error(message);
error.name = "HttpError";
error.httpClient = httpClient;
error.response = response;
error.message = message;
return error;
}
var UriParser = /^((?:(https?:)\/\/)(?:[^:@]*(?:\:[^@]*)?@)?(([a-z\d-\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF\.]+)(?:\:(\d+))?)?)?(?![a-z\d-]+\:)((?:^|\/)[^\?\#]*)?(\?[^#]*)?(#.*)?$/i;
var UriParts = { "protocol": 2, "hostname": 4, "port": 5, "pathname": 6, "search": 7, "hash": 8 };
var UriPorts = { "http:": 80, "https:": 443 };
var jsonpRequestIndex = 0;
//# sourceMappingURL=file:///C|/dev/asyncjs/httpclient.js.map