mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
bf426f9b1d
For [`AsyncIterable`](https://github.com/facebook/react/pull/28847) we encode `AsyncIterator` as a separate tag. Previously we encoded `Iterator` as just an Array. This adds a special encoding for this. Technically this is a breaking change. This is kind of an edge case that you'd care about the difference but it becomes more important to treat these correctly for the warnings here #28853.
876 lines
28 KiB
JavaScript
876 lines
28 KiB
JavaScript
/**
|
|
* 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
|
|
*/
|
|
|
|
import type {
|
|
Thenable,
|
|
PendingThenable,
|
|
FulfilledThenable,
|
|
RejectedThenable,
|
|
ReactCustomFormAction,
|
|
} from 'shared/ReactTypes';
|
|
import type {LazyComponent} from 'react/src/ReactLazy';
|
|
import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';
|
|
|
|
import {
|
|
enableRenderableContext,
|
|
enableBinaryFlight,
|
|
} from 'shared/ReactFeatureFlags';
|
|
|
|
import {
|
|
REACT_ELEMENT_TYPE,
|
|
REACT_LAZY_TYPE,
|
|
REACT_CONTEXT_TYPE,
|
|
REACT_PROVIDER_TYPE,
|
|
getIteratorFn,
|
|
} from 'shared/ReactSymbols';
|
|
|
|
import {
|
|
describeObjectForErrorMessage,
|
|
isSimpleObject,
|
|
objectName,
|
|
} from 'shared/ReactSerializationErrors';
|
|
|
|
import {writeTemporaryReference} from './ReactFlightTemporaryReferences';
|
|
|
|
import isArray from 'shared/isArray';
|
|
import getPrototypeOf from 'shared/getPrototypeOf';
|
|
|
|
const ObjectPrototype = Object.prototype;
|
|
|
|
import {usedWithSSR} from './ReactFlightClientConfig';
|
|
|
|
type ReactJSONValue =
|
|
| string
|
|
| boolean
|
|
| number
|
|
| null
|
|
| $ReadOnlyArray<ReactJSONValue>
|
|
| ReactServerObject;
|
|
|
|
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<
|
|
Function,
|
|
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
|
|
> = new WeakMap();
|
|
|
|
// Serializable values
|
|
export type ReactServerValue =
|
|
// References are passed by their value
|
|
| ServerReference<any>
|
|
// The rest are passed as is. Sub-types can be passed in but lose their
|
|
// subtype, so the receiver can only accept once of these.
|
|
| string
|
|
| boolean
|
|
| number
|
|
| null
|
|
| void
|
|
| bigint
|
|
| $AsyncIterable<ReactServerValue, ReactServerValue, void>
|
|
| $AsyncIterator<ReactServerValue, ReactServerValue, void>
|
|
| Iterable<ReactServerValue>
|
|
| Iterator<ReactServerValue>
|
|
| Array<ReactServerValue>
|
|
| Map<ReactServerValue, ReactServerValue>
|
|
| Set<ReactServerValue>
|
|
| FormData
|
|
| Date
|
|
| ReactServerObject
|
|
| Promise<ReactServerValue>; // Thenable<ReactServerValue>
|
|
|
|
type ReactServerObject = {+[key: string]: ReactServerValue};
|
|
|
|
function serializeByValueID(id: number): string {
|
|
return '$' + id.toString(16);
|
|
}
|
|
|
|
function serializePromiseID(id: number): string {
|
|
return '$@' + id.toString(16);
|
|
}
|
|
|
|
function serializeServerReferenceID(id: number): string {
|
|
return '$F' + id.toString(16);
|
|
}
|
|
|
|
function serializeTemporaryReferenceID(id: number): string {
|
|
return '$T' + id.toString(16);
|
|
}
|
|
|
|
function serializeFormDataReference(id: number): string {
|
|
// Why K? F is "Function". D is "Date". What else?
|
|
return '$K' + id.toString(16);
|
|
}
|
|
|
|
function serializeNumber(number: number): string | number {
|
|
if (Number.isFinite(number)) {
|
|
if (number === 0 && 1 / number === -Infinity) {
|
|
return '$-0';
|
|
} else {
|
|
return number;
|
|
}
|
|
} else {
|
|
if (number === Infinity) {
|
|
return '$Infinity';
|
|
} else if (number === -Infinity) {
|
|
return '$-Infinity';
|
|
} else {
|
|
return '$NaN';
|
|
}
|
|
}
|
|
}
|
|
|
|
function serializeUndefined(): string {
|
|
return '$undefined';
|
|
}
|
|
|
|
function serializeDateFromDateJSON(dateJSON: string): string {
|
|
// JSON.stringify automatically calls Date.prototype.toJSON which calls toISOString.
|
|
// We need only tack on a $D prefix.
|
|
return '$D' + dateJSON;
|
|
}
|
|
|
|
function serializeBigInt(n: bigint): string {
|
|
return '$n' + n.toString(10);
|
|
}
|
|
|
|
function serializeMapID(id: number): string {
|
|
return '$Q' + id.toString(16);
|
|
}
|
|
|
|
function serializeSetID(id: number): string {
|
|
return '$W' + id.toString(16);
|
|
}
|
|
|
|
function serializeBlobID(id: number): string {
|
|
return '$B' + id.toString(16);
|
|
}
|
|
|
|
function serializeIteratorID(id: number): string {
|
|
return '$i' + id.toString(16);
|
|
}
|
|
|
|
function escapeStringValue(value: string): string {
|
|
if (value[0] === '$') {
|
|
// We need to escape $ prefixed strings since we use those to encode
|
|
// references to IDs and as special symbol values.
|
|
return '$' + value;
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
export function processReply(
|
|
root: ReactServerValue,
|
|
formFieldPrefix: string,
|
|
temporaryReferences: void | TemporaryReferenceSet,
|
|
resolve: (string | FormData) => void,
|
|
reject: (error: mixed) => void,
|
|
): void {
|
|
let nextPartId = 1;
|
|
let pendingParts = 0;
|
|
let formData: null | FormData = null;
|
|
|
|
function serializeTypedArray(
|
|
tag: string,
|
|
typedArray: ArrayBuffer | $ArrayBufferView,
|
|
): string {
|
|
const blob = new Blob([typedArray]);
|
|
const blobId = nextPartId++;
|
|
if (formData === null) {
|
|
formData = new FormData();
|
|
}
|
|
formData.append(formFieldPrefix + blobId, blob);
|
|
return '$' + tag + blobId.toString(16);
|
|
}
|
|
|
|
function resolveToJSON(
|
|
this:
|
|
| {+[key: string | number]: ReactServerValue}
|
|
| $ReadOnlyArray<ReactServerValue>,
|
|
key: string,
|
|
value: ReactServerValue,
|
|
): ReactJSONValue {
|
|
const parent = this;
|
|
|
|
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
|
|
if (__DEV__) {
|
|
// $FlowFixMe[incompatible-use]
|
|
const originalValue = parent[key];
|
|
if (
|
|
typeof originalValue === 'object' &&
|
|
originalValue !== value &&
|
|
!(originalValue instanceof Date)
|
|
) {
|
|
if (objectName(originalValue) !== 'Object') {
|
|
console.error(
|
|
'Only plain objects can be passed to Server Functions from the Client. ' +
|
|
'%s objects are not supported.%s',
|
|
objectName(originalValue),
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
} else {
|
|
console.error(
|
|
'Only plain objects can be passed to Server Functions from the Client. ' +
|
|
'Objects with toJSON methods are not supported. Convert it manually ' +
|
|
'to a simple value before passing it to props.%s',
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (value === null) {
|
|
return null;
|
|
}
|
|
|
|
if (typeof value === 'object') {
|
|
switch ((value: any).$$typeof) {
|
|
case REACT_ELEMENT_TYPE: {
|
|
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: {
|
|
// Resolve lazy as if it wasn't here. In the future this will be encoded as a Promise.
|
|
const lazy: LazyComponent<any, any> = (value: any);
|
|
const payload = lazy._payload;
|
|
const init = lazy._init;
|
|
if (formData === null) {
|
|
// Upgrade to use FormData to allow us to stream this value.
|
|
formData = new FormData();
|
|
}
|
|
pendingParts++;
|
|
try {
|
|
const resolvedModel = init(payload);
|
|
// We always outline this as a separate part even though we could inline it
|
|
// because it ensures a more deterministic encoding.
|
|
const lazyId = nextPartId++;
|
|
const partJSON = JSON.stringify(resolvedModel, resolveToJSON);
|
|
// $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
|
|
const data: FormData = formData;
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
data.append(formFieldPrefix + lazyId, partJSON);
|
|
return serializeByValueID(lazyId);
|
|
} catch (x) {
|
|
if (
|
|
typeof x === 'object' &&
|
|
x !== null &&
|
|
typeof x.then === 'function'
|
|
) {
|
|
// Suspended
|
|
pendingParts++;
|
|
const lazyId = nextPartId++;
|
|
const thenable: Thenable<any> = (x: any);
|
|
const retry = function () {
|
|
// While the first promise resolved, its value isn't necessarily what we'll
|
|
// resolve into because we might suspend again.
|
|
try {
|
|
const partJSON = JSON.stringify(value, resolveToJSON);
|
|
// $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
|
|
const data: FormData = formData;
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
data.append(formFieldPrefix + lazyId, partJSON);
|
|
pendingParts--;
|
|
if (pendingParts === 0) {
|
|
resolve(data);
|
|
}
|
|
} catch (reason) {
|
|
reject(reason);
|
|
}
|
|
};
|
|
thenable.then(retry, retry);
|
|
return serializeByValueID(lazyId);
|
|
} else {
|
|
// In the future we could consider serializing this as an error
|
|
// that throws on the server instead.
|
|
reject(x);
|
|
return null;
|
|
}
|
|
} finally {
|
|
pendingParts--;
|
|
}
|
|
}
|
|
}
|
|
|
|
// $FlowFixMe[method-unbinding]
|
|
if (typeof value.then === 'function') {
|
|
// We assume that any object with a .then property is a "Thenable" type,
|
|
// or a Promise type. Either of which can be represented by a Promise.
|
|
if (formData === null) {
|
|
// Upgrade to use FormData to allow us to stream this value.
|
|
formData = new FormData();
|
|
}
|
|
pendingParts++;
|
|
const promiseId = nextPartId++;
|
|
const thenable: Thenable<any> = (value: any);
|
|
thenable.then(
|
|
partValue => {
|
|
try {
|
|
const partJSON = JSON.stringify(partValue, resolveToJSON);
|
|
// $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
|
|
const data: FormData = formData;
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
data.append(formFieldPrefix + promiseId, partJSON);
|
|
pendingParts--;
|
|
if (pendingParts === 0) {
|
|
resolve(data);
|
|
}
|
|
} catch (reason) {
|
|
reject(reason);
|
|
}
|
|
},
|
|
reason => {
|
|
// In the future we could consider serializing this as an error
|
|
// that throws on the server instead.
|
|
reject(reason);
|
|
},
|
|
);
|
|
return serializePromiseID(promiseId);
|
|
}
|
|
if (isArray(value)) {
|
|
// $FlowFixMe[incompatible-return]
|
|
return value;
|
|
}
|
|
// TODO: Should we the Object.prototype.toString.call() to test for cross-realm objects?
|
|
if (value instanceof FormData) {
|
|
if (formData === null) {
|
|
// Upgrade to use FormData to allow us to use rich objects as its values.
|
|
formData = new FormData();
|
|
}
|
|
const data: FormData = formData;
|
|
const refId = nextPartId++;
|
|
// Copy all the form fields with a prefix for this reference.
|
|
// These must come first in the form order because we assume that all the
|
|
// fields are available before this is referenced.
|
|
const prefix = formFieldPrefix + refId + '_';
|
|
// $FlowFixMe[prop-missing]: FormData has forEach.
|
|
value.forEach((originalValue: string | File, originalKey: string) => {
|
|
data.append(prefix + originalKey, originalValue);
|
|
});
|
|
return serializeFormDataReference(refId);
|
|
}
|
|
if (value instanceof Map) {
|
|
const partJSON = JSON.stringify(Array.from(value), resolveToJSON);
|
|
if (formData === null) {
|
|
formData = new FormData();
|
|
}
|
|
const mapId = nextPartId++;
|
|
formData.append(formFieldPrefix + mapId, partJSON);
|
|
return serializeMapID(mapId);
|
|
}
|
|
if (value instanceof Set) {
|
|
const partJSON = JSON.stringify(Array.from(value), resolveToJSON);
|
|
if (formData === null) {
|
|
formData = new FormData();
|
|
}
|
|
const setId = nextPartId++;
|
|
formData.append(formFieldPrefix + setId, partJSON);
|
|
return serializeSetID(setId);
|
|
}
|
|
|
|
if (enableBinaryFlight) {
|
|
if (value instanceof ArrayBuffer) {
|
|
return serializeTypedArray('A', value);
|
|
}
|
|
if (value instanceof Int8Array) {
|
|
// char
|
|
return serializeTypedArray('O', value);
|
|
}
|
|
if (value instanceof Uint8Array) {
|
|
// unsigned char
|
|
return serializeTypedArray('o', value);
|
|
}
|
|
if (value instanceof Uint8ClampedArray) {
|
|
// unsigned clamped char
|
|
return serializeTypedArray('U', value);
|
|
}
|
|
if (value instanceof Int16Array) {
|
|
// sort
|
|
return serializeTypedArray('S', value);
|
|
}
|
|
if (value instanceof Uint16Array) {
|
|
// unsigned short
|
|
return serializeTypedArray('s', value);
|
|
}
|
|
if (value instanceof Int32Array) {
|
|
// long
|
|
return serializeTypedArray('L', value);
|
|
}
|
|
if (value instanceof Uint32Array) {
|
|
// unsigned long
|
|
return serializeTypedArray('l', value);
|
|
}
|
|
if (value instanceof Float32Array) {
|
|
// float
|
|
return serializeTypedArray('G', value);
|
|
}
|
|
if (value instanceof Float64Array) {
|
|
// double
|
|
return serializeTypedArray('g', value);
|
|
}
|
|
if (value instanceof BigInt64Array) {
|
|
// number
|
|
return serializeTypedArray('M', value);
|
|
}
|
|
if (value instanceof BigUint64Array) {
|
|
// unsigned number
|
|
// We use "m" instead of "n" since JSON can start with "null"
|
|
return serializeTypedArray('m', value);
|
|
}
|
|
if (value instanceof DataView) {
|
|
return serializeTypedArray('V', value);
|
|
}
|
|
// TODO: Blob is not available in old Node/browsers. Remove the typeof check later.
|
|
if (typeof Blob === 'function' && value instanceof Blob) {
|
|
if (formData === null) {
|
|
formData = new FormData();
|
|
}
|
|
const blobId = nextPartId++;
|
|
formData.append(formFieldPrefix + blobId, value);
|
|
return serializeBlobID(blobId);
|
|
}
|
|
}
|
|
|
|
const iteratorFn = getIteratorFn(value);
|
|
if (iteratorFn) {
|
|
const iterator = iteratorFn.call(value);
|
|
if (iterator === value) {
|
|
// Iterator, not Iterable
|
|
const partJSON = JSON.stringify(
|
|
Array.from((iterator: any)),
|
|
resolveToJSON,
|
|
);
|
|
if (formData === null) {
|
|
formData = new FormData();
|
|
}
|
|
const iteratorId = nextPartId++;
|
|
formData.append(formFieldPrefix + iteratorId, partJSON);
|
|
return serializeIteratorID(iteratorId);
|
|
}
|
|
return Array.from((iterator: any));
|
|
}
|
|
|
|
// Verify that this is a simple plain object.
|
|
const proto = getPrototypeOf(value);
|
|
if (
|
|
proto !== ObjectPrototype &&
|
|
(proto === null || getPrototypeOf(proto) !== null)
|
|
) {
|
|
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__) {
|
|
if (
|
|
(value: any).$$typeof ===
|
|
(enableRenderableContext ? REACT_CONTEXT_TYPE : REACT_PROVIDER_TYPE)
|
|
) {
|
|
console.error(
|
|
'React Context Providers cannot be passed to Server Functions from the Client.%s',
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
} else if (objectName(value) !== 'Object') {
|
|
console.error(
|
|
'Only plain objects can be passed to Server Functions from the Client. ' +
|
|
'%s objects are not supported.%s',
|
|
objectName(value),
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
} else if (!isSimpleObject(value)) {
|
|
console.error(
|
|
'Only plain objects can be passed to Server Functions from the Client. ' +
|
|
'Classes or other objects with methods are not supported.%s',
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
} else if (Object.getOwnPropertySymbols) {
|
|
const symbols = Object.getOwnPropertySymbols(value);
|
|
if (symbols.length > 0) {
|
|
console.error(
|
|
'Only plain objects can be passed to Server Functions from the Client. ' +
|
|
'Objects with symbol properties like %s are not supported.%s',
|
|
symbols[0].description,
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// $FlowFixMe[incompatible-return]
|
|
return value;
|
|
}
|
|
|
|
if (typeof value === 'string') {
|
|
// TODO: Maybe too clever. If we support URL there's no similar trick.
|
|
if (value[value.length - 1] === 'Z') {
|
|
// Possibly a Date, whose toJSON automatically calls toISOString
|
|
// $FlowFixMe[incompatible-use]
|
|
const originalValue = parent[key];
|
|
if (originalValue instanceof Date) {
|
|
return serializeDateFromDateJSON(value);
|
|
}
|
|
}
|
|
|
|
return escapeStringValue(value);
|
|
}
|
|
|
|
if (typeof value === 'boolean') {
|
|
return value;
|
|
}
|
|
|
|
if (typeof value === 'number') {
|
|
return serializeNumber(value);
|
|
}
|
|
|
|
if (typeof value === 'undefined') {
|
|
return serializeUndefined();
|
|
}
|
|
|
|
if (typeof value === 'function') {
|
|
const metaData = knownServerReferences.get(value);
|
|
if (metaData !== undefined) {
|
|
const metaDataJSON = JSON.stringify(metaData, resolveToJSON);
|
|
if (formData === null) {
|
|
// Upgrade to use FormData to allow us to stream this value.
|
|
formData = new FormData();
|
|
}
|
|
// The reference to this function came from the same client so we can pass it back.
|
|
const refId = nextPartId++;
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
formData.set(formFieldPrefix + refId, metaDataJSON);
|
|
return serializeServerReferenceID(refId);
|
|
}
|
|
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),
|
|
);
|
|
}
|
|
|
|
if (typeof value === 'symbol') {
|
|
if (temporaryReferences === undefined) {
|
|
throw new Error(
|
|
'Symbols cannot be passed to a Server Function without a ' +
|
|
'temporary reference set. Pass a TemporaryReferenceSet to the options.' +
|
|
(__DEV__ ? describeObjectForErrorMessage(parent, key) : ''),
|
|
);
|
|
}
|
|
return serializeTemporaryReferenceID(
|
|
writeTemporaryReference(temporaryReferences, value),
|
|
);
|
|
}
|
|
|
|
if (typeof value === 'bigint') {
|
|
return serializeBigInt(value);
|
|
}
|
|
|
|
throw new Error(
|
|
`Type ${typeof value} is not supported as an argument to a Server Function.`,
|
|
);
|
|
}
|
|
|
|
// $FlowFixMe[incompatible-type] it's not going to be undefined because we'll encode it.
|
|
const json: string = JSON.stringify(root, resolveToJSON);
|
|
if (formData === null) {
|
|
// If it's a simple data structure, we just use plain JSON.
|
|
resolve(json);
|
|
} else {
|
|
// Otherwise, we use FormData to let us stream in the result.
|
|
formData.set(formFieldPrefix + '0', json);
|
|
if (pendingParts === 0) {
|
|
// $FlowFixMe[incompatible-call] this has already been refined.
|
|
resolve(formData);
|
|
}
|
|
}
|
|
}
|
|
|
|
const boundCache: WeakMap<
|
|
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
|
|
Thenable<FormData>,
|
|
> = new WeakMap();
|
|
|
|
function encodeFormData(reference: any): Thenable<FormData> {
|
|
let resolve, reject;
|
|
// We need to have a handle on the thenable so that we can synchronously set
|
|
// its status from processReply, when it can complete synchronously.
|
|
const thenable: Thenable<FormData> = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
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();
|
|
data.append('0', body);
|
|
body = data;
|
|
}
|
|
const fulfilled: FulfilledThenable<FormData> = (thenable: any);
|
|
fulfilled.status = 'fulfilled';
|
|
fulfilled.value = body;
|
|
resolve(body);
|
|
},
|
|
e => {
|
|
const rejected: RejectedThenable<FormData> = (thenable: any);
|
|
rejected.status = 'rejected';
|
|
rejected.reason = e;
|
|
reject(e);
|
|
},
|
|
);
|
|
return thenable;
|
|
}
|
|
|
|
function defaultEncodeFormAction(
|
|
this: any => Promise<any>,
|
|
identifierPrefix: string,
|
|
): ReactCustomFormAction {
|
|
const reference = knownServerReferences.get(this);
|
|
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 data: null | FormData = null;
|
|
let name;
|
|
const boundPromise = reference.bound;
|
|
if (boundPromise !== null) {
|
|
let thenable = boundCache.get(reference);
|
|
if (!thenable) {
|
|
thenable = encodeFormData(reference);
|
|
boundCache.set(reference, thenable);
|
|
}
|
|
if (thenable.status === 'rejected') {
|
|
throw thenable.reason;
|
|
} else if (thenable.status !== 'fulfilled') {
|
|
throw thenable;
|
|
}
|
|
const encodedFormData = thenable.value;
|
|
// This is hacky but we need the identifier prefix to be added to
|
|
// all fields but the suspense cache would break since we might get
|
|
// a new identifier each time. So we just append it at the end instead.
|
|
const prefixedData = new FormData();
|
|
// $FlowFixMe[prop-missing]
|
|
encodedFormData.forEach((value: string | File, key: string) => {
|
|
prefixedData.append('$ACTION_' + identifierPrefix + ':' + key, value);
|
|
});
|
|
data = prefixedData;
|
|
// We encode the name of the prefix containing the data.
|
|
name = '$ACTION_REF_' + identifierPrefix;
|
|
} else {
|
|
// This is the simple case so we can just encode the ID.
|
|
name = '$ACTION_ID_' + reference.id;
|
|
}
|
|
return {
|
|
name: name,
|
|
method: 'POST',
|
|
encType: 'multipart/form-data',
|
|
data: data,
|
|
};
|
|
}
|
|
|
|
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,
|
|
numberOfBoundArgs: number,
|
|
): boolean {
|
|
const reference = knownServerReferences.get(this);
|
|
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.',
|
|
);
|
|
}
|
|
if (reference.id !== referenceId) {
|
|
// These are different functions.
|
|
return false;
|
|
}
|
|
// Now check if the number of bound arguments is the same.
|
|
const boundPromise = reference.bound;
|
|
if (boundPromise === null) {
|
|
// No bound arguments.
|
|
return numberOfBoundArgs === 0;
|
|
}
|
|
// Unwrap the bound arguments array by suspending, if necessary. As with
|
|
// encodeFormData, this means isSignatureEqual can only be called while React
|
|
// is rendering.
|
|
switch (boundPromise.status) {
|
|
case 'fulfilled': {
|
|
const boundArgs = boundPromise.value;
|
|
return boundArgs.length === numberOfBoundArgs;
|
|
}
|
|
case 'pending': {
|
|
throw boundPromise;
|
|
}
|
|
case 'rejected': {
|
|
throw boundPromise.reason;
|
|
}
|
|
default: {
|
|
if (typeof boundPromise.status === 'string') {
|
|
// Only instrument the thenable if the status if not defined.
|
|
} else {
|
|
const pendingThenable: PendingThenable<Array<any>> =
|
|
(boundPromise: any);
|
|
pendingThenable.status = 'pending';
|
|
pendingThenable.then(
|
|
(boundArgs: Array<any>) => {
|
|
const fulfilledThenable: FulfilledThenable<Array<any>> =
|
|
(boundPromise: any);
|
|
fulfilledThenable.status = 'fulfilled';
|
|
fulfilledThenable.value = boundArgs;
|
|
},
|
|
(error: mixed) => {
|
|
const rejectedThenable: RejectedThenable<number> =
|
|
(boundPromise: any);
|
|
rejectedThenable.status = 'rejected';
|
|
rejectedThenable.reason = error;
|
|
},
|
|
);
|
|
}
|
|
throw boundPromise;
|
|
}
|
|
}
|
|
}
|
|
|
|
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: $$FORM_ACTION},
|
|
$$IS_SIGNATURE_EQUAL: {value: isSignatureEqual},
|
|
bind: {value: bind},
|
|
});
|
|
}
|
|
knownServerReferences.set(proxy, reference);
|
|
}
|
|
|
|
// $FlowFixMe[method-unbinding]
|
|
const FunctionBind = Function.prototype.bind;
|
|
// $FlowFixMe[method-unbinding]
|
|
const ArraySlice = Array.prototype.slice;
|
|
function bind(this: Function): Function {
|
|
// $FlowFixMe[unsupported-syntax]
|
|
const newFn = FunctionBind.apply(this, arguments);
|
|
const reference = knownServerReferences.get(this);
|
|
if (reference) {
|
|
if (__DEV__) {
|
|
const thisBind = arguments[0];
|
|
if (thisBind != null) {
|
|
// This doesn't warn in browser environments since it's not instrumented outside
|
|
// usedWithSSR. This makes this an SSR only warning which we don't generally do.
|
|
// TODO: Consider a DEV only instrumentation in the browser.
|
|
console.error(
|
|
'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().',
|
|
);
|
|
}
|
|
}
|
|
const args = ArraySlice.call(arguments, 1);
|
|
let boundPromise = null;
|
|
if (reference.bound !== null) {
|
|
boundPromise = Promise.resolve((reference.bound: any)).then(boundArgs =>
|
|
boundArgs.concat(args),
|
|
);
|
|
} else {
|
|
boundPromise = Promise.resolve(args);
|
|
}
|
|
// 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}, encodeFormAction);
|
|
return proxy;
|
|
}
|