Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/third_party/closure/goog/net/streams/xhrstreamreader.js
1865 views
1
// Copyright 2015 The Closure Library Authors. All Rights Reserved.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
// http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS-IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
/**
16
* @fileoverview the XHR stream reader implements a low-level stream
17
* reader for handling a streamed XHR response body. The reader takes a
18
* StreamParser which may support JSON or any other formats as confirmed by
19
* the Content-Type of the response. The reader may be used as polyfill for
20
* different streams APIs such as Node streams or whatwg streams (Fetch).
21
*
22
* The first version of this implementation only covers functions necessary
23
* to support NodeReadableStream. In a later version, this reader will also
24
* be adapted to whatwg streams.
25
*
26
* For IE, only IE-10 and above are supported.
27
*
28
* TODO(user): xhr polling, stream timeout, CORS and preflight optimization.
29
*/
30
31
goog.provide('goog.net.streams.XhrStreamReader');
32
33
goog.require('goog.events.EventHandler');
34
goog.require('goog.log');
35
goog.require('goog.net.ErrorCode');
36
goog.require('goog.net.EventType');
37
goog.require('goog.net.HttpStatus');
38
goog.require('goog.net.XhrIo');
39
goog.require('goog.net.XmlHttp');
40
goog.require('goog.net.streams.Base64PbStreamParser');
41
goog.require('goog.net.streams.JsonStreamParser');
42
goog.require('goog.net.streams.PbJsonStreamParser');
43
goog.require('goog.net.streams.PbStreamParser');
44
goog.require('goog.string');
45
goog.require('goog.userAgent');
46
47
goog.scope(function() {
48
49
var Base64PbStreamParser =
50
goog.module.get('goog.net.streams.Base64PbStreamParser');
51
var PbJsonStreamParser = goog.module.get('goog.net.streams.PbJsonStreamParser');
52
53
54
/**
55
* The XhrStreamReader class.
56
*
57
* The caller must check isStreamingSupported() first.
58
*
59
* @param {!goog.net.XhrIo} xhr The XhrIo object with its response body to
60
* be handled by NodeReadableStream.
61
* @constructor
62
* @struct
63
* @final
64
* @package
65
*/
66
goog.net.streams.XhrStreamReader = function(xhr) {
67
/**
68
* @const
69
* @private {?goog.log.Logger} the logger.
70
*/
71
this.logger_ = goog.log.getLogger('goog.net.streams.XhrStreamReader');
72
73
/**
74
* The xhr object passed by the application.
75
*
76
* @private {?goog.net.XhrIo} the XHR object for the stream.
77
*/
78
this.xhr_ = xhr;
79
80
/**
81
* To be initialized with the correct content-type.
82
*
83
* @private {?goog.net.streams.StreamParser} the parser for the stream.
84
*/
85
this.parser_ = null;
86
87
/**
88
* The position of where the next unprocessed data starts in the XHR
89
* response text.
90
* @private {number}
91
*/
92
this.pos_ = 0;
93
94
/**
95
* The status (error detail) of the current stream.
96
* @private {!goog.net.streams.XhrStreamReader.Status}
97
*/
98
this.status_ = goog.net.streams.XhrStreamReader.Status.INIT;
99
100
/**
101
* The handler for any status change event.
102
*
103
* @private {?function()} The call back to handle the XHR status change.
104
*/
105
this.statusHandler_ = null;
106
107
/**
108
* The handler for new response data.
109
*
110
* @private {?function(!Array<!Object>)} The call back to handle new
111
* response data, parsed as an array of atomic messages.
112
*/
113
this.dataHandler_ = null;
114
115
/**
116
* An object to keep track of event listeners.
117
*
118
* @private {!goog.events.EventHandler<!goog.net.streams.XhrStreamReader>}
119
*/
120
this.eventHandler_ = new goog.events.EventHandler(this);
121
122
// register the XHR event handler
123
this.eventHandler_.listen(
124
this.xhr_, goog.net.EventType.READY_STATE_CHANGE,
125
this.readyStateChangeHandler_);
126
};
127
128
129
/**
130
* Enum type for current stream status.
131
* @enum {number}
132
*/
133
goog.net.streams.XhrStreamReader.Status = {
134
/**
135
* Init status, with xhr inactive.
136
*/
137
INIT: 0,
138
139
/**
140
* XHR being sent.
141
*/
142
ACTIVE: 1,
143
144
/**
145
* The request was successful, after the request successfully completes.
146
*/
147
SUCCESS: 2,
148
149
/**
150
* Errors due to a non-200 status code or other error conditions.
151
*/
152
XHR_ERROR: 3,
153
154
/**
155
* Errors due to no data being returned.
156
*/
157
NO_DATA: 4,
158
159
/**
160
* Errors due to corrupted or invalid data being received.
161
*/
162
BAD_DATA: 5,
163
164
/**
165
* Errors due to the handler throwing an exception.
166
*/
167
HANDLER_EXCEPTION: 6,
168
169
/**
170
* Errors due to a timeout.
171
*/
172
TIMEOUT: 7,
173
174
/**
175
* The request is cancelled by the application.
176
*/
177
CANCELLED: 8
178
};
179
180
181
/**
182
* Returns whether response streaming is supported on this browser.
183
*
184
* @return {boolean} false if response streaming is not supported.
185
*/
186
goog.net.streams.XhrStreamReader.isStreamingSupported = function() {
187
if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(10)) {
188
// No active-x due to security issues.
189
return false;
190
}
191
192
if (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('420+')) {
193
// Safari 3+
194
// Older versions of Safari always receive null response in INTERACTIVE.
195
return false;
196
}
197
198
if (goog.userAgent.OPERA && !goog.userAgent.WEBKIT) {
199
// Old Opera fires readyState == INTERACTIVE once.
200
// TODO(user): polling the buffer and check the exact Opera version
201
return false;
202
}
203
204
return true;
205
};
206
207
208
/**
209
* Returns a parser that supports the given content-type (mime) and
210
* content-transfer-encoding.
211
*
212
* @return {?goog.net.streams.StreamParser} a parser or null if the content
213
* type or transfer encoding is unsupported.
214
* @private
215
*/
216
goog.net.streams.XhrStreamReader.prototype.getParserByResponseHeader_ =
217
function() {
218
var contentType =
219
this.xhr_.getStreamingResponseHeader(goog.net.XhrIo.CONTENT_TYPE_HEADER);
220
if (!contentType) {
221
goog.log.warning(this.logger_, 'Content-Type unavailable: ' + contentType);
222
return null;
223
}
224
contentType = contentType.toLowerCase();
225
226
if (goog.string.startsWith(contentType, 'application/json')) {
227
if (goog.string.startsWith(contentType, 'application/json+protobuf')) {
228
return new PbJsonStreamParser();
229
}
230
return new goog.net.streams.JsonStreamParser();
231
}
232
233
if (goog.string.startsWith(contentType, 'application/x-protobuf')) {
234
var encoding = this.xhr_.getStreamingResponseHeader(
235
goog.net.XhrIo.CONTENT_TRANSFER_ENCODING);
236
if (!encoding) {
237
return new goog.net.streams.PbStreamParser();
238
}
239
if (encoding.toLowerCase() == 'base64') {
240
return new Base64PbStreamParser();
241
}
242
goog.log.warning(
243
this.logger_, 'Unsupported Content-Transfer-Encoding: ' + encoding +
244
'\nFor Content-Type: ' + contentType);
245
return null;
246
}
247
248
goog.log.warning(this.logger_, 'Unsupported Content-Type: ' + contentType);
249
return null;
250
};
251
252
253
/**
254
* Returns the XHR request object.
255
*
256
* @return {goog.net.XhrIo} The XHR object associated with this reader, or
257
* null if the reader has been cleared.
258
*/
259
goog.net.streams.XhrStreamReader.prototype.getXhr = function() {
260
return this.xhr_;
261
};
262
263
264
/**
265
* Gets the current stream status.
266
*
267
* @return {!goog.net.streams.XhrStreamReader.Status} The stream status.
268
*/
269
goog.net.streams.XhrStreamReader.prototype.getStatus = function() {
270
return this.status_;
271
};
272
273
274
/**
275
* Sets the status handler.
276
*
277
* @param {function()} handler The handler for any status change.
278
*/
279
goog.net.streams.XhrStreamReader.prototype.setStatusHandler = function(
280
handler) {
281
this.statusHandler_ = handler;
282
};
283
284
285
/**
286
* Sets the data handler.
287
*
288
* @param {function(!Array<!Object>)} handler The handler for new data.
289
*/
290
goog.net.streams.XhrStreamReader.prototype.setDataHandler = function(handler) {
291
this.dataHandler_ = handler;
292
};
293
294
295
/**
296
* Handles XHR readystatechange events.
297
*
298
* TODO(user): throttling may be needed.
299
*
300
* @param {!goog.events.Event} event The event.
301
* @private
302
*/
303
goog.net.streams.XhrStreamReader.prototype.readyStateChangeHandler_ = function(
304
event) {
305
var xhr = /** @type {goog.net.XhrIo} */ (event.target);
306
307
308
try {
309
if (xhr == this.xhr_) {
310
this.onReadyStateChanged_();
311
} else {
312
goog.log.warning(this.logger_, 'Called back with an unexpected xhr.');
313
}
314
} catch (ex) {
315
goog.log.error(
316
this.logger_, 'readyStateChangeHandler_ thrown exception' +
317
' ' + ex);
318
// no rethrow
319
this.updateStatus_(
320
goog.net.streams.XhrStreamReader.Status.HANDLER_EXCEPTION);
321
this.clear_();
322
}
323
};
324
325
326
/**
327
* Called from readyStateChangeHandler_.
328
*
329
* @private
330
*/
331
goog.net.streams.XhrStreamReader.prototype.onReadyStateChanged_ = function() {
332
var readyState = this.xhr_.getReadyState();
333
var errorCode = this.xhr_.getLastErrorCode();
334
var statusCode = this.xhr_.getStatus();
335
var responseText = this.xhr_.getResponseText();
336
337
// we get partial results in browsers that support ready state interactive.
338
// We also make sure that getResponseText is not null in interactive mode
339
// before we continue.
340
if (readyState < goog.net.XmlHttp.ReadyState.INTERACTIVE ||
341
readyState == goog.net.XmlHttp.ReadyState.INTERACTIVE && !responseText) {
342
return;
343
}
344
345
// TODO(user): white-list other 2xx responses with application payload
346
var successful =
347
(statusCode == goog.net.HttpStatus.OK ||
348
statusCode == goog.net.HttpStatus.PARTIAL_CONTENT);
349
350
if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
351
if (errorCode == goog.net.ErrorCode.TIMEOUT) {
352
this.updateStatus_(goog.net.streams.XhrStreamReader.Status.TIMEOUT);
353
} else if (errorCode == goog.net.ErrorCode.ABORT) {
354
this.updateStatus_(goog.net.streams.XhrStreamReader.Status.CANCELLED);
355
} else if (!successful) {
356
this.updateStatus_(goog.net.streams.XhrStreamReader.Status.XHR_ERROR);
357
}
358
}
359
360
if (successful && !responseText) {
361
goog.log.warning(
362
this.logger_, 'No response text for xhr ' + this.xhr_.getLastUri() +
363
' status ' + statusCode);
364
}
365
366
if (!this.parser_) {
367
this.parser_ = this.getParserByResponseHeader_();
368
if (this.parser_ == null) {
369
this.updateStatus_(goog.net.streams.XhrStreamReader.Status.BAD_DATA);
370
}
371
}
372
373
if (this.status_ > goog.net.streams.XhrStreamReader.Status.SUCCESS) {
374
this.clear_();
375
return;
376
}
377
378
// Parses and delivers any new data, with error status.
379
if (responseText.length > this.pos_) {
380
var newData = responseText.substr(this.pos_);
381
this.pos_ = responseText.length;
382
try {
383
var messages = this.parser_.parse(newData);
384
if (messages != null) {
385
if (this.dataHandler_) {
386
this.dataHandler_(messages);
387
}
388
}
389
} catch (ex) {
390
goog.log.error(
391
this.logger_, 'Invalid response ' + ex + '\n' + responseText);
392
this.updateStatus_(goog.net.streams.XhrStreamReader.Status.BAD_DATA);
393
this.clear_();
394
return;
395
}
396
}
397
398
if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
399
if (responseText.length == 0) {
400
this.updateStatus_(goog.net.streams.XhrStreamReader.Status.NO_DATA);
401
} else {
402
this.updateStatus_(goog.net.streams.XhrStreamReader.Status.SUCCESS);
403
}
404
this.clear_();
405
return;
406
}
407
408
this.updateStatus_(goog.net.streams.XhrStreamReader.Status.ACTIVE);
409
};
410
411
412
/**
413
* Update the status and may call the handler.
414
*
415
* @param {!goog.net.streams.XhrStreamReader.Status} status The new status
416
* @private
417
*/
418
goog.net.streams.XhrStreamReader.prototype.updateStatus_ = function(status) {
419
var current = this.status_;
420
if (current != status) {
421
this.status_ = status;
422
if (this.statusHandler_) {
423
this.statusHandler_();
424
}
425
}
426
};
427
428
429
/**
430
* Clears after the XHR terminal state is reached.
431
*
432
* @private
433
*/
434
goog.net.streams.XhrStreamReader.prototype.clear_ = function() {
435
this.eventHandler_.removeAll();
436
437
if (this.xhr_) {
438
// clear out before aborting to avoid being reentered inside abort
439
var xhr = this.xhr_;
440
this.xhr_ = null;
441
xhr.abort();
442
xhr.dispose();
443
}
444
};
445
446
}); // goog.scope
447
448