Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use ReadableByteStream/fetch for supporting browsers (Chrome). Close #7. #9

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
[cover]: https://img.shields.io/coveralls/unshiftio/requests/master.svg?style=flat-square
[irc]: https://img.shields.io/badge/IRC-irc.freenode.net%23unshift-00a8ff.svg?style=flat-square

Requests is a small library that implements fully and true streaming XHR for
browsers that support these methods. It uses a variety of proprietary
Requests is a small library that fully implements true streaming XHR for
Node.js and browsers that support these methods. It uses a variety of proprietary
`responseType` properties to force a streaming connection, even for binary data.
For browsers that don't support this we will simply fallback to a regular but
**async** XHR 1/2 request or ActiveXObject in even older deprecated browsers.

- Chrome: `ReadableByteStream`
- Internet Explorer >= 10: `ms-stream`
- FireFox >= 9: `moz-chunked`
- FireFox < 20: `multipart`
Expand Down
151 changes: 122 additions & 29 deletions browser.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
'use strict';

var Requested = require('./requested')
var FetchWrapper = require('./fetch-wrapper.js')
, Requested = require('./requested')
, listeners = require('loads')
, send = require('xhr-send')
, sameOrigin = require('same-origin')
, hang = require('hang')
, AXO = require('axo');

/**
* Root reference for iframes.
* Taken from
* https://github.com/visionmedia/superagent/blob/83892f35fe15676a4567a0eb51eecd096939ad36/lib/client.js#L1
*/
var root;
if (typeof window !== 'undefined') { // Browser window
root = window;
} else if (typeof self !== 'undefined') { // Web Worker
root = self;
} else { // Other environments
console.warn('Using browser-only version of requests in non-browser environment');
root = this;
}

