Skip to content

Commit

Permalink
Callback-based renderer; passes tests and has v1 backwards compatibil…
Browse files Browse the repository at this point in the history
…ity. Might have a stack overflow bug, but haven't repro'ed yet. Also want to change the API to not require a hash promise.
  • Loading branch information
aickin committed Nov 11, 2015
1 parent 5a2312c commit 9159656
Show file tree
Hide file tree
Showing 14 changed files with 1,467 additions and 2 deletions.
2 changes: 2 additions & 0 deletions grunt/tasks/browserify.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ module.exports = function() {
debug: config.debug, // sourcemaps
standalone: config.standalone, // global
paths: paths,
builtins: {},
detectGlobals: false,
};

var bundle = browserify(options);
Expand Down
2 changes: 2 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ var babelOpts = {
_moduleMap: require('fbjs/module-map'),
};

babelOpts["_moduleMap"]["stream"] = "stream";

gulp.task('react:clean', function(cb) {
del([paths.react.lib], cb);
});
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"browserify": "^9.0.3",
"bundle-collapser": "^1.1.1",
"coffee-script": "^1.8.0",
"concat-stream": "^1.5.1",
"del": "^1.2.0",
"derequire": "^2.0.0",
"envify": "^3.0.0",
Expand Down
4 changes: 2 additions & 2 deletions src/React.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ assign(React, {
'ReactDOMServer',
'react-dom/server',
ReactDOMServer,
ReactDOMServer.renderToString
ReactDOMServer.renderToStringStream
),
renderToStaticMarkup: deprecated(
'renderToStaticMarkup',
'ReactDOMServer',
'react-dom/server',
ReactDOMServer,
ReactDOMServer.renderToStaticMarkup
ReactDOMServer.renderToStaticMarkupStream
),
});

Expand Down
3 changes: 3 additions & 0 deletions src/renderers/dom/ReactDOMServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

var ReactDefaultInjection = require('ReactDefaultInjection');
var ReactServerRendering = require('ReactServerRendering');
var ReactServerAsyncRendering = require('ReactServerAsyncRendering');
var ReactVersion = require('ReactVersion');

ReactDefaultInjection.inject();
Expand All @@ -21,6 +22,8 @@ var ReactDOMServer = {
renderToString: ReactServerRendering.renderToString,
renderToStaticMarkup: ReactServerRendering.renderToStaticMarkup,
version: ReactVersion,
renderToStringStream: ReactServerAsyncRendering.renderToStringStream,
renderToStaticMarkupStream: ReactServerAsyncRendering.renderToStaticMarkupStream
};

module.exports = ReactDOMServer;
247 changes: 247 additions & 0 deletions src/renderers/dom/server/ReactServerAsyncRendering.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @typechecks static-only
* @providesModule ReactServerAsyncRendering
*/
'use strict';

var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
var ReactElement = require('ReactElement');
var ReactInstanceHandles = require('ReactInstanceHandles');
var ReactMarkupChecksum = require('ReactMarkupChecksum');
var ReactServerBatchingStrategy = require('ReactServerBatchingStrategy');
var ReactServerRenderingTransaction =
require('ReactServerRenderingTransaction');
var ReactUpdates = require('ReactUpdates');

var emptyObject = require('emptyObject');
var instantiateReactComponent = require('instantiateReactComponent');
var invariant = require('invariant');

var rollingAdler32 = require("rollingAdler32");
var stream = require("stream");

// this is a pass through stream that can calculate the hash that is used to
// checksum react server-rendered elements.
class Adler32Stream extends stream.Transform {
constructor(options) {
super(options);
this.rollingHash = rollingAdler32("");
this.on("end", () => { this.done = true; })
}

_transform(chunk, encoding, next) {
this.rollingHash = rollingAdler32(chunk.toString("utf-8"), this.rollingHash);
this.push(chunk);
next();
}

// returns a promise of a hash that resolves when this readable piped into this is finished.
get hash() {
if (this.done) {
return Promise.resolve(this.rollingHash.hash());
}
return new Promise((resolve, reject) => {
this.on("end", () => {
resolve(this.rollingHash.hash());
});
});
}
}

