// 1. TextDecoder // 2. TextDecoder + stream option (Firefox 38 + - how to detect ?) // 3. Promise.prototype.finally - // 4. fetch + // 5. Response.prototype.body + // 6. ReadableStream.prototype.getReader (no Firefox support yet) // 7. AbortController - (Firefox 57) var s = ""; if (window.Uint8Array != null && window.TextDecoder != null) { var b = function (byte) { var x = new Uint8Array(1); x[0] = byte; return x; }; var textDecoder = new TextDecoder(); var chunk = textDecoder.decode(b(208), {stream: true}) + textDecoder.decode(b(176), {stream: true}); s = chunk; }; console.log('TextDecoder', typeof window.TextDecoder); console.log('TextDecoder+stream', chunk === 'а'); console.log('Promise#finally', window.Promise != null ? typeof window.Promise.prototype.finally : typeof undefined); console.log('fetch', typeof window.fetch); console.log('Response#body', window.Response != null && 'body' in window.Response.prototype); console.log('ReadableStream#getReader', window.ReadableStream != null ? typeof window.ReadableStream.prototype.getReader : typeof undefined); console.log('AbortController', typeof window.AbortController); // an example from https://fetch.spec.whatwg.org/ var url = 'https://matrixcalc.org/jstest/tests.php?events'; function consume(reader) { var responseText = ''; var pump = function () { return reader.read().then(function (result) { if (result.done) { console.log('responseText', responseText); return; } responseText += String.fromCharCode.apply(undefined, result.value); return pump(); }); }; return pump(); } if (window.fetch != null && window.TextDecoder != null) { fetch(url).then(function (response) { var body = response.body; console.log('response.body', typeof body); if (body != null) { console.log('response.body.getReader', typeof body.getReader); if (typeof body.getReader == 'function') { consume(body.getReader()); } } }); } setTimeout(function () { console.log('done'); }, 1000); // Firefox < 40 (no "stream" option), IE, Edge function TextDecoderPolyfill() { } //TODO: streaming TextDecoderPolyfill.prototype.decode = function (octets) { var string = ""; var i = 0; while (i < octets.length) { var octet = octets[i]; var bytesNeeded = 0; var codePoint = 0; if (octet <= 0x7F) { bytesNeeded = 0; codePoint = octet & 0xFF; } else if (octet <= 0xDF) { bytesNeeded = 1; codePoint = octet & 0x1F; } else if (octet <= 0xEF) { bytesNeeded = 2; codePoint = octet & 0x0F; } else if (octet <= 0xF4) { bytesNeeded = 3; codePoint = octet & 0x07; } if (octets.length - i - bytesNeeded > 0) { var k = 0; while (k < bytesNeeded) { octet = octets[i + k + 1]; codePoint = (codePoint << 6) | (octet & 0x3F); k += 1; } } else { codePoint = 0xFFFD; bytesNeeded = octets.length - i; } string += String.fromCodePoint(codePoint); i += bytesNeeded + 1; } return string }; if (Promise.prototype.finally == undefined) { Promise.prototype.finally = function (callback) { return this.then(function (result) { return Promise.resolve(callback()).then(function () { return result; }); }, function (error) { return Promise.resolve(callback()).then(function () { throw error; }); }); }; } function FetchTransport() { //this.controller = undefined; this.reader = undefined; this.lastRequestId = 1; } FetchTransport.prototype.open = function (onStartCallback, onProgressCallback, onFinishCallback, url, withCredentials, headers) { // cache: "no-store" // https://bugs.chromium.org/p/chromium/issues/detail?id=453190 this.cancel(); var textDecoder = new TextDecoder(); var that = this; var lastRequestId = this.lastRequestId; this.controller = new AbortController(); fetch(url, { headers: headers, credentials: withCredentials ? "include" : "same-origin", signal: this.controller.signal }).then(function (response) { if (lastRequestId === that.lastRequestId) { that.reader = response.body.getReader(); onStartCallback(response.status, response.statusText, response.headers.get("Content-Type")); return new Promise(function (resolve, reject) { var readNextChunk = function () { if (that.reader != undefined) { that.reader.read().then(function (result) { if (result.done) { //Note: bytes in textDecoder are ignored resolve(undefined); } else { var chunk = textDecoder.decode(result.value, {stream: true}); //var chunk = String.fromCharCode.apply(undefined, result.value); onProgressCallback(chunk); readNextChunk(); } })["catch"](reject); } else { resolve(undefined); } }; readNextChunk(); }); } return undefined; })["finally"](function () { onFinishCallback(); }); }; FetchTransport.prototype.cancel = function () { this.lastRequestId += 1; if (this.reader != undefined) { this.reader.cancel(); this.reader = undefined; } //if (this.controller != undefined) { // this.controller.abort(); //} }; function XHRTransport(xhr) { this.xhr = xhr; } XHRTransport.prototype.open = function (onStartCallback, onProgressCallback, onFinishCallback, url, withCredentials, headers) { var xhr = this.xhr; xhr.open("GET", url); var offset = 0; xhr.onprogress = function () { var responseText = xhr.responseText; var chunk = responseText.slice(offset); offset += chunk.length; onProgressCallback(chunk); }; xhr.onreadystatechange = function () { if (xhr.readyState === 2) { var status = xhr.status; var statusText = xhr.statusText; var contentType = xhr.getResponseHeader("Content-Type"); onStartCallback(status, statusText, contentType); } else if (xhr.readyState === 4) { onFinishCallback(); } }; xhr.withCredentials = withCredentials; xhr.responseType = "text"; for (var name in headers) { if (Object.prototype.hasOwnProperty.call(headers, name)) { xhr.setRequestHeader(name, headers[name]); } } xhr.send(); }; XHRTransport.prototype.cancel = function () { var xhr = this.xhr; xhr.abort(); }; function EventTarget() { this._listeners = new Map(); } function throwError(e) { setTimeout(function () { throw e; }, 0); } EventTarget.prototype.dispatchEvent = function (event) { event.target = this; var typeListeners = this._listeners.get(event.type); if (typeListeners != undefined) { typeListeners.forEach(function (listener) { try { if (typeof listener.handleEvent === "function") { listener.handleEvent(event); } else { listener.call(this, event); } } catch (e) { throwError(e); } }, this); } }; EventTarget.prototype.addEventListener = function (type, listener) { var listeners = this._listeners; var typeListeners = listeners.get(type); if (typeListeners == undefined) { typeListeners = new Set(); listeners.set(type, typeListeners); } typeListeners.add(listener); }; EventTarget.prototype.removeEventListener = function (type, listener) { var listeners = this._listeners; var typeListeners = listeners.get(type); if (typeListeners != undefined) { typeListeners["delete"](listener); if (typeListeners.size === 0) { listeners["delete"](type); } } };