diff --git a/packages/react-server/core/ReactServerAgent.js b/packages/react-server/core/ReactServerAgent.js index bbc6267d7..b9d64e06f 100644 --- a/packages/react-server/core/ReactServerAgent.js +++ b/packages/react-server/core/ReactServerAgent.js @@ -1,4 +1,5 @@ var RLS = require('./util/RequestLocalStorage').getNamespace() +, Q = require("q") , Cache = require("./ReactServerAgent/Cache") , Request = require("./ReactServerAgent/Request") , Plugins = require("./ReactServerAgent/Plugins") @@ -10,8 +11,17 @@ function makeRequest (method, url) { return new Request(method, url, API.cache()); } +// REALLY don't want to accidentally cache data across requests on the server. +// We throw an error if `preloadDataForURL` is called server-side, but it's +// worth being doubly cautious here. +const DATA_BUNDLE_CACHE = SERVER_SIDE?Object.freeze({}):{}; +const DATA_BUNDLE_PARAMETER = '_react_server_data_bundle'; +const DATA_BUNDLE_OPTS = {[DATA_BUNDLE_PARAMETER]: 1}; + var API = { + DATA_BUNDLE_PARAMETER, + get (url, data) { var req = makeRequest('GET', url); if (data) req.query(data); @@ -97,6 +107,26 @@ var API = { Plugins.forResponse().add(pluginFunc); }, + preloadDataForURL (url) { + if (SERVER_SIDE) throw new Error("Can't preload server-side"); + if (!DATA_BUNDLE_CACHE[url]){ + DATA_BUNDLE_CACHE[url] = API._fetchDataBundle(url); + } + return DATA_BUNDLE_CACHE[url]; + }, + + _fetchDataBundle(url) { + return this.get(url, DATA_BUNDLE_OPTS).then(data => JSON.stringify(data.body)); + }, + + _rehydrateDataBundle(url) { + // If we don't have any then we can't use it. + if (!DATA_BUNDLE_CACHE[url]) return Q(); + + return DATA_BUNDLE_CACHE[url] + .then(data => API.cache().rehydrate(JSON.parse(data))); + }, + } diff --git a/packages/react-server/core/context/Navigator.js b/packages/react-server/core/context/Navigator.js index a2277e59b..83af0e2a3 100644 --- a/packages/react-server/core/context/Navigator.js +++ b/packages/react-server/core/context/Navigator.js @@ -4,6 +4,7 @@ var EventEmitter = require('events').EventEmitter, Router = require('routr'), Q = require('q'), History = require("../components/History"), + ReactServerAgent = require("../ReactServerAgent"), PageUtil = require("../util/PageUtil"); var _ = { @@ -65,7 +66,14 @@ class Navigator extends EventEmitter { // The promise returned from `startRoute()` will be rejected // if we're not going to proceed, so resources will be freed. // - this.startRoute(route, request, type).then(() => { + this + .startRoute(route, request, type) + + // If we've got a preload bundle let's inflate it and avoid + // firing off a bunch of xhr requests during `handleRoute`. + .then(() => ReactServerAgent._rehydrateDataBundle(request.getUrl())) + + .then(() => { if (this._ignoreCurrentNavigation){ // This is a one-time deal. this._ignoreCurrentNavigation = false; diff --git a/packages/react-server/core/renderMiddleware.js b/packages/react-server/core/renderMiddleware.js index d54526db6..fba63d141 100644 --- a/packages/react-server/core/renderMiddleware.js +++ b/packages/react-server/core/renderMiddleware.js @@ -205,6 +205,8 @@ function renderPage(req, res, context, start, page) { lifecycleMethods = fragmentLifecycle(); } else if (PageUtil.PageConfig.get('isRawResponse')){ lifecycleMethods = rawResponseLifecycle(); + } else if (req.query[ReactServerAgent.DATA_BUNDLE_PARAMETER]) { + lifecycleMethods = dataBundleLifecycle(); } else { lifecycleMethods = pageLifecycle(); } @@ -250,6 +252,16 @@ function fragmentLifecycle () { ]; } +function dataBundleLifecycle () { + return [ + Q(), // NOOP lead-in to prime the reduction + setDataBundleContentType, + writeDataBundle, + handleResponseComplete, + endResponse, + ]; +} + function pageLifecycle() { return [ Q(), // This is just a NOOP lead-in to prime the reduction. @@ -268,6 +280,10 @@ function setContentType(req, res, context, start, pageObject) { res.set('Content-Type', pageObject.getContentType()); } +function setDataBundleContentType(req, res) { + res.set('Content-Type', 'application/json'); +} + function writeHeader(req, res, context, start, pageObject) { res.type('html'); res.set('Transfer-Encoding', 'chunked'); @@ -736,6 +752,15 @@ function writeResponseData(req, res, context, start, page) { }); } +function writeDataBundle(req, res) { + + const cache = ReactServerAgent.cache(); + + return Q.allSettled( + cache.getPendingRequests().map(v => v.entry.dfd.promise) + ).then(() => res.write(JSON.stringify(cache.dehydrate()))); +} + function renderElement(res, element, context) { if (element.containerOpen || element.containerClose){