Skip to content

Commit

Permalink
Fixed up polyfills and improved environment detection
Browse files Browse the repository at this point in the history
  • Loading branch information
jonnyreeves committed Sep 21, 2016
1 parent 15b9139 commit ee78621
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 47 deletions.
2 changes: 1 addition & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
lib/
build/
dist/
dist/
4 changes: 3 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"Uint8Array": false,
"TextEncoder": false,
"Promise": false,
"ReadableStream": false
"ReadableStream": false,
"Response": false,
"Symbol": false
},
"parserOptions": {
"ecmaVersion": 6,
Expand Down
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,27 @@ Compatibility layer for efficient streaming of binary data using [WHATWG Streams
## Why
This library provides a consistent, cross browser API for streaming a response from an HTTP server based on the [WHATWG Streams specification](https://streams.spec.whatwg.org/). At the time of writing, Chrome is the only browser to nativley support returning a `ReadableStream` from it's `fetch` implementation - all other browsers need to fall back to `XMLHttpRequest`.

FireFox does provide the ability to efficiently retrieve a byte-stream from a server; however only via it's `XMLHttpRequest` implementation (when using `responsetype=moz-chunked-arraybuffer`). Other browsers do not provide access to the underlying byte-stream and must therefore fall-back to concatenating the response string and then encdoing it into it's UTF-8 byte representation using the [`TextEncoder` API](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder).
FireFox does provide the ability to efficiently retrieve a byte-stream from a server; however only via it's `XMLHttpRequest` implementation (when using `responsetype=moz-chunked-arraybuffer`). Other browsers do not provide access to the underlying byte-stream and must therefore fall-back to concatenating the response string and then encoding it into it's UTF-8 byte representation using the [`TextEncoder` API](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder).

*Nb:* If you are happy using a node-style API (using callbacks and events) I would suggest taking a look at [`stream-http`](https://github.com/jhiesey/stream-http).

## Installation
This package can be installed with `npm`:
$ npm install fetch-readablestream --save

Once installed you can either import it:
```
$ npm install fetch-readablestream --save
```

Once installed you can import it directly:

```js
import fetchStream from 'fetch-readablestream';
```

Or you can add a script tag pointing to the `dist/fetch-readablestream.js` bundle
Or you can add a script tag pointing to the `dist/fetch-readablestream.js` bundle and use the `fetchStream` global:

```html
<script src="./node_modules/fetch-readablestream/dist/fetch-readablestream.js">
<script src="./node_modules/fetch-readablestream/dist/fetch-readablestream.js"></script>
<script>
window.fetchStream('...')
</script>
Expand Down Expand Up @@ -56,11 +59,11 @@ fetchStream('/endpoint')
## Browser Compatibility
`fetch-readablestream` makes the following assumptions on the environment; legacy browsers will need to provide Polyfills for this functionality:

| Feature | Browsers | Polyfill |
|----------------|----------------------------------|----------|
| ReadableStream | Firefox, Safari, IE10, PhantomJS | [web-streams-polyfill](https://www.npmjs.com/package/web-streams-polyfill) |
| TextEncoder | Safari, IE10, PhantomJS | [text-encoding](https://www.npmjs.com/package/text-encoding) |
| Promise | IE10, PhantomJS | [es6-promise-polyfill](https://www.npmjs.com/package/es6-promise-polyfill) |
| Feature | Browsers | Polyfill |
|--------------------------------|----------------------------------|----------|
| ReadableStream | Firefox, Safari, IE11, PhantomJS | [web-streams-polyfill](https://www.npmjs.com/package/web-streams-polyfill) |
| TextEncoder | Safari, IE11, PhantomJS | [text-encoding](https://www.npmjs.com/package/text-encoding) |
| Promise, Symbol, Object.assign | IE11, PhantomJS | [babel-polyfill](https://www.npmjs.com/package/babel-polyfill) |

## Contributing
Use `npm run watch` to fire up karma with live-reloading. Visit http://localhost:9876/ in a bunch of browsers to capture them - the test suite will run automatically and report any failures.
Expand Down
12 changes: 10 additions & 2 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = function(config) {
// Browsers to run on Sauce Labs
// Check out https://saucelabs.com/platforms for all browser/OS combos
var customLaunchers = {
/*
/* Safari appears broken on saucelabs, but works locally.
'SL_Safari': {
base: 'SauceLabs',
browserName: 'safari',
Expand All @@ -25,11 +25,18 @@ module.exports = function(config) {
browserName: 'firefox',
platform: 'linux'
},
/* need to figure out what's broken in edge.
'SL_Edge': {
base: 'SauceLabs',
browserName: 'MicrosoftEdge',
platform: 'Windows 10'
},
*/
'SL_IE10': {
base: 'SauceLabs',
browserName: 'internet explorer',
platform: 'Windows 7',
version: '10'
version: '11'
}
};

Expand Down Expand Up @@ -64,6 +71,7 @@ module.exports = function(config) {

// list of files / patterns to load in the browser
files: [
'./node_modules/babel-polyfill/dist/polyfill.js',
'node_modules/text-encoding/lib/encoding.js',
'node_modules/web-streams-polyfill/dist/polyfill.js',
'build/integration-tests.js'
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"devDependencies": {
"babel-cli": "^6.11.4",
"babel-polyfill": "^6.13.0",
"babel-preset-es2015": "^6.13.2",
"babelify": "^7.3.0",
"browserify": "^13.1.0",
Expand Down
72 changes: 44 additions & 28 deletions src/defaultTransportFactory.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,55 @@
import fetchRequest from './fetch';
import { makeXhrTransport } from './xhr';

// selected is used to cache the detected transport.
let selected = null;

// defaultTransportFactory selects the most appropriate transport based on the
// capabilities of the current environment.
export default function defaultTransportFactory() {
if (!selected) {
if (window.Response && window.Response.prototype.hasOwnProperty("body")) {
selected = fetchRequest;
} else {
const tmpXhr = new XMLHttpRequest();
const mozChunked = 'moz-chunked-arraybuffer';
tmpXhr.responseType = mozChunked;
if (tmpXhr.responseType === mozChunked) {
selected = makeXhrTransport({
responseType: mozChunked,
responseParserFactory: function () {
return response => new Uint8Array(response);
}
});
} else {
selected = makeXhrTransport({
responseType: 'text',
responseParserFactory: function () {
const encoder = new TextEncoder();
let offset = 0;
return function (response) {
const chunk = response.substr(offset);
offset = response.length;
return encoder.encode(chunk, { stream: true });
}
}
});
}
}
selected = detectTransport();
}
return selected;
}

function detectTransport() {
if (typeof Response !== 'undefined' && Response.prototype.hasOwnProperty("body")) {
// fetch with ReadableStream support.
return fetchRequest;
}

const mozChunked = 'moz-chunked-arraybuffer';
if (supportsXhrResponseType(mozChunked)) {
// Firefox, ArrayBuffer support.
return makeXhrTransport({
responseType: mozChunked,
responseParserFactory: function () {
return response => new Uint8Array(response);
}
});
}

// Bog-standard, expensive, text concatenation with byte encoding :(
return makeXhrTransport({
responseType: 'text',
responseParserFactory: function () {
const encoder = new TextEncoder();
let offset = 0;
return function (response) {
const chunk = response.substr(offset);
offset = response.length;
return encoder.encode(chunk, { stream: true });
}
}
});
}

function supportsXhrResponseType(type) {
try {
const tmpXhr = new XMLHttpRequest();
tmpXhr.responseType = type;
return tmpXhr.responseType === type;
} catch (e) { /* IE throws on setting responseType to an unsupported value */ }
return false;
}
22 changes: 22 additions & 0 deletions src/polyfill/Headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,32 @@ export class Headers {
getAll(key) {
return this.h[key.toLowerCase()].concat();
}
entries() {
const items = [];
this.forEach((value, key) => { items.push([key, value]) });
return makeIterator(items);
}

// forEach is not part of the official spec.
forEach(callback, thisArg) {
Object.getOwnPropertyNames(this.h)
.forEach(key => {
this.h[key].forEach(value => callback.call(thisArg, value, key, this));
}, this);
}
}

function makeIterator(items) {
return {
next() {
const value = items.shift();
return {
done: value === undefined,
value: value
}
},
[Symbol.iterator]() {
return this;
}
};
}
21 changes: 17 additions & 4 deletions src/xhr.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export function makeXhrTransport({ responseType, responseParserFactory }) {
xhr.responseType = responseType;
xhr.withCredentials = (options.credentials !== 'omit');
if (options.headers) {
options.headers.forEach((value, key) => xhr.setRequestHeader(key, value));
for (const pair of options.headers.entries()) {
xhr.setRequestHeader(pair[0], pair[1]);
}
}

return new Promise((resolve, reject) => {
Expand All @@ -40,7 +42,7 @@ export function makeXhrTransport({ responseType, responseParserFactory }) {
ok: xhr.status >= 200 && xhr.status < 300,
status: xhr.status,
statusText: xhr.statusText,
url: xhr.responseURL || url
url: makeResponseUrl(xhr.responseURL, url)
});
}
};
Expand Down Expand Up @@ -71,12 +73,23 @@ export function makeXhrTransport({ responseType, responseParserFactory }) {

function makeHeaders() {
// Prefer the native method if provided by the browser.
if (window.Headers) {
return new window.Headers();
if (typeof Headers !== 'undefined') {
return new Headers();
}
return new HeadersPolyfill();
}

function makeResponseUrl(responseUrl, requestUrl) {
if (!responseUrl) {
// best guess; note this will not correctly handle redirects.
if (requestUrl.substring(0, 4) !== "http") {
return location.origin + requestUrl;
}
return requestUrl;
}
return responseUrl;
}

export function parseResposneHeaders(str) {
const hdrs = makeHeaders();
if (str) {
Expand Down
4 changes: 3 additions & 1 deletion test/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ const httpPort = process.env.HTTP_PORT || 2001;
// How frequently should chunks be written to the response? Note that we have no
// control over when chunks are actually emitted to the client so it's best to keep
// this value high and pray to the gods of TCP.
const CHUNK_INTERVAL_MS = process.env.CHUNK_INTERVAL_MS || 250;
//
// Nb: lower values appear to cause problems for internet explorer.
const CHUNK_INTERVAL_MS = process.env.CHUNK_INTERVAL_MS || 1000;

let _lastRequestClosedByClient = false;

Expand Down

0 comments on commit ee78621

Please sign in to comment.