123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446 |
- // Copyright 2015 The Closure Library Authors. All Rights Reserved.
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS-IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- /**
- * @fileoverview the XHR stream reader implements a low-level stream
- * reader for handling a streamed XHR response body. The reader takes a
- * StreamParser which may support JSON or any other formats as confirmed by
- * the Content-Type of the response. The reader may be used as polyfill for
- * different streams APIs such as Node streams or whatwg streams (Fetch).
- *
- * The first version of this implementation only covers functions necessary
- * to support NodeReadableStream. In a later version, this reader will also
- * be adapted to whatwg streams.
- *
- * For IE, only IE-10 and above are supported.
- *
- * TODO(user): xhr polling, stream timeout, CORS and preflight optimization.
- */
- goog.provide('goog.net.streams.XhrStreamReader');
- goog.require('goog.events.EventHandler');
- goog.require('goog.log');
- goog.require('goog.net.ErrorCode');
- goog.require('goog.net.EventType');
- goog.require('goog.net.HttpStatus');
- goog.require('goog.net.XhrIo');
- goog.require('goog.net.XmlHttp');
- goog.require('goog.net.streams.Base64PbStreamParser');
- goog.require('goog.net.streams.JsonStreamParser');
- goog.require('goog.net.streams.PbJsonStreamParser');
- goog.require('goog.net.streams.PbStreamParser');
- goog.require('goog.string');
- goog.require('goog.userAgent');
- goog.scope(function() {
- var Base64PbStreamParser =
- goog.module.get('goog.net.streams.Base64PbStreamParser');
- var PbJsonStreamParser = goog.module.get('goog.net.streams.PbJsonStreamParser');
- /**
- * The XhrStreamReader class.
- *
- * The caller must check isStreamingSupported() first.
- *
- * @param {!goog.net.XhrIo} xhr The XhrIo object with its response body to
- * be handled by NodeReadableStream.
- * @constructor
- * @struct
- * @final
- * @package
- */
- goog.net.streams.XhrStreamReader = function(xhr) {
- /**
- * @const
- * @private {?goog.log.Logger} the logger.
- */
- this.logger_ = goog.log.getLogger('goog.net.streams.XhrStreamReader');
- /**
- * The xhr object passed by the application.
- *
- * @private {?goog.net.XhrIo} the XHR object for the stream.
- */
- this.xhr_ = xhr;
- /**
- * To be initialized with the correct content-type.
- *
- * @private {?goog.net.streams.StreamParser} the parser for the stream.
- */
- this.parser_ = null;
- /**
- * The position of where the next unprocessed data starts in the XHR
- * response text.
- * @private {number}
- */
- this.pos_ = 0;
- /**
- * The status (error detail) of the current stream.
- * @private {!goog.net.streams.XhrStreamReader.Status}
- */
- this.status_ = goog.net.streams.XhrStreamReader.Status.INIT;
- /**
- * The handler for any status change event.
- *
- * @private {?function()} The call back to handle the XHR status change.
- */
- this.statusHandler_ = null;
- /**
- * The handler for new response data.
- *
- * @private {?function(!Array<!Object>)} The call back to handle new
- * response data, parsed as an array of atomic messages.
- */
- this.dataHandler_ = null;
- /**
- * An object to keep track of event listeners.
- *
- * @private {!goog.events.EventHandler<!goog.net.streams.XhrStreamReader>}
- */
- this.eventHandler_ = new goog.events.EventHandler(this);
- // register the XHR event handler
- this.eventHandler_.listen(
- this.xhr_, goog.net.EventType.READY_STATE_CHANGE,
- this.readyStateChangeHandler_);
- };
- /**
- * Enum type for current stream status.
- * @enum {number}
- */
- goog.net.streams.XhrStreamReader.Status = {
- /**
- * Init status, with xhr inactive.
- */
- INIT: 0,
- /**
- * XHR being sent.
- */
- ACTIVE: 1,
- /**
- * The request was successful, after the request successfully completes.
- */
- SUCCESS: 2,
- /**
- * Errors due to a non-200 status code or other error conditions.
- */
- XHR_ERROR: 3,
- /**
- * Errors due to no data being returned.
- */
- NO_DATA: 4,
- /**
- * Errors due to corrupted or invalid data being received.
- */
- BAD_DATA: 5,
- /**
- * Errors due to the handler throwing an exception.
- */
- HANDLER_EXCEPTION: 6,
- /**
- * Errors due to a timeout.
- */
- TIMEOUT: 7,
- /**
- * The request is cancelled by the application.
- */
- CANCELLED: 8
- };
- /**
- * Returns whether response streaming is supported on this browser.
- *
- * @return {boolean} false if response streaming is not supported.
- */
- goog.net.streams.XhrStreamReader.isStreamingSupported = function() {
- if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(10)) {
- // No active-x due to security issues.
- return false;
- }
- if (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('420+')) {
- // Safari 3+
- // Older versions of Safari always receive null response in INTERACTIVE.
- return false;
- }
- if (goog.userAgent.OPERA && !goog.userAgent.WEBKIT) {
- // Old Opera fires readyState == INTERACTIVE once.
- // TODO(user): polling the buffer and check the exact Opera version
- return false;
- }
- return true;
- };
- /**
- * Returns a parser that supports the given content-type (mime) and
- * content-transfer-encoding.
- *
- * @return {?goog.net.streams.StreamParser} a parser or null if the content
- * type or transfer encoding is unsupported.
- * @private
- */
- goog.net.streams.XhrStreamReader.prototype.getParserByResponseHeader_ =
- function() {
- var contentType =
- this.xhr_.getStreamingResponseHeader(goog.net.XhrIo.CONTENT_TYPE_HEADER);
- if (!contentType) {
- goog.log.warning(this.logger_, 'Content-Type unavailable: ' + contentType);
- return null;
- }
- contentType = contentType.toLowerCase();
- if (goog.string.startsWith(contentType, 'application/json')) {
- if (goog.string.startsWith(contentType, 'application/json+protobuf')) {
- return new PbJsonStreamParser();
- }
- return new goog.net.streams.JsonStreamParser();
- }
- if (goog.string.startsWith(contentType, 'application/x-protobuf')) {
- var encoding = this.xhr_.getStreamingResponseHeader(
- goog.net.XhrIo.CONTENT_TRANSFER_ENCODING);
- if (!encoding) {
- return new goog.net.streams.PbStreamParser();
- }
- if (encoding.toLowerCase() == 'base64') {
- return new Base64PbStreamParser();
- }
- goog.log.warning(
- this.logger_, 'Unsupported Content-Transfer-Encoding: ' + encoding +
- '\nFor Content-Type: ' + contentType);
- return null;
- }
- goog.log.warning(this.logger_, 'Unsupported Content-Type: ' + contentType);
- return null;
- };
- /**
- * Returns the XHR request object.
- *
- * @return {goog.net.XhrIo} The XHR object associated with this reader, or
- * null if the reader has been cleared.
- */
- goog.net.streams.XhrStreamReader.prototype.getXhr = function() {
- return this.xhr_;
- };
- /**
- * Gets the current stream status.
- *
- * @return {!goog.net.streams.XhrStreamReader.Status} The stream status.
- */
- goog.net.streams.XhrStreamReader.prototype.getStatus = function() {
- return this.status_;
- };
- /**
- * Sets the status handler.
- *
- * @param {function()} handler The handler for any status change.
- */
- goog.net.streams.XhrStreamReader.prototype.setStatusHandler = function(
- handler) {
- this.statusHandler_ = handler;
- };
- /**
- * Sets the data handler.
- *
- * @param {function(!Array<!Object>)} handler The handler for new data.
- */
- goog.net.streams.XhrStreamReader.prototype.setDataHandler = function(handler) {
- this.dataHandler_ = handler;
- };
- /**
- * Handles XHR readystatechange events.
- *
- * TODO(user): throttling may be needed.
- *
- * @param {!goog.events.Event} event The event.
- * @private
- */
- goog.net.streams.XhrStreamReader.prototype.readyStateChangeHandler_ = function(
- event) {
- var xhr = /** @type {goog.net.XhrIo} */ (event.target);
- try {
- if (xhr == this.xhr_) {
- this.onReadyStateChanged_();
- } else {
- goog.log.warning(this.logger_, 'Called back with an unexpected xhr.');
- }
- } catch (ex) {
- goog.log.error(
- this.logger_, 'readyStateChangeHandler_ thrown exception' +
- ' ' + ex);
- // no rethrow
- this.updateStatus_(
- goog.net.streams.XhrStreamReader.Status.HANDLER_EXCEPTION);
- this.clear_();
- }
- };
- /**
- * Called from readyStateChangeHandler_.
- *
- * @private
- */
- goog.net.streams.XhrStreamReader.prototype.onReadyStateChanged_ = function() {
- var readyState = this.xhr_.getReadyState();
- var errorCode = this.xhr_.getLastErrorCode();
- var statusCode = this.xhr_.getStatus();
- var responseText = this.xhr_.getResponseText();
- // we get partial results in browsers that support ready state interactive.
- // We also make sure that getResponseText is not null in interactive mode
- // before we continue.
- if (readyState < goog.net.XmlHttp.ReadyState.INTERACTIVE ||
- readyState == goog.net.XmlHttp.ReadyState.INTERACTIVE && !responseText) {
- return;
- }
- // TODO(user): white-list other 2xx responses with application payload
- var successful =
- (statusCode == goog.net.HttpStatus.OK ||
- statusCode == goog.net.HttpStatus.PARTIAL_CONTENT);
- if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
- if (errorCode == goog.net.ErrorCode.TIMEOUT) {
- this.updateStatus_(goog.net.streams.XhrStreamReader.Status.TIMEOUT);
- } else if (errorCode == goog.net.ErrorCode.ABORT) {
- this.updateStatus_(goog.net.streams.XhrStreamReader.Status.CANCELLED);
- } else if (!successful) {
- this.updateStatus_(goog.net.streams.XhrStreamReader.Status.XHR_ERROR);
- }
- }
- if (successful && !responseText) {
- goog.log.warning(
- this.logger_, 'No response text for xhr ' + this.xhr_.getLastUri() +
- ' status ' + statusCode);
- }
- if (!this.parser_) {
- this.parser_ = this.getParserByResponseHeader_();
- if (this.parser_ == null) {
- this.updateStatus_(goog.net.streams.XhrStreamReader.Status.BAD_DATA);
- }
- }
- if (this.status_ > goog.net.streams.XhrStreamReader.Status.SUCCESS) {
- this.clear_();
- return;
- }
- // Parses and delivers any new data, with error status.
- if (responseText.length > this.pos_) {
- var newData = responseText.substr(this.pos_);
- this.pos_ = responseText.length;
- try {
- var messages = this.parser_.parse(newData);
- if (messages != null) {
- if (this.dataHandler_) {
- this.dataHandler_(messages);
- }
- }
- } catch (ex) {
- goog.log.error(
- this.logger_, 'Invalid response ' + ex + '\n' + responseText);
- this.updateStatus_(goog.net.streams.XhrStreamReader.Status.BAD_DATA);
- this.clear_();
- return;
- }
- }
- if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
- if (responseText.length == 0) {
- this.updateStatus_(goog.net.streams.XhrStreamReader.Status.NO_DATA);
- } else {
- this.updateStatus_(goog.net.streams.XhrStreamReader.Status.SUCCESS);
- }
- this.clear_();
- return;
- }
- this.updateStatus_(goog.net.streams.XhrStreamReader.Status.ACTIVE);
- };
- /**
- * Update the status and may call the handler.
- *
- * @param {!goog.net.streams.XhrStreamReader.Status} status The new status
- * @private
- */
- goog.net.streams.XhrStreamReader.prototype.updateStatus_ = function(status) {
- var current = this.status_;
- if (current != status) {
- this.status_ = status;
- if (this.statusHandler_) {
- this.statusHandler_();
- }
- }
- };
- /**
- * Clears after the XHR terminal state is reached.
- *
- * @private
- */
- goog.net.streams.XhrStreamReader.prototype.clear_ = function() {
- this.eventHandler_.removeAll();
- if (this.xhr_) {
- // clear out before aborting to avoid being reentered inside abort
- var xhr = this.xhr_;
- this.xhr_ = null;
- xhr.abort();
- xhr.dispose();
- }
- };
- }); // goog.scope
|