@samitouri / QOS-React-2 / commits / ef8bdbecb6

[Flight Reply] Add Reply Encoding (#26360)

This adds `encodeReply` to the Flight Client and `decodeReply` to the Flight Server. Basically, it's a reverse Flight. It serializes values passed from the client to the server. I call this a "Reply". The tradeoffs and implementation details are a bit different so it requires its own implementation but is basically a clone of the Flight Server/Client but in reverse. Either through callServer or ServerContext. The goal of this project is to provide the equivalent serialization as passing props through RSC to client. Except React Elements and Components and such. So that you can pass a value to the client and back and it should have the same serialization constraints so when we add features in one direction we should mostly add it in the other. Browser support for streaming request bodies are currently very limited in that only Chrome supports it. So this doesn't produce a ReadableStream. Instead `encodeReply` produces either a JSON string or FormData. It uses a JSON string if it's a simple enough payload. For advanced features it uses FormData. This will also let the browser stream things like File objects (even though they're not yet supported since it follows the same rules as the other Flight). On the server side, you can either consume this by blocking on generating a FormData object or you can stream in the `multipart/form-data`. Even if the client isn't streaming data, the network does. On Node.js busboy seems to be the canonical library for this, so I exposed a `decodeReplyFromBusboy` in the Node build. However, if there's ever a web-standard way to stream form data, or if a library wins in that space we can support it. We can also just build a multipart parser that takes a ReadableStream built-in. On the server, server references passed as arguments are loaded from Node or Webpack just like the client or SSR does. This means that you can create higher order functions on the client or server. This can be tokenized when done from a server components but this is a security implication as it might be tempting to think that these are not fungible but you can swap one function for another on the client. So you have to basically treat an incoming argument as insecure, even if it's a function. I'm not too happy with the naming parity: Encode `server.renderToReadableStream` Decode: `client.createFromFetch` Decode `client.encodeReply` Decode: `server.decodeReply` This is mainly an implementation details of frameworks but it's annoying nonetheless. This comes from that `renderToReadableStream` does do some "rendering" by unwrapping server components etc. The `create` part comes from the parity with Fizz/Fiber where you `render` on the server and `create` a root on the client. Open to bike-shedding this some more. --------- Co-authored-by: Josh Story <josh.c.story@gmail.com>

