Skip to content

Commit

Permalink
Enable passing Server References from Server to Client
Browse files Browse the repository at this point in the history
As well as a callback for invoking them.
  • Loading branch information
sebmarkbage committed Feb 8, 2023
1 parent 758fc7f commit 9b442d0
Show file tree
Hide file tree
Showing 16 changed files with 408 additions and 70 deletions.
60 changes: 59 additions & 1 deletion packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';

import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';

export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;

export type JSONValue =
| number
| null
Expand Down Expand Up @@ -148,6 +150,7 @@ Chunk.prototype.then = function <T>(

export type ResponseBase = {
_bundlerConfig: BundlerConfig,
_callServer: CallServerCallback,
_chunks: Map<number, SomeChunk<any>>,
...
};
Expand Down Expand Up @@ -469,6 +472,28 @@ function createModelReject<T>(chunk: SomeChunk<T>) {
return (error: mixed) => triggerErrorOnChunk(chunk, error);
}

function createServerReferenceProxy<A: Iterable<any>, T>(
response: Response,
metaData: any,
): (...A) => Promise<T> {
const callServer = response._callServer;
const proxy = function (): Promise<T> {
// $FlowFixMe[method-unbinding]
const args = Array.prototype.slice.call(arguments);
const p = metaData.bound;
if (p.status === INITIALIZED) {
const bound = p.value;
return callServer(metaData, bound.concat(args));
}
// Since this is a fake Promise whose .then doesn't chain, we have to wrap it.
// TODO: Remove the wrapper once that's fixed.
return Promise.resolve(p).then(function (bound) {
return callServer(metaData, bound.concat(args));
});
};
return proxy;
}

export function parseModelString(
response: Response,
parentObject: Object,
Expand Down Expand Up @@ -500,11 +525,33 @@ export function parseModelString(
return chunk;
}
case 'S': {
// Symbol
return Symbol.for(value.substring(2));
}
case 'P': {
// Server Context Provider
return getOrCreateServerContext(value.substring(2)).Provider;
}
case 'F': {
// Server Reference
const id = parseInt(value.substring(2), 16);
const chunk = getChunk(response, id);
switch (chunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(chunk);
break;
}
// The status might have changed after initialization.
switch (chunk.status) {
case INITIALIZED: {
const metadata = chunk.value;
return createServerReferenceProxy(response, metadata);
}
// We always encode it first in the stream so it won't be pending.
default:
throw chunk.reason;
}
}
default: {
// We assume that anything else is a reference ID.
const id = parseInt(value.substring(1), 16);
Expand Down Expand Up @@ -552,10 +599,21 @@ export function parseModelTuple(
return value;
}

export function createResponse(bundlerConfig: BundlerConfig): ResponseBase {
function missingCall() {
throw new Error(
'Trying to call a function from "use server" but the callServer option ' +
'was not implemented in your router runtime.',
);
}

export function createResponse(
bundlerConfig: BundlerConfig,
callServer: void | CallServerCallback,
): ResponseBase {
const chunks: Map<number, SomeChunk<any>> = new Map();
const response = {
_bundlerConfig: bundlerConfig,
_callServer: callServer !== undefined ? callServer : missingCall,
_chunks: chunks,
};
return response;
Expand Down
9 changes: 6 additions & 3 deletions packages/react-client/src/ReactFlightClientStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
* @flow
*/

import type {CallServerCallback} from './ReactFlightClient';
import type {Response} from './ReactFlightClientHostConfigStream';

import type {BundlerConfig} from './ReactFlightClientHostConfig';

import {
Expand Down Expand Up @@ -120,11 +120,14 @@ function createFromJSONCallback(response: Response) {
};
}

export function createResponse(bundlerConfig: BundlerConfig): Response {
export function createResponse(
bundlerConfig: BundlerConfig,
callServer: void | CallServerCallback,
): Response {
// NOTE: CHECK THE COMPILER OUTPUT EACH TIME YOU CHANGE THIS.
// It should be inlined to one object literal but minor changes can break it.
const stringDecoder = supportsBinaryStreams ? createStringDecoder() : null;
const response: any = createResponseBase(bundlerConfig);
const response: any = createResponseBase(bundlerConfig, callServer);
response._partialRow = '';
if (supportsBinaryStreams) {
response._stringDecoder = stringDecoder;
Expand Down
10 changes: 8 additions & 2 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,10 @@ describe('ReactFlight', () => {
<ErrorBoundary expectedMessage="Event handlers cannot be passed to Client Component props.">
<Render promise={ReactNoopFlightClient.read(event)} />
</ErrorBoundary>
<ErrorBoundary expectedMessage="Functions cannot be passed directly to Client Components because they're not serializable.">
<ErrorBoundary
expectedMessage={
'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
}>
<Render promise={ReactNoopFlightClient.read(fn)} />
</ErrorBoundary>
<ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to Client Components.">
Expand All @@ -459,7 +462,10 @@ describe('ReactFlight', () => {
<ErrorBoundary expectedMessage="Event handlers cannot be passed to Client Component props.">
<Render promise={ReactNoopFlightClient.read(eventClient)} />
</ErrorBoundary>
<ErrorBoundary expectedMessage="Functions cannot be passed directly to Client Components because they're not serializable.">
<ErrorBoundary
expectedMessage={
'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
}>
<Render promise={ReactNoopFlightClient.read(fnClient)} />
</ErrorBoundary>
<ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to Client Components.">
Expand Down
3 changes: 3 additions & 0 deletions packages/react-noop-renderer/src/ReactNoopFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ const ReactNoopFlightServer = ReactFlightServer({
isClientReference(reference: Object): boolean {
return reference.$$typeof === Symbol.for('react.client.reference');
},
isServerReference(reference: Object): boolean {
return reference.$$typeof === Symbol.for('react.server.reference');
},
getClientReferenceKey(reference: Object): Object {
return reference;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import hasOwnProperty from 'shared/hasOwnProperty';
import isArray from 'shared/isArray';

export type ClientReference<T> = JSResourceReference<T>;
export type ServerReference<T> = T;
export type ServerReferenceMetaData = {};

import type {
Destination,
Expand All @@ -43,6 +45,10 @@ export function isClientReference(reference: Object): boolean {
return reference instanceof JSResourceReferenceImpl;
}

export function isServerReference(reference: Object): boolean {
return false;
}

export type ClientReferenceKey = ClientReference<any>;

export function getClientReferenceKey(
Expand All @@ -60,6 +66,13 @@ export function resolveModuleMetaData<T>(
return resolveModuleMetaDataImpl(config, resource);
}

export function resolveServerReferenceMetaData<T>(
config: BundlerConfig,
resource: ServerReference<T>,
): ServerReferenceMetaData {
throw new Error('Not implemented.');
}

export type Chunk = RowEncoding;

export function processErrorChunkProd(
Expand Down
9 changes: 9 additions & 0 deletions packages/react-server-dom-webpack/src/ReactFlightDOMClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@ import {
close,
} from 'react-client/src/ReactFlightClientStream';

type CallServerCallback = <A, T>(
{filepath: string, name: string},
args: A,
) => Promise<T>;

export type Options = {
moduleMap?: BundlerConfig,
callServer?: CallServerCallback,
};

function startReadingFromStream(
Expand Down Expand Up @@ -52,6 +58,7 @@ function createFromReadableStream<T>(
): Thenable<T> {
const response: FlightResponse = createResponse(
options && options.moduleMap ? options.moduleMap : null,
options && options.callServer ? options.callServer : undefined,
);
startReadingFromStream(response, stream);
return getRoot(response);
Expand All @@ -63,6 +70,7 @@ function createFromFetch<T>(
): Thenable<T> {
const response: FlightResponse = createResponse(
options && options.moduleMap ? options.moduleMap : null,
options && options.callServer ? options.callServer : undefined,
);
promiseForResponse.then(
function (r) {
Expand All @@ -81,6 +89,7 @@ function createFromXHR<T>(
): Thenable<T> {
const response: FlightResponse = createResponse(
options && options.moduleMap ? options.moduleMap : null,
options && options.callServer ? options.callServer : undefined,
);
let processedLength = 0;
function progress(e: ProgressEvent): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* @flow
*/

import type {ReactModel} from 'react-server/src/ReactFlightServer';

type WebpackMap = {
[filepath: string]: {
[name: string]: ModuleMetaData,
Expand All @@ -15,6 +17,19 @@ type WebpackMap = {

export type BundlerConfig = WebpackMap;

export type ServerReference<T: Function> = T & {
$$typeof: symbol,
$$filepath: string,
$$name: string,
$$bound: Array<ReactModel>,
};

export type ServerReferenceMetaData = {
id: string,
name: string,
bound: Promise<Array<ReactModel>>,
};

// eslint-disable-next-line no-unused-vars
export type ClientReference<T> = {
$$typeof: symbol,
Expand All @@ -33,6 +48,7 @@ export type ModuleMetaData = {
export type ClientReferenceKey = string;

const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference');
const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference');

export function getClientReferenceKey(
reference: ClientReference<any>,
Expand All @@ -49,6 +65,10 @@ export function isClientReference(reference: Object): boolean {
return reference.$$typeof === CLIENT_REFERENCE_TAG;
}

export function isServerReference(reference: Object): boolean {
return reference.$$typeof === SERVER_REFERENCE_TAG;
}

export function resolveModuleMetaData<T>(
config: BundlerConfig,
clientReference: ClientReference<T>,
Expand All @@ -66,3 +86,14 @@ export function resolveModuleMetaData<T>(
return resolvedModuleData;
}
}

export function resolveServerReferenceMetaData<T>(
config: BundlerConfig,
serverReference: ServerReference<T>,
): ServerReferenceMetaData {
return {
id: serverReference.$$filepath,
name: serverReference.$$name,
bound: Promise.resolve(serverReference.$$bound),
};
}
Loading

0 comments on commit 9b442d0

Please sign in to comment.