mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
[Flight] Add Debug Channel option for stateful connection to the backend in DEV (#33627)
This adds plumbing for opening a stream from the Flight Client to the Flight Server so it can ask for more data on-demand. In this mode, the Flight Server keeps the connection open as long as the client is still alive and there's more objects to load. It retains any depth limited objects so that they can be asked for later. In this first PR it just releases the object when it's discovered on the server and doesn't actually lazy load it yet. That's coming in a follow up. This strategy is built on the model that each request has its own channel for this. Instead of some global registry. That ensures that referential identity is preserved within a Request and the Request can refer to previously written objects by reference. The fixture implements a WebSocket per request but it doesn't have to be done that way. It can be multiplexed through an existing WebSocket for example. The current protocol is just a Readable(Stream) on the server and WritableStream on the client. It could even be sent through a HTTP request body if browsers implemented full duplex (which they don't). This PR only implements the direction of messages from Client to Server. However, I also plan on adding Debug Channel in the other direction to allow debug info (optionally) be sent from Server to Client through this channel instead of through the main RSC request. So the `debugChannel` option will be able to take writable or readable or both. --------- Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
This commit is contained in:
committed by
GitHub
parent
12eaef7ef5
commit
bbc13fa17b
@@ -104,6 +104,9 @@ async function renderApp(req, res, next) {
|
||||
if (req.headers['cache-control']) {
|
||||
proxiedHeaders['Cache-Control'] = req.get('cache-control');
|
||||
}
|
||||
if (req.get('rsc-request-id')) {
|
||||
proxiedHeaders['rsc-request-id'] = req.get('rsc-request-id');
|
||||
}
|
||||
|
||||
const requestsPrerender = req.path === '/prerender';
|
||||
|
||||
|
||||
@@ -50,7 +50,27 @@ const {readFile} = require('fs').promises;
|
||||
|
||||
const React = require('react');
|
||||
|
||||
async function renderApp(res, returnValue, formState, noCache) {
|
||||
const activeDebugChannels =
|
||||
process.env.NODE_ENV === 'development' ? new Map() : null;
|
||||
|
||||
function getDebugChannel(req) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
return undefined;
|
||||
}
|
||||
const requestId = req.get('rsc-request-id');
|
||||
if (!requestId) {
|
||||
return undefined;
|
||||
}
|
||||
return activeDebugChannels.get(requestId);
|
||||
}
|
||||
|
||||
async function renderApp(
|
||||
res,
|
||||
returnValue,
|
||||
formState,
|
||||
noCache,
|
||||
promiseForDebugChannel
|
||||
) {
|
||||
const {renderToPipeableStream} = await import(
|
||||
'react-server-dom-webpack/server'
|
||||
);
|
||||
@@ -101,7 +121,9 @@ async function renderApp(res, returnValue, formState, noCache) {
|
||||
);
|
||||
// For client-invoked server actions we refresh the tree and return a return value.
|
||||
const payload = {root, returnValue, formState};
|
||||
const {pipe} = renderToPipeableStream(payload, moduleMap);
|
||||
const {pipe} = renderToPipeableStream(payload, moduleMap, {
|
||||
debugChannel: await promiseForDebugChannel,
|
||||
});
|
||||
pipe(res);
|
||||
}
|
||||
|
||||
@@ -166,7 +188,7 @@ app.get('/', async function (req, res) {
|
||||
if ('prerender' in req.query) {
|
||||
await prerenderApp(res, null, null, noCache);
|
||||
} else {
|
||||
await renderApp(res, null, null, noCache);
|
||||
await renderApp(res, null, null, noCache, getDebugChannel(req));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -204,7 +226,7 @@ app.post('/', bodyParser.text(), async function (req, res) {
|
||||
// We handle the error on the client
|
||||
}
|
||||
// Refresh the client and return the value
|
||||
renderApp(res, result, null, noCache);
|
||||
renderApp(res, result, null, noCache, getDebugChannel(req));
|
||||
} else {
|
||||
// This is the progressive enhancement case
|
||||
const UndiciRequest = require('undici').Request;
|
||||
@@ -220,11 +242,11 @@ app.post('/', bodyParser.text(), async function (req, res) {
|
||||
// Wait for any mutations
|
||||
const result = await action();
|
||||
const formState = decodeFormState(result, formData);
|
||||
renderApp(res, null, formState, noCache);
|
||||
renderApp(res, null, formState, noCache, undefined);
|
||||
} catch (x) {
|
||||
const {setServerState} = await import('../src/ServerState.js');
|
||||
setServerState('Error: ' + x.message);
|
||||
renderApp(res, null, null, noCache);
|
||||
renderApp(res, null, null, noCache, undefined);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -324,7 +346,7 @@ if (process.env.NODE_ENV === 'development') {
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(3001, () => {
|
||||
const httpServer = app.listen(3001, () => {
|
||||
console.log('Regional Flight Server listening on port 3001...');
|
||||
});
|
||||
|
||||
@@ -346,3 +368,27 @@ app.on('error', function (error) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// Open a websocket server for Debug information
|
||||
const WebSocket = require('ws');
|
||||
const webSocketServer = new WebSocket.Server({noServer: true});
|
||||
|
||||
httpServer.on('upgrade', (request, socket, head) => {
|
||||
const DEBUG_CHANNEL_PATH = '/debug-channel?';
|
||||
if (request.url.startsWith(DEBUG_CHANNEL_PATH)) {
|
||||
const requestId = request.url.slice(DEBUG_CHANNEL_PATH.length);
|
||||
const promiseForWs = new Promise(resolve => {
|
||||
webSocketServer.handleUpgrade(request, socket, head, ws => {
|
||||
ws.on('close', () => {
|
||||
activeDebugChannels.delete(requestId);
|
||||
});
|
||||
resolve(ws);
|
||||
});
|
||||
});
|
||||
activeDebugChannels.set(requestId, promiseForWs);
|
||||
} else {
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,7 +123,6 @@ async function ServerComponent({noCache}) {
|
||||
export default async function App({prerender, noCache}) {
|
||||
const res = await fetch('http://localhost:3001/todos');
|
||||
const todos = await res.json();
|
||||
console.log(res);
|
||||
|
||||
const dedupedChild = <ServerComponent noCache={noCache} />;
|
||||
const message = getServerState();
|
||||
|
||||
@@ -42,17 +42,43 @@ function Shell({data}) {
|
||||
}
|
||||
|
||||
async function hydrateApp() {
|
||||
const {root, returnValue, formState} = await createFromFetch(
|
||||
fetch('/', {
|
||||
headers: {
|
||||
Accept: 'text/x-component',
|
||||
},
|
||||
}),
|
||||
{
|
||||
callServer,
|
||||
findSourceMapURL,
|
||||
}
|
||||
);
|
||||
let response;
|
||||
if (
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
typeof WebSocketStream === 'function'
|
||||
) {
|
||||
const requestId = crypto.randomUUID();
|
||||
const wss = new WebSocketStream(
|
||||
'ws://localhost:3001/debug-channel?' + requestId
|
||||
);
|
||||
const debugChannel = await wss.opened;
|
||||
response = createFromFetch(
|
||||
fetch('/', {
|
||||
headers: {
|
||||
Accept: 'text/x-component',
|
||||
'rsc-request-id': requestId,
|
||||
},
|
||||
}),
|
||||
{
|
||||
callServer,
|
||||
debugChannel,
|
||||
findSourceMapURL,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
response = createFromFetch(
|
||||
fetch('/', {
|
||||
headers: {
|
||||
Accept: 'text/x-component',
|
||||
},
|
||||
}),
|
||||
{
|
||||
callServer,
|
||||
findSourceMapURL,
|
||||
}
|
||||
);
|
||||
}
|
||||
const {root, returnValue, formState} = await response;
|
||||
|
||||
ReactDOM.hydrateRoot(
|
||||
document,
|
||||
|
||||
36
packages/react-client/src/ReactFlightClient.js
vendored
36
packages/react-client/src/ReactFlightClient.js
vendored
@@ -328,6 +328,8 @@ export type FindSourceMapURLCallback = (
|
||||
environmentName: string,
|
||||
) => null | string;
|
||||
|
||||
export type DebugChannelCallback = (message: string) => void;
|
||||
|
||||
export type Response = {
|
||||
_bundlerConfig: ServerConsumerModuleMap,
|
||||
_serverReferenceConfig: null | ServerManifest,
|
||||
@@ -351,6 +353,7 @@ export type Response = {
|
||||
_debugRootStack?: null | Error, // DEV-only
|
||||
_debugRootTask?: null | ConsoleTask, // DEV-only
|
||||
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
|
||||
_debugChannel?: void | DebugChannelCallback, // DEV-only
|
||||
_replayConsole: boolean, // DEV-only
|
||||
_rootEnvironmentName: string, // DEV-only, the requested environment name.
|
||||
};
|
||||
@@ -687,6 +690,15 @@ export function reportGlobalError(response: Response, error: Error): void {
|
||||
triggerErrorOnChunk(chunk, error);
|
||||
}
|
||||
});
|
||||
if (__DEV__) {
|
||||
const debugChannel = response._debugChannel;
|
||||
if (debugChannel !== undefined) {
|
||||
// If we don't have any more ways of reading data, we don't have to send any
|
||||
// more neither. So we close the writable side.
|
||||
debugChannel('');
|
||||
response._debugChannel = undefined;
|
||||
}
|
||||
}
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
markAllTracksInOrder();
|
||||
flushComponentPerformance(
|
||||
@@ -1667,6 +1679,14 @@ function parseModelString(
|
||||
}
|
||||
case 'Y': {
|
||||
if (__DEV__) {
|
||||
if (value.length > 2) {
|
||||
const debugChannel = response._debugChannel;
|
||||
if (debugChannel) {
|
||||
const ref = value.slice(2);
|
||||
debugChannel('R:' + ref); // Release this reference immediately
|
||||
}
|
||||
}
|
||||
|
||||
// In DEV mode we encode omitted objects in logs as a getter that throws
|
||||
// so that when you try to access it on the client, you know why that
|
||||
// happened.
|
||||
@@ -1730,9 +1750,10 @@ function ResponseInstance(
|
||||
encodeFormAction: void | EncodeFormActionCallback,
|
||||
nonce: void | string,
|
||||
temporaryReferences: void | TemporaryReferenceSet,
|
||||
findSourceMapURL: void | FindSourceMapURLCallback,
|
||||
replayConsole: boolean,
|
||||
environmentName: void | string,
|
||||
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
|
||||
replayConsole: boolean, // DEV-only
|
||||
environmentName: void | string, // DEV-only
|
||||
debugChannel: void | DebugChannelCallback, // DEV-only
|
||||
) {
|
||||
const chunks: Map<number, SomeChunk<any>> = new Map();
|
||||
this._bundlerConfig = bundlerConfig;
|
||||
@@ -1787,6 +1808,7 @@ function ResponseInstance(
|
||||
);
|
||||
}
|
||||
this._debugFindSourceMapURL = findSourceMapURL;
|
||||
this._debugChannel = debugChannel;
|
||||
this._replayConsole = replayConsole;
|
||||
this._rootEnvironmentName = rootEnv;
|
||||
}
|
||||
@@ -1802,9 +1824,10 @@ export function createResponse(
|
||||
encodeFormAction: void | EncodeFormActionCallback,
|
||||
nonce: void | string,
|
||||
temporaryReferences: void | TemporaryReferenceSet,
|
||||
findSourceMapURL: void | FindSourceMapURLCallback,
|
||||
replayConsole: boolean,
|
||||
environmentName: void | string,
|
||||
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
|
||||
replayConsole: boolean, // DEV-only
|
||||
environmentName: void | string, // DEV-only
|
||||
debugChannel: void | DebugChannelCallback, // DEV-only
|
||||
): Response {
|
||||
// $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
|
||||
return new ResponseInstance(
|
||||
@@ -1818,6 +1841,7 @@ export function createResponse(
|
||||
findSourceMapURL,
|
||||
replayConsole,
|
||||
environmentName,
|
||||
debugChannel,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
139
packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js
vendored
Normal file
139
packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and 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';
|
||||
|
||||
if (typeof Blob === 'undefined') {
|
||||
global.Blob = require('buffer').Blob;
|
||||
}
|
||||
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
|
||||
global.File = require('undici').File;
|
||||
global.FormData = require('undici').FormData;
|
||||
}
|
||||
|
||||
function formatV8Stack(stack) {
|
||||
let v8StyleStack = '';
|
||||
if (stack) {
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
const [name] = stack[i];
|
||||
if (v8StyleStack !== '') {
|
||||
v8StyleStack += '\n';
|
||||
}
|
||||
v8StyleStack += ' in ' + name + ' (at **)';
|
||||
}
|
||||
}
|
||||
return v8StyleStack;
|
||||
}
|
||||
|
||||
function normalizeComponentInfo(debugInfo) {
|
||||
if (Array.isArray(debugInfo.stack)) {
|
||||
const {debugTask, debugStack, ...copy} = debugInfo;
|
||||
copy.stack = formatV8Stack(debugInfo.stack);
|
||||
if (debugInfo.owner) {
|
||||
copy.owner = normalizeComponentInfo(debugInfo.owner);
|
||||
}
|
||||
return copy;
|
||||
} else {
|
||||
return debugInfo;
|
||||
}
|
||||
}
|
||||
|
||||
function getDebugInfo(obj) {
|
||||
const debugInfo = obj._debugInfo;
|
||||
if (debugInfo) {
|
||||
const copy = [];
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
copy.push(normalizeComponentInfo(debugInfo[i]));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
return debugInfo;
|
||||
}
|
||||
|
||||
let act;
|
||||
let React;
|
||||
let ReactNoop;
|
||||
let ReactNoopFlightServer;
|
||||
let ReactNoopFlightClient;
|
||||
|
||||
describe('ReactFlight', () => {
|
||||
beforeEach(() => {
|
||||
// Mock performance.now for timing tests
|
||||
let time = 10;
|
||||
const now = jest.fn().mockImplementation(() => {
|
||||
return time++;
|
||||
});
|
||||
Object.defineProperty(performance, 'timeOrigin', {
|
||||
value: time,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(performance, 'now', {
|
||||
value: now,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
jest.resetModules();
|
||||
jest.mock('react', () => require('react/react.react-server'));
|
||||
ReactNoopFlightServer = require('react-noop-renderer/flight-server');
|
||||
// This stores the state so we need to preserve it
|
||||
const flightModules = require('react-noop-renderer/flight-modules');
|
||||
jest.resetModules();
|
||||
__unmockReact();
|
||||
jest.mock('react-noop-renderer/flight-modules', () => flightModules);
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
|
||||
act = require('internal-test-utils').act;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
// @gate __DEV__ && enableComponentPerformanceTrack
|
||||
it('can render deep but cut off JSX in debug info', async () => {
|
||||
function createDeepJSX(n) {
|
||||
if (n <= 0) {
|
||||
return null;
|
||||
}
|
||||
return <div>{createDeepJSX(n - 1)}</div>;
|
||||
}
|
||||
|
||||
function ServerComponent(props) {
|
||||
return <div>not using props</div>;
|
||||
}
|
||||
|
||||
const debugChannel = {onMessage(message) {}};
|
||||
|
||||
const transport = ReactNoopFlightServer.render(
|
||||
{
|
||||
root: (
|
||||
<ServerComponent>
|
||||
{createDeepJSX(100) /* deper than objectLimit */}
|
||||
</ServerComponent>
|
||||
),
|
||||
},
|
||||
{debugChannel},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const rootModel = await ReactNoopFlightClient.read(transport, {
|
||||
debugChannel,
|
||||
});
|
||||
const root = rootModel.root;
|
||||
const children = getDebugInfo(root)[1].props.children;
|
||||
expect(children.type).toBe('div');
|
||||
expect(children.props.children.type).toBe('div');
|
||||
ReactNoop.render(root);
|
||||
});
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
|
||||
});
|
||||
});
|
||||
@@ -171,6 +171,7 @@ export function experimental_renderToHTML(
|
||||
undefined,
|
||||
'Markup',
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
const flightResponse = createFlightResponse(
|
||||
null,
|
||||
|
||||
@@ -56,6 +56,7 @@ const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({
|
||||
|
||||
type ReadOptions = {|
|
||||
findSourceMapURL?: FindSourceMapURLCallback,
|
||||
debugChannel?: {onMessage: (message: string) => void},
|
||||
close?: boolean,
|
||||
|};
|
||||
|
||||
@@ -71,6 +72,9 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
|
||||
options !== undefined ? options.findSourceMapURL : undefined,
|
||||
true,
|
||||
undefined,
|
||||
__DEV__ && options !== undefined && options.debugChannel !== undefined
|
||||
? options.debugChannel.onMessage
|
||||
: undefined,
|
||||
);
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
processBinaryChunk(response, source[i], 0);
|
||||
|
||||
@@ -71,6 +71,7 @@ type Options = {
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
signal?: AbortSignal,
|
||||
debugChannel?: {onMessage?: (message: string) => void},
|
||||
onError?: (error: mixed) => void,
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
@@ -87,6 +88,7 @@ function render(model: ReactClientValue, options?: Options): Destination {
|
||||
undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
__DEV__ && options && options.debugChannel !== undefined,
|
||||
);
|
||||
const signal = options ? options.signal : undefined;
|
||||
if (signal) {
|
||||
@@ -100,6 +102,11 @@ function render(model: ReactClientValue, options?: Options): Destination {
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (__DEV__ && options && options.debugChannel !== undefined) {
|
||||
options.debugChannel.onMessage = message => {
|
||||
ReactNoopFlightServer.resolveDebugMessage(request, message);
|
||||
};
|
||||
}
|
||||
ReactNoopFlightServer.startWork(request);
|
||||
ReactNoopFlightServer.startFlowing(request, destination);
|
||||
return destination;
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
|
||||
import type {
|
||||
Response as FlightResponse,
|
||||
FindSourceMapURLCallback,
|
||||
DebugChannelCallback,
|
||||
} from 'react-client/src/ReactFlightClient';
|
||||
|
||||
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
|
||||
@@ -43,12 +44,31 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
|
||||
export type Options = {
|
||||
moduleBaseURL?: string,
|
||||
callServer?: CallServerCallback,
|
||||
debugChannel?: {writable?: WritableStream, ...},
|
||||
temporaryReferences?: TemporaryReferenceSet,
|
||||
findSourceMapURL?: FindSourceMapURLCallback,
|
||||
replayConsoleLogs?: boolean,
|
||||
environmentName?: string,
|
||||
};
|
||||
|
||||
function createDebugCallbackFromWritableStream(
|
||||
debugWritable: WritableStream,
|
||||
): DebugChannelCallback {
|
||||
const textEncoder = new TextEncoder();
|
||||
const writer = debugWritable.getWriter();
|
||||
return message => {
|
||||
if (message === '') {
|
||||
writer.close();
|
||||
} else {
|
||||
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
|
||||
// Therefore, we can't report errors from this write back to the Response object.
|
||||
if (__DEV__) {
|
||||
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createResponseFromOptions(options: void | Options) {
|
||||
return createResponse(
|
||||
options && options.moduleBaseURL ? options.moduleBaseURL : '',
|
||||
@@ -67,6 +87,12 @@ function createResponseFromOptions(options: void | Options) {
|
||||
__DEV__ && options && options.environmentName
|
||||
? options.environmentName
|
||||
: undefined,
|
||||
__DEV__ &&
|
||||
options &&
|
||||
options.debugChannel !== undefined &&
|
||||
options.debugChannel.writable !== undefined
|
||||
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
|
||||
import type {Writable} from 'stream';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
|
||||
import type {Duplex} from 'stream';
|
||||
|
||||
import {Readable} from 'stream';
|
||||
|
||||
import {
|
||||
@@ -27,6 +29,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -50,6 +54,12 @@ export {
|
||||
registerClientReference,
|
||||
} from '../ReactFlightESMReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigNode';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -67,7 +77,69 @@ function createCancelHandler(request: Request, reason: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadable(
|
||||
request: Request,
|
||||
stream: Readable | WebSocket,
|
||||
): void {
|
||||
const stringDecoder = createStringDecoder();
|
||||
let lastWasPartial = false;
|
||||
let stringBuffer = '';
|
||||
function onData(chunk: string | Uint8Array) {
|
||||
if (typeof chunk === 'string') {
|
||||
if (lastWasPartial) {
|
||||
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
|
||||
lastWasPartial = false;
|
||||
}
|
||||
stringBuffer += chunk;
|
||||
} else {
|
||||
const buffer: Uint8Array = (chunk: any);
|
||||
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
|
||||
lastWasPartial = true;
|
||||
}
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
}
|
||||
function onError(error: mixed) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
function onClose() {
|
||||
closeDebugChannel(request);
|
||||
}
|
||||
if (
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.addEventListener === 'function' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.binaryType === 'string'
|
||||
) {
|
||||
const ws: WebSocket = (stream: any);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.addEventListener('message', event => {
|
||||
// $FlowFixMe
|
||||
onData(event.data);
|
||||
});
|
||||
ws.addEventListener('error', event => {
|
||||
// $FlowFixMe
|
||||
onError(event.error);
|
||||
});
|
||||
ws.addEventListener('close', onClose);
|
||||
} else {
|
||||
const readable: Readable = (stream: any);
|
||||
readable.on('data', onData);
|
||||
readable.on('error', onError);
|
||||
readable.on('end', onClose);
|
||||
}
|
||||
}
|
||||
|
||||
type Options = {
|
||||
debugChannel?: Readable | Duplex | WebSocket,
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
onError?: (error: mixed) => void,
|
||||
@@ -86,6 +158,7 @@ function renderToPipeableStream(
|
||||
moduleBasePath: ClientManifest,
|
||||
options?: Options,
|
||||
): PipeableStream {
|
||||
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
moduleBasePath,
|
||||
@@ -95,9 +168,13 @@ function renderToPipeableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannel !== undefined,
|
||||
);
|
||||
let hasStartedFlowing = false;
|
||||
startWork(request);
|
||||
if (debugChannel !== undefined) {
|
||||
startReadingFromDebugChannelReadable(request, debugChannel);
|
||||
}
|
||||
return {
|
||||
pipe<T: Writable>(destination: T): T {
|
||||
if (hasStartedFlowing) {
|
||||
@@ -126,11 +203,12 @@ function renderToPipeableStream(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeWritable(readable: any): Writable {
|
||||
// The current host config expects a Writable so we create
|
||||
// a fake writable for now to push into the Readable.
|
||||
return ({
|
||||
write(chunk) {
|
||||
write(chunk: string | Uint8Array) {
|
||||
return readable.push(chunk);
|
||||
},
|
||||
end() {
|
||||
@@ -184,6 +262,7 @@ function prerenderToNodeStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -287,8 +366,8 @@ function decodeReply<T>(
|
||||
export {
|
||||
renderToPipeableStream,
|
||||
prerenderToNodeStream,
|
||||
decodeReplyFromBusboy,
|
||||
decodeReply,
|
||||
decodeReplyFromBusboy,
|
||||
decodeAction,
|
||||
decodeFormState,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
*/
|
||||
|
||||
import type {Thenable} from 'shared/ReactTypes.js';
|
||||
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
|
||||
import type {
|
||||
Response as FlightResponse,
|
||||
DebugChannelCallback,
|
||||
} from 'react-client/src/ReactFlightClient';
|
||||
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
|
||||
import type {ServerReferenceId} from '../client/ReactFlightClientConfigBundlerParcel';
|
||||
|
||||
@@ -76,6 +79,24 @@ export function createServerReference<A: Iterable<any>, T>(
|
||||
);
|
||||
}
|
||||
|
||||
function createDebugCallbackFromWritableStream(
|
||||
debugWritable: WritableStream,
|
||||
): DebugChannelCallback {
|
||||
const textEncoder = new TextEncoder();
|
||||
const writer = debugWritable.getWriter();
|
||||
return message => {
|
||||
if (message === '') {
|
||||
writer.close();
|
||||
} else {
|
||||
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
|
||||
// Therefore, we can't report errors from this write back to the Response object.
|
||||
if (__DEV__) {
|
||||
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
@@ -104,6 +125,7 @@ function startReadingFromStream(
|
||||
}
|
||||
|
||||
export type Options = {
|
||||
debugChannel?: {writable?: WritableStream, ...},
|
||||
temporaryReferences?: TemporaryReferenceSet,
|
||||
replayConsoleLogs?: boolean,
|
||||
environmentName?: string,
|
||||
@@ -128,6 +150,12 @@ export function createFromReadableStream<T>(
|
||||
__DEV__ && options && options.environmentName
|
||||
? options.environmentName
|
||||
: undefined,
|
||||
__DEV__ &&
|
||||
options &&
|
||||
options.debugChannel !== undefined &&
|
||||
options.debugChannel.writable !== undefined
|
||||
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
|
||||
: undefined,
|
||||
);
|
||||
startReadingFromStream(response, stream);
|
||||
return getRoot(response);
|
||||
@@ -152,6 +180,12 @@ export function createFromFetch<T>(
|
||||
__DEV__ && options && options.environmentName
|
||||
? options.environmentName
|
||||
: undefined,
|
||||
__DEV__ &&
|
||||
options &&
|
||||
options.debugChannel !== undefined &&
|
||||
options.debugChannel.writable !== undefined
|
||||
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
|
||||
: undefined,
|
||||
);
|
||||
promiseForResponse.then(
|
||||
function (r) {
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
import type {ReactFormState, Thenable} from 'shared/ReactTypes';
|
||||
import {
|
||||
preloadModule,
|
||||
@@ -24,6 +27,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -42,12 +47,19 @@ export {
|
||||
registerServerReference,
|
||||
} from '../ReactFlightParcelReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
export type {TemporaryReferenceSet};
|
||||
|
||||
type Options = {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
@@ -57,10 +69,55 @@ type Options = {
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
export function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
options?: Options,
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
null,
|
||||
@@ -70,6 +127,7 @@ export function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -83,6 +141,9 @@ export function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
@@ -117,9 +178,6 @@ export function prerender(
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
start: (controller): ?Promise<void> => {
|
||||
startWork(request);
|
||||
},
|
||||
pull: (controller): ?Promise<void> => {
|
||||
startFlowing(request, controller);
|
||||
},
|
||||
@@ -144,6 +202,7 @@ export function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
import type {ReactFormState, Thenable} from 'shared/ReactTypes';
|
||||
import {
|
||||
preloadModule,
|
||||
@@ -26,6 +29,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -47,12 +52,19 @@ export {
|
||||
registerServerReference,
|
||||
} from '../ReactFlightParcelReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
export type {TemporaryReferenceSet};
|
||||
|
||||
type Options = {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
@@ -62,10 +74,55 @@ type Options = {
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
export function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
options?: Options,
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
null,
|
||||
@@ -75,6 +132,7 @@ export function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -88,6 +146,9 @@ export function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
@@ -122,9 +183,6 @@ export function prerender(
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
start: (controller): ?Promise<void> => {
|
||||
startWork(request);
|
||||
},
|
||||
pull: (controller): ?Promise<void> => {
|
||||
startFlowing(request, controller);
|
||||
},
|
||||
@@ -149,6 +207,7 @@ export function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -20,6 +20,8 @@ import type {
|
||||
ServerReferenceId,
|
||||
} from '../client/ReactFlightClientConfigBundlerParcel';
|
||||
|
||||
import type {Duplex} from 'stream';
|
||||
|
||||
import {Readable} from 'stream';
|
||||
|
||||
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
|
||||
@@ -31,6 +33,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -49,6 +53,7 @@ import {
|
||||
decodeAction as decodeActionImpl,
|
||||
decodeFormState as decodeFormStateImpl,
|
||||
} from 'react-server/src/ReactFlightActionServer';
|
||||
|
||||
import {
|
||||
preloadModule,
|
||||
requireModule,
|
||||
@@ -60,6 +65,12 @@ export {
|
||||
registerServerReference,
|
||||
} from '../ReactFlightParcelReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigNode';
|
||||
|
||||
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -79,7 +90,69 @@ function createCancelHandler(request: Request, reason: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadable(
|
||||
request: Request,
|
||||
stream: Readable | WebSocket,
|
||||
): void {
|
||||
const stringDecoder = createStringDecoder();
|
||||
let lastWasPartial = false;
|
||||
let stringBuffer = '';
|
||||
function onData(chunk: string | Uint8Array) {
|
||||
if (typeof chunk === 'string') {
|
||||
if (lastWasPartial) {
|
||||
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
|
||||
lastWasPartial = false;
|
||||
}
|
||||
stringBuffer += chunk;
|
||||
} else {
|
||||
const buffer: Uint8Array = (chunk: any);
|
||||
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
|
||||
lastWasPartial = true;
|
||||
}
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
}
|
||||
function onError(error: mixed) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
function onClose() {
|
||||
closeDebugChannel(request);
|
||||
}
|
||||
if (
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.addEventListener === 'function' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.binaryType === 'string'
|
||||
) {
|
||||
const ws: WebSocket = (stream: any);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.addEventListener('message', event => {
|
||||
// $FlowFixMe
|
||||
onData(event.data);
|
||||
});
|
||||
ws.addEventListener('error', event => {
|
||||
// $FlowFixMe
|
||||
onError(event.error);
|
||||
});
|
||||
ws.addEventListener('close', onClose);
|
||||
} else {
|
||||
const readable: Readable = (stream: any);
|
||||
readable.on('data', onData);
|
||||
readable.on('error', onError);
|
||||
readable.on('end', onClose);
|
||||
}
|
||||
}
|
||||
|
||||
type Options = {
|
||||
debugChannel?: Readable | Duplex | WebSocket,
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
onError?: (error: mixed) => void,
|
||||
@@ -97,6 +170,7 @@ export function renderToPipeableStream(
|
||||
model: ReactClientValue,
|
||||
options?: Options,
|
||||
): PipeableStream {
|
||||
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
null,
|
||||
@@ -106,9 +180,13 @@ export function renderToPipeableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannel !== undefined,
|
||||
);
|
||||
let hasStartedFlowing = false;
|
||||
startWork(request);
|
||||
if (debugChannel !== undefined) {
|
||||
startReadingFromDebugChannelReadable(request, debugChannel);
|
||||
}
|
||||
return {
|
||||
pipe<T: Writable>(destination: T): T {
|
||||
if (hasStartedFlowing) {
|
||||
@@ -149,7 +227,7 @@ function createFakeWritableFromReadableStreamController(
|
||||
chunk = textEncoder.encode(chunk);
|
||||
}
|
||||
controller.enqueue(chunk);
|
||||
// in web streams there is no backpressure so we can alwas write more
|
||||
// in web streams there is no backpressure so we can always write more
|
||||
return true;
|
||||
},
|
||||
end() {
|
||||
@@ -167,13 +245,58 @@ function createFakeWritableFromReadableStreamController(
|
||||
}: any);
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
export function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
|
||||
options?: Options & {
|
||||
options?: Omit<Options, 'debugChannel'> & {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
signal?: AbortSignal,
|
||||
},
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
null,
|
||||
@@ -183,6 +306,7 @@ export function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -196,6 +320,9 @@ export function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
let writable: Writable;
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
@@ -275,6 +402,7 @@ export function prerenderToNodeStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -296,7 +424,6 @@ export function prerenderToNodeStream(
|
||||
|
||||
export function prerender(
|
||||
model: ReactClientValue,
|
||||
|
||||
options?: Options & {
|
||||
signal?: AbortSignal,
|
||||
},
|
||||
@@ -338,6 +465,7 @@ export function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
|
||||
import type {
|
||||
Response as FlightResponse,
|
||||
FindSourceMapURLCallback,
|
||||
DebugChannelCallback,
|
||||
} from 'react-client/src/ReactFlightClient';
|
||||
|
||||
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
|
||||
@@ -42,12 +43,31 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
|
||||
|
||||
export type Options = {
|
||||
callServer?: CallServerCallback,
|
||||
debugChannel?: {writable?: WritableStream, ...},
|
||||
temporaryReferences?: TemporaryReferenceSet,
|
||||
findSourceMapURL?: FindSourceMapURLCallback,
|
||||
replayConsoleLogs?: boolean,
|
||||
environmentName?: string,
|
||||
};
|
||||
|
||||
function createDebugCallbackFromWritableStream(
|
||||
debugWritable: WritableStream,
|
||||
): DebugChannelCallback {
|
||||
const textEncoder = new TextEncoder();
|
||||
const writer = debugWritable.getWriter();
|
||||
return message => {
|
||||
if (message === '') {
|
||||
writer.close();
|
||||
} else {
|
||||
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
|
||||
// Therefore, we can't report errors from this write back to the Response object.
|
||||
if (__DEV__) {
|
||||
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createResponseFromOptions(options: void | Options) {
|
||||
return createResponse(
|
||||
null,
|
||||
@@ -66,6 +86,12 @@ function createResponseFromOptions(options: void | Options) {
|
||||
__DEV__ && options && options.environmentName
|
||||
? options.environmentName
|
||||
: undefined,
|
||||
__DEV__ &&
|
||||
options &&
|
||||
options.debugChannel !== undefined &&
|
||||
options.debugChannel.writable !== undefined
|
||||
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
|
||||
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
|
||||
@@ -19,6 +22,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -38,6 +43,12 @@ export {
|
||||
createClientModuleProxy,
|
||||
} from '../ReactFlightTurbopackReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -45,6 +56,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
|
||||
export type {TemporaryReferenceSet};
|
||||
|
||||
type Options = {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
@@ -54,11 +66,56 @@ type Options = {
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
turbopackMap: ClientManifest,
|
||||
options?: Options,
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
turbopackMap,
|
||||
@@ -68,6 +125,7 @@ function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -81,6 +139,9 @@ function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
@@ -116,9 +177,6 @@ function prerender(
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
start: (controller): ?Promise<void> => {
|
||||
startWork(request);
|
||||
},
|
||||
pull: (controller): ?Promise<void> => {
|
||||
startFlowing(request, controller);
|
||||
},
|
||||
@@ -143,6 +201,7 @@ function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
|
||||
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
|
||||
@@ -21,6 +24,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -43,6 +48,12 @@ export {
|
||||
createClientModuleProxy,
|
||||
} from '../ReactFlightTurbopackReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -50,6 +61,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
|
||||
export type {TemporaryReferenceSet};
|
||||
|
||||
type Options = {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
@@ -59,11 +71,56 @@ type Options = {
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
turbopackMap: ClientManifest,
|
||||
options?: Options,
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
turbopackMap,
|
||||
@@ -73,6 +130,7 @@ function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -86,6 +144,9 @@ function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
@@ -121,9 +182,6 @@ function prerender(
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
start: (controller): ?Promise<void> => {
|
||||
startWork(request);
|
||||
},
|
||||
pull: (controller): ?Promise<void> => {
|
||||
startFlowing(request, controller);
|
||||
},
|
||||
@@ -148,6 +206,7 @@ function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
|
||||
import type {Writable} from 'stream';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
|
||||
import type {Duplex} from 'stream';
|
||||
|
||||
import {Readable} from 'stream';
|
||||
|
||||
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
|
||||
@@ -29,6 +31,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -54,6 +58,12 @@ export {
|
||||
createClientModuleProxy,
|
||||
} from '../ReactFlightTurbopackReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigNode';
|
||||
|
||||
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -73,7 +83,69 @@ function createCancelHandler(request: Request, reason: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadable(
|
||||
request: Request,
|
||||
stream: Readable | WebSocket,
|
||||
): void {
|
||||
const stringDecoder = createStringDecoder();
|
||||
let lastWasPartial = false;
|
||||
let stringBuffer = '';
|
||||
function onData(chunk: string | Uint8Array) {
|
||||
if (typeof chunk === 'string') {
|
||||
if (lastWasPartial) {
|
||||
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
|
||||
lastWasPartial = false;
|
||||
}
|
||||
stringBuffer += chunk;
|
||||
} else {
|
||||
const buffer: Uint8Array = (chunk: any);
|
||||
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
|
||||
lastWasPartial = true;
|
||||
}
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
}
|
||||
function onError(error: mixed) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
function onClose() {
|
||||
closeDebugChannel(request);
|
||||
}
|
||||
if (
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.addEventListener === 'function' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.binaryType === 'string'
|
||||
) {
|
||||
const ws: WebSocket = (stream: any);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.addEventListener('message', event => {
|
||||
// $FlowFixMe
|
||||
onData(event.data);
|
||||
});
|
||||
ws.addEventListener('error', event => {
|
||||
// $FlowFixMe
|
||||
onError(event.error);
|
||||
});
|
||||
ws.addEventListener('close', onClose);
|
||||
} else {
|
||||
const readable: Readable = (stream: any);
|
||||
readable.on('data', onData);
|
||||
readable.on('error', onError);
|
||||
readable.on('end', onClose);
|
||||
}
|
||||
}
|
||||
|
||||
type Options = {
|
||||
debugChannel?: Readable | Duplex | WebSocket,
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
onError?: (error: mixed) => void,
|
||||
@@ -92,6 +164,7 @@ function renderToPipeableStream(
|
||||
turbopackMap: ClientManifest,
|
||||
options?: Options,
|
||||
): PipeableStream {
|
||||
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
turbopackMap,
|
||||
@@ -101,9 +174,13 @@ function renderToPipeableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannel !== undefined,
|
||||
);
|
||||
let hasStartedFlowing = false;
|
||||
startWork(request);
|
||||
if (debugChannel !== undefined) {
|
||||
startReadingFromDebugChannelReadable(request, debugChannel);
|
||||
}
|
||||
return {
|
||||
pipe<T: Writable>(destination: T): T {
|
||||
if (hasStartedFlowing) {
|
||||
@@ -162,13 +239,59 @@ function createFakeWritableFromReadableStreamController(
|
||||
}: any);
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
turbopackMap: ClientManifest,
|
||||
options?: Options & {
|
||||
options?: Omit<Options, 'debugChannel'> & {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
signal?: AbortSignal,
|
||||
},
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
turbopackMap,
|
||||
@@ -178,6 +301,7 @@ function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -191,6 +315,9 @@ function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
let writable: Writable;
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
@@ -271,6 +398,7 @@ function prerenderToNodeStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -334,6 +462,7 @@ function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
|
||||
import type {
|
||||
Response as FlightResponse,
|
||||
FindSourceMapURLCallback,
|
||||
DebugChannelCallback,
|
||||
} from 'react-client/src/ReactFlightClient';
|
||||
|
||||
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
|
||||
@@ -42,12 +43,31 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
|
||||
|
||||
export type Options = {
|
||||
callServer?: CallServerCallback,
|
||||
debugChannel?: {writable?: WritableStream, ...},
|
||||
temporaryReferences?: TemporaryReferenceSet,
|
||||
findSourceMapURL?: FindSourceMapURLCallback,
|
||||
replayConsoleLogs?: boolean,
|
||||
environmentName?: string,
|
||||
};
|
||||
|
||||
function createDebugCallbackFromWritableStream(
|
||||
debugWritable: WritableStream,
|
||||
): DebugChannelCallback {
|
||||
const textEncoder = new TextEncoder();
|
||||
const writer = debugWritable.getWriter();
|
||||
return message => {
|
||||
if (message === '') {
|
||||
writer.close();
|
||||
} else {
|
||||
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
|
||||
// Therefore, we can't report errors from this write back to the Response object.
|
||||
if (__DEV__) {
|
||||
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createResponseFromOptions(options: void | Options) {
|
||||
return createResponse(
|
||||
null,
|
||||
@@ -66,6 +86,12 @@ function createResponseFromOptions(options: void | Options) {
|
||||
__DEV__ && options && options.environmentName
|
||||
? options.environmentName
|
||||
: undefined,
|
||||
__DEV__ &&
|
||||
options &&
|
||||
options.debugChannel !== undefined &&
|
||||
options.debugChannel.writable !== undefined
|
||||
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
|
||||
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
|
||||
@@ -19,6 +22,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -38,6 +43,12 @@ export {
|
||||
createClientModuleProxy,
|
||||
} from '../ReactFlightWebpackReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -45,6 +56,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
|
||||
export type {TemporaryReferenceSet};
|
||||
|
||||
type Options = {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
@@ -54,11 +66,56 @@ type Options = {
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
webpackMap: ClientManifest,
|
||||
options?: Options,
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
webpackMap,
|
||||
@@ -68,6 +125,7 @@ function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -81,6 +139,9 @@ function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
@@ -116,9 +177,6 @@ function prerender(
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
start: (controller): ?Promise<void> => {
|
||||
startWork(request);
|
||||
},
|
||||
pull: (controller): ?Promise<void> => {
|
||||
startFlowing(request, controller);
|
||||
},
|
||||
@@ -143,6 +201,7 @@ function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
|
||||
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
|
||||
@@ -21,6 +24,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -43,6 +48,12 @@ export {
|
||||
createClientModuleProxy,
|
||||
} from '../ReactFlightWebpackReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -50,6 +61,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
|
||||
export type {TemporaryReferenceSet};
|
||||
|
||||
type Options = {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
@@ -59,11 +71,56 @@ type Options = {
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
webpackMap: ClientManifest,
|
||||
options?: Options,
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
webpackMap,
|
||||
@@ -73,6 +130,7 @@ function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -86,6 +144,9 @@ function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
@@ -145,6 +206,7 @@ function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
|
||||
import type {Writable} from 'stream';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
|
||||
import type {Duplex} from 'stream';
|
||||
|
||||
import {Readable} from 'stream';
|
||||
|
||||
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
|
||||
@@ -29,6 +31,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -54,6 +58,12 @@ export {
|
||||
createClientModuleProxy,
|
||||
} from '../ReactFlightWebpackReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigNode';
|
||||
|
||||
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -73,7 +83,69 @@ function createCancelHandler(request: Request, reason: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadable(
|
||||
request: Request,
|
||||
stream: Readable | WebSocket,
|
||||
): void {
|
||||
const stringDecoder = createStringDecoder();
|
||||
let lastWasPartial = false;
|
||||
let stringBuffer = '';
|
||||
function onData(chunk: string | Uint8Array) {
|
||||
if (typeof chunk === 'string') {
|
||||
if (lastWasPartial) {
|
||||
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
|
||||
lastWasPartial = false;
|
||||
}
|
||||
stringBuffer += chunk;
|
||||
} else {
|
||||
const buffer: Uint8Array = (chunk: any);
|
||||
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
|
||||
lastWasPartial = true;
|
||||
}
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
}
|
||||
function onError(error: mixed) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
function onClose() {
|
||||
closeDebugChannel(request);
|
||||
}
|
||||
if (
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.addEventListener === 'function' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.binaryType === 'string'
|
||||
) {
|
||||
const ws: WebSocket = (stream: any);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.addEventListener('message', event => {
|
||||
// $FlowFixMe
|
||||
onData(event.data);
|
||||
});
|
||||
ws.addEventListener('error', event => {
|
||||
// $FlowFixMe
|
||||
onError(event.error);
|
||||
});
|
||||
ws.addEventListener('close', onClose);
|
||||
} else {
|
||||
const readable: Readable = (stream: any);
|
||||
readable.on('data', onData);
|
||||
readable.on('error', onError);
|
||||
readable.on('end', onClose);
|
||||
}
|
||||
}
|
||||
|
||||
type Options = {
|
||||
debugChannel?: Readable | Duplex | WebSocket,
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
onError?: (error: mixed) => void,
|
||||
@@ -92,6 +164,7 @@ function renderToPipeableStream(
|
||||
webpackMap: ClientManifest,
|
||||
options?: Options,
|
||||
): PipeableStream {
|
||||
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
webpackMap,
|
||||
@@ -101,9 +174,13 @@ function renderToPipeableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannel !== undefined,
|
||||
);
|
||||
let hasStartedFlowing = false;
|
||||
startWork(request);
|
||||
if (debugChannel !== undefined) {
|
||||
startReadingFromDebugChannelReadable(request, debugChannel);
|
||||
}
|
||||
return {
|
||||
pipe<T: Writable>(destination: T): T {
|
||||
if (hasStartedFlowing) {
|
||||
@@ -162,13 +239,59 @@ function createFakeWritableFromReadableStreamController(
|
||||
}: any);
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
webpackMap: ClientManifest,
|
||||
options?: Options & {
|
||||
options?: Omit<Options, 'debugChannel'> & {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
signal?: AbortSignal,
|
||||
},
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
webpackMap,
|
||||
@@ -178,6 +301,7 @@ function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -191,6 +315,9 @@ function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
let writable: Writable;
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
@@ -271,6 +398,7 @@ function prerenderToNodeStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -334,6 +462,7 @@ function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
156
packages/react-server/src/ReactFlightServer.js
vendored
156
packages/react-server/src/ReactFlightServer.js
vendored
@@ -404,6 +404,13 @@ type Task = {
|
||||
|
||||
interface Reference {}
|
||||
|
||||
type ReactClientReference = Reference & ReactClientValue;
|
||||
|
||||
type DeferredDebugStore = {
|
||||
retained: Map<number, ReactClientReference | string>,
|
||||
existing: Map<ReactClientReference | string, number>,
|
||||
};
|
||||
|
||||
const OPENING = 10;
|
||||
const OPEN = 11;
|
||||
const ABORTING = 12;
|
||||
@@ -451,6 +458,7 @@ export type Request = {
|
||||
filterStackFrame: (url: string, functionName: string) => boolean,
|
||||
didWarnForKey: null | WeakSet<ReactComponentInfo>,
|
||||
writtenDebugObjects: WeakMap<Reference, string>,
|
||||
deferredDebugObjects: null | DeferredDebugStore,
|
||||
};
|
||||
|
||||
const {
|
||||
@@ -495,13 +503,14 @@ function RequestInstance(
|
||||
model: ReactClientValue,
|
||||
bundlerConfig: ClientManifest,
|
||||
onError: void | ((error: mixed) => ?string),
|
||||
identifierPrefix?: string,
|
||||
onPostpone: void | ((reason: string) => void),
|
||||
onAllReady: () => void,
|
||||
onFatalError: (error: mixed) => void,
|
||||
identifierPrefix?: string,
|
||||
temporaryReferences: void | TemporaryReferenceSet,
|
||||
environmentName: void | string | (() => string), // DEV-only
|
||||
filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
|
||||
onAllReady: () => void,
|
||||
onFatalError: (error: mixed) => void,
|
||||
keepDebugAlive: boolean, // DEV-only
|
||||
) {
|
||||
if (
|
||||
ReactSharedInternals.A !== null &&
|
||||
@@ -571,6 +580,12 @@ function RequestInstance(
|
||||
: filterStackFrame;
|
||||
this.didWarnForKey = null;
|
||||
this.writtenDebugObjects = new WeakMap();
|
||||
this.deferredDebugObjects = keepDebugAlive
|
||||
? {
|
||||
retained: new Map(),
|
||||
existing: new Map(),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
let timeOrigin: number;
|
||||
@@ -615,6 +630,7 @@ export function createRequest(
|
||||
temporaryReferences: void | TemporaryReferenceSet,
|
||||
environmentName: void | string | (() => string), // DEV-only
|
||||
filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
|
||||
keepDebugAlive: boolean, // DEV-only
|
||||
): Request {
|
||||
if (__DEV__) {
|
||||
resetOwnerStackLimit();
|
||||
@@ -626,13 +642,14 @@ export function createRequest(
|
||||
model,
|
||||
bundlerConfig,
|
||||
onError,
|
||||
identifierPrefix,
|
||||
onPostpone,
|
||||
noop,
|
||||
noop,
|
||||
identifierPrefix,
|
||||
temporaryReferences,
|
||||
environmentName,
|
||||
filterStackFrame,
|
||||
noop,
|
||||
noop,
|
||||
keepDebugAlive,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -647,6 +664,7 @@ export function createPrerenderRequest(
|
||||
temporaryReferences: void | TemporaryReferenceSet,
|
||||
environmentName: void | string | (() => string), // DEV-only
|
||||
filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
|
||||
keepDebugAlive: boolean, // DEV-only
|
||||
): Request {
|
||||
if (__DEV__) {
|
||||
resetOwnerStackLimit();
|
||||
@@ -658,13 +676,14 @@ export function createPrerenderRequest(
|
||||
model,
|
||||
bundlerConfig,
|
||||
onError,
|
||||
identifierPrefix,
|
||||
onPostpone,
|
||||
onAllReady,
|
||||
onFatalError,
|
||||
identifierPrefix,
|
||||
temporaryReferences,
|
||||
environmentName,
|
||||
filterStackFrame,
|
||||
onAllReady,
|
||||
onFatalError,
|
||||
keepDebugAlive,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2331,7 +2350,21 @@ function serializeSymbolReference(name: string): string {
|
||||
return '$S' + name;
|
||||
}
|
||||
|
||||
function serializeLimitedObject(): string {
|
||||
function serializeDeferredObject(
|
||||
request: Request,
|
||||
value: ReactClientReference | string,
|
||||
): string {
|
||||
const deferredDebugObjects = request.deferredDebugObjects;
|
||||
if (deferredDebugObjects !== null) {
|
||||
// This client supports a long lived connection. We can assign this object
|
||||
// an ID to be lazy loaded later.
|
||||
// This keeps the connection alive until we ask for it or release it.
|
||||
request.pendingChunks++;
|
||||
const id = request.nextChunkId++;
|
||||
deferredDebugObjects.existing.set(value, id);
|
||||
deferredDebugObjects.retained.set(id, value);
|
||||
return '$Y' + id.toString(16);
|
||||
}
|
||||
return '$Y';
|
||||
}
|
||||
|
||||
@@ -4058,12 +4091,25 @@ function renderDebugModel(
|
||||
|
||||
if (counter.objectLimit <= 0 && !doNotLimit.has(value)) {
|
||||
// We've reached our max number of objects to serialize across the wire so we serialize this
|
||||
// as a marker so that the client can error when this is accessed by the console.
|
||||
return serializeLimitedObject();
|
||||
// as a marker so that the client can error or lazy load this when accessed by the console.
|
||||
return serializeDeferredObject(request, value);
|
||||
}
|
||||
|
||||
counter.objectLimit--;
|
||||
|
||||
const deferredDebugObjects = request.deferredDebugObjects;
|
||||
if (deferredDebugObjects !== null) {
|
||||
const deferredId = deferredDebugObjects.existing.get(value);
|
||||
// We earlier deferred this same object. We're now going to eagerly emit it so let's emit it
|
||||
// at the same ID that we already used to refer to it.
|
||||
if (deferredId !== undefined) {
|
||||
deferredDebugObjects.existing.delete(value);
|
||||
deferredDebugObjects.retained.delete(deferredId);
|
||||
emitOutlinedDebugModelChunk(request, deferredId, counter, value);
|
||||
return serializeByValueID(deferredId);
|
||||
}
|
||||
}
|
||||
|
||||
switch ((value: any).$$typeof) {
|
||||
case REACT_ELEMENT_TYPE: {
|
||||
const element: ReactElement = (value: any);
|
||||
@@ -4235,6 +4281,13 @@ function renderDebugModel(
|
||||
}
|
||||
}
|
||||
if (value.length >= 1024) {
|
||||
// Large strings are counted towards the object limit.
|
||||
if (counter.objectLimit <= 0) {
|
||||
// We've reached our max number of objects to serialize across the wire so we serialize this
|
||||
// as a marker so that the client can error or lazy load this when accessed by the console.
|
||||
return serializeDeferredObject(request, value);
|
||||
}
|
||||
counter.objectLimit--;
|
||||
// For large strings, we encode them outside the JSON payload so that we
|
||||
// don't have to double encode and double parse the strings. This can also
|
||||
// be more compact in case the string has a lot of escaped characters.
|
||||
@@ -5254,3 +5307,82 @@ export function abort(request: Request, reason: mixed): void {
|
||||
fatalError(request, error);
|
||||
}
|
||||
}
|
||||
|
||||
function fromHex(str: string): number {
|
||||
return parseInt(str, 16);
|
||||
}
|
||||
|
||||
export function resolveDebugMessage(request: Request, message: string): void {
|
||||
if (!__DEV__) {
|
||||
// These errors should never make it into a build so we don't need to encode them in codes.json
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
throw new Error(
|
||||
'resolveDebugMessage should never be called in production mode. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
const deferredDebugObjects = request.deferredDebugObjects;
|
||||
if (deferredDebugObjects === null) {
|
||||
throw new Error(
|
||||
"resolveDebugMessage/closeDebugChannel should not be called for a Request that wasn't kept alive. This is a bug in React.",
|
||||
);
|
||||
}
|
||||
// This function lets the client ask for more data lazily through the debug channel.
|
||||
const command = message.charCodeAt(0);
|
||||
const ids = message.slice(2).split(',').map(fromHex);
|
||||
switch (command) {
|
||||
case 82 /* "R" */:
|
||||
// Release IDs
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
const retainedValue = deferredDebugObjects.retained.get(id);
|
||||
if (retainedValue !== undefined) {
|
||||
// We're no longer blocked on this. We won't emit it.
|
||||
request.pendingChunks--;
|
||||
deferredDebugObjects.retained.delete(id);
|
||||
deferredDebugObjects.existing.delete(retainedValue);
|
||||
enqueueFlush(request);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 81 /* "Q" */:
|
||||
// Query IDs
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
const retainedValue = deferredDebugObjects.retained.get(id);
|
||||
if (retainedValue !== undefined) {
|
||||
// If we still have this object, and haven't emitted it before, emit it on the stream.
|
||||
const counter = {objectLimit: 10};
|
||||
emitOutlinedDebugModelChunk(request, id, counter, retainedValue);
|
||||
enqueueFlush(request);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
'Unknown command. The debugChannel was not wired up properly.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function closeDebugChannel(request: Request): void {
|
||||
if (!__DEV__) {
|
||||
// These errors should never make it into a build so we don't need to encode them in codes.json
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
throw new Error(
|
||||
'closeDebugChannel should never be called in production mode. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
// This clears all remaining deferred objects, potentially resulting in the completion of the Request.
|
||||
const deferredDebugObjects = request.deferredDebugObjects;
|
||||
if (deferredDebugObjects === null) {
|
||||
throw new Error(
|
||||
"resolveDebugMessage/closeDebugChannel should not be called for a Request that wasn't kept alive. This is a bug in React.",
|
||||
);
|
||||
}
|
||||
deferredDebugObjects.retained.forEach((value, id) => {
|
||||
request.pendingChunks--;
|
||||
deferredDebugObjects.retained.delete(id);
|
||||
deferredDebugObjects.existing.delete(value);
|
||||
});
|
||||
enqueueFlush(request);
|
||||
}
|
||||
|
||||
@@ -548,5 +548,7 @@
|
||||
"560": "Cannot use a startGestureTransition() with a comment node root.",
|
||||
"561": "This rendered a large document (>%s kB) without any Suspense boundaries around most of it. That can delay initial paint longer than necessary. To improve load performance, add a <Suspense> or <SuspenseList> around the content you expect to be below the header or below the fold. In the meantime, the content will deopt to paint arbitrary incomplete pieces of HTML.",
|
||||
"562": "The render was aborted due to a fatal error.",
|
||||
"563": "This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources."
|
||||
"563": "This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources.",
|
||||
"564": "Unknown command. The debugChannel was not wired up properly.",
|
||||
"565": "resolveDebugMessage/closeDebugChannel should not be called for a Request that wasn't kept alive. This is a bug in React."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user