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 custom encoding of the form action #27563

Merged
merged 1 commit into from
Feb 13, 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
12 changes: 9 additions & 3 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ import type {
HintModel,
} from 'react-server/src/ReactFlightServerConfig';

import type {CallServerCallback} from './ReactFlightReplyClient';
import type {
CallServerCallback,
EncodeFormActionCallback,
} from './ReactFlightReplyClient';

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

Expand All @@ -53,7 +56,7 @@ import {
REACT_POSTPONE_TYPE,
} from 'shared/ReactSymbols';

export type {CallServerCallback};
export type {CallServerCallback, EncodeFormActionCallback};

type UninitializedModel = string;

Expand Down Expand Up @@ -206,6 +209,7 @@ export type Response = {
_bundlerConfig: SSRModuleMap,
_moduleLoading: ModuleLoading,
_callServer: CallServerCallback,
_encodeFormAction: void | EncodeFormActionCallback,
_nonce: ?string,
_chunks: Map<number, SomeChunk<any>>,
_fromJSON: (key: string, value: JSONValue) => any,
Expand Down Expand Up @@ -592,7 +596,7 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
},
);
};
registerServerReference(proxy, metaData);
registerServerReference(proxy, metaData, response._encodeFormAction);
return proxy;
}

Expand Down Expand Up @@ -785,13 +789,15 @@ export function createResponse(
bundlerConfig: SSRModuleMap,
moduleLoading: ModuleLoading,
callServer: void | CallServerCallback,
encodeFormAction: void | EncodeFormActionCallback,
nonce: void | string,
): Response {
const chunks: Map<number, SomeChunk<any>> = new Map();
const response: Response = {
_bundlerConfig: bundlerConfig,
_moduleLoading: moduleLoading,
_callServer: callServer !== undefined ? callServer : missingCall,
_encodeFormAction: encodeFormAction,
_nonce: nonce,
_chunks: chunks,
_stringDecoder: createStringDecoder(),
Expand Down
59 changes: 54 additions & 5 deletions packages/react-client/src/ReactFlightReplyClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export opaque type ServerReference<T> = T;

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

export type EncodeFormActionCallback = <A>(
id: any,
args: Promise<A>,
) => ReactCustomFormAction;

export type ServerReferenceId = any;

const knownServerReferences: WeakMap<
Expand Down Expand Up @@ -454,7 +459,7 @@ function encodeFormData(reference: any): Thenable<FormData> {
return thenable;
}

export function encodeFormAction(
function defaultEncodeFormAction(
this: any => Promise<any>,
identifierPrefix: string,
): ReactCustomFormAction {
Expand Down Expand Up @@ -503,6 +508,25 @@ export function encodeFormAction(
};
}

function customEncodeFormAction(
proxy: any => Promise<any>,
identifierPrefix: string,
encodeFormAction: EncodeFormActionCallback,
): ReactCustomFormAction {
const reference = knownServerReferences.get(proxy);
if (!reference) {
throw new Error(
'Tried to encode a Server Action from a different instance than the encoder is from. ' +
'This is a bug in React.',
);
}
let boundPromise: Promise<Array<any>> = (reference.bound: any);
if (boundPromise === null) {
boundPromise = Promise.resolve([]);
}
return encodeFormAction(reference.id, boundPromise);
}

function isSignatureEqual(
this: any => Promise<any>,
referenceId: ServerReferenceId,
Expand Down Expand Up @@ -569,13 +593,27 @@ function isSignatureEqual(
export function registerServerReference(
proxy: any,
reference: {id: ServerReferenceId, bound: null | Thenable<Array<any>>},
encodeFormAction: void | EncodeFormActionCallback,
) {
// Expose encoder for use by SSR, as well as a special bind that can be used to
// keep server capabilities.
if (usedWithSSR) {
// Only expose this in builds that would actually use it. Not needed on the client.
const $$FORM_ACTION =
encodeFormAction === undefined
? defaultEncodeFormAction
: function (
this: any => Promise<any>,
identifierPrefix: string,
): ReactCustomFormAction {
return customEncodeFormAction(
this,
identifierPrefix,
encodeFormAction,
);
};
Object.defineProperties((proxy: any), {
$$FORM_ACTION: {value: encodeFormAction},
$$FORM_ACTION: {value: $$FORM_ACTION},
$$IS_SIGNATURE_EQUAL: {value: isSignatureEqual},
bind: {value: bind},
});
Expand All @@ -587,7 +625,7 @@ export function registerServerReference(
const FunctionBind = Function.prototype.bind;
// $FlowFixMe[method-unbinding]
const ArraySlice = Array.prototype.slice;
function bind(this: Function) {
function bind(this: Function): Function {
// $FlowFixMe[unsupported-syntax]
const newFn = FunctionBind.apply(this, arguments);
const reference = knownServerReferences.get(this);
Expand All @@ -601,20 +639,31 @@ function bind(this: Function) {
} else {
boundPromise = Promise.resolve(args);
}
registerServerReference(newFn, {id: reference.id, bound: boundPromise});
// Expose encoder for use by SSR, as well as a special bind that can be used to
// keep server capabilities.
if (usedWithSSR) {
// Only expose this in builds that would actually use it. Not needed on the client.
Object.defineProperties((newFn: any), {
$$FORM_ACTION: {value: this.$$FORM_ACTION},
$$IS_SIGNATURE_EQUAL: {value: isSignatureEqual},
bind: {value: bind},
});
}
knownServerReferences.set(newFn, {id: reference.id, bound: boundPromise});
}
return newFn;
}

export function createServerReference<A: Iterable<any>, T>(
id: ServerReferenceId,
callServer: CallServerCallback,
encodeFormAction?: EncodeFormActionCallback,
): (...A) => Promise<T> {
const proxy = function (): Promise<T> {
// $FlowFixMe[method-unbinding]
const args = Array.prototype.slice.call(arguments);
return callServer(id, args);
};
registerServerReference(proxy, {id, bound: null});
registerServerReference(proxy, {id, bound: null}, encodeFormAction);
return proxy;
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function createResponseFromOptions(options: void | Options) {
options && options.moduleBaseURL ? options.moduleBaseURL : '',
null,
options && options.callServer ? options.callServer : undefined,
undefined, // encodeFormAction
undefined, // nonce
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @flow
*/

import type {Thenable} from 'shared/ReactTypes.js';
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js';

import type {Response} from 'react-client/src/ReactFlightClient';

Expand Down Expand Up @@ -38,8 +38,14 @@ export function createServerReference<A: Iterable<any>, T>(
return createServerReferenceImpl(id, noServerCall);
}

type EncodeFormActionCallback = <A>(
id: any,
args: Promise<A>,
) => ReactCustomFormAction;

export type Options = {
nonce?: string,
encodeFormAction?: EncodeFormActionCallback,
};

function createFromNodeStream<T>(
Expand All @@ -52,6 +58,7 @@ function createFromNodeStream<T>(
moduleRootPath,
moduleBaseURL,
noServerCall,
options ? options.encodeFormAction : undefined,
options && typeof options.nonce === 'string' ? options.nonce : undefined,
);
stream.on('data', chunk => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function createResponseFromOptions(options: void | Options) {
null,
null,
options && options.callServer ? options.callServer : undefined,
undefined, // encodeFormAction
undefined, // nonce
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @flow
*/

import type {Thenable} from 'shared/ReactTypes.js';
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js';

import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';

Expand Down Expand Up @@ -51,16 +51,23 @@ export function createServerReference<A: Iterable<any>, T>(
return createServerReferenceImpl(id, noServerCall);
}

type EncodeFormActionCallback = <A>(
id: any,
args: Promise<A>,
) => ReactCustomFormAction;

export type Options = {
ssrManifest: SSRManifest,
nonce?: string,
encodeFormAction?: EncodeFormActionCallback,
};

function createResponseFromOptions(options: Options) {
return createResponse(
options.ssrManifest.moduleMap,
options.ssrManifest.moduleLoading,
noServerCall,
options.encodeFormAction,
typeof options.nonce === 'string' ? options.nonce : undefined,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @flow
*/

import type {Thenable} from 'shared/ReactTypes.js';
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js';

import type {Response} from 'react-client/src/ReactFlightClient';

Expand Down Expand Up @@ -40,9 +40,6 @@ function noServerCall() {
'to pass data to Client Components instead.',
);
}
export type Options = {
nonce?: string,
};

export function createServerReference<A: Iterable<any>, T>(
id: any,
Expand All @@ -51,6 +48,16 @@ export function createServerReference<A: Iterable<any>, T>(
return createServerReferenceImpl(id, noServerCall);
}

type EncodeFormActionCallback = <A>(
id: any,
args: Promise<A>,
) => ReactCustomFormAction;

export type Options = {
nonce?: string,
encodeFormAction?: EncodeFormActionCallback,
};

function createFromNodeStream<T>(
stream: Readable,
ssrManifest: SSRManifest,
Expand All @@ -60,6 +67,7 @@ function createFromNodeStream<T>(
ssrManifest.moduleMap,
ssrManifest.moduleLoading,
noServerCall,
options ? options.encodeFormAction : undefined,
options && typeof options.nonce === 'string' ? options.nonce : undefined,
);
stream.on('data', chunk => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function createResponseFromOptions(options: void | Options) {
null,
null,
options && options.callServer ? options.callServer : undefined,
undefined, // encodeFormAction
undefined, // nonce
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @flow
*/

import type {Thenable} from 'shared/ReactTypes.js';
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js';

import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';

Expand Down Expand Up @@ -51,16 +51,23 @@ export function createServerReference<A: Iterable<any>, T>(
return createServerReferenceImpl(id, noServerCall);
}

type EncodeFormActionCallback = <A>(
id: any,
args: Promise<A>,
) => ReactCustomFormAction;

export type Options = {
ssrManifest: SSRManifest,
nonce?: string,
encodeFormAction?: EncodeFormActionCallback,
};

function createResponseFromOptions(options: Options) {
return createResponse(
options.ssrManifest.moduleMap,
options.ssrManifest.moduleLoading,
noServerCall,
options.encodeFormAction,
typeof options.nonce === 'string' ? options.nonce : undefined,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @flow
*/

import type {Thenable} from 'shared/ReactTypes.js';
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js';

import type {Response} from 'react-client/src/ReactFlightClient';

Expand Down Expand Up @@ -48,8 +48,14 @@ export function createServerReference<A: Iterable<any>, T>(
return createServerReferenceImpl(id, noServerCall);
}

type EncodeFormActionCallback = <A>(
id: any,
args: Promise<A>,
) => ReactCustomFormAction;

export type Options = {
nonce?: string,
encodeFormAction?: EncodeFormActionCallback,
};

function createFromNodeStream<T>(
Expand All @@ -61,6 +67,7 @@ function createFromNodeStream<T>(
ssrManifest.moduleMap,
ssrManifest.moduleLoading,
noServerCall,
options ? options.encodeFormAction : undefined,
options && typeof options.nonce === 'string' ? options.nonce : undefined,
);
stream.on('data', chunk => {
Expand Down