/**
* RequestS(tream).
*
Expand All @@ -19,7 +36,7 @@ var Requested = require('./requested')
*
* @constructor
* @param {String} url The URL we want to request.
* @param {Object} options Various of request options.
* @param {Object} options Various request options.
* @api public
*/
var Requests = module.exports = Requested.extend({
Expand Down Expand Up @@ -64,15 +81,17 @@ var Requests = module.exports = Requested.extend({
/**
* Initialize and start requesting the supplied resource.
*
* @param {Object} options Passed in defaults.
* @param {String} url The URL we want to request.
* @param {Object} options Various request options.
* @api private
*/
open: function open() {
open: function open(url, options) {
var what
, slice = true
, requests = this
, socket = requests.socket;

var slice = (requests.hasOwnProperty('slice')) ? requests.slice : true;

requests.on('stream', function stream(data) {
if (!slice) {
return requests.emit('data', data);
Expand All @@ -94,15 +113,46 @@ var Requests = module.exports = Requested.extend({
});

if (this.timeout) {
// NOTE the "+" before this.timeout just ensures
// socket.timeout is a number.
socket.timeout = +this.timeout;
}

if ('cors' === this.mode.toLowerCase() && 'withCredentials' in socket) {
socket.withCredentials = true;
// Polyfilling XMLHttpRequest to accept fetch options
// see https://fetch.spec.whatwg.org/#cors-protocol-and-credentials and
// https://fetch.spec.whatwg.org/#concept-request-credentials-mode
//
// ...credentials mode, which is "omit", "same-origin", or "include".
// Unless stated otherwise, it is "omit".
//
// When request's mode is "navigate", its credentials mode is assumed to
// be "include" and fetch does not currently account for other values.
// If HTML changes here, this standard will need corresponding changes.
if ('withCredentials' in socket) {
var credentials = this.credentials;
if (credentials) {
credentials = credentials.toLowerCase();
} else if (this.mode) {
var mode = this.mode.toLowerCase();
if (mode === 'navigate') {
credentials = 'include';
}
}

if (credentials) {
if (credentials === 'include') {
socket.withCredentials = true;
} else {
var origin = root.location.origin || (root.location.protocol + '//' + root.location.host);
if (credentials === 'same-origin' && sameOrigin(origin, url)) {
socket.withCredentials = true;
}
}
}
}

//
// ActiveXObject will throw an `Type Mismatch` exception when setting the to
// ActiveXObject will throw a `Type Mismatch` exception when setting the to
// an null-value and to be consistent with all XHR implementations we're going
// to cast the value to a string.
//
Expand All @@ -116,15 +166,16 @@ var Requests = module.exports = Requested.extend({
// already eliminates duplicate headers.
//
for (what in this.headers) {
if (this.headers[what] !== undefined && this.socket.setRequestHeader) {
this.socket.setRequestHeader(what, this.headers[what] +'');
if (this.headers[what] !== undefined && socket.setRequestHeader) {
socket.setRequestHeader(what, this.headers[what] + '');
}
}

//
// Set the correct responseType method.
//
if (requests.streaming) {
// TODO how should fetch/ReadableByteStream be handled here?
if (requests.streaming && (requests.method !== 'FETCH')) {
if (!this.body || 'string' === typeof this.body) {
if ('multipart' in socket) {
socket.multipart = true;
Expand All @@ -142,17 +193,37 @@ var Requests = module.exports = Requested.extend({
}
}

// Polyfill XMLHttpRequest to use the fetch headers API
var fetchOrFake = {}
if (requests.method !== 'FETCH') {
// fetchOrFake is fake
fetchOrFake.response = {
headers: {
get: socket.getResponseHeader
}
};
fetchOrFake.request = socket.request || {};
// TODO this is just a start
} else {
// fetchOrFake is real fetch
fetchOrFake = socket;
}

listeners(socket, requests, requests.streaming);
requests.emit('before', socket);

send(socket, this.body, hang(function send(err) {
if (err) {
requests.emit('error', err);
requests.emit('end', err);
}
requests.emit('before', fetchOrFake);

if (requests.method !== 'FETCH') {
send(socket, this.body, hang(function send(err) {
if (err) {
requests.emit('error', err);
requests.emit('end', err);
}

requests.emit('send');
}));
// NOTE the send event for fetch is in fetch-wrapper.js
requests.emit('send');
}));
}
},

/**
Expand All @@ -179,6 +250,20 @@ var Requests = module.exports = Requested.extend({
}
});

/**
* Create a new FetchWrapper.
*
* @returns {FetchWrapper}
* @type {Object} requests
* @api private
*/
Requests.FETCH = function create(requests) {
// TODO we need to pass the requests object to FetchWrapper,
// This seems kludgy because it's not parallel with Requests.XHR and Requests.AXO.
requests.slice = false;
return new FetchWrapper(requests);
};

/**
* Create a new XMLHttpRequest.
*
Expand Down Expand Up @@ -229,7 +314,11 @@ Requests.active = {};
* @type {String}
* @public
*/
Requests.method = !!Requests.XHR() ? 'XHR' : (!!Requests.AXO() ? 'AXO' : '');
if (typeof root.ReadableByteStream === 'function') {
Requests.method = 'FETCH';
} else {
Requests.method = !!Requests.XHR() ? 'XHR' : (!!Requests.AXO() ? 'AXO' : '');
}

/**
* Boolean indicating
Expand All @@ -240,13 +329,13 @@ Requests.method = !!Requests.XHR() ? 'XHR' : (!!Requests.AXO() ? 'AXO' : '');
Requests.supported = !!Requests.method;

/**
* The different type of `responseType` parsers that are supported in this XHR
* The different types of `responseType` parsers that are supported in this XHR
* implementation.
*
* @type {Object}
* @public
*/
Requests.type = 'XHR' === Requests.method ? (function detect() {
Requests.type = ('XHR' === Requests.method) ? (function detect() {
var types = 'arraybuffer,blob,document,json,text,moz-blob,moz-chunked-text,moz-chunked-arraybuffer,ms-stream'.split(',')
, supported = {}
, type, xhr, prop;
Expand Down Expand Up @@ -282,20 +371,24 @@ Requests.type = 'XHR' === Requests.method ? (function detect() {
* @type {Boolean}
* @private
*/
Requests.streaming = 'XHR' === Requests.method && (
'multipart' in XMLHttpRequest.prototype
|| Requests.type.mozchunkedarraybuffer
|| Requests.type.mozchunkedtext
|| Requests.type.msstream
|| Requests.type.mozblob
);
Requests.streaming = (Requests.method === 'FETCH') ||
(
(Requests.method === 'XHR') && (
'multipart' in XMLHttpRequest.prototype ||
Requests.type.mozchunkedarraybuffer ||
Requests.type.mozchunkedtext ||
Requests.type.msstream ||
Requests.type.mozblob
)
);

//
// IE has a bug which causes IE10 to freeze when close WebPage during an XHR
// request: https://support.microsoft.com/kb/2856746
//
// The solution is to completely clean up all active running requests.
//
// TODO global vs. root? do we need both?
if (global.attachEvent) global.attachEvent('onunload', function reap() {
for (var id in Requests.active) {
Requests.active[id].destroy();
Expand Down
59 changes: 59 additions & 0 deletions fetch-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// From https://github.com/jonnyreeves/chunked-request/blob/4dd6b7568e79a920f6cab3cd4d91d1e8d30b0798/src/impl/fetch.js

//var Requested = require('./requested');
function FetchWrapper(requests) {
var fetchWrapper = this;
fetchWrapper.requests = requests;

var headers = requests.headers;
var mode = requests.mode;
var body = requests.body;
var credentials = requests.credentials;

var decoder = new TextDecoder();
fetchWrapper.decoder = decoder;

fetchWrapper.fetchOptions = {
headers: headers,
mode: mode,
body: body,
credentials: credentials
};
}

FetchWrapper.prototype.onError = function(err) {
var fetchWrapper = this;
console.error(err.message);
console.error(err.stack);
//fetchWrapper.requests.emit('error', err);
}

FetchWrapper.prototype.pump = function(reader, res) {
var fetchWrapper = this;
return reader.read()
.then(function(result) {
if (result.done) {
fetchWrapper.requests.emit('end');
// NOTE: when result.done = true, result.value will always be null
return;
}
fetchWrapper.requests.emit('stream', fetchWrapper.decoder.decode(result.value));
return fetchWrapper.pump(reader, res);
}, function(err) {
fetchWrapper.requests.emit('error', err);
});
}

// TODO is the third arg supposed to be "streaming"?
FetchWrapper.prototype.open = function(method, url, streaming) {
var fetchWrapper = this;
fetch(url, fetchWrapper.fetchOptions)
.then(function(res) {
fetchWrapper.response = res;
fetchWrapper.requests.emit('send', fetchWrapper);
return fetchWrapper.pump(res.body.getReader(), res)
})
.catch(fetchWrapper.onError);
}

module.exports = FetchWrapper;
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "requests",
"version": "0.1.7",
"description": "An streaming XHR abstraction that works in browsers and node.js",
"description": "A streaming XHR abstraction that works in browsers and node.js",
"main": "index.js",
"browser": "browser.js",
"scripts": {
Expand Down Expand Up @@ -43,11 +43,12 @@
"extendible": "0.1.x",
"hang": "1.0.x",
"loads": "0.0.x",
"same-origin": "^0.1.1",
"xhr-send": "1.0.x"
},
"devDependencies": {
"argh": "0.1.x",
"assume": "1.3.x",
"assume": "1.4.x",
"async-each": "0.1.x",
"browserify": "11.x.x",
"http-proxy": "1.11.x",
Expand Down
10 changes: 7 additions & 3 deletions requested.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ function Requested(url, options) {
this.writable = false;

if (this.initialize) this.initialize(url);
if (!this.manual && this.open) this.open(url);
// TODO AR changed what is passed in to this.open().
// But what was happening did not match the
// documentation for this.open anyway.
if (!this.manual && this.open) this.open(url, options);
}

Requested.extend = require('extendible');
Expand Down Expand Up @@ -70,7 +73,7 @@ Requested.prototype.merge = function merge(target) {

/**
* The defaults for the Requests. These values will be used if no options object
* or matching key is provided. It can be override globally if needed but this
* or matching key is provided. It can be overriden globally if needed, but this
* is not advised as it can have some potential side affects for other libraries
* that use this module.
*
Expand All @@ -81,7 +84,8 @@ Requested.defaults = {
streaming: false,
manual: false,
method: 'GET',
mode: 'cors',
// A request has an associated mode, which is "same-origin", "cors", "no-cors", "navigate",
// or "websocket". Unless stated otherwise, it is "no-cors".
headers: {
//
// We're forcing text/plain mode by default to ensure that regular
Expand Down
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ kill.hooks = [];
wd: argv.wd,
ui: argv.ui
})
.on('error', console.error)
.bundle(next);
}
]);