Sebastian Markbåge committed Mar 10, 2023 at 11:36 UTC ef8bdbecb6dbb9743b895c2e867e5a5264dd6651
27 files changed +1535 -413
fixtures/flight/package.json
+1
@@ -17,6 +17,7 @@
17 "babel-preset-react-app": "^10.0.1",
18 "body-parser": "^1.20.1",
19 "browserslist": "^4.18.1",
20 + "busboy": "^1.6.0",
21 "camelcase": "^6.2.1",
22 "case-sensitive-paths-webpack-plugin": "^2.4.0",
23 "compression": "^1.7.4",
fixtures/flight/server/region.js
+14 -5
@@ -33,6 +33,7 @@ if (typeof fetch === 'undefined') {
33
34 const express = require('express');
35 const bodyParser = require('body-parser');
36 +const busboy = require('busboy');
37 const app = express();
38 const compress = require('compression');
39
@@ -95,9 +96,8 @@ app.get('/', async function (req, res) {
96 });
97
98 app.post('/', bodyParser.text(), async function (req, res) {
98 - const {renderToPipeableStream} = await import(
99 - 'react-server-dom-webpack/server'
100 - );
99 + const {renderToPipeableStream, decodeReply, decodeReplyFromBusboy} =
100 + await import('react-server-dom-webpack/server');
101 const serverReference = req.get('rsc-action');
102 const [filepath, name] = serverReference.split('#');
103 const action = (await import(filepath))[name];
@@ -108,9 +108,18 @@ app.post('/', bodyParser.text(), async function (req, res) {
108 throw new Error('Invalid action');
109 }
110
111 - const args = JSON.parse(req.body);
112 - const result = action.apply(null, args);
111 + let args;
112 + if (req.is('multipart/form-data')) {
113 + // Use busboy to streamingly parse the reply from form-data.
114 + const bb = busboy({headers: req.headers});
115 + const reply = decodeReplyFromBusboy(bb);
116 + req.pipe(bb);
117 + args = await reply;
118 + } else {
119 + args = await decodeReply(req.body);
120 + }
121
122 + const result = action.apply(null, args);
123 const {pipe} = renderToPipeableStream(result, {});
124 pipe(res);
125 });
fixtures/flight/src/actions.js
+1 -9
@@ -1,13 +1,5 @@
1 'use server';
2
3 export async function like() {
4 - return new Promise((resolve, reject) =>
5 - setTimeout(
6 - () =>
7 - Math.random() > 0.5
8 - ? resolve('Liked')
9 - : reject(new Error('Failed to like')),
10 - 500
11 - )
12 - );
4 + return new Promise((resolve, reject) => resolve('Liked'));
5 }
fixtures/flight/src/index.js
+5 -5
@@ -1,28 +1,28 @@
1 import * as React from 'react';
2 import {Suspense} from 'react';
3 import ReactDOM from 'react-dom/client';
4 -import ReactServerDOMReader from 'react-server-dom-webpack/client';
4 +import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client';
5
6 // TODO: This should be a dependency of the App but we haven't implemented CSS in Node yet.
7 import './style.css';
8
9 -let data = ReactServerDOMReader.createFromFetch(
9 +let data = createFromFetch(
10 fetch('/', {
11 headers: {
12 Accept: 'text/x-component',
13 },
14 }),
15 {
16 - callServer(id, args) {
16 + async callServer(id, args) {
17 const response = fetch('/', {
18 method: 'POST',
19 headers: {
20 Accept: 'text/x-component',
21 'rsc-action': id,
22 },
23 - body: JSON.stringify(args),
23 + body: await encodeReply(args),
24 });
25 - return ReactServerDOMReader.createFromFetch(response);
25 + return createFromFetch(response);
26 },
27 }
28 );
packages/react-client/src/ReactFlightClient.js
+3
@@ -25,6 +25,8 @@ import {
25 parseModel,
26 } from './ReactFlightClientHostConfig';
27
28 +import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
29 +
30 import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
31
32 import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
@@ -495,6 +497,7 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
497 return callServer(metaData.id, bound.concat(args));
498 });
499 };
500 + knownServerReferences.set(proxy, metaData);
501 return proxy;
502 }
503
packages/react-client/src/ReactFlightReplyClient.js new
+278
@@ -0,0 +1,278 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Thenable} from 'shared/ReactTypes';
11 +
12 +import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
13 +
14 +import {
15 + REACT_ELEMENT_TYPE,
16 + REACT_LAZY_TYPE,
17 + REACT_PROVIDER_TYPE,
18 +} from 'shared/ReactSymbols';
19 +
20 +import {
21 + describeObjectForErrorMessage,
22 + isSimpleObject,
23 + objectName,
24 +} from 'shared/ReactSerializationErrors';
25 +
26 +import isArray from 'shared/isArray';
27 +
28 +type ReactJSONValue =
29 + | string
30 + | boolean
31 + | number
32 + | null
33 + | $ReadOnlyArray<ReactJSONValue>
34 + | ReactServerObject;
35 +
36 +export opaque type ServerReference<T> = T;
37 +
38 +// Serializable values
39 +export type ReactServerValue =
40 + // References are passed by their value
41 + | ServerReference<any>
42 + // The rest are passed as is. Sub-types can be passed in but lose their
43 + // subtype, so the receiver can only accept once of these.
44 + | string
45 + | boolean
46 + | number
47 + | symbol
48 + | null
49 + | Iterable<ReactServerValue>
50 + | Array<ReactServerValue>
51 + | ReactServerObject
52 + | Promise<ReactServerValue>; // Thenable<ReactServerValue>
53 +
54 +type ReactServerObject = {+[key: string]: ReactServerValue};
55 +
56 +// function serializeByValueID(id: number): string {
57 +// return '$' + id.toString(16);
58 +// }
59 +
60 +function serializePromiseID(id: number): string {
61 + return '$@' + id.toString(16);
62 +}
63 +
64 +function serializeServerReferenceID(id: number): string {
65 + return '$F' + id.toString(16);
66 +}
67 +
68 +function serializeSymbolReference(name: string): string {
69 + return '$S' + name;
70 +}
71 +
72 +function escapeStringValue(value: string): string {
73 + if (value[0] === '$') {
74 + // We need to escape $ prefixed strings since we use those to encode
75 + // references to IDs and as special symbol values.
76 + return '$' + value;
77 + } else {
78 + return value;
79 + }
80 +}
81 +
82 +export function processReply(
83 + root: ReactServerValue,
84 + resolve: (string | FormData) => void,
85 + reject: (error: mixed) => void,
86 +): void {
87 + let nextPartId = 1;
88 + let pendingParts = 0;
89 + let formData: null | FormData = null;
90 +
91 + function resolveToJSON(
92 + this:
93 + | {+[key: string | number]: ReactServerValue}
94 + | $ReadOnlyArray<ReactServerValue>,
95 + key: string,
96 + value: ReactServerValue,
97 + ): ReactJSONValue {
98 + const parent = this;
99 + if (__DEV__) {
100 + // $FlowFixMe
101 + const originalValue = this[key];
102 + if (typeof originalValue === 'object' && originalValue !== value) {
103 + if (objectName(originalValue) !== 'Object') {
104 + console.error(
105 + 'Only plain objects can be passed to Server Functions from the Client. ' +
106 + '%s objects are not supported.%s',
107 + objectName(originalValue),
108 + describeObjectForErrorMessage(parent, key),
109 + );
110 + } else {
111 + console.error(
112 + 'Only plain objects can be passed to Server Functions from the Client. ' +
113 + 'Objects with toJSON methods are not supported. Convert it manually ' +
114 + 'to a simple value before passing it to props.%s',
115 + describeObjectForErrorMessage(parent, key),
116 + );
117 + }
118 + }
119 + }
120 +
121 + if (value === null) {
122 + return null;
123 + }
124 +
125 + if (typeof value === 'object') {
126 + // $FlowFixMe[method-unbinding]
127 + if (typeof value.then === 'function') {
128 + // We assume that any object with a .then property is a "Thenable" type,
129 + // or a Promise type. Either of which can be represented by a Promise.
130 + if (formData === null) {
131 + // Upgrade to use FormData to allow us to stream this value.
132 + formData = new FormData();
133 + }
134 + pendingParts++;
135 + const promiseId = nextPartId++;
136 + const thenable: Thenable<any> = (value: any);
137 + thenable.then(
138 + partValue => {
139 + const partJSON = JSON.stringify(partValue, resolveToJSON);
140 + // $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
141 + const data: FormData = formData;
142 + // eslint-disable-next-line react-internal/safe-string-coercion
143 + data.append('' + promiseId, partJSON);
144 + pendingParts--;
145 + if (pendingParts === 0) {
146 + resolve(data);
147 + }
148 + },
149 + reason => {
150 + // In the future we could consider serializing this as an error
151 + // that throws on the server instead.
152 + reject(reason);
153 + },
154 + );
155 + return serializePromiseID(promiseId);
156 + }
157 +
158 + if (__DEV__) {
159 + if (value !== null && !isArray(value)) {
160 + // Verify that this is a simple plain object.
161 + if ((value: any).$$typeof === REACT_ELEMENT_TYPE) {
162 + console.error(
163 + 'React Element cannot be passed to Server Functions from the Client.%s',
164 + describeObjectForErrorMessage(parent, key),
165 + );
166 + } else if ((value: any).$$typeof === REACT_LAZY_TYPE) {
167 + console.error(
168 + 'React Lazy cannot be passed to Server Functions from the Client.%s',
169 + describeObjectForErrorMessage(parent, key),
170 + );
171 + } else if ((value: any).$$typeof === REACT_PROVIDER_TYPE) {
172 + console.error(
173 + 'React Context Providers cannot be passed to Server Functions from the Client.%s',
174 + describeObjectForErrorMessage(parent, key),
175 + );
176 + } else if (objectName(value) !== 'Object') {
177 + console.error(
178 + 'Only plain objects can be passed to Client Components from Server Components. ' +
179 + '%s objects are not supported.%s',
180 + objectName(value),
181 + describeObjectForErrorMessage(parent, key),
182 + );
183 + } else if (!isSimpleObject(value)) {
184 + console.error(
185 + 'Only plain objects can be passed to Client Components from Server Components. ' +
186 + 'Classes or other objects with methods are not supported.%s',
187 + describeObjectForErrorMessage(parent, key),
188 + );
189 + } else if (Object.getOwnPropertySymbols) {
190 + const symbols = Object.getOwnPropertySymbols(value);
191 + if (symbols.length > 0) {
192 + console.error(
193 + 'Only plain objects can be passed to Client Components from Server Components. ' +
194 + 'Objects with symbol properties like %s are not supported.%s',
195 + symbols[0].description,
196 + describeObjectForErrorMessage(parent, key),
197 + );
198 + }
199 + }
200 + }
201 + }
202 +
203 + // $FlowFixMe
204 + return value;
205 + }
206 +
207 + if (typeof value === 'string') {
208 + return escapeStringValue(value);
209 + }
210 +
211 + if (
212 + typeof value === 'boolean' ||
213 + typeof value === 'number' ||
214 + typeof value === 'undefined'
215 + ) {
216 + return value;
217 + }
218 +
219 + if (typeof value === 'function') {
220 + const metaData = knownServerReferences.get(value);
221 + if (metaData !== undefined) {
222 + const metaDataJSON = JSON.stringify(metaData, resolveToJSON);
223 + if (formData === null) {
224 + // Upgrade to use FormData to allow us to stream this value.
225 + formData = new FormData();
226 + }
227 + // The reference to this function came from the same client so we can pass it back.
228 + const refId = nextPartId++;
229 + // eslint-disable-next-line react-internal/safe-string-coercion
230 + formData.set('' + refId, metaDataJSON);
231 + return serializeServerReferenceID(refId);
232 + }
233 + throw new Error(
234 + 'Client Functions cannot be passed directly to Server Functions. ' +
235 + 'Only Functions passed from the Server can be passed back again.',
236 + );
237 + }
238 +
239 + if (typeof value === 'symbol') {
240 + // $FlowFixMe `description` might be undefined
241 + const name: string = value.description;
242 + if (Symbol.for(name) !== value) {
243 + throw new Error(
244 + 'Only global symbols received from Symbol.for(...) can be passed to Server Functions. ' +
245 + `The symbol Symbol.for(${
246 + // $FlowFixMe `description` might be undefined
247 + value.description
248 + }) cannot be found among global symbols.`,
249 + );
250 + }
251 + return serializeSymbolReference(name);
252 + }
253 +
254 + if (typeof value === 'bigint') {
255 + throw new Error(
256 + `BigInt (${value}) is not yet supported as an argument to a Server Function.`,
257 + );
258 + }
259 +
260 + throw new Error(
261 + `Type ${typeof value} is not supported as an argument to a Server Function.`,
262 + );
263 + }
264 +
265 + // $FlowFixMe[incompatible-type] it's not going to be undefined because we'll encode it.
266 + const json: string = JSON.stringify(root, resolveToJSON);
267 + if (formData === null) {
268 + // If it's a simple data structure, we just use plain JSON.
269 + resolve(json);
270 + } else {
271 + // Otherwise, we use FormData to let us stream in the result.
272 + formData.set('0', json);
273 + if (pendingParts === 0) {
274 + // $FlowFixMe[incompatible-call] this has already been refined.
275 + resolve(formData);
276 + }
277 + }
278 +}
packages/react-client/src/ReactFlightServerReferenceRegistry.js new
+17
@@ -0,0 +1,17 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Thenable} from 'shared/ReactTypes';
11 +
12 +type ServerReferenceId = any;
13 +
14 +export const knownServerReferences: WeakMap<
15 + Function,
16 + {id: ServerReferenceId, bound: null | Thenable<Array<any>>},
17 +> = new WeakMap();
packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js
+3
@@ -27,9 +27,12 @@ declare var $$$hostConfig: any;
27
28 export type Response = any;
29 export opaque type SSRManifest = mixed;
30 +export opaque type ServerManifest = mixed;
31 +export opaque type ServerReferenceId = string;
32 export opaque type ClientReferenceMetadata = mixed;
33 export opaque type ClientReference<T> = mixed; // eslint-disable-line no-unused-vars
34 export const resolveClientReference = $$$hostConfig.resolveClientReference;
35 +export const resolveServerReference = $$$hostConfig.resolveServerReference;
36 export const preloadModule = $$$hostConfig.preloadModule;
37 export const requireModule = $$$hostConfig.requireModule;
38
packages/react-client/src/forks/ReactFlightClientHostConfig.dom-bun.js
+3
@@ -12,8 +12,11 @@ export * from 'react-client/src/ReactFlightClientHostConfigStream';
12
13 export type Response = any;
14 export opaque type SSRManifest = mixed;
15 +export opaque type ServerManifest = mixed;
16 +export opaque type ServerReferenceId = string;
17 export opaque type ClientReferenceMetadata = mixed;
18 export opaque type ClientReference<T> = mixed; // eslint-disable-line no-unused-vars
19 export const resolveClientReference: any = null;
20 +export const resolveServerReference: any = null;
21 export const preloadModule: any = null;
22 export const requireModule: any = null;
packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js
+9
@@ -32,6 +32,8 @@ import isArray from 'shared/isArray';
32 export type {ClientReferenceMetadata} from 'ReactFlightDOMRelayClientIntegration';
33
34 export type SSRManifest = null;
35 +export type ServerManifest = null;
36 +export type ServerReferenceId = string;
37
38 export type UninitializedModel = JSONValue;
39
@@ -44,6 +46,13 @@ export function resolveClientReference<T>(
46 return resolveClientReferenceImpl(metadata);
47 }
48
49 +export function resolveServerReference<T>(
50 + bundlerConfig: ServerManifest,
51 + id: ServerReferenceId,
52 +): ClientReference<T> {
53 + throw new Error('Not implemented.');
54 +}
55 +
56 function parseModelRecursively(
57 response: Response,
58 parentObj: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
packages/react-server-dom-webpack/src/ReactFlightClientNodeBundlerConfig.js
+14
@@ -19,6 +19,10 @@ export type SSRManifest = {
19 },
20 };
21
22 +export type ServerManifest = void;
23 +
24 +export type ServerReferenceId = string;
25 +
26 export opaque type ClientReferenceMetadata = {
27 id: string,
28 chunks: Array<string>,
@@ -39,6 +43,16 @@ export function resolveClientReference<T>(
43 return resolvedModuleData;
44 }
45
46 +export function resolveServerReference<T>(
47 + bundlerConfig: ServerManifest,
48 + id: ServerReferenceId,
49 +): ClientReference<T> {
50 + const idx = id.lastIndexOf('#');
51 + const specifier = id.substr(0, idx);
52 + const name = id.substr(idx + 1);
53 + return {specifier, name};
54 +}
55 +
56 const asyncModuleCache: Map<string, Thenable<any>> = new Map();
57
58 export function preloadModule<T>(
packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js
+14
@@ -19,6 +19,12 @@ export type SSRManifest = null | {
19 },
20 };
21
22 +export type ServerManifest = {
23 + [id: string]: ClientReference<any>,
24 +};
25 +
26 +export type ServerReferenceId = string;
27 +
28 export opaque type ClientReferenceMetadata = {
29 id: string,
30 chunks: Array<string>,
@@ -49,6 +55,14 @@ export function resolveClientReference<T>(
55 return metadata;
56 }
57
58 +export function resolveServerReference<T>(
59 + bundlerConfig: ServerManifest,
60 + id: ServerReferenceId,
61 +): ClientReference<T> {
62 + // This needs to return async: true if it's an async module.
63 + return bundlerConfig[id];
64 +}
65 +
66 // The chunk cache contains all the chunks we've preloaded so far.
67 // If they're still pending they're a thenable. This map also exists
68 // in Webpack but unfortunately it's not exposed so we have to
packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js
+15 -1
@@ -11,6 +11,8 @@ import type {Thenable} from 'shared/ReactTypes.js';
11
12 import type {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream';
13
14 +import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
15 +
16 import {
17 createResponse,
18 getRoot,
@@ -20,6 +22,8 @@ import {
22 close,
23 } from 'react-client/src/ReactFlightClientStream';
24
25 +import {processReply} from 'react-client/src/ReactFlightReplyClient';
26 +
27 type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
28
29 export type Options = {
@@ -111,4 +115,14 @@ function createFromXHR<T>(
115 return getRoot(response);
116 }
117
114 -export {createFromXHR, createFromFetch, createFromReadableStream};
118 +function encodeReply(
119 + value: ReactServerValue,
120 +): Promise<
121 + string | URLSearchParams | FormData,
122 +> /* We don't use URLSearchParams yet but maybe */ {
123 + return new Promise((resolve, reject) => {
124 + processReply(value, resolve, reject);
125 + });
126 +}
127 +
128 +export {createFromXHR, createFromFetch, createFromReadableStream, encodeReply};
packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js
+1 -1
@@ -30,7 +30,7 @@ function noServerCall() {
30 }
31
32 export type Options = {
33 - moduleMap?: SSRManifest,
33 + moduleMap?: $NonMaybeType<SSRManifest>,
34 };
35
36 function createResponseFromOptions(options: void | Options) {
packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js
+33 -2
@@ -8,8 +8,9 @@
8 */
9
10 import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
11 -import type {ServerContextJSONValue} from 'shared/ReactTypes';
11 +import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes';
12 import type {ClientManifest} from './ReactFlightServerWebpackBundlerConfig';
13 +import type {ServerManifest} from 'react-client/src/ReactFlightClientHostConfig';
14
15 import {
16 createRequest,
@@ -18,6 +19,14 @@ import {
19 abort,
20 } from 'react-server/src/ReactFlightServer';
21
22 +import {
23 + createResponse,
24 + close,
25 + resolveField,
26 + resolveFile,
27 + getRoot,
28 +} from 'react-server/src/ReactFlightReplyServer';
29 +
30 type Options = {
31 identifierPrefix?: string,
32 signal?: AbortSignal,
@@ -66,4 +75,26 @@ function renderToReadableStream(
75 return stream;
76 }
77
69 -export {renderToReadableStream};
78 +function decodeReply<T>(
79 + body: string | FormData,
80 + webpackMap: ServerManifest,
81 +): Thenable<T> {
82 + const response = createResponse(webpackMap);
83 + if (typeof body === 'string') {
84 + resolveField(response, 0, body);
85 + } else {
86 + // $FlowFixMe[prop-missing] Flow doesn't know that forEach exists.
87 + body.forEach((value: string | File, key: string) => {
88 + const id = +key;
89 + if (typeof value === 'string') {
90 + resolveField(response, id, value);
91 + } else {
92 + resolveFile(response, id, value);
93 + }
94 + });
95 + }
96 + close(response);
97 + return getRoot(response);
98 +}
99 +
100 +export {renderToReadableStream, decodeReply};
packages/react-server-dom-webpack/src/ReactFlightDOMServerEdge.js
+33 -2
@@ -8,8 +8,9 @@
8 */
9
10 import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
11 -import type {ServerContextJSONValue} from 'shared/ReactTypes';
11 +import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes';
12 import type {ClientManifest} from './ReactFlightServerWebpackBundlerConfig';
13 +import type {ServerManifest} from 'react-client/src/ReactFlightClientHostConfig';
14
15 import {
16 createRequest,
@@ -18,6 +19,14 @@ import {
19 abort,
20 } from 'react-server/src/ReactFlightServer';
21
22 +import {
23 + createResponse,
24 + close,
25 + resolveField,
26 + resolveFile,
27 + getRoot,
28 +} from 'react-server/src/ReactFlightReplyServer';
29 +
30 type Options = {
31 identifierPrefix?: string,
32 signal?: AbortSignal,
@@ -66,4 +75,26 @@ function renderToReadableStream(
75 return stream;
76 }
77
69 -export {renderToReadableStream};
78 +function decodeReply<T>(
79 + body: string | FormData,
80 + webpackMap: ServerManifest,
81 +): Thenable<T> {
82 + const response = createResponse(webpackMap);
83 + if (typeof body === 'string') {
84 + resolveField(response, 0, body);
85 + } else {
86 + // $FlowFixMe[prop-missing] Flow doesn't know that forEach exists.
87 + body.forEach((value: string | File, key: string) => {
88 + const id = +key;
89 + if (typeof value === 'string') {
90 + resolveField(response, id, value);
91 + } else {
92 + resolveFile(response, id, value);
93 + }
94 + });
95 + }
96 + close(response);
97 + return getRoot(response);
98 +}
99 +
100 +export {renderToReadableStream, decodeReply};
packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js
+73 -2
@@ -13,8 +13,10 @@ import type {
13 } from 'react-server/src/ReactFlightServer';
14 import type {Destination} from 'react-server/src/ReactServerStreamConfigNode';
15 import type {ClientManifest} from './ReactFlightServerWebpackBundlerConfig';
16 +import type {ServerManifest} from 'react-client/src/ReactFlightClientHostConfig';
17 +import type {Busboy} from 'busboy';
18 import type {Writable} from 'stream';
17 -import type {ServerContextJSONValue} from 'shared/ReactTypes';
19 +import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes';
20
21 import {
22 createRequest,
@@ -23,6 +25,18 @@ import {
25 abort,
26 } from 'react-server/src/ReactFlightServer';
27
28 +import {
29 + createResponse,
30 + reportGlobalError,
31 + close,
32 + resolveField,
33 + resolveFile,
34 + resolveFileInfo,
35 + resolveFileChunk,
36 + resolveFileComplete,
37 + getRoot,
38 +} from 'react-server/src/ReactFlightReplyServer';
39 +
40 function createDrainHandler(destination: Destination, request: Request) {
41 return () => startFlowing(request, destination);
42 }
@@ -70,4 +84,61 @@ function renderToPipeableStream(
84 };
85 }
86
73 -export {renderToPipeableStream};
87 +function decodeReplyFromBusboy<T>(
88 + busboyStream: Busboy,
89 + webpackMap: ServerManifest,
90 +): Thenable<T> {
91 + const response = createResponse(webpackMap);
92 + busboyStream.on('field', (name, value) => {
93 + const id = +name;
94 + resolveField(response, id, value);
95 + });
96 + busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
97 + if (encoding.toLowerCase() === 'base64') {
98 + throw new Error(
99 + "React doesn't accept base64 encoded file uploads because we don't expect " +
100 + "form data passed from a browser to ever encode data that way. If that's " +
101 + 'the wrong assumption, we can easily fix it.',
102 + );
103 + }
104 + const id = +name;
105 + const file = resolveFileInfo(response, id, filename, mimeType);
106 + value.on('data', chunk => {
107 + resolveFileChunk(response, file, chunk);
108 + });
109 + value.on('end', () => {
110 + resolveFileComplete(response, file);
111 + });
112 + });
113 + busboyStream.on('finish', () => {
114 + close(response);
115 + });
116 + busboyStream.on('error', err => {
117 + reportGlobalError(response, err);
118 + });
119 + return getRoot(response);
120 +}
121 +
122 +function decodeReply<T>(
123 + body: string | FormData,
124 + webpackMap: ServerManifest,
125 +): Thenable<T> {
126 + const response = createResponse(webpackMap);
127 + if (typeof body === 'string') {
128 + resolveField(response, 0, body);
129 + } else {
130 + // $FlowFixMe[prop-missing] Flow doesn't know that forEach exists.
131 + body.forEach((value: string | File, key: string) => {
132 + const id = +key;
133 + if (typeof value === 'string') {
134 + resolveField(response, id, value);
135 + } else {
136 + resolveFile(response, id, value);
137 + }
138 + });
139 + }
140 + close(response);
141 + return getRoot(response);
142 +}
143 +
144 +export {renderToPipeableStream, decodeReplyFromBusboy, decodeReply};
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+40 -40
@@ -26,8 +26,8 @@ let webpackMap;
26 let Stream;
27 let React;
28 let ReactDOMClient;
29 -let ReactServerDOMWriter;
30 -let ReactServerDOMReader;
29 +let ReactServerDOMServer;
30 +let ReactServerDOMClient;
31 let Suspense;
32 let ErrorBoundary;
33
@@ -45,8 +45,8 @@ describe('ReactFlightDOM', () => {
45 use = React.use;
46 Suspense = React.Suspense;
47 ReactDOMClient = require('react-dom/client');
48 - ReactServerDOMWriter = require('react-server-dom-webpack/server.node.unbundled');
49 - ReactServerDOMReader = require('react-server-dom-webpack/client');
48 + ReactServerDOMClient = require('react-server-dom-webpack/client');
49 + ReactServerDOMServer = require('react-server-dom-webpack/server.node.unbundled');
50
51 ErrorBoundary = class extends React.Component {
52 state = {hasError: false, error: null};
@@ -109,12 +109,12 @@ describe('ReactFlightDOM', () => {
109 }
110
111 const {writable, readable} = getTestStream();
112 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
112 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
113 <App />,
114 webpackMap,
115 );
116 pipe(writable);
117 - const response = ReactServerDOMReader.createFromReadableStream(readable);
117 + const response = ReactServerDOMClient.createFromReadableStream(readable);
118 const model = await response;
119 expect(model).toEqual({
120 html: (
@@ -159,12 +159,12 @@ describe('ReactFlightDOM', () => {
159 }
160
161 const {writable, readable} = getTestStream();
162 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
162 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
163 <RootModel />,
164 webpackMap,
165 );
166 pipe(writable);
167 - const response = ReactServerDOMReader.createFromReadableStream(readable);
167 + const response = ReactServerDOMClient.createFromReadableStream(readable);
168
169 const container = document.createElement('div');
170 const root = ReactDOMClient.createRoot(container);
@@ -196,12 +196,12 @@ describe('ReactFlightDOM', () => {
196 }
197
198 const {writable, readable} = getTestStream();
199 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
199 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
200 <RootModel />,
201 webpackMap,
202 );
203 pipe(writable);
204 - const response = ReactServerDOMReader.createFromReadableStream(readable);
204 + const response = ReactServerDOMClient.createFromReadableStream(readable);
205
206 const container = document.createElement('div');
207 const root = ReactDOMClient.createRoot(container);
@@ -231,12 +231,12 @@ describe('ReactFlightDOM', () => {
231 }
232
233 const {writable, readable} = getTestStream();
234 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
234 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
235 <RootModel />,
236 webpackMap,
237 );
238 pipe(writable);
239 - const response = ReactServerDOMReader.createFromReadableStream(readable);
239 + const response = ReactServerDOMClient.createFromReadableStream(readable);
240
241 const container = document.createElement('div');
242 const root = ReactDOMClient.createRoot(container);
@@ -281,12 +281,12 @@ describe('ReactFlightDOM', () => {
281 );
282
283 const {writable, readable} = getTestStream();
284 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
284 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
285 <Component greeting={hi} />,
286 webpackMap,
287 );
288 pipe(writable);
289 - const response = ReactServerDOMReader.createFromReadableStream(readable);
289 + const response = ReactServerDOMClient.createFromReadableStream(readable);
290
291 const container = document.createElement('div');
292 const root = ReactDOMClient.createRoot(container);
@@ -319,12 +319,12 @@ describe('ReactFlightDOM', () => {
319 const {Component} = clientExports(Module);
320
321 const {writable, readable} = getTestStream();
322 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
322 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
323 <Component greeting={'Hello'} />,
324 webpackMap,
325 );
326 pipe(writable);
327 - const response = ReactServerDOMReader.createFromReadableStream(readable);
327 + const response = ReactServerDOMClient.createFromReadableStream(readable);
328
329 const container = document.createElement('div');
330 const root = ReactDOMClient.createRoot(container);
@@ -360,12 +360,12 @@ describe('ReactFlightDOM', () => {
360 const AsyncModuleRef2 = await clientExports(AsyncModule2);
361
362 const {writable, readable} = getTestStream();
363 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
363 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
364 <AsyncModuleRef text={AsyncModuleRef2.exportName} />,
365 webpackMap,
366 );
367 pipe(writable);
368 - const response = ReactServerDOMReader.createFromReadableStream(readable);
368 + const response = ReactServerDOMClient.createFromReadableStream(readable);
369
370 const container = document.createElement('div');
371 const root = ReactDOMClient.createRoot(container);
@@ -399,12 +399,12 @@ describe('ReactFlightDOM', () => {
399 }
400
401 const {writable, readable} = getTestStream();
402 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
402 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
403 <ServerComponent />,
404 webpackMap,
405 );
406 pipe(writable);
407 - const response = ReactServerDOMReader.createFromReadableStream(readable);
407 + const response = ReactServerDOMClient.createFromReadableStream(readable);
408
409 const container = document.createElement('div');
410 const root = ReactDOMClient.createRoot(container);
@@ -437,12 +437,12 @@ describe('ReactFlightDOM', () => {
437 const ThenRef = clientExports(thenExports).then;
438
439 const {writable, readable} = getTestStream();
440 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
440 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
441 <ThenRef />,
442 webpackMap,
443 );
444 pipe(writable);
445 - const response = ReactServerDOMReader.createFromReadableStream(readable);
445 + const response = ReactServerDOMClient.createFromReadableStream(readable);
446
447 const container = document.createElement('div');
448 const root = ReactDOMClient.createRoot(container);
@@ -592,7 +592,7 @@ describe('ReactFlightDOM', () => {
592 }
593
594 const {writable, readable} = getTestStream();
595 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
595 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
596 model,
597 webpackMap,
598 {
@@ -603,7 +603,7 @@ describe('ReactFlightDOM', () => {
603 },
604 );
605 pipe(writable);
606 - const response = ReactServerDOMReader.createFromReadableStream(readable);
606 + const response = ReactServerDOMClient.createFromReadableStream(readable);
607
608 const container = document.createElement('div');
609 const root = ReactDOMClient.createRoot(container);
@@ -715,12 +715,12 @@ describe('ReactFlightDOM', () => {
715 const root = ReactDOMClient.createRoot(container);
716
717 const stream1 = getTestStream();
718 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
718 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
719 <App color="red" />,
720 webpackMap,
721 );
722 pipe(stream1.writable);
723 - const response1 = ReactServerDOMReader.createFromReadableStream(
723 + const response1 = ReactServerDOMClient.createFromReadableStream(
724 stream1.readable,
725 );
726 await act(() => {
@@ -743,12 +743,12 @@ describe('ReactFlightDOM', () => {
743 inputB.value = 'goodbye';
744
745 const stream2 = getTestStream();
746 - const {pipe: pipe2} = ReactServerDOMWriter.renderToPipeableStream(
746 + const {pipe: pipe2} = ReactServerDOMServer.renderToPipeableStream(
747 <App color="blue" />,
748 webpackMap,
749 );
750 pipe2(stream2.writable);
751 - const response2 = ReactServerDOMReader.createFromReadableStream(
751 + const response2 = ReactServerDOMClient.createFromReadableStream(
752 stream2.readable,
753 );
754 await act(() => {
@@ -776,7 +776,7 @@ describe('ReactFlightDOM', () => {
776 const reportedErrors = [];
777
778 const {writable, readable} = getTestStream();
779 - const {pipe, abort} = ReactServerDOMWriter.renderToPipeableStream(
779 + const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
780 <div>
781 <InfiniteSuspend />
782 </div>,
@@ -790,7 +790,7 @@ describe('ReactFlightDOM', () => {
790 },
791 );
792 pipe(writable);
793 - const response = ReactServerDOMReader.createFromReadableStream(readable);
793 + const response = ReactServerDOMClient.createFromReadableStream(readable);
794
795 const container = document.createElement('div');
796 const root = ReactDOMClient.createRoot(container);
@@ -841,7 +841,7 @@ describe('ReactFlightDOM', () => {
841 const ClientReference = clientModuleError(new Error('module init error'));
842
843 const {writable, readable} = getTestStream();
844 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
844 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
845 <div>
846 <ClientComponent prop={ClientReference} />
847 </div>,
@@ -853,7 +853,7 @@ describe('ReactFlightDOM', () => {
853 },
854 );
855 pipe(writable);
856 - const response = ReactServerDOMReader.createFromReadableStream(readable);
856 + const response = ReactServerDOMClient.createFromReadableStream(readable);
857
858 const container = document.createElement('div');
859 const root = ReactDOMClient.createRoot(container);
@@ -892,7 +892,7 @@ describe('ReactFlightDOM', () => {
892 );
893
894 const {writable, readable} = getTestStream();
895 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
895 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
896 <div>
897 <ClientComponent prop={ClientReference} />
898 </div>,
@@ -904,7 +904,7 @@ describe('ReactFlightDOM', () => {
904 },
905 );
906 pipe(writable);
907 - const response = ReactServerDOMReader.createFromReadableStream(readable);
907 + const response = ReactServerDOMClient.createFromReadableStream(readable);
908
909 const container = document.createElement('div');
910 const root = ReactDOMClient.createRoot(container);
@@ -952,7 +952,7 @@ describe('ReactFlightDOM', () => {
952 }
953
954 const {writable, readable} = getTestStream();
955 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
955 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
956 <div>
957 <ClientComponent />
958 </div>,
@@ -966,7 +966,7 @@ describe('ReactFlightDOM', () => {
966 );
967 pipe(writable);
968
969 - const response = ReactServerDOMReader.createFromReadableStream(readable);
969 + const response = ReactServerDOMClient.createFromReadableStream(readable);
970
971 const container = document.createElement('div');
972 const root = ReactDOMClient.createRoot(container);
@@ -1032,12 +1032,12 @@ describe('ReactFlightDOM', () => {
1032 }
1033
1034 const {writable, readable} = getTestStream();
1035 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
1035 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1036 <ServerComponent />,
1037 webpackMap,
1038 );
1039 pipe(writable);
1040 - const response = ReactServerDOMReader.createFromReadableStream(readable);
1040 + const response = ReactServerDOMClient.createFromReadableStream(readable);
1041
1042 const container = document.createElement('div');
1043 const root = ReactDOMClient.createRoot(container);
@@ -1089,7 +1089,7 @@ describe('ReactFlightDOM', () => {
1089 }
1090
1091 const {writable, readable} = getTestStream();
1092 - const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
1092 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1093 <ServerComponent />,
1094 webpackMap,
1095 {
@@ -1100,7 +1100,7 @@ describe('ReactFlightDOM', () => {
1100 },
1101 );
1102 pipe(writable);
1103 - const response = ReactServerDOMReader.createFromReadableStream(readable);
1103 + const response = ReactServerDOMClient.createFromReadableStream(readable);
1104
1105 const container = document.createElement('div');
1106 const root = ReactDOMClient.createRoot(container);
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+72 -57
@@ -22,8 +22,8 @@ let webpackServerMap;
22 let act;
23 let React;
24 let ReactDOMClient;
25 -let ReactServerDOMWriter;
26 -let ReactServerDOMReader;
25 +let ReactServerDOMServer;
26 +let ReactServerDOMClient;
27 let Suspense;
28 let use;
29
@@ -38,8 +38,8 @@ describe('ReactFlightDOMBrowser', () => {
38 webpackServerMap = WebpackMock.webpackServerMap;
39 React = require('react');
40 ReactDOMClient = require('react-dom/client');
41 - ReactServerDOMWriter = require('react-server-dom-webpack/server.browser');
42 - ReactServerDOMReader = require('react-server-dom-webpack/client');
41 + ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
42 + ReactServerDOMClient = require('react-server-dom-webpack/client');
43 Suspense = React.Suspense;
44 use = React.use;
45 });
@@ -74,6 +74,21 @@ describe('ReactFlightDOMBrowser', () => {
74 throw theInfinitePromise;
75 }
76
77 + function requireServerRef(ref) {
78 + const metaData = webpackServerMap[ref];
79 + const mod = __webpack_require__(metaData.id);
80 + if (metaData.name === '*') {
81 + return mod;
82 + }
83 + return mod[metaData.name];
84 + }
85 +
86 + async function callServer(actionId, body) {
87 + const fn = requireServerRef(actionId);
88 + const args = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
89 + return fn.apply(null, args);
90 + }
91 +
92 it('should resolve HTML using W3C streams', async () => {
93 function Text({children}) {
94 return <span>{children}</span>;
@@ -94,8 +109,8 @@ describe('ReactFlightDOMBrowser', () => {
109 return model;
110 }
111
97 - const stream = ReactServerDOMWriter.renderToReadableStream(<App />);
98 - const response = ReactServerDOMReader.createFromReadableStream(stream);
112 + const stream = ReactServerDOMServer.renderToReadableStream(<App />);
113 + const response = ReactServerDOMClient.createFromReadableStream(stream);
114 const model = await response;
115 expect(model).toEqual({
116 html: (
@@ -127,8 +142,8 @@ describe('ReactFlightDOMBrowser', () => {
142 return model;
143 }
144
130 - const stream = ReactServerDOMWriter.renderToReadableStream(<App />);
131 - const response = ReactServerDOMReader.createFromReadableStream(stream);
145 + const stream = ReactServerDOMServer.renderToReadableStream(<App />);
146 + const response = ReactServerDOMClient.createFromReadableStream(stream);
147 const model = await response;
148 expect(model).toEqual({
149 html: (
@@ -250,7 +265,7 @@ describe('ReactFlightDOMBrowser', () => {
265 return use(response).rootContent;
266 }
267
253 - const stream = ReactServerDOMWriter.renderToReadableStream(
268 + const stream = ReactServerDOMServer.renderToReadableStream(
269 model,
270 webpackMap,
271 {
@@ -260,7 +275,7 @@ describe('ReactFlightDOMBrowser', () => {
275 },
276 },
277 );
263 - const response = ReactServerDOMReader.createFromReadableStream(stream);
278 + const response = ReactServerDOMClient.createFromReadableStream(stream);
279
280 const container = document.createElement('div');
281 const root = ReactDOMClient.createRoot(container);
@@ -389,7 +404,7 @@ describe('ReactFlightDOMBrowser', () => {
404 rootContent: <ProfileContent />,
405 };
406
392 - const stream = ReactServerDOMWriter.renderToReadableStream(
407 + const stream = ReactServerDOMServer.renderToReadableStream(
408 model,
409 webpackMap,
410 );
@@ -489,7 +504,7 @@ describe('ReactFlightDOMBrowser', () => {
504 }
505
506 const controller = new AbortController();
492 - const stream = ReactServerDOMWriter.renderToReadableStream(
507 + const stream = ReactServerDOMServer.renderToReadableStream(
508 <div>
509 <InfiniteSuspend />
510 </div>,
@@ -503,7 +518,7 @@ describe('ReactFlightDOMBrowser', () => {
518 },
519 },
520 );
506 - const response = ReactServerDOMReader.createFromReadableStream(stream);
521 + const response = ReactServerDOMClient.createFromReadableStream(stream);
522
523 const container = document.createElement('div');
524 const root = ReactDOMClient.createRoot(container);
@@ -544,8 +559,8 @@ describe('ReactFlightDOMBrowser', () => {
559 );
560 }
561
547 - const stream = ReactServerDOMWriter.renderToReadableStream(<Server />);
548 - const response = ReactServerDOMReader.createFromReadableStream(stream);
562 + const stream = ReactServerDOMServer.renderToReadableStream(<Server />);
563 + const response = ReactServerDOMClient.createFromReadableStream(stream);
564
565 function Client() {
566 return use(response);
@@ -578,8 +593,8 @@ describe('ReactFlightDOMBrowser', () => {
593 </ContextA.Provider>
594 );
595 }
581 - const stream = ReactServerDOMWriter.renderToReadableStream(<Server />);
582 - const response = ReactServerDOMReader.createFromReadableStream(stream);
596 + const stream = ReactServerDOMServer.renderToReadableStream(<Server />);
597 + const response = ReactServerDOMClient.createFromReadableStream(stream);
598
599 function Client() {
600 return use(response);
@@ -609,8 +624,8 @@ describe('ReactFlightDOMBrowser', () => {
624 );
625 }
626
612 - const stream = ReactServerDOMWriter.renderToReadableStream(<Parent />);
613 - const response = ReactServerDOMReader.createFromReadableStream(stream);
627 + const stream = ReactServerDOMServer.renderToReadableStream(<Parent />);
628 + const response = ReactServerDOMClient.createFromReadableStream(stream);
629
630 function Client() {
631 return use(response);
@@ -643,7 +658,7 @@ describe('ReactFlightDOMBrowser', () => {
658 }
659
660 const reportedErrors = [];
646 - const stream = ReactServerDOMWriter.renderToReadableStream(
661 + const stream = ReactServerDOMServer.renderToReadableStream(
662 <Server />,
663 webpackMap,
664 {
@@ -653,7 +668,7 @@ describe('ReactFlightDOMBrowser', () => {
668 },
669 },
670 );
656 - const response = ReactServerDOMReader.createFromReadableStream(stream);
671 + const response = ReactServerDOMClient.createFromReadableStream(stream);
672
673 class ErrorBoundary extends React.Component {
674 state = {error: null};
@@ -703,8 +718,8 @@ describe('ReactFlightDOMBrowser', () => {
718 return use(thenable);
719 }
720
706 - const stream = ReactServerDOMWriter.renderToReadableStream(<Server />);
707 - const response = ReactServerDOMReader.createFromReadableStream(stream);
721 + const stream = ReactServerDOMServer.renderToReadableStream(<Server />);
722 + const response = ReactServerDOMClient.createFromReadableStream(stream);
723
724 function Client() {
725 return use(response);
@@ -739,8 +754,8 @@ describe('ReactFlightDOMBrowser', () => {
754
755 // Because the thenable resolves synchronously, we should be able to finish
756 // rendering synchronously, with no fallback.
742 - const stream = ReactServerDOMWriter.renderToReadableStream(<Server />);
743 - const response = ReactServerDOMReader.createFromReadableStream(stream);
757 + const stream = ReactServerDOMServer.renderToReadableStream(<Server />);
758 + const response = ReactServerDOMClient.createFromReadableStream(stream);
759
760 function Client() {
761 return use(response);
@@ -754,16 +769,7 @@ describe('ReactFlightDOMBrowser', () => {
769 expect(container.innerHTML).toBe('Hi');
770 });
771
757 - function requireServerRef(ref) {
758 - const metaData = webpackServerMap[ref];
759 - const mod = __webpack_require__(metaData.id);
760 - if (metaData.name === '*') {
761 - return mod;
762 - }
763 - return mod[metaData.name];
764 - }
765 -
766 - it('can pass a function by reference from server to client', async () => {
772 + it('can pass a higher order function by reference from server to client', async () => {
773 let actionProxy;
774
775 function Client({action}) {
@@ -771,24 +777,33 @@ describe('ReactFlightDOMBrowser', () => {
777 return 'Click Me';
778 }
779
774 - function send(text) {
780 + function greet(transform, text) {
781 + return 'Hello ' + transform(text);
782 + }
783 +
784 + function upper(text) {
785 return text.toUpperCase();
786 }
787
778 - const ServerModule = serverExports({
779 - send,
788 + const ServerModuleA = serverExports({
789 + greet,
790 + });
791 + const ServerModuleB = serverExports({
792 + upper,
793 });
794 const ClientRef = clientExports(Client);
795
783 - const stream = ReactServerDOMWriter.renderToReadableStream(
784 - <ClientRef action={ServerModule.send} />,
796 + const boundFn = ServerModuleA.greet.bind(null, ServerModuleB.upper);
797 +
798 + const stream = ReactServerDOMServer.renderToReadableStream(
799 + <ClientRef action={boundFn} />,
800 webpackMap,
801 );
802
788 - const response = ReactServerDOMReader.createFromReadableStream(stream, {
803 + const response = ReactServerDOMClient.createFromReadableStream(stream, {
804 async callServer(ref, args) {
790 - const fn = requireServerRef(ref);
791 - return fn.apply(null, args);
805 + const body = await ReactServerDOMClient.encodeReply(args);
806 + return callServer(ref, body);
807 },
808 });
809
@@ -803,10 +818,10 @@ describe('ReactFlightDOMBrowser', () => {
818 });
819 expect(container.innerHTML).toBe('Click Me');
820 expect(typeof actionProxy).toBe('function');
806 - expect(actionProxy).not.toBe(send);
821 + expect(actionProxy).not.toBe(boundFn);
822
823 const result = await actionProxy('hi');
809 - expect(result).toBe('HI');
824 + expect(result).toBe('Hello HI');
825 });
826
827 it('can bind arguments to a server reference', async () => {
@@ -822,15 +837,15 @@ describe('ReactFlightDOMBrowser', () => {
837 });
838 const ClientRef = clientExports(Client);
839
825 - const stream = ReactServerDOMWriter.renderToReadableStream(
840 + const stream = ReactServerDOMServer.renderToReadableStream(
841 <ClientRef action={greet.bind(null, 'Hello', 'World')} />,
842 webpackMap,
843 );
844
830 - const response = ReactServerDOMReader.createFromReadableStream(stream, {
831 - async callServer(ref, args) {
832 - const fn = requireServerRef(ref);
833 - return fn.apply(null, args);
845 + const response = ReactServerDOMClient.createFromReadableStream(stream, {
846 + async callServer(actionId, args) {
847 + const body = await ReactServerDOMClient.encodeReply(args);
848 + return callServer(actionId, body);
849 },
850 });
851
@@ -866,17 +881,17 @@ describe('ReactFlightDOMBrowser', () => {
881 const ServerModule = serverExports({send});
882 const ClientRef = clientExports(Client);
883
869 - const stream = ReactServerDOMWriter.renderToReadableStream(
884 + const stream = ReactServerDOMServer.renderToReadableStream(
885 <ClientRef action={ServerModule.send} />,
886 webpackMap,
887 );
888
874 - const response = ReactServerDOMReader.createFromReadableStream(stream, {
875 - async callServer(ref, args) {
876 - const fn = requireServerRef(ref);
877 - return ReactServerDOMReader.createFromReadableStream(
878 - ReactServerDOMWriter.renderToReadableStream(
879 - fn.apply(null, args),
889 + const response = ReactServerDOMClient.createFromReadableStream(stream, {
890 + async callServer(actionId, args) {
891 + const body = await ReactServerDOMClient.encodeReply(args);
892 + return ReactServerDOMClient.createFromReadableStream(
893 + ReactServerDOMServer.renderToReadableStream(
894 + callServer(actionId, body),
895 null,
896 {onError: error => 'test-error-digest'},
897 ),
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+6 -6
@@ -24,8 +24,8 @@ let webpackMap;
24 let webpackModules;
25 let React;
26 let ReactDOMServer;
27 -let ReactServerDOMWriter;
28 -let ReactServerDOMReader;
27 +let ReactServerDOMServer;
28 +let ReactServerDOMClient;
29 let use;
30
31 describe('ReactFlightDOMEdge', () => {
@@ -37,8 +37,8 @@ describe('ReactFlightDOMEdge', () => {
37 webpackModules = WebpackMock.webpackModules;
38 React = require('react');
39 ReactDOMServer = require('react-dom/server.edge');
40 - ReactServerDOMWriter = require('react-server-dom-webpack/server.edge');
41 - ReactServerDOMReader = require('react-server-dom-webpack/client.edge');
40 + ReactServerDOMServer = require('react-server-dom-webpack/server.edge');
41 + ReactServerDOMClient = require('react-server-dom-webpack/client.edge');
42 use = React.use;
43 });
44
@@ -81,11 +81,11 @@ describe('ReactFlightDOMEdge', () => {
81 return <ClientComponentOnTheClient />;
82 }
83
84 - const stream = ReactServerDOMWriter.renderToReadableStream(
84 + const stream = ReactServerDOMServer.renderToReadableStream(
85 <App />,
86 webpackMap,
87 );
88 - const response = ReactServerDOMReader.createFromReadableStream(stream, {
88 + const response = ReactServerDOMClient.createFromReadableStream(stream, {
89 moduleMap: translationMap,
90 });
91
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+6 -6
@@ -18,8 +18,8 @@ let webpackMap;
18 let webpackModules;
19 let React;
20 let ReactDOMServer;
21 -let ReactServerDOMWriter;
22 -let ReactServerDOMReader;
21 +let ReactServerDOMServer;
22 +let ReactServerDOMClient;
23 let Stream;
24 let use;
25
@@ -32,8 +32,8 @@ describe('ReactFlightDOMNode', () => {
32 webpackModules = WebpackMock.webpackModules;
33 React = require('react');
34 ReactDOMServer = require('react-dom/server.node');
35 - ReactServerDOMWriter = require('react-server-dom-webpack/server.node');
36 - ReactServerDOMReader = require('react-server-dom-webpack/client.node');
35 + ReactServerDOMServer = require('react-server-dom-webpack/server.node');
36 + ReactServerDOMClient = require('react-server-dom-webpack/client.node');
37 Stream = require('stream');
38 use = React.use;
39 });
@@ -83,12 +83,12 @@ describe('ReactFlightDOMNode', () => {
83 return <ClientComponentOnTheClient />;
84 }
85
86 - const stream = ReactServerDOMWriter.renderToPipeableStream(
86 + const stream = ReactServerDOMServer.renderToPipeableStream(
87 <App />,
88 webpackMap,
89 );
90 const readable = new Stream.PassThrough();
91 - const response = ReactServerDOMReader.createFromNodeStream(
91 + const response = ReactServerDOMClient.createFromNodeStream(
92 readable,
93 translationMap,
94 );
packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js
+9
@@ -32,6 +32,8 @@ import isArray from 'shared/isArray';
32 export type {ClientReferenceMetadata} from 'ReactFlightNativeRelayClientIntegration';
33
34 export type SSRManifest = null;
35 +export type ServerManifest = null;
36 +export type ServerReferenceId = string;
37
38 export type UninitializedModel = JSONValue;
39
@@ -44,6 +46,13 @@ export function resolveClientReference<T>(
46 return resolveClientReferenceImpl(metadata);
47 }
48
49 +export function resolveServerReference<T>(
50 + bundlerConfig: ServerManifest,
51 + id: ServerReferenceId,
52 +): ClientReference<T> {
53 + throw new Error('Not implemented.');
54 +}
55 +
56 function parseModelRecursively(
57 response: Response,
58 parentObj: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
packages/react-server/src/ReactFlightReplyServer.js new
+496
@@ -0,0 +1,496 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Thenable} from 'shared/ReactTypes';
11 +
12 +// The server acts as a Client of itself when resolving Server References.
13 +// That's why we import the Client configuration from the Server.
14 +// Everything is aliased as their Server equivalence for clarity.
15 +import type {
16 + ServerReferenceId,
17 + ServerManifest,
18 + ClientReference as ServerReference,
19 +} from 'react-client/src/ReactFlightClientHostConfig';
20 +
21 +import {
22 + resolveServerReference,
23 + preloadModule,
24 + requireModule,
25 +} from 'react-client/src/ReactFlightClientHostConfig';
26 +
27 +export type JSONValue =
28 + | number
29 + | null
30 + | boolean
31 + | string
32 + | {+[key: string]: JSONValue}
33 + | $ReadOnlyArray<JSONValue>;
34 +
35 +const PENDING = 'pending';
36 +const BLOCKED = 'blocked';
37 +const RESOLVED_MODEL = 'resolved_model';
38 +const INITIALIZED = 'fulfilled';
39 +const ERRORED = 'rejected';
40 +
41 +type PendingChunk<T> = {
42 + status: 'pending',
43 + value: null | Array<(T) => mixed>,
44 + reason: null | Array<(mixed) => mixed>,
45 + _response: Response,
46 + then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
47 +};
48 +type BlockedChunk<T> = {
49 + status: 'blocked',
50 + value: null | Array<(T) => mixed>,
51 + reason: null | Array<(mixed) => mixed>,
52 + _response: Response,
53 + then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
54 +};
55 +type ResolvedModelChunk<T> = {
56 + status: 'resolved_model',
57 + value: string,
58 + reason: null,
59 + _response: Response,
60 + then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
61 +};
62 +type InitializedChunk<T> = {
63 + status: 'fulfilled',
64 + value: T,
65 + reason: null,
66 + _response: Response,
67 + then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
68 +};
69 +type ErroredChunk<T> = {
70 + status: 'rejected',
71 + value: null,
72 + reason: mixed,
73 + _response: Response,
74 + then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
75 +};
76 +type SomeChunk<T> =
77 + | PendingChunk<T>
78 + | BlockedChunk<T>
79 + | ResolvedModelChunk<T>
80 + | InitializedChunk<T>
81 + | ErroredChunk<T>;
82 +
83 +// $FlowFixMe[missing-this-annot]
84 +function Chunk(status: any, value: any, reason: any, response: Response) {
85 + this.status = status;
86 + this.value = value;
87 + this.reason = reason;
88 + this._response = response;
89 +}
90 +// We subclass Promise.prototype so that we get other methods like .catch
91 +Chunk.prototype = (Object.create(Promise.prototype): any);
92 +// TODO: This doesn't return a new Promise chain unlike the real .then
93 +Chunk.prototype.then = function <T>(
94 + this: SomeChunk<T>,
95 + resolve: (value: T) => mixed,
96 + reject: (reason: mixed) => mixed,
97 +) {
98 + const chunk: SomeChunk<T> = this;
99 + // If we have resolved content, we try to initialize it first which
100 + // might put us back into one of the other states.
101 + switch (chunk.status) {
102 + case RESOLVED_MODEL:
103 + initializeModelChunk(chunk);
104 + break;
105 + }
106 + // The status might have changed after initialization.
107 + switch (chunk.status) {
108 + case INITIALIZED:
109 + resolve(chunk.value);
110 + break;
111 + case PENDING:
112 + case BLOCKED:
113 + if (resolve) {
114 + if (chunk.value === null) {
115 + chunk.value = ([]: Array<(T) => mixed>);
116 + }
117 + chunk.value.push(resolve);
118 + }
119 + if (reject) {
120 + if (chunk.reason === null) {
121 + chunk.reason = ([]: Array<(mixed) => mixed>);
122 + }
123 + chunk.reason.push(reject);
124 + }
125 + break;
126 + default:
127 + reject(chunk.reason);
128 + break;
129 + }
130 +};
131 +
132 +export type Response = {
133 + _bundlerConfig: ServerManifest,
134 + _chunks: Map<number, SomeChunk<any>>,
135 + _fromJSON: (key: string, value: JSONValue) => any,
136 +};
137 +
138 +export function getRoot<T>(response: Response): Thenable<T> {
139 + const chunk = getChunk(response, 0);
140 + return (chunk: any);
141 +}
142 +
143 +function createPendingChunk<T>(response: Response): PendingChunk<T> {
144 + // $FlowFixMe Flow doesn't support functions as constructors
145 + return new Chunk(PENDING, null, null, response);
146 +}
147 +
148 +function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void {
149 + for (let i = 0; i < listeners.length; i++) {
150 + const listener = listeners[i];
151 + listener(value);
152 + }
153 +}
154 +
155 +function wakeChunkIfInitialized<T>(
156 + chunk: SomeChunk<T>,
157 + resolveListeners: Array<(T) => mixed>,
158 + rejectListeners: null | Array<(mixed) => mixed>,
159 +): void {
160 + switch (chunk.status) {
161 + case INITIALIZED:
162 + wakeChunk(resolveListeners, chunk.value);
163 + break;
164 + case PENDING:
165 + case BLOCKED:
166 + chunk.value = resolveListeners;
167 + chunk.reason = rejectListeners;
168 + break;
169 + case ERRORED:
170 + if (rejectListeners) {
171 + wakeChunk(rejectListeners, chunk.reason);
172 + }
173 + break;
174 + }
175 +}
176 +
177 +function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: mixed): void {
178 + if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
179 + // We already resolved. We didn't expect to see this.
180 + return;
181 + }
182 + const listeners = chunk.reason;
183 + const erroredChunk: ErroredChunk<T> = (chunk: any);
184 + erroredChunk.status = ERRORED;
185 + erroredChunk.reason = error;
186 + if (listeners !== null) {
187 + wakeChunk(listeners, error);
188 + }
189 +}
190 +
191 +function createResolvedModelChunk<T>(
192 + response: Response,
193 + value: string,
194 +): ResolvedModelChunk<T> {
195 + // $FlowFixMe Flow doesn't support functions as constructors
196 + return new Chunk(RESOLVED_MODEL, value, null, response);
197 +}
198 +
199 +function resolveModelChunk<T>(chunk: SomeChunk<T>, value: string): void {
200 + if (chunk.status !== PENDING) {
201 + // We already resolved. We didn't expect to see this.
202 + return;
203 + }
204 + const resolveListeners = chunk.value;
205 + const rejectListeners = chunk.reason;
206 + const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
207 + resolvedChunk.status = RESOLVED_MODEL;
208 + resolvedChunk.value = value;
209 + if (resolveListeners !== null) {
210 + // This is unfortunate that we're reading this eagerly if
211 + // we already have listeners attached since they might no
212 + // longer be rendered or might not be the highest pri.
213 + initializeModelChunk(resolvedChunk);
214 + // The status might have changed after initialization.
215 + wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
216 + }
217 +}
218 +
219 +function bindArgs(fn: any, args: any) {
220 + return fn.bind.apply(fn, [null].concat(args));
221 +}
222 +
223 +function loadServerReference<T>(
224 + response: Response,
225 + id: ServerReferenceId,
226 + bound: null | Thenable<Array<any>>,
227 + parentChunk: SomeChunk<T>,
228 + parentObject: Object,
229 + key: string,
230 +): T {
231 + const serverReference: ServerReference<T> =
232 + resolveServerReference<$FlowFixMe>(response._bundlerConfig, id);
233 + // We expect most servers to not really need this because you'd just have all
234 + // the relevant modules already loaded but it allows for lazy loading of code
235 + // if needed.
236 + const preloadPromise = preloadModule(serverReference);
237 + let promise: Promise<T>;
238 + if (bound) {
239 + promise = Promise.all([(bound: any), preloadPromise]).then(
240 + ([args]: Array<any>) => bindArgs(requireModule(serverReference), args),
241 + );
242 + } else {
243 + if (preloadPromise) {
244 + promise = Promise.resolve(preloadPromise).then(() =>
245 + requireModule(serverReference),
246 + );
247 + } else {
248 + // Synchronously available
249 + return requireModule(serverReference);
250 + }
251 + }
252 + promise.then(
253 + createModelResolver(parentChunk, parentObject, key),
254 + createModelReject(parentChunk),
255 + );
256 + // We need a placeholder value that will be replaced later.
257 + return (null: any);
258 +}
259 +
260 +let initializingChunk: ResolvedModelChunk<any> = (null: any);
261 +let initializingChunkBlockedModel: null | {deps: number, value: any} = null;
262 +function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
263 + const prevChunk = initializingChunk;
264 + const prevBlocked = initializingChunkBlockedModel;
265 + initializingChunk = chunk;
266 + initializingChunkBlockedModel = null;
267 + try {
268 + const value: T = JSON.parse(chunk.value, chunk._response._fromJSON);
269 + if (
270 + initializingChunkBlockedModel !== null &&
271 + initializingChunkBlockedModel.deps > 0
272 + ) {
273 + initializingChunkBlockedModel.value = value;
274 + // We discovered new dependencies on modules that are not yet resolved.
275 + // We have to go the BLOCKED state until they're resolved.
276 + const blockedChunk: BlockedChunk<T> = (chunk: any);
277 + blockedChunk.status = BLOCKED;
278 + blockedChunk.value = null;
279 + blockedChunk.reason = null;
280 + } else {
281 + const initializedChunk: InitializedChunk<T> = (chunk: any);
282 + initializedChunk.status = INITIALIZED;
283 + initializedChunk.value = value;
284 + }
285 + } catch (error) {
286 + const erroredChunk: ErroredChunk<T> = (chunk: any);
287 + erroredChunk.status = ERRORED;
288 + erroredChunk.reason = error;
289 + } finally {
290 + initializingChunk = prevChunk;
291 + initializingChunkBlockedModel = prevBlocked;
292 + }
293 +}
294 +
295 +// Report that any missing chunks in the model is now going to throw this
296 +// error upon read. Also notify any pending promises.
297 +export function reportGlobalError(response: Response, error: Error): void {
298 + response._chunks.forEach(chunk => {
299 + // If this chunk was already resolved or errored, it won't
300 + // trigger an error but if it wasn't then we need to
301 + // because we won't be getting any new data to resolve it.
302 + if (chunk.status === PENDING) {
303 + triggerErrorOnChunk(chunk, error);
304 + }
305 + });
306 +}
307 +
308 +function getChunk(response: Response, id: number): SomeChunk<any> {
309 + const chunks = response._chunks;
310 + let chunk = chunks.get(id);
311 + if (!chunk) {
312 + chunk = createPendingChunk(response);
313 + chunks.set(id, chunk);
314 + }
315 + return chunk;
316 +}
317 +
318 +function createModelResolver<T>(
319 + chunk: SomeChunk<T>,
320 + parentObject: Object,
321 + key: string,
322 +): (value: any) => void {
323 + let blocked;
324 + if (initializingChunkBlockedModel) {
325 + blocked = initializingChunkBlockedModel;
326 + blocked.deps++;
327 + } else {
328 + blocked = initializingChunkBlockedModel = {
329 + deps: 1,
330 + value: null,
331 + };
332 + }
333 + return value => {
334 + parentObject[key] = value;
335 + blocked.deps--;
336 + if (blocked.deps === 0) {
337 + if (chunk.status !== BLOCKED) {
338 + return;
339 + }
340 + const resolveListeners = chunk.value;
341 + const initializedChunk: InitializedChunk<T> = (chunk: any);
342 + initializedChunk.status = INITIALIZED;
343 + initializedChunk.value = blocked.value;
344 + if (resolveListeners !== null) {
345 + wakeChunk(resolveListeners, blocked.value);
346 + }
347 + }
348 + };
349 +}
350 +
351 +function createModelReject<T>(chunk: SomeChunk<T>): (error: mixed) => void {
352 + return (error: mixed) => triggerErrorOnChunk(chunk, error);
353 +}
354 +
355 +function parseModelString(
356 + response: Response,
357 + parentObject: Object,
358 + key: string,
359 + value: string,
360 +): any {
361 + if (value[0] === '$') {
362 + switch (value[1]) {
363 + case '$': {
364 + // This was an escaped string value.
365 + return value.substring(1);
366 + }
367 + case '@': {
368 + // Promise
369 + const id = parseInt(value.substring(2), 16);
370 + const chunk = getChunk(response, id);
371 + return chunk;
372 + }
373 + case 'S': {
374 + // Symbol
375 + return Symbol.for(value.substring(2));
376 + }
377 + case 'F': {
378 + // Server Reference
379 + const id = parseInt(value.substring(2), 16);
380 + const chunk = getChunk(response, id);
381 + if (chunk.status === RESOLVED_MODEL) {
382 + initializeModelChunk(chunk);
383 + }
384 + if (chunk.status !== INITIALIZED) {
385 + // We know that this is emitted earlier so otherwise it's an error.
386 + throw chunk.reason;
387 + }
388 + // TODO: Just encode this in the reference inline instead of as a model.
389 + const metaData: {id: ServerReferenceId, bound: Thenable<Array<any>>} =
390 + chunk.value;
391 + return loadServerReference(
392 + response,
393 + metaData.id,
394 + metaData.bound,
395 + initializingChunk,
396 + parentObject,
397 + key,
398 + );
399 + }
400 + default: {
401 + // We assume that anything else is a reference ID.
402 + const id = parseInt(value.substring(1), 16);
403 + const chunk = getChunk(response, id);
404 + switch (chunk.status) {
405 + case RESOLVED_MODEL:
406 + initializeModelChunk(chunk);
407 + break;
408 + }
409 + // The status might have changed after initialization.
410 + switch (chunk.status) {
411 + case INITIALIZED:
412 + return chunk.value;
413 + case PENDING:
414 + case BLOCKED:
415 + const parentChunk = initializingChunk;
416 + chunk.then(
417 + createModelResolver(parentChunk, parentObject, key),
418 + createModelReject(parentChunk),
419 + );
420 + return null;
421 + default:
422 + throw chunk.reason;
423 + }
424 + }
425 + }
426 + }
427 + return value;
428 +}
429 +
430 +export function createResponse(bundlerConfig: ServerManifest): Response {
431 + const chunks: Map<number, SomeChunk<any>> = new Map();
432 + const response: Response = {
433 + _bundlerConfig: bundlerConfig,
434 + _chunks: chunks,
435 + _fromJSON: function (this: any, key: string, value: JSONValue) {
436 + if (typeof value === 'string') {
437 + // We can't use .bind here because we need the "this" value.
438 + return parseModelString(response, this, key, value);
439 + }
440 + return value;
441 + },
442 + };
443 + return response;
444 +}
445 +
446 +export function resolveField(
447 + response: Response,
448 + id: number,
449 + model: string,
450 +): void {
451 + const chunks = response._chunks;
452 + const chunk = chunks.get(id);
453 + if (!chunk) {
454 + chunks.set(id, createResolvedModelChunk(response, model));
455 + } else {
456 + resolveModelChunk(chunk, model);
457 + }
458 +}
459 +
460 +export function resolveFile(response: Response, id: number, file: File): void {
461 + throw new Error('Not implemented.');
462 +}
463 +
464 +export opaque type FileHandle = {};
465 +
466 +export function resolveFileInfo(
467 + response: Response,
468 + id: number,
469 + filename: string,
470 + mime: string,
471 +): FileHandle {
472 + throw new Error('Not implemented.');
473 +}
474 +
475 +export function resolveFileChunk(
476 + response: Response,
477 + handle: FileHandle,
478 + chunk: Uint8Array,
479 +): void {
480 + throw new Error('Not implemented.');
481 +}
482 +
483 +export function resolveFileComplete(
484 + response: Response,
485 + handle: FileHandle,
486 +): void {
487 + throw new Error('Not implemented.');
488 +}
489 +
490 +export function close(response: Response): void {
491 + // In case there are any remaining unresolved chunks, they won't
492 + // be resolved now. So we need to issue an error to those.
493 + // Ideally we should be able to early bail out if we kept a
494 + // ref count of pending chunks.
495 + reportGlobalError(response, new Error('Connection closed.'));
496 +}
packages/react-server/src/ReactFlightServer.js
+10 -276
@@ -82,10 +82,17 @@ import {
82 REACT_LAZY_TYPE,
83 REACT_MEMO_TYPE,
84 REACT_PROVIDER_TYPE,
85 - REACT_SUSPENSE_TYPE,
86 - REACT_SUSPENSE_LIST_TYPE,
85 } from 'shared/ReactSymbols';
86
87 +import {
88 + describeValueForErrorMessage,
89 + describeObjectForErrorMessage,
90 + isSimpleObject,
91 + jsxPropsParents,
92 + jsxChildrenParents,
93 + objectName,
94 +} from 'shared/ReactSerializationErrors';
95 +
96 import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
97 import ReactSharedInternals from 'shared/ReactSharedInternals';
98 import isArray from 'shared/isArray';
@@ -233,11 +240,6 @@ function createRootContext(
240
241 const POP = {};
242
236 -// Used for DEV messages to keep track of which parent rendered some props,
237 -// in case they error.
238 -const jsxPropsParents: WeakMap<any, any> = new WeakMap();
239 -const jsxChildrenParents: WeakMap<any, any> = new WeakMap();
240 -
243 function serializeThenable(request: Request, thenable: Thenable<any>): number {
244 request.pendingChunks++;
245 const newTask = createTask(
@@ -644,7 +646,7 @@ function serializeServerReference(
646
647 function escapeStringValue(value: string): string {
648 if (value[0] === '$') {
647 - // We need to escape $ or @ prefixed strings since we use those to encode
649 + // We need to escape $ prefixed strings since we use those to encode
650 // references to IDs and as special symbol values.
651 return '$' + value;
652 } else {
@@ -652,274 +654,6 @@ function escapeStringValue(value: string): string {
654 }
655 }
656
655 -function isObjectPrototype(object: any): boolean {
656 - if (!object) {
657 - return false;
658 - }
659 - const ObjectPrototype = Object.prototype;
660 - if (object === ObjectPrototype) {
661 - return true;
662 - }
663 - // It might be an object from a different Realm which is
664 - // still just a plain simple object.
665 - if (Object.getPrototypeOf(object)) {
666 - return false;
667 - }
668 - const names = Object.getOwnPropertyNames(object);
669 - for (let i = 0; i < names.length; i++) {
670 - if (!(names[i] in ObjectPrototype)) {
671 - return false;
672 - }
673 - }
674 - return true;
675 -}
676 -
677 -function isSimpleObject(object: any): boolean {
678 - if (!isObjectPrototype(Object.getPrototypeOf(object))) {
679 - return false;
680 - }
681 - const names = Object.getOwnPropertyNames(object);
682 - for (let i = 0; i < names.length; i++) {
683 - const descriptor = Object.getOwnPropertyDescriptor(object, names[i]);
684 - if (!descriptor) {
685 - return false;
686 - }
687 - if (!descriptor.enumerable) {
688 - if (
689 - (names[i] === 'key' || names[i] === 'ref') &&
690 - typeof descriptor.get === 'function'
691 - ) {
692 - // React adds key and ref getters to props objects to issue warnings.
693 - // Those getters will not be transferred to the client, but that's ok,
694 - // so we'll special case them.
695 - continue;
696 - }
697 - return false;
698 - }
699 - }
700 - return true;
701 -}
702 -
703 -function objectName(object: mixed): string {
704 - // $FlowFixMe[method-unbinding]
705 - const name = Object.prototype.toString.call(object);
706 - return name.replace(/^\[object (.*)\]$/, function (m, p0) {
707 - return p0;
708 - });
709 -}
710 -
711 -function describeKeyForErrorMessage(key: string): string {
712 - const encodedKey = JSON.stringify(key);
713 - return '"' + key + '"' === encodedKey ? key : encodedKey;
714 -}
715 -
716 -function describeValueForErrorMessage(value: ReactClientValue): string {
717 - switch (typeof value) {
718 - case 'string': {
719 - return JSON.stringify(
720 - value.length <= 10 ? value : value.substr(0, 10) + '...',
721 - );
722 - }
723 - case 'object': {
724 - if (isArray(value)) {
725 - return '[...]';
726 - }
727 - const name = objectName(value);
728 - if (name === 'Object') {
729 - return '{...}';
730 - }
731 - return name;
732 - }
733 - case 'function':
734 - return 'function';
735 - default:
736 - // eslint-disable-next-line react-internal/safe-string-coercion
737 - return String(value);
738 - }
739 -}
740 -
741 -function describeElementType(type: any): string {
742 - if (typeof type === 'string') {
743 - return type;
744 - }
745 - switch (type) {
746 - case REACT_SUSPENSE_TYPE:
747 - return 'Suspense';
748 - case REACT_SUSPENSE_LIST_TYPE:
749 - return 'SuspenseList';
750 - }
751 - if (typeof type === 'object') {
752 - switch (type.$$typeof) {
753 - case REACT_FORWARD_REF_TYPE:
754 - return describeElementType(type.render);
755 - case REACT_MEMO_TYPE:
756 - return describeElementType(type.type);
757 - case REACT_LAZY_TYPE: {
758 - const lazyComponent: LazyComponent<any, any> = (type: any);
759 - const payload = lazyComponent._payload;
760 - const init = lazyComponent._init;
761 - try {
762 - // Lazy may contain any component type so we recursively resolve it.
763 - return describeElementType(init(payload));
764 - } catch (x) {}
765 - }
766 - }
767 - }
768 - return '';
769 -}
770 -
771 -function describeObjectForErrorMessage(
772 - objectOrArray:
773 - | {+[key: string | number]: ReactClientValue, ...}
774 - | $ReadOnlyArray<ReactClientValue>,
775 - expandedName?: string,
776 -): string {
777 - const objKind = objectName(objectOrArray);
778 - if (objKind !== 'Object' && objKind !== 'Array') {
779 - return objKind;
780 - }
781 - let str = '';
782 - let start = -1;
783 - let length = 0;
784 - if (isArray(objectOrArray)) {
785 - if (__DEV__ && jsxChildrenParents.has(objectOrArray)) {
786 - // Print JSX Children
787 - const type = jsxChildrenParents.get(objectOrArray);
788 - str = '<' + describeElementType(type) + '>';
789 - const array: $ReadOnlyArray<ReactClientValue> = objectOrArray;
790 - for (let i = 0; i < array.length; i++) {
791 - const value = array[i];
792 - let substr;
793 - if (typeof value === 'string') {
794 - substr = value;
795 - } else if (typeof value === 'object' && value !== null) {
796 - // $FlowFixMe[incompatible-call] found when upgrading Flow
797 - substr = '{' + describeObjectForErrorMessage(value) + '}';
798 - } else {
799 - substr = '{' + describeValueForErrorMessage(value) + '}';
800 - }
801 - if ('' + i === expandedName) {
802 - start = str.length;
803 - length = substr.length;
804 - str += substr;
805 - } else if (substr.length < 15 && str.length + substr.length < 40) {
806 - str += substr;
807 - } else {
808 - str += '{...}';
809 - }
810 - }
811 - str += '</' + describeElementType(type) + '>';
812 - } else {
813 - // Print Array
814 - str = '[';
815 - const array: $ReadOnlyArray<ReactClientValue> = objectOrArray;
816 - for (let i = 0; i < array.length; i++) {
817 - if (i > 0) {
818 - str += ', ';
819 - }
820 - const value = array[i];
821 - let substr;
822 - if (typeof value === 'object' && value !== null) {
823 - // $FlowFixMe[incompatible-call] found when upgrading Flow
824 - substr = describeObjectForErrorMessage(value);
825 - } else {
826 - substr = describeValueForErrorMessage(value);
827 - }
828 - if ('' + i === expandedName) {
829 - start = str.length;
830 - length = substr.length;
831 - str += substr;
832 - } else if (substr.length < 10 && str.length + substr.length < 40) {
833 - str += substr;
834 - } else {
835 - str += '...';
836 - }
837 - }
838 - str += ']';
839 - }
840 - } else {
841 - if (objectOrArray.$$typeof === REACT_ELEMENT_TYPE) {
842 - str = '<' + describeElementType(objectOrArray.type) + '/>';
843 - } else if (__DEV__ && jsxPropsParents.has(objectOrArray)) {
844 - // Print JSX
845 - const type = jsxPropsParents.get(objectOrArray);
846 - str = '<' + (describeElementType(type) || '...');
847 - const object: {+[key: string | number]: ReactClientValue, ...} =
848 - objectOrArray;
849 - const names = Object.keys(object);
850 - for (let i = 0; i < names.length; i++) {
851 - str += ' ';
852 - const name = names[i];
853 - str += describeKeyForErrorMessage(name) + '=';
854 - const value = object[name];
855 - let substr;
856 - if (
857 - name === expandedName &&
858 - typeof value === 'object' &&
859 - value !== null
860 - ) {
861 - // $FlowFixMe[incompatible-call] found when upgrading Flow
862 - substr = describeObjectForErrorMessage(value);
863 - } else {
864 - substr = describeValueForErrorMessage(value);
865 - }
866 - if (typeof value !== 'string') {
867 - substr = '{' + substr + '}';
868 - }
869 - if (name === expandedName) {
870 - start = str.length;
871 - length = substr.length;
872 - str += substr;
873 - } else if (substr.length < 10 && str.length + substr.length < 40) {
874 - str += substr;
875 - } else {
876 - str += '...';
877 - }
878 - }
879 - str += '>';
880 - } else {
881 - // Print Object
882 - str = '{';
883 - const object: {+[key: string | number]: ReactClientValue, ...} =
884 - objectOrArray;
885 - const names = Object.keys(object);
886 - for (let i = 0; i < names.length; i++) {
887 - if (i > 0) {
888 - str += ', ';
889 - }
890 - const name = names[i];
891 - str += describeKeyForErrorMessage(name) + ': ';
892 - const value = object[name];
893 - let substr;
894 - if (typeof value === 'object' && value !== null) {
895 - // $FlowFixMe[incompatible-call] found when upgrading Flow
896 - substr = describeObjectForErrorMessage(value);
897 - } else {
898 - substr = describeValueForErrorMessage(value);
899 - }
900 - if (name === expandedName) {
901 - start = str.length;
902 - length = substr.length;
903 - str += substr;
904 - } else if (substr.length < 10 && str.length + substr.length < 40) {
905 - str += substr;
906 - } else {
907 - str += '...';
908 - }
909 - }
910 - str += '}';
911 - }
912 - }
913 - if (expandedName === undefined) {
914 - return str;
915 - }
916 - if (start > -1 && length > 0) {
917 - const highlight = ' '.repeat(start) + '^'.repeat(length);
918 - return '\n ' + str + '\n ' + highlight;
919 - }
920 - return '\n ' + str;
921 -}
922 -
657 let insideContextProps = null;
658 let isInsideContextValue = false;
659
packages/shared/ReactSerializationErrors.js new
+290
@@ -0,0 +1,290 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import {
11 + REACT_ELEMENT_TYPE,
12 + REACT_FORWARD_REF_TYPE,
13 + REACT_LAZY_TYPE,
14 + REACT_MEMO_TYPE,
15 + REACT_SUSPENSE_TYPE,
16 + REACT_SUSPENSE_LIST_TYPE,
17 +} from 'shared/ReactSymbols';
18 +
19 +import type {LazyComponent} from 'react/src/ReactLazy';
20 +
21 +import isArray from 'shared/isArray';
22 +
23 +// Used for DEV messages to keep track of which parent rendered some props,
24 +// in case they error.
25 +export const jsxPropsParents: WeakMap<any, any> = new WeakMap();
26 +export const jsxChildrenParents: WeakMap<any, any> = new WeakMap();
27 +
28 +function isObjectPrototype(object: any): boolean {
29 + if (!object) {
30 + return false;
31 + }
32 + const ObjectPrototype = Object.prototype;
33 + if (object === ObjectPrototype) {
34 + return true;
35 + }
36 + // It might be an object from a different Realm which is
37 + // still just a plain simple object.
38 + if (Object.getPrototypeOf(object)) {
39 + return false;
40 + }
41 + const names = Object.getOwnPropertyNames(object);
42 + for (let i = 0; i < names.length; i++) {
43 + if (!(names[i] in ObjectPrototype)) {
44 + return false;
45 + }
46 + }
47 + return true;
48 +}
49 +
50 +export function isSimpleObject(object: any): boolean {
51 + if (!isObjectPrototype(Object.getPrototypeOf(object))) {
52 + return false;
53 + }
54 + const names = Object.getOwnPropertyNames(object);
55 + for (let i = 0; i < names.length; i++) {
56 + const descriptor = Object.getOwnPropertyDescriptor(object, names[i]);
57 + if (!descriptor) {
58 + return false;
59 + }
60 + if (!descriptor.enumerable) {
61 + if (
62 + (names[i] === 'key' || names[i] === 'ref') &&
63 + typeof descriptor.get === 'function'
64 + ) {
65 + // React adds key and ref getters to props objects to issue warnings.
66 + // Those getters will not be transferred to the client, but that's ok,
67 + // so we'll special case them.
68 + continue;
69 + }
70 + return false;
71 + }
72 + }
73 + return true;
74 +}
75 +
76 +export function objectName(object: mixed): string {
77 + // $FlowFixMe[method-unbinding]
78 + const name = Object.prototype.toString.call(object);
79 + return name.replace(/^\[object (.*)\]$/, function (m, p0) {
80 + return p0;
81 + });
82 +}
83 +
84 +function describeKeyForErrorMessage(key: string): string {
85 + const encodedKey = JSON.stringify(key);
86 + return '"' + key + '"' === encodedKey ? key : encodedKey;
87 +}
88 +
89 +export function describeValueForErrorMessage(value: mixed): string {
90 + switch (typeof value) {
91 + case 'string': {
92 + return JSON.stringify(
93 + value.length <= 10 ? value : value.substr(0, 10) + '...',
94 + );
95 + }
96 + case 'object': {
97 + if (isArray(value)) {
98 + return '[...]';
99 + }
100 + const name = objectName(value);
101 + if (name === 'Object') {
102 + return '{...}';
103 + }
104 + return name;
105 + }
106 + case 'function':
107 + return 'function';
108 + default:
109 + // eslint-disable-next-line react-internal/safe-string-coercion
110 + return String(value);
111 + }
112 +}
113 +
114 +function describeElementType(type: any): string {
115 + if (typeof type === 'string') {
116 + return type;
117 + }
118 + switch (type) {
119 + case REACT_SUSPENSE_TYPE:
120 + return 'Suspense';
121 + case REACT_SUSPENSE_LIST_TYPE:
122 + return 'SuspenseList';
123 + }
124 + if (typeof type === 'object') {
125 + switch (type.$$typeof) {
126 + case REACT_FORWARD_REF_TYPE:
127 + return describeElementType(type.render);
128 + case REACT_MEMO_TYPE:
129 + return describeElementType(type.type);
130 + case REACT_LAZY_TYPE: {
131 + const lazyComponent: LazyComponent<any, any> = (type: any);
132 + const payload = lazyComponent._payload;
133 + const init = lazyComponent._init;
134 + try {
135 + // Lazy may contain any component type so we recursively resolve it.
136 + return describeElementType(init(payload));
137 + } catch (x) {}
138 + }
139 + }
140 + }
141 + return '';
142 +}
143 +
144 +export function describeObjectForErrorMessage(
145 + objectOrArray: {+[key: string | number]: mixed, ...} | $ReadOnlyArray<mixed>,
146 + expandedName?: string,
147 +): string {
148 + const objKind = objectName(objectOrArray);
149 + if (objKind !== 'Object' && objKind !== 'Array') {
150 + return objKind;
151 + }
152 + let str = '';
153 + let start = -1;
154 + let length = 0;
155 + if (isArray(objectOrArray)) {
156 + if (__DEV__ && jsxChildrenParents.has(objectOrArray)) {
157 + // Print JSX Children
158 + const type = jsxChildrenParents.get(objectOrArray);
159 + str = '<' + describeElementType(type) + '>';
160 + const array: $ReadOnlyArray<mixed> = objectOrArray;
161 + for (let i = 0; i < array.length; i++) {
162 + const value = array[i];
163 + let substr;
164 + if (typeof value === 'string') {
165 + substr = value;
166 + } else if (typeof value === 'object' && value !== null) {
167 + // $FlowFixMe[incompatible-call] found when upgrading Flow
168 + substr = '{' + describeObjectForErrorMessage(value) + '}';
169 + } else {
170 + substr = '{' + describeValueForErrorMessage(value) + '}';
171 + }
172 + if ('' + i === expandedName) {
173 + start = str.length;
174 + length = substr.length;
175 + str += substr;
176 + } else if (substr.length < 15 && str.length + substr.length < 40) {
177 + str += substr;
178 + } else {
179 + str += '{...}';
180 + }
181 + }
182 + str += '</' + describeElementType(type) + '>';
183 + } else {
184 + // Print Array
185 + str = '[';
186 + const array: $ReadOnlyArray<mixed> = objectOrArray;
187 + for (let i = 0; i < array.length; i++) {
188 + if (i > 0) {
189 + str += ', ';
190 + }
191 + const value = array[i];
192 + let substr;
193 + if (typeof value === 'object' && value !== null) {
194 + // $FlowFixMe[incompatible-call] found when upgrading Flow
195 + substr = describeObjectForErrorMessage(value);
196 + } else {
197 + substr = describeValueForErrorMessage(value);
198 + }
199 + if ('' + i === expandedName) {
200 + start = str.length;
201 + length = substr.length;
202 + str += substr;
203 + } else if (substr.length < 10 && str.length + substr.length < 40) {
204 + str += substr;
205 + } else {
206 + str += '...';
207 + }
208 + }
209 + str += ']';
210 + }
211 + } else {
212 + if (objectOrArray.$$typeof === REACT_ELEMENT_TYPE) {
213 + str = '<' + describeElementType(objectOrArray.type) + '/>';
214 + } else if (__DEV__ && jsxPropsParents.has(objectOrArray)) {
215 + // Print JSX
216 + const type = jsxPropsParents.get(objectOrArray);
217 + str = '<' + (describeElementType(type) || '...');
218 + const object: {+[key: string | number]: mixed, ...} = objectOrArray;
219 + const names = Object.keys(object);
220 + for (let i = 0; i < names.length; i++) {
221 + str += ' ';
222 + const name = names[i];
223 + str += describeKeyForErrorMessage(name) + '=';
224 + const value = object[name];
225 + let substr;
226 + if (
227 + name === expandedName &&
228 + typeof value === 'object' &&
229 + value !== null
230 + ) {
231 + // $FlowFixMe[incompatible-call] found when upgrading Flow
232 + substr = describeObjectForErrorMessage(value);
233 + } else {
234 + substr = describeValueForErrorMessage(value);
235 + }
236 + if (typeof value !== 'string') {
237 + substr = '{' + substr + '}';
238 + }
239 + if (name === expandedName) {
240 + start = str.length;
241 + length = substr.length;
242 + str += substr;
243 + } else if (substr.length < 10 && str.length + substr.length < 40) {
244 + str += substr;
245 + } else {
246 + str += '...';
247 + }
248 + }
249 + str += '>';
250 + } else {
251 + // Print Object
252 + str = '{';
253 + const object: {+[key: string | number]: mixed, ...} = objectOrArray;
254 + const names = Object.keys(object);
255 + for (let i = 0; i < names.length; i++) {
256 + if (i > 0) {
257 + str += ', ';
258 + }
259 + const name = names[i];
260 + str += describeKeyForErrorMessage(name) + ': ';
261 + const value = object[name];
262 + let substr;
263 + if (typeof value === 'object' && value !== null) {
264 + // $FlowFixMe[incompatible-call] found when upgrading Flow
265 + substr = describeObjectForErrorMessage(value);
266 + } else {
267 + substr = describeValueForErrorMessage(value);
268 + }
269 + if (name === expandedName) {
270 + start = str.length;
271 + length = substr.length;
272 + str += substr;
273 + } else if (substr.length < 10 && str.length + substr.length < 40) {
274 + str += substr;
275 + } else {
276 + str += '...';
277 + }
278 + }
279 + str += '}';
280 + }
281 + }
282 + if (expandedName === undefined) {
283 + return str;
284 + }
285 + if (start > -1 && length > 0) {
286 + const highlight = ' '.repeat(start) + '^'.repeat(length);
287 + return '\n ' + str + '\n ' + highlight;
288 + }
289 + return '\n ' + str;
290 +}
scripts/error-codes/codes.json
+6 -1
@@ -453,5 +453,10 @@
453 "465": "enableFizzExternalRuntime without enableFloat is not supported. This should never appear in production, since it means you are using a misconfigured React bundle.",
454 "466": "Trying to call a function from \"use server\" but the callServer option was not implemented in your router runtime.",
455 "467": "Update hook called on initial render. This is likely a bug in React. Please file an issue.",
456 - "468": "getNodesForType encountered a type it did not expect: \"%s\". This is a bug in React."
456 + "468": "getNodesForType encountered a type it did not expect: \"%s\". This is a bug in React.",
457 + "469": "Client Functions cannot be passed directly to Server Functions. Only Functions passed from the Server can be passed back again.",
458 + "470": "Only global symbols received from Symbol.for(...) can be passed to Server Functions. The symbol Symbol.for(%s) cannot be found among global symbols.",
459 + "471": "BigInt (%s) is not yet supported as an argument to a Server Function.",
460 + "472": "Type %s is not supported as an argument to a Server Function.",
461 + "473": "React doesn't accept base64 encoded file uploads because we don't except form data passed from a browser to ever encode data that way. If that's the wrong assumption, we can easily fix it."
462 }
\ No newline at end of file
scripts/flow/environment.js
+83
@@ -169,6 +169,89 @@ declare module 'util' {
169 }
170 }
171
172 +declare module 'busboy' {
173 + import type {Writable, Readable} from 'stream';
174 +
175 + declare interface Info {
176 + encoding: string;
177 + mimeType: string;
178 + }
179 +
180 + declare interface FileInfo extends Info {
181 + filename: string;
182 + }
183 +
184 + declare interface FieldInfo extends Info {
185 + nameTruncated: boolean;
186 + valueTruncated: boolean;
187 + }
188 +
189 + declare interface BusboyEvents {
190 + file: (name: string, stream: Readable, info: FileInfo) => void;
191 + field: (name: string, value: string, info: FieldInfo) => void;
192 + partsLimit: () => void;
193 + filesLimit: () => void;
194 + fieldsLimit: () => void;
195 + error: (error: mixed) => void;
196 + close: () => void;
197 + }
198 + declare interface Busboy extends Writable {
199 + addListener<Event: $Keys<BusboyEvents>>(
200 + event: Event,
201 + listener: BusboyEvents[Event],
202 + ): Busboy;
203 + addListener(
204 + event: string | symbol,
205 + listener: (...args: any[]) => void,
206 + ): Busboy;
207 +
208 + on<Event: $Keys<BusboyEvents>>(
209 + event: Event,
210 + listener: BusboyEvents[Event],
211 + ): Busboy;
212 + on(event: string | symbol, listener: (...args: any[]) => void): Busboy;
213 +
214 + once<Event: $Keys<BusboyEvents>>(
215 + event: Event,
216 + listener: BusboyEvents[Event],
217 + ): Busboy;
218 + once(event: string | symbol, listener: (...args: any[]) => void): Busboy;
219 +
220 + removeListener<Event: $Keys<BusboyEvents>>(
221 + event: Event,
222 + listener: BusboyEvents[Event],
223 + ): Busboy;
224 + removeListener(
225 + event: string | symbol,
226 + listener: (...args: any[]) => void,
227 + ): Busboy;
228 +
229 + off<Event: $Keys<BusboyEvents>>(
230 + event: Event,
231 + listener: BusboyEvents[Event],
232 + ): Busboy;
233 + off(event: string | symbol, listener: (...args: any[]) => void): Busboy;
234 +
235 + prependListener<Event: $Keys<BusboyEvents>>(
236 + event: Event,
237 + listener: BusboyEvents[Event],
238 + ): Busboy;
239 + prependListener(
240 + event: string | symbol,
241 + listener: (...args: any[]) => void,
242 + ): Busboy;
243 +
244 + prependOnceListener<Event: $Keys<BusboyEvents>>(
245 + event: Event,
246 + listener: BusboyEvents[Event],
247 + ): Busboy;
248 + prependOnceListener(
249 + event: string | symbol,
250 + listener: (...args: any[]) => void,
251 + ): Busboy;
252 + }
253 +}
254 +
255 declare module 'pg/lib/utils' {
256 declare module.exports: {
257 prepareValue(val: any): mixed,