Skip to content

Commit

Permalink
Add Experimental Flight Infrastructure (#16398)
Browse files Browse the repository at this point in the history
* Add Flight Build and Unify HostFormat Config between Flight and Fizz

* Add basic resolution of models

* Add basic Flight fixture

Demonstrates the streaming protocol.

* Rename to flight-server to distinguish from the client parts

* Add Flight Client package and entry point

* Fix fixture
  • Loading branch information
sebmarkbage authored Oct 29, 2019
1 parent 6cd365c commit f4e974d
Show file tree
Hide file tree
Showing 75 changed files with 1,651 additions and 102 deletions.
84 changes: 84 additions & 0 deletions fixtures/flight-browser/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html style="width: 100%; height: 100%; overflow: hidden">
<head>
<meta charset="utf-8">
<title>Flight Example</title>
</head>
<body>
<h1>Flight Example</h1>
<div id="container">
<p>
To install React, follow the instructions on
<a href="https://github.com/facebook/react/">GitHub</a>.
</p>
<p>
If you can see this, React is <strong>not</strong> working right.
If you checked out the source from GitHub make sure to run <code>npm run build</code>.
</p>
</div>
<script src="../../build/dist/react.development.js"></script>
<script src="../../build/dist/react-dom.development.js"></script>
<script src="../../build/dist/react-dom-unstable-flight-client.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
<script type="text/babel">
function Text({children}) {
return <span>{children}</span>;
}
function HTML() {
return (
<div>
<Text>hello</Text>
<Text>world</Text>
</div>
);
}

let model = {
title: 'Title',
content: {
__html: <HTML />,
}
};

let stream = ReactFlightDOMClient.renderToReadableStream(model);
let response = new Response(stream, {
headers: {'Content-Type': 'text/html'},
});
display(response);

async function display(responseToDisplay) {
let blob = await responseToDisplay.blob();
let url = URL.createObjectURL(blob);
let response = await fetch(url);
let body = await response.body;

let reader = body.getReader();
let charsReceived = 0;
let decoder = new TextDecoder();

let json = '';
reader.read().then(function processChunk({ done, value }) {
if (done) {
renderResult(json);
return;
}
json += decoder.decode(value);
return reader.read().then(processChunk);
});
}

function Shell({ model }) {
return <div>
<h1>{model.title}</h1>
<div dangerouslySetInnerHTML={model.content} />
</div>;
}

function renderResult(json) {
let model = JSON.parse(json);
let container = document.getElementById('container');
ReactDOM.render(<Shell model={model} />, container);
}
</script>
</body>
</html>
7 changes: 7 additions & 0 deletions packages/react-dom/npm/unstable-flight-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-dom-unstable-flight-client.production.min.js');
} else {
module.exports = require('./cjs/react-dom-unstable-flight-client.development.js');
}
7 changes: 7 additions & 0 deletions packages/react-dom/npm/unstable-flight-server.browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-dom-unstable-flight-server.browser.production.min.js');
} else {
module.exports = require('./cjs/react-dom-unstable-flight-server.browser.development.js');
}
3 changes: 3 additions & 0 deletions packages/react-dom/npm/unstable-flight-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports = require('./unstable-flight-server.node');
7 changes: 7 additions & 0 deletions packages/react-dom/npm/unstable-flight-server.node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-dom-unstable-flight-server.node.production.min.js');
} else {
module.exports = require('./cjs/react-dom-unstable-flight-server.node.development.js');
}
6 changes: 5 additions & 1 deletion packages/react-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,17 @@
"unstable-fizz.js",
"unstable-fizz.browser.js",
"unstable-fizz.node.js",
"unstable-flight-server.js",
"unstable-flight-server.browser.js",
"unstable-flight-server.node.js",
"unstable-native-dependencies.js",
"cjs/",
"umd/"
],
"browser": {
"./server.js": "./server.browser.js",
"./unstable-fizz.js": "./unstable-fizz.browser.js"
"./unstable-fizz.js": "./unstable-fizz.browser.js",
"./unstable-flight-server.js": "./unstable-flight-server.browser.js"
},
"browserify": {
"transform": [
Expand Down
34 changes: 34 additions & 0 deletions packages/react-dom/src/client/flight/ReactFlightDOMClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import type {ReactModel} from 'react-flight/src/ReactFlightClient';

import {
createRequest,
startWork,
startFlowing,
} from 'react-flight/inline.dom-browser';

function renderToReadableStream(model: ReactModel): ReadableStream {
let request;
return new ReadableStream({
start(controller) {
request = createRequest(model, controller);
startWork(request);
},
pull(controller) {
startFlowing(request, controller.desiredSize);
},
cancel(reason) {},
});
}

export default {
renderToReadableStream,
};
50 changes: 50 additions & 0 deletions packages/react-dom/src/client/flight/ReactFlightDOMHostConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

export type Destination = ReadableStreamController;

export function scheduleWork(callback: () => void) {
callback();
}

export function flushBuffered(destination: Destination) {
// WHATWG Streams do not yet have a way to flush the underlying
// transform streams. https://github.com/whatwg/streams/issues/960
}

export function beginWriting(destination: Destination) {}

export function writeChunk(destination: Destination, buffer: Uint8Array) {
destination.enqueue(buffer);
}

export function completeWriting(destination: Destination) {}

export function close(destination: Destination) {
destination.close();
}

const textEncoder = new TextEncoder();

export function convertStringToBuffer(content: string): Uint8Array {
return textEncoder.encode(content);
}

export function formatChunkAsString(type: string, props: Object): string {
let str = '<' + type + '>';
if (typeof props.children === 'string') {
str += props.children;
}
str += '</' + type + '>';
return str;
}

export function formatChunk(type: string, props: Object): Uint8Array {
return convertStringToBuffer(formatChunkAsString(type, props));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/

'use strict';

// Polyfills for test environment
global.ReadableStream = require('@mattiasbuelens/web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;

let React;
let ReactFlightDOMServer;

describe('ReactFlightDOM', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactFlightDOMServer = require('react-dom/unstable-flight-server.browser');
});

async function readResult(stream) {
let reader = stream.getReader();
let result = '';
while (true) {
let {done, value} = await reader.read();
if (done) {
return result;
}
result += Buffer.from(value).toString('utf8');
}
}

it('should resolve HTML', async () => {
function Text({children}) {
return <span>{children}</span>;
}
function HTML() {
return (
<div>
<Text>hello</Text>
<Text>world</Text>
</div>
);
}

let model = {
html: <HTML />,
};
let stream = ReactFlightDOMServer.renderToReadableStream(model);
jest.runAllTimers();
let result = JSON.parse(await readResult(stream));
expect(result).toEqual({
html: '<div><span>hello</span><span>world</span></div>',
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/

'use strict';

let Stream;
let React;
let ReactFlightDOMServer;

describe('ReactFlightDOM', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactFlightDOMServer = require('react-dom/unstable-flight-server');
Stream = require('stream');
});

function getTestWritable() {
let writable = new Stream.PassThrough();
writable.setEncoding('utf8');
writable.result = '';
writable.on('data', chunk => (writable.result += chunk));
return writable;
}

it('should resolve HTML', () => {
function Text({children}) {
return <span>{children}</span>;
}
function HTML() {
return (
<div>
<Text>hello</Text>
<Text>world</Text>
</div>
);
}

let writable = getTestWritable();
let model = {
html: <HTML />,
};
ReactFlightDOMServer.pipeToNodeWritable(model, writable);
jest.runAllTimers();
let result = JSON.parse(writable.result);
expect(result).toEqual({
html: '<div><span>hello</span><span>world</span></div>',
});
});
});
2 changes: 1 addition & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
createRequest,
startWork,
startFlowing,
} from 'react-stream/inline.dom-browser';
} from 'react-server/inline.dom-browser';

function renderToReadableStream(children: ReactNodeList): ReadableStream {
let request;
Expand Down
2 changes: 1 addition & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import type {ReactNodeList} from 'shared/ReactTypes';
import type {Writable} from 'stream';

import {createRequest, startWork, startFlowing} from 'react-stream/inline.dom';
import {createRequest, startWork, startFlowing} from 'react-server/inline.dom';

function createDrainHandler(destination, request) {
return () => startFlowing(request, 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@
* @flow
*/

import {convertStringToBuffer} from 'react-stream/src/ReactFizzHostConfig';
import {convertStringToBuffer} from 'react-server/src/ReactServerHostConfig';

export function formatChunk(type: string, props: Object): Uint8Array {
export function formatChunkAsString(type: string, props: Object): string {
let str = '<' + type + '>';
if (typeof props.children === 'string') {
str += props.children;
}
str += '</' + type + '>';
return convertStringToBuffer(str);
return str;
}

export function formatChunk(type: string, props: Object): Uint8Array {
return convertStringToBuffer(formatChunkAsString(type, props));
}
Loading

0 comments on commit f4e974d

Please sign in to comment.