Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Flight] Allow lazily resolving outlined models #28780

Merged
merged 2 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 106 additions & 77 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,8 @@ function createModelResolver<T>(
parentObject: Object,
key: string,
cyclic: boolean,
response: Response,
map: (response: Response, model: any) => T,
): (value: any) => void {
let blocked;
if (initializingChunkBlockedModel) {
Expand All @@ -595,12 +597,12 @@ function createModelResolver<T>(
};
}
return value => {
parentObject[key] = value;
parentObject[key] = map(response, value);

// If this is the root object for a model reference, where `blocked.value`
// is a stale `null`, the resolved value can be used directly.
if (key === '' && blocked.value === null) {
blocked.value = value;
blocked.value = parentObject[key];
}

blocked.deps--;
Expand Down Expand Up @@ -651,24 +653,103 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
return proxy;
}

function getOutlinedModel(response: Response, id: number): any {
function getOutlinedModel<T>(
response: Response,
id: number,
parentObject: Object,
key: string,
map: (response: Response, model: any) => T,
): T {
const chunk = getChunk(response, id);
switch (chunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(chunk);
break;
case RESOLVED_MODULE:
initializeModuleChunk(chunk);
break;
}
// The status might have changed after initialization.
switch (chunk.status) {
case INITIALIZED: {
return chunk.value;
}
// We always encode it first in the stream so it won't be pending.
case INITIALIZED:
const chunkValue = map(response, chunk.value);
if (__DEV__ && chunk._debugInfo) {
// If we have a direct reference to an object that was rendered by a synchronous
// server component, it might have some debug info about how it was rendered.
// We forward this to the underlying object. This might be a React Element or
// an Array fragment.
// If this was a string / number return value we lose the debug info. We choose
// that tradeoff to allow sync server components to return plain values and not
// use them as React Nodes necessarily. We could otherwise wrap them in a Lazy.
if (
typeof chunkValue === 'object' &&
chunkValue !== null &&
(Array.isArray(chunkValue) ||
chunkValue.$$typeof === REACT_ELEMENT_TYPE) &&
!chunkValue._debugInfo
) {
// We should maybe use a unique symbol for arrays but this is a React owned array.
// $FlowFixMe[prop-missing]: This should be added to elements.
Object.defineProperty((chunkValue: any), '_debugInfo', {
configurable: false,
enumerable: false,
writable: true,
value: chunk._debugInfo,
});
}
}
return chunkValue;
case PENDING:
case BLOCKED:
case CYCLIC:
const parentChunk = initializingChunk;
chunk.then(
createModelResolver(
parentChunk,
parentObject,
key,
chunk.status === CYCLIC,
response,
map,
),
createModelReject(parentChunk),
);
return (null: any);
default:
throw chunk.reason;
}
}

function createMap(
response: Response,
model: Array<[any, any]>,
): Map<any, any> {
return new Map(model);
}

function createSet(response: Response, model: Array<any>): Set<any> {
return new Set(model);
}

function createBlob(response: Response, model: Array<any>): Blob {
return new Blob(model.slice(1), {type: model[0]});
}

function createFormData(
response: Response,
model: Array<[any, any]>,
): FormData {
const formData = new FormData();
for (let i = 0; i < model.length; i++) {
formData.append(model[i][0], model[i][1]);
}
return formData;
}

function createModel(response: Response, model: any): any {
return model;
}

function parseModelString(
response: Response,
parentObject: Object,
Expand Down Expand Up @@ -710,8 +791,13 @@ function parseModelString(
case 'F': {
// Server Reference
const id = parseInt(value.slice(2), 16);
const metadata = getOutlinedModel(response, id);
return createServerReferenceProxy(response, metadata);
return getOutlinedModel(
response,
id,
parentObject,
key,
createServerReferenceProxy,
);
}
case 'T': {
// Temporary Reference
Expand All @@ -728,33 +814,31 @@ function parseModelString(
case 'Q': {
// Map
const id = parseInt(value.slice(2), 16);
const data = getOutlinedModel(response, id);
return new Map(data);
return getOutlinedModel(response, id, parentObject, key, createMap);
}
case 'W': {
// Set
const id = parseInt(value.slice(2), 16);
const data = getOutlinedModel(response, id);
return new Set(data);
return getOutlinedModel(response, id, parentObject, key, createSet);
}
case 'B': {
// Blob
if (enableBinaryFlight) {
const id = parseInt(value.slice(2), 16);
const data = getOutlinedModel(response, id);
return new Blob(data.slice(1), {type: data[0]});
return getOutlinedModel(response, id, parentObject, key, createBlob);
}
return undefined;
}
case 'K': {
// FormData
const id = parseInt(value.slice(2), 16);
const data = getOutlinedModel(response, id);
const formData = new FormData();
for (let i = 0; i < data.length; i++) {
formData.append(data[i][0], data[i][1]);
}
return formData;
return getOutlinedModel(
response,
id,
parentObject,
key,
createFormData,
);
}
case 'I': {
// $Infinity
Expand Down Expand Up @@ -803,62 +887,7 @@ function parseModelString(
default: {
// We assume that anything else is a reference ID.
const id = parseInt(value.slice(1), 16);
const chunk = getChunk(response, id);
switch (chunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(chunk);
break;
case RESOLVED_MODULE:
initializeModuleChunk(chunk);
break;
}
// The status might have changed after initialization.
switch (chunk.status) {
case INITIALIZED:
const chunkValue = chunk.value;
if (__DEV__ && chunk._debugInfo) {
// If we have a direct reference to an object that was rendered by a synchronous
// server component, it might have some debug info about how it was rendered.
// We forward this to the underlying object. This might be a React Element or
// an Array fragment.
// If this was a string / number return value we lose the debug info. We choose
// that tradeoff to allow sync server components to return plain values and not
// use them as React Nodes necessarily. We could otherwise wrap them in a Lazy.
if (
typeof chunkValue === 'object' &&
chunkValue !== null &&
(Array.isArray(chunkValue) ||
chunkValue.$$typeof === REACT_ELEMENT_TYPE) &&
!chunkValue._debugInfo
) {
// We should maybe use a unique symbol for arrays but this is a React owned array.
// $FlowFixMe[prop-missing]: This should be added to elements.
Object.defineProperty(chunkValue, '_debugInfo', {
configurable: false,
enumerable: false,
writable: true,
value: chunk._debugInfo,
});
}
}
return chunkValue;
case PENDING:
case BLOCKED:
case CYCLIC:
const parentChunk = initializingChunk;
chunk.then(
createModelResolver(
parentChunk,
parentObject,
key,
chunk.status === CYCLIC,
),
createModelReject(parentChunk),
);
return null;
default:
throw chunk.reason;
}
return getOutlinedModel(response, id, parentObject, key, createModel);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ global.TextDecoder = require('util').TextDecoder;
if (typeof Blob === 'undefined') {
global.Blob = require('buffer').Blob;
}
if (typeof File === 'undefined') {
global.File = require('buffer').File;
}

// Don't wait before processing work on the server.
// TODO: we can replace this with FlightServer.act().
Expand Down Expand Up @@ -352,6 +355,81 @@ describe('ReactFlightDOMEdge', () => {
expect(await result.arrayBuffer()).toEqual(await blob.arrayBuffer());
});

if (typeof FormData !== 'undefined' && typeof File !== 'undefined') {
// @gate enableBinaryFlight
it('can transport FormData (blobs)', async () => {
const bytes = new Uint8Array([
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
]);
const blob = new Blob([bytes, bytes], {
type: 'application/x-test',
});

const formData = new FormData();
formData.append('hi', 'world');
formData.append('file', blob, 'filename.test');

expect(formData.get('file') instanceof File).toBe(true);
expect(formData.get('file').name).toBe('filename.test');

const stream = passThrough(
ReactServerDOMServer.renderToReadableStream(formData),
);
const result = await ReactServerDOMClient.createFromReadableStream(
stream,
{
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);

expect(result instanceof FormData).toBe(true);
expect(result.get('hi')).toBe('world');
const resultBlob = result.get('file');
expect(resultBlob instanceof Blob).toBe(true);
expect(resultBlob.name).toBe('blob'); // We should not pass through the file name for security.
expect(resultBlob.size).toBe(bytes.length * 2);
expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
});
}

it('can pass an async import that resolves later to an outline object like a Map', async () => {
let resolve;
const promise = new Promise(r => (resolve = r));

const asyncClient = clientExports(promise);

// We await the value on the servers so it's an async value that the client should wait for
const awaitedValue = await asyncClient;

const map = new Map();
map.set('value', awaitedValue);

const stream = passThrough(
ReactServerDOMServer.renderToReadableStream(map, webpackMap),
);

// Parsing the root blocks because the module hasn't loaded yet
const resultPromise = ReactServerDOMClient.createFromReadableStream(
stream,
{
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);

// Afterwards we finally resolve the module value so it's available on the client
resolve('hello');

const result = await resultPromise;
expect(result instanceof Map).toBe(true);
expect(result.get('value')).toBe('hello');
});

it('warns if passing a this argument to bind() of a server reference', async () => {
const ServerModule = serverExports({
greet: function () {},
Expand Down
Loading