[Flight] Add Client Infrastructure (#17234)
* Change demo to server * Expose client in package.json * Reorganize tests We don't want unit tests but instead test how both server and clients work together. So this merges server/client test files. * Fill in the client implementation a bit * Use new client in fixture * Add Promise/Uint8Array to lint rule I'll probably end up deleting these deps later but they're here for now.
This commit is contained in:
parent
36fd29f09f
commit
fadc97167f
|
@ -18,9 +18,12 @@
|
|||
</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-server.browser.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">
|
||||
let Suspense = React.Suspense;
|
||||
|
||||
function Text({children}) {
|
||||
return <span>{children}</span>;
|
||||
}
|
||||
|
@ -40,7 +43,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
let stream = ReactFlightDOMClient.renderToReadableStream(model);
|
||||
let stream = ReactFlightDOMServer.renderToReadableStream(model);
|
||||
let response = new Response(stream, {
|
||||
headers: {'Content-Type': 'text/html'},
|
||||
});
|
||||
|
@ -49,35 +52,35 @@
|
|||
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 data = ReactFlightDOMClient.readFromFetch(
|
||||
fetch(url)
|
||||
);
|
||||
// The client also supports XHR streaming.
|
||||
// var xhr = new XMLHttpRequest();
|
||||
// xhr.open('GET', url);
|
||||
// let data = ReactFlightDOMClient.readFromXHR(xhr);
|
||||
// xhr.send();
|
||||
|
||||
let json = '';
|
||||
reader.read().then(function processChunk({ done, value }) {
|
||||
if (done) {
|
||||
renderResult(json);
|
||||
return;
|
||||
}
|
||||
json += decoder.decode(value);
|
||||
return reader.read().then(processChunk);
|
||||
});
|
||||
renderResult(data);
|
||||
}
|
||||
|
||||
function Shell({ model }) {
|
||||
function Shell({ data }) {
|
||||
let model = data.model;
|
||||
return <div>
|
||||
<h1>{model.title}</h1>
|
||||
<div dangerouslySetInnerHTML={model.content} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
function renderResult(json) {
|
||||
let model = JSON.parse(json);
|
||||
function renderResult(data) {
|
||||
let container = document.getElementById('container');
|
||||
ReactDOM.render(<Shell model={model} />, container);
|
||||
ReactDOM.render(
|
||||
<Suspense fallback="Loading...">
|
||||
<Shell data={data} />
|
||||
</Suspense>,
|
||||
container
|
||||
);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
"unstable-fizz.js",
|
||||
"unstable-fizz.browser.js",
|
||||
"unstable-fizz.node.js",
|
||||
"unstable-flight-client.js",
|
||||
"unstable-flight-server.js",
|
||||
"unstable-flight-server.browser.js",
|
||||
"unstable-flight-server.node.js",
|
||||
|
|
|
@ -7,28 +7,80 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactModel} from 'react-flight/src/ReactFlightClient';
|
||||
import type {ReactModelRoot} from 'react-flight/src/ReactFlightClient';
|
||||
|
||||
import {
|
||||
createRequest,
|
||||
startWork,
|
||||
startFlowing,
|
||||
} from 'react-flight/inline.dom-browser';
|
||||
createResponse,
|
||||
getModelRoot,
|
||||
reportGlobalError,
|
||||
processStringChunk,
|
||||
processBinaryChunk,
|
||||
complete,
|
||||
} from 'react-flight/inline.dom';
|
||||
|
||||
function renderToReadableStream(model: ReactModel): ReadableStream {
|
||||
let request;
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
request = createRequest(model, controller);
|
||||
startWork(request);
|
||||
function startReadingFromStream(response, stream: ReadableStream): void {
|
||||
let reader = stream.getReader();
|
||||
function progress({done, value}) {
|
||||
if (done) {
|
||||
complete(response);
|
||||
return;
|
||||
}
|
||||
let buffer: Uint8Array = (value: any);
|
||||
processBinaryChunk(response, buffer, 0);
|
||||
return reader.read().then(progress, error);
|
||||
}
|
||||
function error(e) {
|
||||
reportGlobalError(response, e);
|
||||
}
|
||||
reader.read().then(progress, error);
|
||||
}
|
||||
|
||||
function readFromReadableStream<T>(stream: ReadableStream): ReactModelRoot<T> {
|
||||
let response = createResponse(stream);
|
||||
startReadingFromStream(response, stream);
|
||||
return getModelRoot(response);
|
||||
}
|
||||
|
||||
function readFromFetch<T>(
|
||||
promiseForResponse: Promise<Response>,
|
||||
): ReactModelRoot<T> {
|
||||
let response = createResponse(promiseForResponse);
|
||||
promiseForResponse.then(
|
||||
function(r) {
|
||||
startReadingFromStream(response, (r.body: any));
|
||||
},
|
||||
pull(controller) {
|
||||
startFlowing(request, controller.desiredSize);
|
||||
function(e) {
|
||||
reportGlobalError(response, e);
|
||||
},
|
||||
cancel(reason) {},
|
||||
});
|
||||
);
|
||||
return getModelRoot(response);
|
||||
}
|
||||
|
||||
function readFromXHR<T>(request: XMLHttpRequest): ReactModelRoot<T> {
|
||||
let response = createResponse(request);
|
||||
let processedLength = 0;
|
||||
function progress(e: ProgressEvent): void {
|
||||
let chunk = request.responseText;
|
||||
processStringChunk(response, chunk, processedLength);
|
||||
processedLength = chunk.length;
|
||||
}
|
||||
function load(e: ProgressEvent): void {
|
||||
progress(e);
|
||||
complete(response);
|
||||
}
|
||||
function error(e: ProgressEvent): void {
|
||||
reportGlobalError(response, new TypeError('Network error'));
|
||||
}
|
||||
request.addEventListener('progress', progress);
|
||||
request.addEventListener('load', load);
|
||||
request.addEventListener('error', error);
|
||||
request.addEventListener('abort', error);
|
||||
request.addEventListener('timeout', error);
|
||||
return getModelRoot(response);
|
||||
}
|
||||
|
||||
export default {
|
||||
renderToReadableStream,
|
||||
readFromXHR,
|
||||
readFromFetch,
|
||||
readFromReadableStream,
|
||||
};
|
||||
|
|
|
@ -7,44 +7,28 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
export type Destination = ReadableStreamController;
|
||||
export type Source = Promise<Response> | ReadableStream | XMLHttpRequest;
|
||||
|
||||
export function scheduleWork(callback: () => void) {
|
||||
callback();
|
||||
export type StringDecoder = TextDecoder;
|
||||
|
||||
export const supportsBinaryStreams = true;
|
||||
|
||||
export function createStringDecoder(): StringDecoder {
|
||||
return new TextDecoder();
|
||||
}
|
||||
|
||||
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
|
||||
const decoderOptions = {stream: true};
|
||||
|
||||
export function readPartialStringChunk(
|
||||
decoder: StringDecoder,
|
||||
buffer: Uint8Array,
|
||||
): string {
|
||||
return decoder.decode(buffer, decoderOptions);
|
||||
}
|
||||
|
||||
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));
|
||||
export function readFinalStringChunk(
|
||||
decoder: StringDecoder,
|
||||
buffer: Uint8Array,
|
||||
): string {
|
||||
return decoder.decode(buffer);
|
||||
}
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
/**
|
||||
* 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>',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,57 +0,0 @@
|
|||
/**
|
||||
* 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>',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,138 +7,115 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import type {Destination} from './ReactFlightClientHostConfig';
|
||||
import type {Source, StringDecoder} from './ReactFlightClientHostConfig';
|
||||
|
||||
import {
|
||||
scheduleWork,
|
||||
beginWriting,
|
||||
writeChunk,
|
||||
completeWriting,
|
||||
flushBuffered,
|
||||
close,
|
||||
convertStringToBuffer,
|
||||
formatChunkAsString,
|
||||
supportsBinaryStreams,
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from './ReactFlightClientHostConfig';
|
||||
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
|
||||
|
||||
export type ReactModel =
|
||||
| React$Element<any>
|
||||
| string
|
||||
| boolean
|
||||
| number
|
||||
| null
|
||||
| Iterable<ReactModel>
|
||||
| ReactModelObject;
|
||||
export type ReactModelRoot<T> = {|
|
||||
model: T,
|
||||
|};
|
||||
|
||||
type ReactJSONValue =
|
||||
| string
|
||||
| boolean
|
||||
| number
|
||||
| null
|
||||
| Array<ReactModel>
|
||||
| ReactModelObject;
|
||||
|
||||
type ReactModelObject = {
|
||||
+[key: string]: ReactModel,
|
||||
type OpaqueResponse = {
|
||||
source: Source,
|
||||
modelRoot: ReactModelRoot<any>,
|
||||
partialRow: string,
|
||||
stringDecoder: StringDecoder,
|
||||
rootPing: () => void,
|
||||
};
|
||||
|
||||
type OpaqueRequest = {
|
||||
destination: Destination,
|
||||
model: ReactModel,
|
||||
completedChunks: Array<Uint8Array>,
|
||||
flowing: boolean,
|
||||
};
|
||||
|
||||
export function createRequest(
|
||||
model: ReactModel,
|
||||
destination: Destination,
|
||||
): OpaqueRequest {
|
||||
return {destination, model, completedChunks: [], flowing: false};
|
||||
}
|
||||
|
||||
function resolveChildToHostFormat(child: ReactJSONValue): string {
|
||||
if (typeof child === 'string') {
|
||||
return child;
|
||||
} else if (typeof child === 'number') {
|
||||
return '' + child;
|
||||
} else if (typeof child === 'boolean' || child === null) {
|
||||
// Booleans are like null when they're React children.
|
||||
return '';
|
||||
} else if (Array.isArray(child)) {
|
||||
return (child: Array<ReactModel>)
|
||||
.map(c => resolveChildToHostFormat(resolveModelToJSON('', c)))
|
||||
.join('');
|
||||
} else {
|
||||
throw new Error('Object models are not valid as children of host nodes.');
|
||||
}
|
||||
}
|
||||
|
||||
function resolveElementToHostFormat(type: string, props: Object): string {
|
||||
let child = resolveModelToJSON('', props.children);
|
||||
let childString = resolveChildToHostFormat(child);
|
||||
return formatChunkAsString(
|
||||
type,
|
||||
Object.assign({}, props, {children: childString}),
|
||||
export function createResponse(source: Source): OpaqueResponse {
|
||||
let modelRoot = {};
|
||||
Object.defineProperty(
|
||||
modelRoot,
|
||||
'model',
|
||||
({
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
throw rootPromise;
|
||||
},
|
||||
}: any),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveModelToJSON(key: string, value: ReactModel): ReactJSONValue {
|
||||
while (value && value.$$typeof === REACT_ELEMENT_TYPE) {
|
||||
let element: React$Element<any> = (value: any);
|
||||
let type = element.type;
|
||||
let props = element.props;
|
||||
if (typeof type === 'function') {
|
||||
// This is a nested view model.
|
||||
value = type(props);
|
||||
continue;
|
||||
} else if (typeof type === 'string') {
|
||||
// This is a host element. E.g. HTML.
|
||||
return resolveElementToHostFormat(type, props);
|
||||
} else {
|
||||
throw new Error('Unsupported type.');
|
||||
}
|
||||
let rootPing;
|
||||
let rootPromise = new Promise(resolve => {
|
||||
rootPing = resolve;
|
||||
});
|
||||
|
||||
let response: OpaqueResponse = ({
|
||||
source,
|
||||
modelRoot,
|
||||
partialRow: '',
|
||||
rootPing,
|
||||
}: any);
|
||||
if (supportsBinaryStreams) {
|
||||
response.stringDecoder = createStringDecoder();
|
||||
}
|
||||
return value;
|
||||
return response;
|
||||
}
|
||||
|
||||
function performWork(request: OpaqueRequest): void {
|
||||
let rootModel = request.model;
|
||||
request.model = null;
|
||||
let json = JSON.stringify(rootModel, resolveModelToJSON);
|
||||
request.completedChunks.push(convertStringToBuffer(json));
|
||||
if (request.flowing) {
|
||||
flushCompletedChunks(request);
|
||||
}
|
||||
|
||||
flushBuffered(request.destination);
|
||||
}
|
||||
|
||||
function flushCompletedChunks(request: OpaqueRequest) {
|
||||
let destination = request.destination;
|
||||
let chunks = request.completedChunks;
|
||||
request.completedChunks = [];
|
||||
|
||||
beginWriting(destination);
|
||||
try {
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
let chunk = chunks[i];
|
||||
writeChunk(destination, chunk);
|
||||
}
|
||||
} finally {
|
||||
completeWriting(destination);
|
||||
}
|
||||
close(destination);
|
||||
}
|
||||
|
||||
export function startWork(request: OpaqueRequest): void {
|
||||
request.flowing = true;
|
||||
scheduleWork(() => performWork(request));
|
||||
}
|
||||
|
||||
export function startFlowing(
|
||||
request: OpaqueRequest,
|
||||
desiredBytes: number,
|
||||
// Report that any missing chunks in the model is now going to throw this
|
||||
// error upon read. Also notify any pending promises.
|
||||
export function reportGlobalError(
|
||||
response: OpaqueResponse,
|
||||
error: Error,
|
||||
): void {
|
||||
request.flowing = false;
|
||||
flushCompletedChunks(request);
|
||||
Object.defineProperty(
|
||||
response.modelRoot,
|
||||
'model',
|
||||
({
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
throw error;
|
||||
},
|
||||
}: any),
|
||||
);
|
||||
response.rootPing();
|
||||
}
|
||||
|
||||
export function processStringChunk(
|
||||
response: OpaqueResponse,
|
||||
chunk: string,
|
||||
offset: number,
|
||||
): void {
|
||||
response.partialRow += chunk.substr(offset);
|
||||
}
|
||||
|
||||
export function processBinaryChunk(
|
||||
response: OpaqueResponse,
|
||||
chunk: Uint8Array,
|
||||
offset: number,
|
||||
): void {
|
||||
if (!supportsBinaryStreams) {
|
||||
throw new Error("This environment don't support binary chunks.");
|
||||
}
|
||||
response.partialRow += readPartialStringChunk(response.stringDecoder, chunk);
|
||||
}
|
||||
|
||||
let emptyBuffer = new Uint8Array(0);
|
||||
export function complete(response: OpaqueResponse): void {
|
||||
if (supportsBinaryStreams) {
|
||||
// This should never be needed since we're expected to have complete
|
||||
// code units at the end of JSON.
|
||||
response.partialRow += readFinalStringChunk(
|
||||
response.stringDecoder,
|
||||
emptyBuffer,
|
||||
);
|
||||
}
|
||||
let modelRoot = response.modelRoot;
|
||||
let model = JSON.parse(response.partialRow);
|
||||
Object.defineProperty(modelRoot, 'model', {
|
||||
value: model,
|
||||
});
|
||||
response.rootPing();
|
||||
}
|
||||
|
||||
export function getModelRoot<T>(response: OpaqueResponse): ReactModelRoot<T> {
|
||||
return response.modelRoot;
|
||||
}
|
||||
|
|
|
@ -11,13 +11,15 @@
|
|||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactNoopFlightServer;
|
||||
let ReactNoopFlightClient;
|
||||
|
||||
describe('ReactFlightClient', () => {
|
||||
describe('ReactFlight', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
React = require('react');
|
||||
ReactNoopFlightServer = require('react-noop-renderer/flight-server');
|
||||
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
|
||||
});
|
||||
|
||||
|
@ -30,9 +32,11 @@ describe('ReactFlightClient', () => {
|
|||
bar: [<Bar text="a" />, <Bar text="b" />],
|
||||
};
|
||||
}
|
||||
let result = ReactNoopFlightClient.render({
|
||||
let transport = ReactNoopFlightServer.render({
|
||||
foo: <Foo />,
|
||||
});
|
||||
expect(result).toEqual([{foo: {bar: ['A', 'B']}}]);
|
||||
let root = ReactNoopFlightClient.read(transport);
|
||||
let model = root.model;
|
||||
expect(model).toEqual({foo: {bar: ['A', 'B']}});
|
||||
});
|
||||
});
|
|
@ -24,14 +24,10 @@
|
|||
// really an argument to a top-level wrapping function.
|
||||
|
||||
declare var $$$hostConfig: any;
|
||||
export opaque type Destination = mixed; // eslint-disable-line no-undef
|
||||
export opaque type Source = mixed; // eslint-disable-line no-undef
|
||||
export opaque type StringDecoder = mixed; // eslint-disable-line no-undef
|
||||
|
||||
export const formatChunkAsString = $$$hostConfig.formatChunkAsString;
|
||||
export const formatChunk = $$$hostConfig.formatChunk;
|
||||
export const scheduleWork = $$$hostConfig.scheduleWork;
|
||||
export const beginWriting = $$$hostConfig.beginWriting;
|
||||
export const writeChunk = $$$hostConfig.writeChunk;
|
||||
export const completeWriting = $$$hostConfig.completeWriting;
|
||||
export const flushBuffered = $$$hostConfig.flushBuffered;
|
||||
export const close = $$$hostConfig.close;
|
||||
export const convertStringToBuffer = $$$hostConfig.convertStringToBuffer;
|
||||
export const supportsBinaryStreams = $$$hostConfig.supportsBinaryStreams;
|
||||
export const createStringDecoder = $$$hostConfig.createStringDecoder;
|
||||
export const readPartialStringChunk = $$$hostConfig.readPartialStringChunk;
|
||||
export const readFinalStringChunk = $$$hostConfig.readFinalStringChunk;
|
||||
|
|
|
@ -14,41 +14,30 @@
|
|||
* environment.
|
||||
*/
|
||||
|
||||
import type {ReactModel} from 'react-flight/inline-typed';
|
||||
import type {ReactModelRoot} from 'react-flight/inline-typed';
|
||||
|
||||
import ReactFlightClient from 'react-flight';
|
||||
|
||||
type Destination = Array<string>;
|
||||
type Source = Array<string>;
|
||||
|
||||
const ReactNoopFlightClient = ReactFlightClient({
|
||||
scheduleWork(callback: () => void) {
|
||||
callback();
|
||||
},
|
||||
beginWriting(destination: Destination): void {},
|
||||
writeChunk(destination: Destination, buffer: Uint8Array): void {
|
||||
destination.push(JSON.parse(Buffer.from((buffer: any)).toString('utf8')));
|
||||
},
|
||||
completeWriting(destination: Destination): void {},
|
||||
close(destination: Destination): void {},
|
||||
flushBuffered(destination: Destination): void {},
|
||||
convertStringToBuffer(content: string): Uint8Array {
|
||||
return Buffer.from(content, 'utf8');
|
||||
},
|
||||
formatChunkAsString(type: string, props: Object): string {
|
||||
return JSON.stringify({type, props});
|
||||
},
|
||||
formatChunk(type: string, props: Object): Uint8Array {
|
||||
return Buffer.from(JSON.stringify({type, props}), 'utf8');
|
||||
},
|
||||
const {
|
||||
createResponse,
|
||||
getModelRoot,
|
||||
processStringChunk,
|
||||
complete,
|
||||
} = ReactFlightClient({
|
||||
supportsBinaryStreams: false,
|
||||
});
|
||||
|
||||
function render(model: ReactModel): Destination {
|
||||
let destination: Destination = [];
|
||||
let request = ReactNoopFlightClient.createRequest(model, destination);
|
||||
ReactNoopFlightClient.startWork(request);
|
||||
return destination;
|
||||
function read<T>(source: Source): ReactModelRoot<T> {
|
||||
let response = createResponse(source);
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
processStringChunk(response, source[i], 0);
|
||||
}
|
||||
complete(response);
|
||||
return getModelRoot(response);
|
||||
}
|
||||
|
||||
export default {
|
||||
render,
|
||||
read,
|
||||
};
|
||||
|
|
|
@ -26,7 +26,7 @@ const ReactNoopFlightServer = ReactFlightStreamer({
|
|||
},
|
||||
beginWriting(destination: Destination): void {},
|
||||
writeChunk(destination: Destination, buffer: Uint8Array): void {
|
||||
destination.push(JSON.parse(Buffer.from((buffer: any)).toString('utf8')));
|
||||
destination.push(Buffer.from((buffer: any)).toString('utf8'));
|
||||
},
|
||||
completeWriting(destination: Destination): void {},
|
||||
close(destination: Destination): void {},
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
/**
|
||||
* 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 React;
|
||||
let ReactNoopFlight;
|
||||
|
||||
describe('ReactFlightServer', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
React = require('react');
|
||||
ReactNoopFlight = require('react-noop-renderer/flight-server');
|
||||
});
|
||||
|
||||
it('can resolve a model', () => {
|
||||
function Bar({text}) {
|
||||
return text.toUpperCase();
|
||||
}
|
||||
function Foo() {
|
||||
return {
|
||||
bar: [<Bar text="a" />, <Bar text="b" />],
|
||||
};
|
||||
}
|
||||
let result = ReactNoopFlight.render({
|
||||
foo: <Foo />,
|
||||
});
|
||||
expect(result).toEqual([{foo: {bar: ['A', 'B']}}]);
|
||||
});
|
||||
});
|
|
@ -28,6 +28,10 @@ module.exports = {
|
|||
SharedArrayBuffer: true,
|
||||
Int32Array: true,
|
||||
ArrayBuffer: true,
|
||||
|
||||
// Flight
|
||||
Uint8Array: true,
|
||||
Promise: true,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 5,
|
||||
|
|
|
@ -29,6 +29,10 @@ module.exports = {
|
|||
SharedArrayBuffer: true,
|
||||
Int32Array: true,
|
||||
ArrayBuffer: true,
|
||||
|
||||
// Flight
|
||||
Uint8Array: true,
|
||||
Promise: true,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 5,
|
||||
|
|
|
@ -31,6 +31,10 @@ module.exports = {
|
|||
SharedArrayBuffer: true,
|
||||
Int32Array: true,
|
||||
ArrayBuffer: true,
|
||||
|
||||
// Flight
|
||||
Uint8Array: true,
|
||||
Promise: true,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 5,
|
||||
|
|
Loading…
Reference in New Issue