From 68b2a2aafe3b596b4e52aa40da9465db7d41b86f Mon Sep 17 00:00:00 2001
From: Alex Nicksay <nicksay@google.com>
Date: Mon, 2 Mar 2015 01:51:06 -0500
Subject: [PATCH] Pass shared values when processing multipart responses.

This change allows values from one part of a multipart response to be handled
during processing of subsequent parts.

The motivating use case is to allow a response to specify a "name" to use during
CSS-based animation once and have it apply to all subsequent parts.
For example instead of:

```json
[{"name": "my-page", "body": {"id-1": "content-1"}},
 {"name": "my-page", "body": {"id-2": "content-2"}},
 {"name": "my-page", "body": {"id-3": "content-3"}}]
```

The following would now be processed as if the above were sent:

```json
[{"name": "my-page", "body": {"id-1": "content-1"}},
 {"body": {"id-2": "content-2"}},
 {"body": {"id-3": "content-3"}}]
```

Progress on #299.

SAVEPOINT

handle shared values

revert server

revert islated client changes

revert parts of spf.nav

revert parts of spf.nav.response
---
 src/client/nav/nav.js      | 20 ++++++-----
 src/client/nav/request.js  | 69 ++++++++++++++++++++++++++++----------
 src/client/nav/response.js | 10 ++++--
 3 files changed, 70 insertions(+), 29 deletions(-)

