Skip to content
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
90 changes: 57 additions & 33 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';

import {getOwnerStackByComponentInfoInDev} from 'shared/ReactComponentInfoStack';

import hasOwnProperty from 'shared/hasOwnProperty';

import {injectInternals} from './ReactFlightClientDevToolsHook';

import {OMITTED_PROP_ERROR} from 'shared/ReactFlightPropertyAccess';
Expand Down Expand Up @@ -159,6 +161,8 @@ const INITIALIZED = 'fulfilled';
const ERRORED = 'rejected';
const HALTED = 'halted'; // DEV-only. Means it never resolves even if connection closes.

const __PROTO__ = '__proto__';

type PendingChunk<T> = {
status: 'pending',
value: null | Array<InitializationReference | (T => mixed)>,
Expand Down Expand Up @@ -1544,7 +1548,16 @@ function fulfillReference(
}
}
}
value = value[path[i]];
const name = path[i];
if (
typeof value === 'object' &&
value !== null &&
hasOwnProperty.call(value, name)
) {
value = value[name];
} else {
throw new Error('Invalid reference.');
}
}

while (
Expand Down Expand Up @@ -1580,7 +1593,9 @@ function fulfillReference(
}

const mappedValue = map(response, value, parentObject, key);
parentObject[key] = mappedValue;
if (key !== __PROTO__) {
parentObject[key] = mappedValue;
}

// If this is the root object for a model reference, where `handler.value`
// is a stale `null`, the resolved value can be used directly.
Expand Down Expand Up @@ -1849,7 +1864,9 @@ function loadServerReference<A: Iterable<any>, T>(
response._encodeFormAction,
);

parentObject[key] = resolvedValue;
if (key !== __PROTO__) {
parentObject[key] = resolvedValue;
}

// If this is the root object for a model reference, where `handler.value`
// is a stale `null`, the resolved value can be used directly.
Expand Down Expand Up @@ -2231,29 +2248,31 @@ function defineLazyGetter<T>(
): any {
// We don't immediately initialize it even if it's resolved.
// Instead, we wait for the getter to get accessed.
Object.defineProperty(parentObject, key, {
get: function () {
if (chunk.status === RESOLVED_MODEL) {
// If it was now resolved, then we initialize it. This may then discover
// a new set of lazy references that are then asked for eagerly in case
// we get that deep.
initializeModelChunk(chunk);
}
switch (chunk.status) {
case INITIALIZED: {
return chunk.value;
if (key !== __PROTO__) {
Object.defineProperty(parentObject, key, {
get: function () {
if (chunk.status === RESOLVED_MODEL) {
// If it was now resolved, then we initialize it. This may then discover
// a new set of lazy references that are then asked for eagerly in case
// we get that deep.
initializeModelChunk(chunk);
}
case ERRORED:
throw chunk.reason;
}
// Otherwise, we didn't have enough time to load the object before it was
// accessed or the connection closed. So we just log that it was omitted.
// TODO: We should ideally throw here to indicate a difference.
return OMITTED_PROP_ERROR;
},
enumerable: true,
configurable: false,
});
switch (chunk.status) {
case INITIALIZED: {
return chunk.value;
}
case ERRORED:
throw chunk.reason;
}
// Otherwise, we didn't have enough time to load the object before it was
// accessed or the connection closed. So we just log that it was omitted.
// TODO: We should ideally throw here to indicate a difference.
return OMITTED_PROP_ERROR;
},
enumerable: true,
configurable: false,
});
}
return null;
}

Expand Down Expand Up @@ -2564,14 +2583,16 @@ function parseModelString(
// In DEV mode we encode omitted objects in logs as a getter that throws
// so that when you try to access it on the client, you know why that
// happened.
Object.defineProperty(parentObject, key, {
get: function () {
// TODO: We should ideally throw here to indicate a difference.
return OMITTED_PROP_ERROR;
},
enumerable: true,
configurable: false,
});
if (key !== __PROTO__) {
Object.defineProperty(parentObject, key, {
get: function () {
// TODO: We should ideally throw here to indicate a difference.
return OMITTED_PROP_ERROR;
},
enumerable: true,
configurable: false,
});
}
return null;
}
// Fallthrough
Expand Down Expand Up @@ -5183,6 +5204,9 @@ function parseModel<T>(response: Response, json: UninitializedModel): T {
function createFromJSONCallback(response: Response) {
// $FlowFixMe[missing-this-annot]
return function (key: string, value: JSONValue) {
if (key === __PROTO__) {
return undefined;
}
if (typeof value === 'string') {
// We can't use .bind here because we need the "this" value.
return parseModelString(response, this, key, value);
Expand Down
20 changes: 19 additions & 1 deletion packages/react-client/src/ReactFlightReplyClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export type ReactServerValue =

type ReactServerObject = {+[key: string]: ReactServerValue};

const __PROTO__ = '__proto__';

function serializeByValueID(id: number): string {
return '$' + id.toString(16);
}
Expand Down Expand Up @@ -361,6 +363,15 @@ export function processReply(
): ReactJSONValue {
const parent = this;

if (__DEV__) {
if (key === __PROTO__) {
console.error(
'Expected not to serialize an object with own property `__proto__`. When parsed this property will be omitted.%s',
describeObjectForErrorMessage(parent, key),
);
}
}

// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
if (__DEV__) {
// $FlowFixMe[incompatible-use]
Expand Down Expand Up @@ -780,6 +791,10 @@ export function processReply(
if (typeof value === 'function') {
const referenceClosure = knownServerReferences.get(value);
if (referenceClosure !== undefined) {
const existingReference = writtenObjects.get(value);
if (existingReference !== undefined) {
return existingReference;
}
const {id, bound} = referenceClosure;
const referenceClosureJSON = JSON.stringify({id, bound}, resolveToJSON);
if (formData === null) {
Expand All @@ -789,7 +804,10 @@ export function processReply(
// The reference to this function came from the same client so we can pass it back.
const refId = nextPartId++;
formData.set(formFieldPrefix + refId, referenceClosureJSON);
return serializeServerReferenceID(refId);
const serverReferenceId = serializeServerReferenceID(refId);
// Store the server reference ID for deduplication.
writtenObjects.set(value, serverReferenceId);
return serverReferenceId;
}
if (temporaryReferences !== undefined && key.indexOf(':') === -1) {
// TODO: If the property name contains a colon, we don't dedupe. Escape instead.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export * from 'react-client/src/ReactClientConsoleConfigPlain';
export type ModuleLoading = null;
export type ServerConsumerModuleMap = null;
export opaque type ServerManifest = null;
export opaque type ServerReferenceId = string;
export type ServerReferenceId = string;
export opaque type ClientReferenceMetadata = null;
export opaque type ClientReference<T> = null; // eslint-disable-line no-unused-vars

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,12 +328,17 @@ function prerenderToNodeStream(
function decodeReplyFromBusboy<T>(
busboyStream: Busboy,
moduleBasePath: ServerManifest,
options?: {temporaryReferences?: TemporaryReferenceSet},
options?: {
temporaryReferences?: TemporaryReferenceSet,
arraySizeLimit?: number,
},
): Thenable<T> {
const response = createResponse(
moduleBasePath,
'',
options ? options.temporaryReferences : undefined,
undefined,
options ? options.arraySizeLimit : undefined,
);
let pendingFiles = 0;
const queuedFields: Array<string> = [];
Expand Down Expand Up @@ -399,7 +404,10 @@ function decodeReplyFromBusboy<T>(
function decodeReply<T>(
body: string | FormData,
moduleBasePath: ServerManifest,
options?: {temporaryReferences?: TemporaryReferenceSet},
options?: {
temporaryReferences?: TemporaryReferenceSet,
arraySizeLimit?: number,
},
): Thenable<T> {
if (typeof body === 'string') {
const form = new FormData();
Expand All @@ -411,6 +419,7 @@ function decodeReply<T>(
'',
options ? options.temporaryReferences : undefined,
body,
options ? options.arraySizeLimit : undefined,
);
const root = getRoot<T>(response);
close(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,10 @@ export function registerServerActions(manifest: ServerManifest) {

export function decodeReply<T>(
body: string | FormData,
options?: {temporaryReferences?: TemporaryReferenceSet},
options?: {
temporaryReferences?: TemporaryReferenceSet,
arraySizeLimit?: number,
},
): Thenable<T> {
if (typeof body === 'string') {
const form = new FormData();
Expand All @@ -257,6 +260,7 @@ export function decodeReply<T>(
'',
options ? options.temporaryReferences : undefined,
body,
options ? options.arraySizeLimit : undefined,
);
const root = getRoot<T>(response);
close(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,10 @@ export function registerServerActions(manifest: ServerManifest) {

export function decodeReply<T>(
body: string | FormData,
options?: {temporaryReferences?: TemporaryReferenceSet},
options?: {
temporaryReferences?: TemporaryReferenceSet,
arraySizeLimit?: number,
},
): Thenable<T> {
if (typeof body === 'string') {
const form = new FormData();
Expand All @@ -262,6 +265,7 @@ export function decodeReply<T>(
'',
options ? options.temporaryReferences : undefined,
body,
options ? options.arraySizeLimit : undefined,
);
const root = getRoot<T>(response);
close(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -556,12 +556,17 @@ export function registerServerActions(manifest: ServerManifest) {

export function decodeReplyFromBusboy<T>(
busboyStream: Busboy,
options?: {temporaryReferences?: TemporaryReferenceSet},
options?: {
temporaryReferences?: TemporaryReferenceSet,
arraySizeLimit?: number,
},
): Thenable<T> {
const response = createResponse(
serverManifest,
'',
options ? options.temporaryReferences : undefined,
undefined,
options ? options.arraySizeLimit : undefined,
);
let pendingFiles = 0;
const queuedFields: Array<string> = [];
Expand Down Expand Up @@ -626,7 +631,10 @@ export function decodeReplyFromBusboy<T>(

export function decodeReply<T>(
body: string | FormData,
options?: {temporaryReferences?: TemporaryReferenceSet},
options?: {
temporaryReferences?: TemporaryReferenceSet,
arraySizeLimit?: number,
},
): Thenable<T> {
if (typeof body === 'string') {
const form = new FormData();
Expand All @@ -638,6 +646,7 @@ export function decodeReply<T>(
'',
options ? options.temporaryReferences : undefined,
body,
options ? options.arraySizeLimit : undefined,
);
const root = getRoot<T>(response);
close(response);
Expand All @@ -646,7 +655,10 @@ export function decodeReply<T>(

export function decodeReplyFromAsyncIterable<T>(
iterable: AsyncIterable<[string, string | File]>,
options?: {temporaryReferences?: TemporaryReferenceSet},
options?: {
temporaryReferences?: TemporaryReferenceSet,
arraySizeLimit?: number,
},
): Thenable<T> {
const iterator: AsyncIterator<[string, string | File]> =
iterable[ASYNC_ITERATOR]();
Expand All @@ -655,6 +667,8 @@ export function decodeReplyFromAsyncIterable<T>(
serverManifest,
'',
options ? options.temporaryReferences : undefined,
undefined,
options ? options.arraySizeLimit : undefined,
);

function progress(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,10 @@ function prerender(
function decodeReply<T>(
body: string | FormData,
turbopackMap: ServerManifest,
options?: {temporaryReferences?: TemporaryReferenceSet},
options?: {
temporaryReferences?: TemporaryReferenceSet,
arraySizeLimit?: number,
},
): Thenable<T> {
if (typeof body === 'string') {
const form = new FormData();
Expand All @@ -251,6 +254,7 @@ function decodeReply<T>(
'',
options ? options.temporaryReferences : undefined,
body,
options ? options.arraySizeLimit : undefined,
);
const root = getRoot<T>(response);
close(response);
Expand Down
Loading
Loading