Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
BrandonJS.xhr - Provides a helper which mimicks the WinJS.xhr interface but uses WinRT's new HttpClient under the hood if available.
// This file is part of the Brandon Library for JavaScript (BrandonJS)
// Original author: Brandon Paddock, www.brandonlive.com
// Published under Creative Commons Attribution 4.0 International Public License
// http://creativecommons.org/licenses/by/4.0/legalcode
// BrandonJS.xhr
// Provides a helper which mimicks WinJS.xhr but using WinRT's new HttpClient if available.
// If WinRT is not available, it falls back to WinJS.xhr.
// This mimicks enough for common usage scenarios, but you may need to extend it for some cases.
var hasWinRT = false;
(function (global) {
hasWinRT = !!global.Windows;
})(this);
(function () {
"use strict";
// NOTE! By default, HttpClient doesn't send a UA string.
// By default we'll use what XHR would have used.
// You can change this here or per-request/client via options.userAgent.
var defaultUserAgent = navigator.userAgent;
// If we don't have WinRT, bail out and wire up to WinJS.xhr
if (!hasWinRT) {
WinJS.Namespace.define("BrandonJS", {
xhr: WinJS.xhr,
});
return;
}
var Scheduler = WinJS.Utilities.Scheduler;
var winhttp = Windows.Web.Http;
var utf8 = Windows.Storage.Streams.UnicodeEncoding.utf8;
function schedule(f, arg, priority) {
Scheduler.schedule(function () {
f(arg);
}, priority, null, "BrandonJS.xhr");
}
// A mock XHR object we'll return in place of the real one you'd get from WinJS.xhr
var FakeXMLHttpRequest = WinJS.Class.define(function () {
this.readyState = 0;
this.response = null;
this.responseBody = null;
this.responseText = null;
this.responseType = null;
this.responseXML = null;
this.status = 0;
this.statusText = 0;
this.timeout = 0;
this.withCredentials = false;
}, {
abort: function () {
// Not implemented
debugger;
},
getAllResponseHeaders: function () {
// Not implemented
debugger;
},
getReponseHeader: function (name) {
var value = null;
if (this._httpResponse) {
value = this._httpResponse.headers.lookup(name);
}
return value;
},
open: function () {
// Not implemented
debugger;
},
overrideMimeType: function () {
// Not implemented
debugger;
},
send: function () {
// Not implemented
debugger;
},
setRequestHeader: function () {
// Not implemented
debugger;
},
_setReadyState: function (state) {
if (state != this.readyState) {
this.readyState = state;
this.dispatchEvent("readystatechanged");
}
},
});
WinJS.Class.mix(FakeXMLHttpRequest, WinJS.Utilities.eventMixin);
WinJS.Class.mix(FakeXMLHttpRequest, WinJS.Utilities.createEventProperties("readystatechanged"));
var namedClients = {};
var xhr = function (options) {
/// <signature helpKeyword="BrandonJS.xhr">
/// <summary locid="BrandonJS.xhr">
/// Wraps calls to HttpClient in a promise mimicking XmlHttpRequest.
/// </summary>
/// <param name="options" type="Object">
/// The options that are applied to the XMLHttpRequest object. They are: type,
/// url, user, password, headers, responseType, data, and customRequestInitializer.
/// </param>
/// <returns type="WinJS.Promise">
/// A promise that returns the XMLHttpRequest object when it completes.
/// </returns>
/// </signature>
if (options.responseType) { debugger; } // Not implemented
var sendPromise;
return new WinJS.Promise(
function (reportComplete, reportError, reportProgress) {
/// <returns value="reportComplete(new FakeXMLHttpRequest())">
var priority = options.priority || Scheduler.currentPriority;
var fakeRequest = new FakeXMLHttpRequest();
var httpClient;
// The sessionName option tells us to cache and reuse the HttpClient, and optimizaton for related requests.
// See HttpClient documentation for details about client reuse.
if (options.sessionName) {
httpClient = namedClients[options.sessionName];
}
if (!httpClient) {
// We must create HttpBaseProtocolFilter so we can disable UI.
// This is important because HttpClient gives weird error behaviors when used from a WebWorker if you don't do this.
// That is, it mangles error messages (i.e. returns an HWND error instead of 401) even in cases where it would never show UI.
var filter = new winhttp.Filters.HttpBaseProtocolFilter();
filter.allowUI = false;
httpClient = new winhttp.HttpClient(filter);
if (options.sessionName) {
namedClients[options.sessionName] = httpClient;
}
var headers = httpClient.defaultRequestHeaders;
headers.accept.parseAdd("*/*");
headers.acceptEncoding.parseAdd("gzip, deflate");
if (options.userAgent || defaultUserAgent) {
// Note: If you don't specify this, no UA will be sent!
headers.userAgent.parseAdd(options.userAgent || defaultUserAgent);
}
}
var uri = new Windows.Foundation.Uri(options.url);
var method = winhttp.HttpMethod[options.type ? options.type.toLowerCase() : "get"];
var request = new winhttp.HttpRequestMessage(method, uri);
var contentType;
Object.keys(options.headers || {}).forEach(function (k) {
request.headers.tryAppendWithoutValidation(k, options.headers[k]);
if (k.toLowerCase() == "content-type") {
contentType = options.headers[k];
}
});
if (options.data) {
if (typeof options.data == "string") {
request.content = new winhttp.HttpStringContent(options.data, utf8, contentType);
}
else {
// Unfortunately I didn't see a way to support JS multipart objects here.
// So callers must update to HttpMultipartFormDataContent.
request.content = options.data;
}
}
else if (options.multipartData) {
request.content = options.multipartData;
}
if (options.customRequestInitializer || options.user || options.password) {
// Not supported! (at least not yet - feel free to add!)
debugger;
//options.customRequestInitializer(req);
}
sendPromise = httpClient.sendRequestAsync(request, winhttp.HttpCompletionOption.responseHeadersRead);
fakeRequest._setReadyState(1);
sendPromise.then(function (response) {
fakeRequest._setReadyState(2);
fakeRequest._httpResponse = response;
fakeRequest.status = response.statusCode;
fakeRequest.statusText = response.reasonPhrase;
schedule(reportProgress, fakeRequest, priority);
response.content.readAsStringAsync().then(function (text) {
fakeRequest._setReadyState(4);
fakeRequest.status = response.statusCode;
fakeRequest.statusText = response.reasonPhrase;
fakeRequest.responseText = text;
if (response.isSuccessStatusCode) {
schedule(reportComplete, fakeRequest, priority);
}
else {
schedule(reportError, fakeRequest, priority);
}
}, function (error) {
schedule(reportError, fakeRequest, priority);
});
}, function (error) {
if (error.message) {
if (error.number == -2147023728) {
debugger; // Should be fixed now.
// Really annoying but HttpClient gives these for 401s on background threads.
fakeRequest.status = 401;
fakeRequest.statusText = "Unable to authorize your account. Ensure your PC's clock is set correctly.";
}
if (!fakeRequest.statusText) {
// For some reason the actual error messages all have this string appended to the front, so lop it off.
// TODO: Handle this for other languages...
fakeRequest.statusText = error.message.replace("The text associated with this error code could not be found.\r\n\r\n", "");
}
else {
fakeRequest.message = error.message;
}
}
schedule(reportError, fakeRequest, priority);
}, function (progress) {
if ((fakeRequest.readyState == 2) && (progress.stage > 80)) {
fakeRequest._setReadyState(3);
}
fakeRequest.httpProgressStage = progress.stage; // Not an XHR thing, but may be useful.
schedule(reportProgress, fakeRequest, priority);
});
},
function () {
sendPromise.cancel();
}
);
};
WinJS.Namespace.define("BrandonJS", {
xhr: xhr,
});
})();

pke commented Mar 29, 2014

Why are you schedule the call of the promise handlers instead of directly calling them?

Owner

BrandonLive commented Oct 21, 2015

@pke Just saw your comment. Two reasons:

  1. This is what WinJS.xhr does :-)
  2. This allows the callbacks to be fired with the desired priority by the WinJS task scheduler.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment