Path: blob/trunk/third_party/closure/goog/net/streams/xhrstreamreader.js
1865 views
// Copyright 2015 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview the XHR stream reader implements a low-level stream16* reader for handling a streamed XHR response body. The reader takes a17* StreamParser which may support JSON or any other formats as confirmed by18* the Content-Type of the response. The reader may be used as polyfill for19* different streams APIs such as Node streams or whatwg streams (Fetch).20*21* The first version of this implementation only covers functions necessary22* to support NodeReadableStream. In a later version, this reader will also23* be adapted to whatwg streams.24*25* For IE, only IE-10 and above are supported.26*27* TODO(user): xhr polling, stream timeout, CORS and preflight optimization.28*/2930goog.provide('goog.net.streams.XhrStreamReader');3132goog.require('goog.events.EventHandler');33goog.require('goog.log');34goog.require('goog.net.ErrorCode');35goog.require('goog.net.EventType');36goog.require('goog.net.HttpStatus');37goog.require('goog.net.XhrIo');38goog.require('goog.net.XmlHttp');39goog.require('goog.net.streams.Base64PbStreamParser');40goog.require('goog.net.streams.JsonStreamParser');41goog.require('goog.net.streams.PbJsonStreamParser');42goog.require('goog.net.streams.PbStreamParser');43goog.require('goog.string');44goog.require('goog.userAgent');4546goog.scope(function() {4748var Base64PbStreamParser =49goog.module.get('goog.net.streams.Base64PbStreamParser');50var PbJsonStreamParser = goog.module.get('goog.net.streams.PbJsonStreamParser');515253/**54* The XhrStreamReader class.55*56* The caller must check isStreamingSupported() first.57*58* @param {!goog.net.XhrIo} xhr The XhrIo object with its response body to59* be handled by NodeReadableStream.60* @constructor61* @struct62* @final63* @package64*/65goog.net.streams.XhrStreamReader = function(xhr) {66/**67* @const68* @private {?goog.log.Logger} the logger.69*/70this.logger_ = goog.log.getLogger('goog.net.streams.XhrStreamReader');7172/**73* The xhr object passed by the application.74*75* @private {?goog.net.XhrIo} the XHR object for the stream.76*/77this.xhr_ = xhr;7879/**80* To be initialized with the correct content-type.81*82* @private {?goog.net.streams.StreamParser} the parser for the stream.83*/84this.parser_ = null;8586/**87* The position of where the next unprocessed data starts in the XHR88* response text.89* @private {number}90*/91this.pos_ = 0;9293/**94* The status (error detail) of the current stream.95* @private {!goog.net.streams.XhrStreamReader.Status}96*/97this.status_ = goog.net.streams.XhrStreamReader.Status.INIT;9899/**100* The handler for any status change event.101*102* @private {?function()} The call back to handle the XHR status change.103*/104this.statusHandler_ = null;105106/**107* The handler for new response data.108*109* @private {?function(!Array<!Object>)} The call back to handle new110* response data, parsed as an array of atomic messages.111*/112this.dataHandler_ = null;113114/**115* An object to keep track of event listeners.116*117* @private {!goog.events.EventHandler<!goog.net.streams.XhrStreamReader>}118*/119this.eventHandler_ = new goog.events.EventHandler(this);120121// register the XHR event handler122this.eventHandler_.listen(123this.xhr_, goog.net.EventType.READY_STATE_CHANGE,124this.readyStateChangeHandler_);125};126127128/**129* Enum type for current stream status.130* @enum {number}131*/132goog.net.streams.XhrStreamReader.Status = {133/**134* Init status, with xhr inactive.135*/136INIT: 0,137138/**139* XHR being sent.140*/141ACTIVE: 1,142143/**144* The request was successful, after the request successfully completes.145*/146SUCCESS: 2,147148/**149* Errors due to a non-200 status code or other error conditions.150*/151XHR_ERROR: 3,152153/**154* Errors due to no data being returned.155*/156NO_DATA: 4,157158/**159* Errors due to corrupted or invalid data being received.160*/161BAD_DATA: 5,162163/**164* Errors due to the handler throwing an exception.165*/166HANDLER_EXCEPTION: 6,167168/**169* Errors due to a timeout.170*/171TIMEOUT: 7,172173/**174* The request is cancelled by the application.175*/176CANCELLED: 8177};178179180/**181* Returns whether response streaming is supported on this browser.182*183* @return {boolean} false if response streaming is not supported.184*/185goog.net.streams.XhrStreamReader.isStreamingSupported = function() {186if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(10)) {187// No active-x due to security issues.188return false;189}190191if (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('420+')) {192// Safari 3+193// Older versions of Safari always receive null response in INTERACTIVE.194return false;195}196197if (goog.userAgent.OPERA && !goog.userAgent.WEBKIT) {198// Old Opera fires readyState == INTERACTIVE once.199// TODO(user): polling the buffer and check the exact Opera version200return false;201}202203return true;204};205206207/**208* Returns a parser that supports the given content-type (mime) and209* content-transfer-encoding.210*211* @return {?goog.net.streams.StreamParser} a parser or null if the content212* type or transfer encoding is unsupported.213* @private214*/215goog.net.streams.XhrStreamReader.prototype.getParserByResponseHeader_ =216function() {217var contentType =218this.xhr_.getStreamingResponseHeader(goog.net.XhrIo.CONTENT_TYPE_HEADER);219if (!contentType) {220goog.log.warning(this.logger_, 'Content-Type unavailable: ' + contentType);221return null;222}223contentType = contentType.toLowerCase();224225if (goog.string.startsWith(contentType, 'application/json')) {226if (goog.string.startsWith(contentType, 'application/json+protobuf')) {227return new PbJsonStreamParser();228}229return new goog.net.streams.JsonStreamParser();230}231232if (goog.string.startsWith(contentType, 'application/x-protobuf')) {233var encoding = this.xhr_.getStreamingResponseHeader(234goog.net.XhrIo.CONTENT_TRANSFER_ENCODING);235if (!encoding) {236return new goog.net.streams.PbStreamParser();237}238if (encoding.toLowerCase() == 'base64') {239return new Base64PbStreamParser();240}241goog.log.warning(242this.logger_, 'Unsupported Content-Transfer-Encoding: ' + encoding +243'\nFor Content-Type: ' + contentType);244return null;245}246247goog.log.warning(this.logger_, 'Unsupported Content-Type: ' + contentType);248return null;249};250251252/**253* Returns the XHR request object.254*255* @return {goog.net.XhrIo} The XHR object associated with this reader, or256* null if the reader has been cleared.257*/258goog.net.streams.XhrStreamReader.prototype.getXhr = function() {259return this.xhr_;260};261262263/**264* Gets the current stream status.265*266* @return {!goog.net.streams.XhrStreamReader.Status} The stream status.267*/268goog.net.streams.XhrStreamReader.prototype.getStatus = function() {269return this.status_;270};271272273/**274* Sets the status handler.275*276* @param {function()} handler The handler for any status change.277*/278goog.net.streams.XhrStreamReader.prototype.setStatusHandler = function(279handler) {280this.statusHandler_ = handler;281};282283284/**285* Sets the data handler.286*287* @param {function(!Array<!Object>)} handler The handler for new data.288*/289goog.net.streams.XhrStreamReader.prototype.setDataHandler = function(handler) {290this.dataHandler_ = handler;291};292293294/**295* Handles XHR readystatechange events.296*297* TODO(user): throttling may be needed.298*299* @param {!goog.events.Event} event The event.300* @private301*/302goog.net.streams.XhrStreamReader.prototype.readyStateChangeHandler_ = function(303event) {304var xhr = /** @type {goog.net.XhrIo} */ (event.target);305306307try {308if (xhr == this.xhr_) {309this.onReadyStateChanged_();310} else {311goog.log.warning(this.logger_, 'Called back with an unexpected xhr.');312}313} catch (ex) {314goog.log.error(315this.logger_, 'readyStateChangeHandler_ thrown exception' +316' ' + ex);317// no rethrow318this.updateStatus_(319goog.net.streams.XhrStreamReader.Status.HANDLER_EXCEPTION);320this.clear_();321}322};323324325/**326* Called from readyStateChangeHandler_.327*328* @private329*/330goog.net.streams.XhrStreamReader.prototype.onReadyStateChanged_ = function() {331var readyState = this.xhr_.getReadyState();332var errorCode = this.xhr_.getLastErrorCode();333var statusCode = this.xhr_.getStatus();334var responseText = this.xhr_.getResponseText();335336// we get partial results in browsers that support ready state interactive.337// We also make sure that getResponseText is not null in interactive mode338// before we continue.339if (readyState < goog.net.XmlHttp.ReadyState.INTERACTIVE ||340readyState == goog.net.XmlHttp.ReadyState.INTERACTIVE && !responseText) {341return;342}343344// TODO(user): white-list other 2xx responses with application payload345var successful =346(statusCode == goog.net.HttpStatus.OK ||347statusCode == goog.net.HttpStatus.PARTIAL_CONTENT);348349if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {350if (errorCode == goog.net.ErrorCode.TIMEOUT) {351this.updateStatus_(goog.net.streams.XhrStreamReader.Status.TIMEOUT);352} else if (errorCode == goog.net.ErrorCode.ABORT) {353this.updateStatus_(goog.net.streams.XhrStreamReader.Status.CANCELLED);354} else if (!successful) {355this.updateStatus_(goog.net.streams.XhrStreamReader.Status.XHR_ERROR);356}357}358359if (successful && !responseText) {360goog.log.warning(361this.logger_, 'No response text for xhr ' + this.xhr_.getLastUri() +362' status ' + statusCode);363}364365if (!this.parser_) {366this.parser_ = this.getParserByResponseHeader_();367if (this.parser_ == null) {368this.updateStatus_(goog.net.streams.XhrStreamReader.Status.BAD_DATA);369}370}371372if (this.status_ > goog.net.streams.XhrStreamReader.Status.SUCCESS) {373this.clear_();374return;375}376377// Parses and delivers any new data, with error status.378if (responseText.length > this.pos_) {379var newData = responseText.substr(this.pos_);380this.pos_ = responseText.length;381try {382var messages = this.parser_.parse(newData);383if (messages != null) {384if (this.dataHandler_) {385this.dataHandler_(messages);386}387}388} catch (ex) {389goog.log.error(390this.logger_, 'Invalid response ' + ex + '\n' + responseText);391this.updateStatus_(goog.net.streams.XhrStreamReader.Status.BAD_DATA);392this.clear_();393return;394}395}396397if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {398if (responseText.length == 0) {399this.updateStatus_(goog.net.streams.XhrStreamReader.Status.NO_DATA);400} else {401this.updateStatus_(goog.net.streams.XhrStreamReader.Status.SUCCESS);402}403this.clear_();404return;405}406407this.updateStatus_(goog.net.streams.XhrStreamReader.Status.ACTIVE);408};409410411/**412* Update the status and may call the handler.413*414* @param {!goog.net.streams.XhrStreamReader.Status} status The new status415* @private416*/417goog.net.streams.XhrStreamReader.prototype.updateStatus_ = function(status) {418var current = this.status_;419if (current != status) {420this.status_ = status;421if (this.statusHandler_) {422this.statusHandler_();423}424}425};426427428/**429* Clears after the XHR terminal state is reached.430*431* @private432*/433goog.net.streams.XhrStreamReader.prototype.clear_ = function() {434this.eventHandler_.removeAll();435436if (this.xhr_) {437// clear out before aborting to avoid being reentered inside abort438var xhr = this.xhr_;439this.xhr_ = null;440xhr.abort();441xhr.dispose();442}443};444445}); // goog.scope446447448