diff --git a/src/client/nav/nav.js b/src/client/nav/nav.js
index 7c2663d4..e1a7a34f 100644
--- a/src/client/nav/nav.js
+++ b/src/client/nav/nav.js
@@ -663,9 +663,10 @@ spf.nav.handleNavigateError_ = function(options, url, err) {
  * @param {spf.nav.Info} info The navigation info object.
  * @param {string} url The requested URL, without the SPF identifier.
  * @param {spf.SingleResponse} partial The partial response object.
+ * @param {!Object.<string>} shared A shared values object.
  * @private
  */
-spf.nav.handleNavigatePart_ = function(options, info, url, partial) {
+spf.nav.handleNavigatePart_ = function(options, info, url, partial, shared) {
   // Reload if the "part process" event is canceled.
   if (!spf.nav.dispatchPartProcess_(url, partial, options)) {
     spf.nav.reload(url, spf.nav.ReloadReason.PART_PROCESS_CANCELED);
@@ -685,7 +686,7 @@ spf.nav.handleNavigatePart_ = function(options, info, url, partial) {
   }
 
   try {
-    spf.nav.response.process(url, partial, info, function() {
+    spf.nav.response.process(url, partial, info, shared, function() {
       spf.nav.dispatchPartDone_(url, partial, options);
     });
   } catch (err) {
@@ -749,7 +750,7 @@ spf.nav.handleNavigateSuccess_ = function(options, info, url, response) {
     // so an empty object is used to ensure events/callbacks are properly
     // queued after existing ones from any ongoing part prcoessing.
     var r = /** @type {spf.SingleResponse} */ (multipart ? {} : response);
-    spf.nav.response.process(url, r, info, function() {
+    spf.nav.response.process(url, r, info, null, function() {
       // If this navigation was from history, attempt to scroll to the previous
       // position after all processing is complete.  This should not be done
       // earlier because the prevous position might rely on page width/height
@@ -1032,9 +1033,11 @@ spf.nav.handleLoadError_ = function(isPrefetch, options, info, url, err) {
  * @param {spf.nav.Info} info The navigation info object.
  * @param {string} url The requested URL, without the SPF identifier.
  * @param {spf.SingleResponse} partial The partial response object.
+ * @param {!Object.<string>} shared A shared values object.
  * @private
  */
-spf.nav.handleLoadPart_ = function(isPrefetch, options, info, url, partial) {
+spf.nav.handleLoadPart_ = function(isPrefetch, options, info, url, partial,
+                                   shared) {
   // Abort the load/prefetch if the "part process" callback is canceled.
   // Note: pass "true" to only execute callbacks and not dispatch events.
   if (!spf.nav.dispatchPartProcess_(url, partial, options, true)) {
@@ -1080,7 +1083,7 @@ spf.nav.handleLoadPart_ = function(isPrefetch, options, info, url, partial) {
   var processFn = isPrefetch ?
       spf.nav.response.preprocess :
       spf.nav.response.process;
-  processFn(url, partial, info, function() {
+  processFn(url, partial, info, shared, function() {
     // Note: pass "true" to only execute callbacks and not dispatch events.
     spf.nav.dispatchPartDone_(url, partial, options, true);
   });
@@ -1164,7 +1167,7 @@ spf.nav.handleLoadSuccess_ = function(isPrefetch, options, info, url,
     // so an empty object is used to ensure the callback is properly
     // queued after existing ones from any ongoing part prcoessing.
     var r = /** @type {spf.SingleResponse} */ (multipart ? {} : response);
-    processFn(url, r, info, function() {
+    processFn(url, r, info, null, function() {
       // Note: pass "true" to only execute callbacks and not dispatch events.
       spf.nav.dispatchDone_(url, response, options, true);
     });
@@ -1226,14 +1229,15 @@ spf.nav.process = function(response, opt_callback) {
   };
   if (multipart) {
     var parts = response['parts'];
+    var shared = response;  // Top-level properties act as shared values.
     for (var i = 0; i < parts.length; i++) {
       var fn = spf.bind(done, null, i, parts.length - 1);
-      spf.nav.response.process(url, parts[i], null, fn);
+      spf.nav.response.process(url, parts[i], null, shared, fn);
     }
   } else {
     response = /** @type {spf.SingleResponse} */ (response);
     var fn = spf.bind(done, null, 0, 0);
-    spf.nav.response.process(url, response, null, fn);
+    spf.nav.response.process(url, response, null, null, fn);
   }
 };
 
diff --git a/src/client/nav/request.js b/src/client/nav/request.js
index f871e812..458e154f 100644
--- a/src/client/nav/request.js
+++ b/src/client/nav/request.js
@@ -209,8 +209,9 @@ spf.nav.request.handleResponseFromCache_ = function(url, options, timing,
   }
   if (options.onPart && response['type'] == 'multipart') {
     var parts = response['parts'];
+    var shared = response;  // Top-level properties act as shared values.
     for (var i = 0; i < parts.length; i++) {
-      options.onPart(url, parts[i]);
+      options.onPart(url, parts[i], shared);
     }
   }
   spf.nav.request.done_(url, options, timing, response, updateCache);
@@ -269,10 +270,11 @@ spf.nav.request.handleChunkFromXHR_ = function(url, options, chunking,
     }
     return;
   }
-  if (options.onPart) {
-    for (var i = 0; i < parsed.parts.length; i++) {
-      spf.debug.debug('    parsed part', parsed.parts[i]);
-      options.onPart(url, parsed.parts[i]);
+  for (var i = 0; i < parsed.parts.length; i++) {
+    spf.debug.debug('    parsed part', parsed.parts[i]);
+    spf.nav.request.copySharedFrom_(chunking.shared, parsed.parts[i]);
+    if (options.onPart) {
+      options.onPart(url, parsed.parts[i], chunking.shared);
     }
   }
   chunking.complete = chunking.complete.concat(parsed.parts);
@@ -363,33 +365,27 @@ spf.nav.request.handleCompleteFromXHR_ = function(url, options, timing,
     }
     return;
   }
-  if (options.onPart && parts.length > 1) {
-    // Only execute callbacks for parts that have not already been processed.
+  if (parts.length > 1) {
     // In case there is an edge case where some parts were parsed on-the-fly
     // but the entire response needed a full parse here, start iteration where
     // the chunk processing left off.  This is mostly a safety measure and
     // the number of chunks processed here should be 0.
     for (var i = chunking.complete.length; i < parts.length; i++) {
       spf.debug.debug('    parsed part', parts[i]);
-      options.onPart(url, parts[i]);
+      spf.nav.request.copySharedFrom_(chunking.shared, parts[i]);
+      if (options.onPart) {
+        // Only execute callbacks for parts that have not been processed.
+        options.onPart(url, parts[i], chunking.shared);
+      }
     }
   }
   var response;
   if (parts.length > 1) {
-    var cacheType;
-    for (var i = 0, l = parts.length; i < l; i++) {
-      var part = parts[i];
-      if (part['cacheType']) {
-        cacheType = part['cacheType'];
-      }
-    }
     response = /** @type {spf.MultipartResponse} */ ({
       'parts': parts,
       'type': 'multipart'
     });
-    if (cacheType) {
-      response['cacheType'] = cacheType;
-    }
+    spf.nav.request.copySharedTo_(response, chunking.shared);
   } else if (parts.length == 1) {
     response = /** @type {spf.SingleResponse} */(parts[0]);
   } else {
@@ -431,6 +427,37 @@ spf.nav.request.done_ = function(url, options, timing, response, cache) {
 };
 
 
+/**
+ * Copy shared values from a response.
+ *
+ * @param {!Object.<string>} shared The shared values.
+ * @param {spf.SingleResponse} response The response.
+ * @private
+ */
+spf.nav.request.copySharedFrom_ = function(shared, response) {
+  if ('cacheType' in response) {
+    shared['cacheType'] = response['cacheType'];
+  }
+  if ('name' in response) {
+    shared['name'] = response['name'];
+  }
+};
+
+
+/**
+ * Copy shared values to a response.
+ *
+ * @param {spf.SingleResponse} response The response.
+ * @param {!Object.<string>} shared The shared values.
+ * @private
+ */
+spf.nav.request.copySharedTo_ = function(response, shared) {
+  for (var k in shared) {
+    response[k] = shared[k];
+  }
+};
+
+
 /**
  * @param {string} url The requested URL, without the SPF identifier.
  * @param {string|null|undefined} opt_current The current page's URL. Some
@@ -556,6 +583,12 @@ spf.nav.request.Chunking_ = function() {
    * @type {!Array}
    */
   this.complete = [];
+  /**
+   * Values for response fields that are present in one part but meant to be
+   * shared across all (e.g. cacheType, name).
+   * @type {!Object.<string>}
+   */
+  this.shared = {};
 };
 
 
diff --git a/src/client/nav/response.js b/src/client/nav/response.js
index 3b6b1a80..aa18cb6f 100644
--- a/src/client/nav/response.js
+++ b/src/client/nav/response.js
@@ -113,12 +113,14 @@ spf.nav.response.parse = function(text, opt_multipart, opt_lastDitch) {
  * @param {string} url The URL of the response being processed.
  * @param {spf.SingleResponse} response The SPF response object to process.
  * @param {spf.nav.Info=} opt_info The navigation info object.
+ * @param {Object.<string>=} opt_shared A shared values object.
  * @param {function(string, spf.SingleResponse)=} opt_callback Function to
  *     execute when processing is done; the first argument is `url`,
  *     the second argument is `response`.
  */
-spf.nav.response.process = function(url, response, opt_info, opt_callback) {
-  spf.debug.info('nav.response.process ', response, opt_info);
+spf.nav.response.process = function(url, response, opt_info, opt_shared,
+                                    opt_callback) {
+  spf.debug.info('nav.response.process ', response, opt_info, opt_shared);
 
   var isNavigate = opt_info && spf.string.startsWith(opt_info.type, 'navigate');
   var isReverse = opt_info && opt_info.reverse;
@@ -362,11 +364,13 @@ spf.nav.response.process = function(url, response, opt_info, opt_callback) {
  * @param {string} url The URL of the response being preprocessed.
  * @param {spf.SingleResponse} response The SPF response object to preprocess.
  * @param {spf.nav.Info=} opt_info The navigation info object.
+ * @param {Object.<string>=} opt_shared A shared values object.
  * @param {function(string, spf.SingleResponse)=} opt_callback Function to
  *     execute when preprocessing is done; the first argument is `url`,
  *     the second argument is `response`.
  */
-spf.nav.response.preprocess = function(url, response, opt_info, opt_callback) {
+spf.nav.response.preprocess = function(url, response, opt_info, opt_shared,
+                                       opt_callback) {
   spf.debug.info('nav.response.preprocess ', response);
   // Convert the URL to absolute, to be used for finding the task queue.
   var key = 'preprocess ' + spf.url.absolute(url);