| var capability = require('./capability') | |
| var inherits = require('inherits') | |
| var stream = require('readable-stream') | |
| var rStates = exports.readyStates = { | |
| UNSENT: 0, | |
| OPENED: 1, | |
| HEADERS_RECEIVED: 2, | |
| LOADING: 3, | |
| DONE: 4 | |
| } | |
| var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode, fetchTimer) { | |
| var self = this | |
| stream.Readable.call(self) | |
| self._mode = mode | |
| self.headers = {} | |
| self.rawHeaders = [] | |
| self.trailers = {} | |
| self.rawTrailers = [] | |
| // Fake the 'close' event, but only once 'end' fires | |
| self.on('end', function () { | |
| // The nextTick is necessary to prevent the 'request' module from causing an infinite loop | |
| process.nextTick(function () { | |
| self.emit('close') | |
| }) | |
| }) | |
| if (mode === 'fetch') { | |
| self._fetchResponse = response | |
| self.url = response.url | |
| self.statusCode = response.status | |
| self.statusMessage = response.statusText | |
| response.headers.forEach(function (header, key){ | |
| self.headers[key.toLowerCase()] = header | |
| self.rawHeaders.push(key, header) | |
| }) | |
| if (capability.writableStream) { | |
| var writable = new WritableStream({ | |
| write: function (chunk) { | |
| return new Promise(function (resolve, reject) { | |
| if (self._destroyed) { | |
| reject() | |
| } else if(self.push(Buffer.from(chunk))) { | |
| resolve() | |
| } else { | |
| self._resumeFetch = resolve | |
| } | |
| }) | |
| }, | |
| close: function () { | |
| global.clearTimeout(fetchTimer) | |
| if (!self._destroyed) | |
| self.push(null) | |
| }, | |
| abort: function (err) { | |
| if (!self._destroyed) | |
| self.emit('error', err) | |
| } | |
| }) | |
| try { | |
| response.body.pipeTo(writable).catch(function (err) { | |
| global.clearTimeout(fetchTimer) | |
| if (!self._destroyed) | |
| self.emit('error', err) | |
| }) | |
| return | |
| } catch (e) {} // pipeTo method isn't defined. Can't find a better way to feature test this | |
| } | |
| // fallback for when writableStream or pipeTo aren't available | |
| var reader = response.body.getReader() | |
| function read () { | |
| reader.read().then(function (result) { | |
| if (self._destroyed) | |
| return | |
| if (result.done) { | |
| global.clearTimeout(fetchTimer) | |
| self.push(null) | |
| return | |
| } | |
| self.push(Buffer.from(result.value)) | |
| read() | |
| }).catch(function (err) { | |
| global.clearTimeout(fetchTimer) | |
| if (!self._destroyed) | |
| self.emit('error', err) | |
| }) | |
| } | |
| read() | |
| } else { | |
| self._xhr = xhr | |
| self._pos = 0 | |
| self.url = xhr.responseURL | |
| self.statusCode = xhr.status | |
| self.statusMessage = xhr.statusText | |
| var headers = xhr.getAllResponseHeaders().split(/\r?\n/) | |
| headers.forEach(function (header) { | |
| var matches = header.match(/^([^:]+):\s*(.*)/) | |
| if (matches) { | |
| var key = matches[1].toLowerCase() | |
| if (key === 'set-cookie') { | |
| if (self.headers[key] === undefined) { | |
| self.headers[key] = [] | |
| } | |
| self.headers[key].push(matches[2]) | |
| } else if (self.headers[key] !== undefined) { | |
| self.headers[key] += ', ' + matches[2] | |
| } else { | |
| self.headers[key] = matches[2] | |
| } | |
| self.rawHeaders.push(matches[1], matches[2]) | |
| } | |
| }) | |
| self._charset = 'x-user-defined' | |
| if (!capability.overrideMimeType) { | |
| var mimeType = self.rawHeaders['mime-type'] | |
| if (mimeType) { | |
| var charsetMatch = mimeType.match(/;\s*charset=([^;])(;|$)/) | |
| if (charsetMatch) { | |
| self._charset = charsetMatch[1].toLowerCase() | |
| } | |
| } | |
| if (!self._charset) | |
| self._charset = 'utf-8' // best guess | |
| } | |
| } | |
| } | |
| inherits(IncomingMessage, stream.Readable) | |
| IncomingMessage.prototype._read = function () { | |
| var self = this | |
| var resolve = self._resumeFetch | |
| if (resolve) { | |
| self._resumeFetch = null | |
| resolve() | |
| } | |
| } | |
| IncomingMessage.prototype._onXHRProgress = function () { | |
| var self = this | |
| var xhr = self._xhr | |
| var response = null | |
| switch (self._mode) { | |
| case 'text': | |
| response = xhr.responseText | |
| if (response.length > self._pos) { | |
| var newData = response.substr(self._pos) | |
| if (self._charset === 'x-user-defined') { | |
| var buffer = Buffer.alloc(newData.length) | |
| for (var i = 0; i < newData.length; i++) | |
| buffer[i] = newData.charCodeAt(i) & 0xff | |
| self.push(buffer) | |
| } else { | |
| self.push(newData, self._charset) | |
| } | |
| self._pos = response.length | |
| } | |
| break | |
| case 'arraybuffer': | |
| if (xhr.readyState !== rStates.DONE || !xhr.response) | |
| break | |
| response = xhr.response | |
| self.push(Buffer.from(new Uint8Array(response))) | |
| break | |
| case 'moz-chunked-arraybuffer': // take whole | |
| response = xhr.response | |
| if (xhr.readyState !== rStates.LOADING || !response) | |
| break | |
| self.push(Buffer.from(new Uint8Array(response))) | |
| break | |
| case 'ms-stream': | |
| response = xhr.response | |
| if (xhr.readyState !== rStates.LOADING) | |
| break | |
| var reader = new global.MSStreamReader() | |
| reader.onprogress = function () { | |
| if (reader.result.byteLength > self._pos) { | |
| self.push(Buffer.from(new Uint8Array(reader.result.slice(self._pos)))) | |
| self._pos = reader.result.byteLength | |
| } | |
| } | |
| reader.onload = function () { | |
| self.push(null) | |
| } | |
| // reader.onerror = ??? // TODO: this | |
| reader.readAsArrayBuffer(response) | |
| break | |
| } | |
| // The ms-stream case handles end separately in reader.onload() | |
| if (self._xhr.readyState === rStates.DONE && self._mode !== 'ms-stream') { | |
| self.push(null) | |
| } | |
| } |