Skip to content

Commit

Permalink
Allow advanced users to enforce JSON response types in XHRs.
Browse files Browse the repository at this point in the history
Add support for setting the XHR `responseType` property to `"json"`.  This
defines the response data as JSON and prevents the browser from handling other
types of data.  It allows JSON parsing to be done off of the main thread,
reducing CPU contention that can happen with large responses and low-power
devices; this can yield smoother animations during navigation, for example.
However, it prevents processing chunked responses on-the-fly (i.e.
pseudo-streaming), effectively eliminating most of the benefits of multipart
responses.  Guard this behavior behind an "advanced" config flag and provide
no default to prevent accidental usage.  Setting `advanced-response-type-json`
to `true` will enable the feature.

Closes youtube#317.
  • Loading branch information
nicksay committed Apr 13, 2015
1 parent b5e810e commit 4387d09
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 108 deletions.
61 changes: 46 additions & 15 deletions src/client/nav/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@ spf.nav.request.send = function(url, opt_options) {
onDone: handleComplete,
onTimeout: handleComplete
};
// As an advanced option, allow XHR requests to enforce JSON responses.
// This can make response parsing more efficient by reducing contention on
// the main thread (especially for very large responses), but as a
// side-effect, it removes the ability to parse chunked multipart responses
// on-the-fly.
if (spf.config.get('advanced-response-type-json')) {
xhrOpts.responseType = 'json';
}
var xhr;
if (options.method == 'POST') {
xhr = spf.net.xhr.post(requestUrl, options.postData, xhrOpts);
Expand Down Expand Up @@ -293,8 +301,10 @@ spf.nav.request.handleChunkFromXHR_ = function(url, options, chunking,
*/
spf.nav.request.handleCompleteFromXHR_ = function(url, options, timing,
chunking, xhr) {
spf.debug.debug('nav.request.handleCompleteFromXHR_ ',
url, {'extra': chunking.extra, 'complete': xhr.responseText});
spf.debug.debug('nav.request.handleCompleteFromXHR_ ', url,
xhr.responseType == 'json' ?
xhr.response :
{'extra': chunking.extra, 'complete': xhr.responseText});
// Record the timing information from the XHR.
if (xhr['timing']) {
for (var t in xhr['timing']) {
Expand Down Expand Up @@ -347,22 +357,43 @@ spf.nav.request.handleCompleteFromXHR_ = function(url, options, timing,
xhr, '', true);
}
}
// Always attempt a full parse with error handling.
// A multipart response parsed on-the-fly via chunking may be invalid JSON if
// the response is truncated early. (If truncated just after a token,
// the chunking.extra value will be empty and no additional chunk parsing
// will be done, but the overall response will stil be invalid.)

var parts;
try {
var parsed = spf.nav.response.parse(xhr.responseText);
parts = parsed.parts;
} catch (err) {
spf.debug.debug(' JSON parse failed');
if (options.onError) {
options.onError(url, err);
if (xhr.responseType == 'json') {
// If using the JSON `responseType`, parsing is complete and no chunking
// has been handled on-the-fly.
if (!xhr.response) {
spf.debug.debug(' JSON parse failed');
if (options.onError) {
options.onError(url, new Error('JSON response parsing failed'));
}
return;
}
parts = (typeof xhr.response.length == 'number') ?
xhr.response :
[xhr.response];
if (spf.config.get('experimental-parse-extract')) {
parts = spf.nav.response.extract(parts);
}
} else {
// Otherwise, parsing may need to be done. Always attempt a full parse with
// error handling. A multipart response parsed on-the-fly via chunking may
// be invalid JSON if the response is truncated early. (If truncated just
// after a token, the chunking.extra value will be empty and no additional
// chunk parsing will be done, but the overall response will stil be
// invalid.)
try {
var parsed = spf.nav.response.parse(xhr.responseText);
parts = parsed.parts;
} catch (err) {
spf.debug.debug(' JSON parse failed');
if (options.onError) {
options.onError(url, err);
}
return;
}
return;
}

if (options.onPart && parts.length > 1) {
// Only execute callbacks for parts that have not already been processed.
// In case there is an edge case where some parts were parsed on-the-fly
Expand Down
47 changes: 20 additions & 27 deletions src/client/nav/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,28 +89,17 @@ spf.nav.response.parse = function(text, opt_multipart, opt_lastDitch) {
}
}
if (spf.config.get('experimental-parse-extract')) {
for (var i = 0; i < parts.length; i++) {
parts[i] = spf.nav.response.extract(
/** @type {spf.SingleResponse} */(parts[i]));
}
parts = spf.nav.response.extract(parts);
}
return {
parts: /** @type {Array.<spf.SingleResponse>} */(parts),
extra: extra
};
} else {
var response = JSON.parse(text);
var parts;
if (typeof response.length == 'number') {
parts = response;
} else {
parts = [response];
}
var parts = (typeof response.length == 'number') ? response : [response];
if (spf.config.get('experimental-parse-extract')) {
for (var i = 0; i < parts.length; i++) {
parts[i] = spf.nav.response.extract(
/** @type {spf.SingleResponse} */(parts[i]));
}
parts = spf.nav.response.extract(parts);
}
return {
parts: /** @type {Array.<spf.SingleResponse>} */(parts),
Expand Down Expand Up @@ -506,23 +495,27 @@ spf.nav.response.completeAnimation_ = function(data) {
/**
* Extracts all resources from HTML in a SPF response.
*
* @param {spf.SingleResponse} response The SPF response object to extract.
* @return {spf.SingleResponse} The response, updated to have resources
* extracted from HTML strings. This does not create a new object and
* modifies the passed response in-place.
* @param {T} response The SPF response object to extract.
* @return {T} The response, updated to have resources extracted from HTML
* strings. This does not create a new object and modifies the passed
* response in-place.
* @template T
*/
spf.nav.response.extract = function(response) {
spf.debug.debug('spf.nav.response.extract', response);
if (response['head']) {
response['head'] = spf.nav.response.extract_(response['head']);
}
if (response['body']) {
for (var id in response['body']) {
response['body'][id] = spf.nav.response.extract_(response['body'][id]);
var parts = (typeof response.length == 'number') ? response : [response];
for (var i = 0, l = parts.length; i < l; i++) {
if (parts[i]['head']) {
parts[i]['head'] = spf.nav.response.extract_(parts[i]['head']);
}
if (parts[i]['body']) {
for (var id in parts[i]['body']) {
parts[i]['body'][id] = spf.nav.response.extract_(parts[i]['body'][id]);
}
}
if (parts[i]['foot']) {
parts[i]['foot'] = spf.nav.response.extract_(parts[i]['foot']);
}
}
if (response['foot']) {
response['foot'] = spf.nav.response.extract_(response['foot']);
}
return response;
};
Expand Down
Loading

0 comments on commit 4387d09

Please sign in to comment.