Skip to content

Commit

Permalink
Temporary references
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage committed Mar 15, 2024
1 parent 1580a43 commit 7a52f2c
Show file tree
Hide file tree
Showing 16 changed files with 371 additions and 17 deletions.
19 changes: 19 additions & 0 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import type {

import type {Postpone} from 'react/src/ReactPostpone';

import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';

import {
enableBinaryFlight,
enablePostpone,
Expand All @@ -55,6 +57,8 @@ import {

import {registerServerReference} from './ReactFlightReplyClient';

import {readTemporaryReference} from './ReactFlightTemporaryReferences';

import {
REACT_LAZY_TYPE,
REACT_ELEMENT_TYPE,
Expand Down Expand Up @@ -224,6 +228,7 @@ export type Response = {
_rowTag: number, // 0 indicates that we're currently parsing the row ID
_rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline.
_buffer: Array<Uint8Array>, // chunks received so far as part of this row
_tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from
};

function readChunk<T>(chunk: SomeChunk<T>): T {
Expand Down Expand Up @@ -689,6 +694,18 @@ function parseModelString(
const metadata = getOutlinedModel(response, id);
return createServerReferenceProxy(response, metadata);
}
case 'T': {
// Temporary Reference
const id = parseInt(value.slice(2), 16);
const temporaryReferences = response._tempRefs;
if (temporaryReferences == null) {
throw new Error(
'Missing a temporary reference set but the RSC response returned a temporary reference. ' +
'Pass a temporaryReference option with the set that was used with the reply.',
);
}
return readTemporaryReference(temporaryReferences, id);
}
case 'Q': {
// Map
const id = parseInt(value.slice(2), 16);
Expand Down Expand Up @@ -837,6 +854,7 @@ export function createResponse(
callServer: void | CallServerCallback,
encodeFormAction: void | EncodeFormActionCallback,
nonce: void | string,
temporaryReferences: void | TemporaryReferenceSet,
): Response {
const chunks: Map<number, SomeChunk<any>> = new Map();
const response: Response = {
Expand All @@ -853,6 +871,7 @@ export function createResponse(
_rowTag: 0,
_rowLength: 0,
_buffer: [],
_tempRefs: temporaryReferences,
};
// Don't inline this call because it causes closure to outline the call above.
response._fromJSON = createFromJSONCallback(response);
Expand Down
44 changes: 35 additions & 9 deletions packages/react-client/src/ReactFlightReplyClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
ReactCustomFormAction,
} from 'shared/ReactTypes';
import type {LazyComponent} from 'react/src/ReactLazy';
import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';

import {enableRenderableContext} from 'shared/ReactFeatureFlags';

Expand All @@ -32,6 +33,8 @@ import {
objectName,
} from 'shared/ReactSerializationErrors';

import {writeTemporaryReference} from './ReactFlightTemporaryReferences';

import isArray from 'shared/isArray';
import getPrototypeOf from 'shared/getPrototypeOf';

Expand Down Expand Up @@ -98,6 +101,10 @@ function serializeServerReferenceID(id: number): string {
return '$F' + id.toString(16);
}

function serializeTemporaryReferenceID(id: number): string {
return '$T' + id.toString(16);
}

function serializeSymbolReference(name: string): string {
return '$S' + name;
}
Expand Down Expand Up @@ -160,6 +167,7 @@ function escapeStringValue(value: string): string {
export function processReply(
root: ReactServerValue,
formFieldPrefix: string,
temporaryReferences: void | TemporaryReferenceSet,
resolve: (string | FormData) => void,
reject: (error: mixed) => void,
): void {
Expand Down Expand Up @@ -210,9 +218,15 @@ export function processReply(
if (typeof value === 'object') {
switch ((value: any).$$typeof) {
case REACT_ELEMENT_TYPE: {
throw new Error(
'React Element cannot be passed to Server Functions from the Client.' +
(__DEV__ ? describeObjectForErrorMessage(parent, key) : ''),
if (temporaryReferences === undefined) {
throw new Error(
'React Element cannot be passed to Server Functions from the Client without a ' +
'temporary reference set. Pass a TemporaryReferenceSet to the options.' +
(__DEV__ ? describeObjectForErrorMessage(parent, key) : ''),
);
}
return serializeTemporaryReferenceID(
writeTemporaryReference(temporaryReferences, value),
);
}
case REACT_LAZY_TYPE: {
Expand Down Expand Up @@ -366,9 +380,15 @@ export function processReply(
proto !== ObjectPrototype &&
(proto === null || getPrototypeOf(proto) !== null)
) {
throw new Error(
'Only plain objects, and a few built-ins, can be passed to Server Actions. ' +
'Classes or null prototypes are not supported.',
if (temporaryReferences === undefined) {
throw new Error(
'Only plain objects, and a few built-ins, can be passed to Server Actions. ' +
'Classes or null prototypes are not supported.',
);
}
// We can serialize class instances as temporary references.
return serializeTemporaryReferenceID(
writeTemporaryReference(temporaryReferences, value),
);
}
if (__DEV__) {
Expand Down Expand Up @@ -450,9 +470,14 @@ export function processReply(
formData.set(formFieldPrefix + refId, metaDataJSON);
return serializeServerReferenceID(refId);
}
throw new Error(
'Client Functions cannot be passed directly to Server Functions. ' +
'Only Functions passed from the Server can be passed back again.',
if (temporaryReferences === undefined) {
throw new Error(
'Client Functions cannot be passed directly to Server Functions. ' +
'Only Functions passed from the Server can be passed back again.',
);
}
return serializeTemporaryReferenceID(
writeTemporaryReference(temporaryReferences, value),
);
}

Expand Down Expand Up @@ -511,6 +536,7 @@ function encodeFormData(reference: any): Thenable<FormData> {
processReply(
reference,
'',
undefined, // TODO: This means React Elements can't be used as state in progressive enhancement.
(body: string | FormData) => {
if (typeof body === 'string') {
const data = new FormData();
Expand Down
41 changes: 41 additions & 0 deletions packages/react-client/src/ReactFlightTemporaryReferences.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* 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
*/

interface Reference {}

export opaque type TemporaryReferenceSet = Array<Reference>;

export function createTemporaryReferenceSet(): TemporaryReferenceSet {
return [];
}

export function writeTemporaryReference(
set: TemporaryReferenceSet,
object: Reference,
): number {
// We always create a new entry regardless if we've already written the same
// object. This ensures that we always generate a deterministic encoding of
// each slot in the reply for cacheability.
const newId = set.length;
set.push(object);
return newId;
}

export function readTemporaryReference(
set: TemporaryReferenceSet,
id: number,
): Reference {
if (id < 0 || id >= set.length) {
throw new Error(
"The RSC response contained a reference that doesn't exist in the temporary reference set. " +
'Always pass the matching set that was used to create the reply when parsing its response.',
);
}
return set[id];
}
21 changes: 20 additions & 1 deletion packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ import {
createServerReference,
} from 'react-client/src/ReactFlightReplyClient';

import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';

export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';

export type {TemporaryReferenceSet};

type CallServerCallback = <A, T>(string, args: A) => Promise<T>;

export type Options = {
moduleBaseURL?: string,
callServer?: CallServerCallback,
temporaryReferences?: TemporaryReferenceSet,
};

function createResponseFromOptions(options: void | Options) {
Expand All @@ -40,6 +47,9 @@ function createResponseFromOptions(options: void | Options) {
options && options.callServer ? options.callServer : undefined,
undefined, // encodeFormAction
undefined, // nonce
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
);
}

Expand Down Expand Up @@ -97,11 +107,20 @@ function createFromFetch<T>(

function encodeReply(
value: ReactServerValue,
options?: {temporaryReferences?: TemporaryReferenceSet},
): Promise<
string | URLSearchParams | FormData,
> /* We don't use URLSearchParams yet but maybe */ {
return new Promise((resolve, reject) => {
processReply(value, '', resolve, reject);
processReply(
value,
'',
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
resolve,
reject,
);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ function createFromNodeStream<T>(
noServerCall,
options ? options.encodeFormAction : undefined,
options && typeof options.nonce === 'string' ? options.nonce : undefined,
undefined, // TODO: If encodeReply is supported, this should support temporaryReferences
);
stream.on('data', chunk => {
processBinaryChunk(response, chunk);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,17 @@ import {
createServerReference,
} from 'react-client/src/ReactFlightReplyClient';

import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';

export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';

export type {TemporaryReferenceSet};

type CallServerCallback = <A, T>(string, args: A) => Promise<T>;

export type Options = {
callServer?: CallServerCallback,
temporaryReferences?: TemporaryReferenceSet,
};

function createResponseFromOptions(options: void | Options) {
Expand All @@ -39,6 +46,9 @@ function createResponseFromOptions(options: void | Options) {
options && options.callServer ? options.callServer : undefined,
undefined, // encodeFormAction
undefined, // nonce
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
);
}

Expand Down Expand Up @@ -96,11 +106,20 @@ function createFromFetch<T>(

function encodeReply(
value: ReactServerValue,
options?: {temporaryReferences?: TemporaryReferenceSet},
): Promise<
string | URLSearchParams | FormData,
> /* We don't use URLSearchParams yet but maybe */ {
return new Promise((resolve, reject) => {
processReply(value, '', resolve, reject);
processReply(
value,
'',
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
resolve,
reject,
);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ import {
createServerReference as createServerReferenceImpl,
} from 'react-client/src/ReactFlightReplyClient';

import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';

export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';

export type {TemporaryReferenceSet};

function noServerCall() {
throw new Error(
'Server Functions cannot be called during initial render. ' +
Expand All @@ -60,6 +66,7 @@ export type Options = {
ssrManifest: SSRManifest,
nonce?: string,
encodeFormAction?: EncodeFormActionCallback,
temporaryReferences?: TemporaryReferenceSet,
};

function createResponseFromOptions(options: Options) {
Expand All @@ -69,6 +76,9 @@ function createResponseFromOptions(options: Options) {
noServerCall,
options.encodeFormAction,
typeof options.nonce === 'string' ? options.nonce : undefined,
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
);
}

Expand Down Expand Up @@ -126,11 +136,20 @@ function createFromFetch<T>(

function encodeReply(
value: ReactServerValue,
options?: {temporaryReferences?: TemporaryReferenceSet},
): Promise<
string | URLSearchParams | FormData,
> /* We don't use URLSearchParams yet but maybe */ {
return new Promise((resolve, reject) => {
processReply(value, '', resolve, reject);
processReply(
value,
'',
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
resolve,
reject,
);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ function createFromNodeStream<T>(
noServerCall,
options ? options.encodeFormAction : undefined,
options && typeof options.nonce === 'string' ? options.nonce : undefined,
undefined, // TODO: If encodeReply is supported, this should support temporaryReferences
);
stream.on('data', chunk => {
processBinaryChunk(response, chunk);
Expand Down
Loading

0 comments on commit 7a52f2c

Please sign in to comment.