react/packages/react-server-native-relay/src/ReactFlightClientConfigNati...

100 lines
2.6 KiB
JavaScript
Raw Normal View History

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient';
import type {JSResourceReference} from 'JSResourceReference';
import type {ClientReferenceMetadata} from 'ReactFlightNativeRelayClientIntegration';
export type ClientReference<T> = JSResourceReference<T>;
import {
parseModelString,
parseModelTuple,
} from 'react-client/src/ReactFlightClient';
export {
preloadModule,
requireModule,
} from 'ReactFlightNativeRelayClientIntegration';
import {resolveClientReference as resolveClientReferenceImpl} from 'ReactFlightNativeRelayClientIntegration';
import isArray from 'shared/isArray';
export type {ClientReferenceMetadata} from 'ReactFlightNativeRelayClientIntegration';
export type SSRManifest = null;
[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>
2023-03-11 00:36:15 +08:00
export type ServerManifest = null;
export type ServerReferenceId = string;
export type UninitializedModel = JSONValue;
export type Response = ResponseBase;
export function resolveClientReference<T>(
bundlerConfig: SSRManifest,
metadata: ClientReferenceMetadata,
): ClientReference<T> {
return resolveClientReferenceImpl(metadata);
}
[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>
2023-03-11 00:36:15 +08:00
export function resolveServerReference<T>(
bundlerConfig: ServerManifest,
id: ServerReferenceId,
): ClientReference<T> {
throw new Error('Not implemented.');
}
function parseModelRecursively(
response: Response,
parentObj: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
key: string,
value: JSONValue,
): $FlowFixMe {
if (typeof value === 'string') {
[Flight] Implement FlightClient in terms of Thenable/Promises instead of throwing Promises (#25260) * [Flight] Align Chunks with Thenable used with experimental_use Use the field names used by the Thenable data structure passed to use(). These are considered public in this model. This adds another field since we use a separate field name for "reason". * Implement Thenable Protocol on Chunks This doesn't just ping but resolves/rejects with the value. * Subclass Promises * Pass key through JSON parsing * Wait for preloadModules before resolving module chunks * Initialize lazy resolved values before reading the result * Block a model from initializing if its direct dependencies are pending If a module is blocked, then we can't complete initializing a model. However, we can still let it parse, and then fill in the missing pieces later. We need to block it from resolving until all dependencies have filled in which we can do with a ref count. * Treat blocked modules or models as a special status We currently loop over all chunks at the end to error them if they're still pending. We shouldn't do this if they're pending because they're blocked on an external resource like a module because the module might not resolve before the Flight connection closes and that's not an error. In an alternative solution I had a set that tracked pending chunks and removed one at a time. While the loop at the end is faster it's more work as we go. I figured the extra status might also help debugging. For modules we can probably assume no forward references, and the first async module we can just use the promise as the chunk. So we could probably get away with this only on models that are blocked by modules.
2022-09-15 08:13:33 +08:00
return parseModelString(response, parentObj, key, value);
}
if (typeof value === 'object' && value !== null) {
if (isArray(value)) {
const parsedValue: Array<$FlowFixMe> = [];
for (let i = 0; i < value.length; i++) {
(parsedValue: any)[i] = parseModelRecursively(
response,
value,
[Flight] Implement FlightClient in terms of Thenable/Promises instead of throwing Promises (#25260) * [Flight] Align Chunks with Thenable used with experimental_use Use the field names used by the Thenable data structure passed to use(). These are considered public in this model. This adds another field since we use a separate field name for "reason". * Implement Thenable Protocol on Chunks This doesn't just ping but resolves/rejects with the value. * Subclass Promises * Pass key through JSON parsing * Wait for preloadModules before resolving module chunks * Initialize lazy resolved values before reading the result * Block a model from initializing if its direct dependencies are pending If a module is blocked, then we can't complete initializing a model. However, we can still let it parse, and then fill in the missing pieces later. We need to block it from resolving until all dependencies have filled in which we can do with a ref count. * Treat blocked modules or models as a special status We currently loop over all chunks at the end to error them if they're still pending. We shouldn't do this if they're pending because they're blocked on an external resource like a module because the module might not resolve before the Flight connection closes and that's not an error. In an alternative solution I had a set that tracked pending chunks and removed one at a time. While the loop at the end is faster it's more work as we go. I figured the extra status might also help debugging. For modules we can probably assume no forward references, and the first async module we can just use the promise as the chunk. So we could probably get away with this only on models that are blocked by modules.
2022-09-15 08:13:33 +08:00
'' + i,
value[i],
);
}
return parseModelTuple(response, parsedValue);
} else {
const parsedValue = {};
for (const innerKey in value) {
(parsedValue: any)[innerKey] = parseModelRecursively(
response,
value,
[Flight] Implement FlightClient in terms of Thenable/Promises instead of throwing Promises (#25260) * [Flight] Align Chunks with Thenable used with experimental_use Use the field names used by the Thenable data structure passed to use(). These are considered public in this model. This adds another field since we use a separate field name for "reason". * Implement Thenable Protocol on Chunks This doesn't just ping but resolves/rejects with the value. * Subclass Promises * Pass key through JSON parsing * Wait for preloadModules before resolving module chunks * Initialize lazy resolved values before reading the result * Block a model from initializing if its direct dependencies are pending If a module is blocked, then we can't complete initializing a model. However, we can still let it parse, and then fill in the missing pieces later. We need to block it from resolving until all dependencies have filled in which we can do with a ref count. * Treat blocked modules or models as a special status We currently loop over all chunks at the end to error them if they're still pending. We shouldn't do this if they're pending because they're blocked on an external resource like a module because the module might not resolve before the Flight connection closes and that's not an error. In an alternative solution I had a set that tracked pending chunks and removed one at a time. While the loop at the end is faster it's more work as we go. I figured the extra status might also help debugging. For modules we can probably assume no forward references, and the first async module we can just use the promise as the chunk. So we could probably get away with this only on models that are blocked by modules.
2022-09-15 08:13:33 +08:00
innerKey,
value[innerKey],
);
}
return parsedValue;
}
}
return value;
}
const dummy = {};
export function parseModel<T>(response: Response, json: UninitializedModel): T {
[Flight] Implement FlightClient in terms of Thenable/Promises instead of throwing Promises (#25260) * [Flight] Align Chunks with Thenable used with experimental_use Use the field names used by the Thenable data structure passed to use(). These are considered public in this model. This adds another field since we use a separate field name for "reason". * Implement Thenable Protocol on Chunks This doesn't just ping but resolves/rejects with the value. * Subclass Promises * Pass key through JSON parsing * Wait for preloadModules before resolving module chunks * Initialize lazy resolved values before reading the result * Block a model from initializing if its direct dependencies are pending If a module is blocked, then we can't complete initializing a model. However, we can still let it parse, and then fill in the missing pieces later. We need to block it from resolving until all dependencies have filled in which we can do with a ref count. * Treat blocked modules or models as a special status We currently loop over all chunks at the end to error them if they're still pending. We shouldn't do this if they're pending because they're blocked on an external resource like a module because the module might not resolve before the Flight connection closes and that's not an error. In an alternative solution I had a set that tracked pending chunks and removed one at a time. While the loop at the end is faster it's more work as we go. I figured the extra status might also help debugging. For modules we can probably assume no forward references, and the first async module we can just use the promise as the chunk. So we could probably get away with this only on models that are blocked by modules.
2022-09-15 08:13:33 +08:00
return (parseModelRecursively(response, dummy, '', json): any);
}
[Float][Flight] Flight support for Float (#26502) Stacked on #26557 Supporting Float methods such as ReactDOM.preload() are challenging for flight because it does not have an easy means to convey direct executions in other environments. Because the flight wire format is a JSON-like serialization that is expected to be rendered it currently only describes renderable elements. We need a way to convey a function invocation that gets run in the context of the client environment whether that is Fizz or Fiber. Fiber is somewhat straightforward because the HostDispatcher is always active and we can just have the FlightClient dispatch the serialized directive. Fizz is much more challenging becaue the dispatcher is always scoped but the specific request the dispatch belongs to is not readily available. Environments that support AsyncLocalStorage (or in the future AsyncContext) we will use this to be able to resolve directives in Fizz to the appropriate Request. For other environments directives will be elided. Right now this is pragmatic and non-breaking because all directives are opportunistic and non-critical. If this changes in the future we will need to reconsider how widespread support for async context tracking is. For Flight, if AsyncLocalStorage is available Float methods can be called before and after await points and be expected to work. If AsyncLocalStorage is not available float methods called in the sync phase of a component render will be captured but anything after an await point will be a noop. If a float call is dropped in this manner a DEV warning should help you realize your code may need to be modified. This PR also introduces a way for resources (Fizz) and hints (Flight) to flush even if there is not active task being worked on. This will help when Float methods are called in between async points within a function execution but the task is blocked on the entire function finishing. This PR also introduces deduping of Hints in Flight using the same resource keys used in Fizz. This will help shrink payload sizes when the same hint is attempted to emit over and over again
2023-04-22 11:45:51 +08:00
export function dispatchHint(code: string, model: mixed) {}