class RenderStream extends stream.Readable {
constructor(componentInstance, id, transaction, context, options, maxStackDepth) {
super(options);
this.buffer = "";
this.componentInstance = componentInstance;
this.id = id;
this.transaction = transaction;
this.context = context;
this.maxStackDepth = maxStackDepth || 500;
this.nextTickCalls = 0;
}

_read(n) {
var bufferToPush;
// it's possible that the last chunk added bumped the buffer up to > 2 * n, which means we will
// need to go through multiple read calls to drain it down to < n.
if (this.done) {
this.push(null);
return;
}
if (this.buffer.length >= n) {
bufferToPush = this.buffer.substring(0, n);
this.buffer = this.buffer.substring(n);
this.push(bufferToPush);
return;
}
if (!this.continuation) {
this.stackDepth = 0;
// start the rendering chain.
this.componentInstance.mountComponentAsync(this.id, this.transaction, this.context,
(text, cb) => {
this.buffer += text;
if (this.buffer.length >= n) {
this.continuation = cb;
bufferToPush = this.buffer.substring(0, n);
this.buffer = this.buffer.substring(n);
this.push(bufferToPush);
} else {
// continue rendering until we have enough text to call this.push().
// sometimes do this as process.nextTick to get out of stack overflows.
if (this.stackDepth >= this.maxStackDepth) {
process.nextTick(cb);
} else {
this.stackDepth++;
cb();
this.stackDepth--;
}
}
},
() => {
// the rendering is finished; we should push out the last of the buffer.
this.done = true;
this.push(this.buffer);
})

} else {
// continue with the rendering.
this.continuation();
}
}
}

/**
* @param {ReactElement} element
* @return {string} the HTML markup
*/
function renderToStringStream(element, res, {syncBatching = false} = {}) {
invariant(
ReactElement.isValidElement(element),
'renderToStringStream(): You must pass a valid ReactElement.'
);

var transaction;

// NOTE that we never change this, which means that client rendering code cannot be used
// in conjunction with this code.
ReactUpdates.injection.injectBatchingStrategy(ReactServerBatchingStrategy);

var id = ReactInstanceHandles.createReactRootID();
transaction = ReactServerRenderingTransaction.getPooled(false);

if (res) {
return new Promise((resolve, reject) => {
console.warn("You are using version 0.1.x of the API, which has been deprecated. Please update your client code to use the 0.2.x API, which is based on streams.");
var hash = rollingAdler32("");
var readable = transaction.perform(function() {
var buffer = "";
var componentInstance = instantiateReactComponent(element, null);
componentInstance.mountComponentAsync(id, transaction, emptyObject,
(text, cb) => {
hash = rollingAdler32(text, hash);
buffer += text;
if (buffer.length >= 16 * 1024) {
res.write(buffer);
buffer = "";
process.nextTick(cb);
} else {
cb();
}
},
() => {
res.write(buffer);
ReactServerRenderingTransaction.release(transaction);
resolve(hash.hash());
});
return null;
}, null);
});
} else {
var readable = transaction.perform(function() {
var componentInstance = instantiateReactComponent(element, null);
return new RenderStream(componentInstance, id, transaction, emptyObject);
}, null);

readable.on("end", () => {
ReactServerRenderingTransaction.release(transaction);
// Revert to the DOM batching strategy since these two renderers
// currently share these stateful modules.
// NOTE: THIS SHOULD ONLY BE DONE IN TESTS OR OTHER ENVIRONMENTS KNOWN TO BE SYNCHRONOUS.
if (syncBatching) ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
});

// since Adler32Stream has a .hash property, this automagically adds that property to the result.
return readable.pipe(new Adler32Stream());
}
}

/**
* @param {ReactElement} element
* @return {string} the HTML markup, without the extra React ID and checksum
* (for generating static pages)
*/
function renderToStaticMarkupStream(element, res) {
invariant(
ReactElement.isValidElement(element),
'renderToStaticMarkupStream(): You must pass a valid ReactElement.'
);

var transaction;

// NOTE that we never change this, which means that client rendering code cannot be used
// in conjunction with this code.
ReactUpdates.injection.injectBatchingStrategy(ReactServerBatchingStrategy);

var id = ReactInstanceHandles.createReactRootID();
transaction = ReactServerRenderingTransaction.getPooled(true);

if (res) {
return new Promise((resolve, reject) => {
console.warn("You are using version 0.1.x of the API, which has been deprecated. Please update your client code to use the 0.2.x API, which is based on streams.");
let readable = transaction.perform(function() {
let buffer = "";
const componentInstance = instantiateReactComponent(element, null);
componentInstance.mountComponentAsync(id, transaction, emptyObject,
(text, cb) => {
buffer += text;
if (buffer.length >= 16 * 1024) {
res.write(buffer);
buffer = "";
process.nextTick(cb);
} else {
cb();
}
},
() => {
res.write(buffer);
ReactServerRenderingTransaction.release(transaction);
resolve();
});
return null;
}, null);
});
} else {
var readable = transaction.perform(function() {
var componentInstance = instantiateReactComponent(element, null);
return new RenderStream(componentInstance, id, transaction, emptyObject);
}, null);

readable.on("end", () => {
ReactServerRenderingTransaction.release(transaction);
});

return readable;
}
}

module.exports = {
renderToStringStream: renderToStringStream,
renderToStaticMarkupStream: renderToStaticMarkupStream,
};
Loading

0 comments on commit 9159656

Please sign in to comment.