/** * Copyright (c) Facebook, Inc. and its 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 {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes'; import type { Destination, Chunk, BundlerConfig, ModuleMetaData, ModuleReference, ModuleKey, } from './ReactFlightServerConfig'; import { scheduleWork, beginWriting, writeChunk, completeWriting, flushBuffered, close, closeWithError, processModelChunk, processModuleChunk, processSymbolChunk, processErrorChunk, resolveModuleMetaData, getModuleKey, isModuleReference, } from './ReactFlightServerConfig'; import { REACT_ELEMENT_TYPE, REACT_FORWARD_REF_TYPE, REACT_FRAGMENT_TYPE, REACT_LAZY_TYPE, REACT_MEMO_TYPE, } from 'shared/ReactSymbols'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; const isArray = Array.isArray; type ReactJSONValue = | string | boolean | number | null | $ReadOnlyArray | ReactModelObject; export type ReactModel = | React$Element | string | boolean | number | null | Iterable | ReactModelObject; type ReactModelObject = {+[key: string]: ReactModel}; type Segment = { id: number, model: ReactModel, ping: () => void, }; export type Request = { destination: Destination, bundlerConfig: BundlerConfig, cache: Map, nextChunkId: number, pendingChunks: number, pingedSegments: Array, completedModuleChunks: Array, completedJSONChunks: Array, completedErrorChunks: Array, writtenSymbols: Map, writtenModules: Map, onError: (error: mixed) => void, flowing: boolean, toJSON: (key: string, value: ReactModel) => ReactJSONValue, }; const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; function defaultErrorHandler(error: mixed) { console['error'](error); // Don't transform to our wrapper } export function createRequest( model: ReactModel, destination: Destination, bundlerConfig: BundlerConfig, onError: (error: mixed) => void = defaultErrorHandler, ): Request { const pingedSegments = []; const request = { destination, bundlerConfig, cache: new Map(), nextChunkId: 0, pendingChunks: 0, pingedSegments: pingedSegments, completedModuleChunks: [], completedJSONChunks: [], completedErrorChunks: [], writtenSymbols: new Map(), writtenModules: new Map(), onError, flowing: false, toJSON: function(key: string, value: ReactModel): ReactJSONValue { return resolveModelToJSON(request, this, key, value); }, }; request.pendingChunks++; const rootSegment = createSegment(request, model); pingedSegments.push(rootSegment); return request; } function attemptResolveElement( type: any, key: null | React$Key, ref: mixed, props: any, ): ReactModel { if (ref !== null && ref !== undefined) { // When the ref moves to the regular props object this will implicitly // throw for functions. We could probably relax it to a DEV warning for other // cases. invariant( false, 'Refs cannot be used in server components, nor passed to client components.', ); } if (typeof type === 'function') { // This is a server-side component. return type(props); } else if (typeof type === 'string') { // This is a host element. E.g. HTML. return [REACT_ELEMENT_TYPE, type, key, props]; } else if (typeof type === 'symbol') { if (type === REACT_FRAGMENT_TYPE) { // For key-less fragments, we add a small optimization to avoid serializing // it as a wrapper. // TODO: If a key is specified, we should propagate its key to any children. // Same as if a server component has a key. return props.children; } // This might be a built-in React component. We'll let the client decide. // Any built-in works as long as its props are serializable. return [REACT_ELEMENT_TYPE, type, key, props]; } else if (type != null && typeof type === 'object') { if (isModuleReference(type)) { // This is a reference to a client component. return [REACT_ELEMENT_TYPE, type, key, props]; } switch (type.$$typeof) { case REACT_FORWARD_REF_TYPE: { const render = type.render; return render(props, undefined); } case REACT_MEMO_TYPE: { return attemptResolveElement(type.type, key, ref, props); } } } invariant( false, 'Unsupported server component type: %s', describeValueForErrorMessage(type), ); } function pingSegment(request: Request, segment: Segment): void { const pingedSegments = request.pingedSegments; pingedSegments.push(segment); if (pingedSegments.length === 1) { scheduleWork(() => performWork(request)); } } function createSegment(request: Request, model: ReactModel): Segment { const id = request.nextChunkId++; const segment = { id, model, ping: () => pingSegment(request, segment), }; return segment; } function serializeByValueID(id: number): string { return '$' + id.toString(16); } function serializeByRefID(id: number): string { return '@' + id.toString(16); } function escapeStringValue(value: string): string { if (value[0] === '$' || value[0] === '@') { // We need to escape $ or @ prefixed strings since we use those to encode // references to IDs and as special symbol values. return '$' + value; } else { return value; } } function isObjectPrototype(object): boolean { if (!object) { return false; } // $FlowFixMe const ObjectPrototype = Object.prototype; if (object === ObjectPrototype) { return true; } // It might be an object from a different Realm which is // still just a plain simple object. if (Object.getPrototypeOf(object)) { return false; } const names = Object.getOwnPropertyNames(object); for (let i = 0; i < names.length; i++) { if (!(names[i] in ObjectPrototype)) { return false; } } return true; } function isSimpleObject(object): boolean { if (!isObjectPrototype(Object.getPrototypeOf(object))) { return false; } const names = Object.getOwnPropertyNames(object); for (let i = 0; i < names.length; i++) { const descriptor = Object.getOwnPropertyDescriptor(object, names[i]); if (!descriptor) { return false; } if (!descriptor.enumerable) { if ( (names[i] === 'key' || names[i] === 'ref') && typeof descriptor.get === 'function' ) { // React adds key and ref getters to props objects to issue warnings. // Those getters will not be transferred to the client, but that's ok, // so we'll special case them. continue; } return false; } } return true; } function objectName(object): string { const name = Object.prototype.toString.call(object); return name.replace(/^\[object (.*)\]$/, function(m, p0) { return p0; }); } function describeKeyForErrorMessage(key: string): string { const encodedKey = JSON.stringify(key); return '"' + key + '"' === encodedKey ? key : encodedKey; } function describeValueForErrorMessage(value: ReactModel): string { switch (typeof value) { case 'string': { return JSON.stringify( value.length <= 10 ? value : value.substr(0, 10) + '...', ); } case 'object': { if (isArray(value)) { return '[...]'; } const name = objectName(value); if (name === 'Object') { return '{...}'; } return name; } case 'function': return 'function'; default: // eslint-disable-next-line return String(value); } } function describeObjectForErrorMessage( objectOrArray: | {+[key: string | number]: ReactModel} | $ReadOnlyArray, expandedName?: string, ): string { if (isArray(objectOrArray)) { let str = '['; // $FlowFixMe: Should be refined by now. const array: $ReadOnlyArray = objectOrArray; for (let i = 0; i < array.length; i++) { if (i > 0) { str += ', '; } if (i > 6) { str += '...'; break; } const value = array[i]; if ( '' + i === expandedName && typeof value === 'object' && value !== null ) { str += describeObjectForErrorMessage(value); } else { str += describeValueForErrorMessage(value); } } str += ']'; return str; } else { let str = '{'; // $FlowFixMe: Should be refined by now. const object: {+[key: string | number]: ReactModel} = objectOrArray; const names = Object.keys(object); for (let i = 0; i < names.length; i++) { if (i > 0) { str += ', '; } if (i > 6) { str += '...'; break; } const name = names[i]; str += describeKeyForErrorMessage(name) + ': '; const value = object[name]; if ( name === expandedName && typeof value === 'object' && value !== null ) { str += describeObjectForErrorMessage(value); } else { str += describeValueForErrorMessage(value); } } str += '}'; return str; } } export function resolveModelToJSON( request: Request, parent: {+[key: string | number]: ReactModel} | $ReadOnlyArray, key: string, value: ReactModel, ): ReactJSONValue { if (__DEV__) { // $FlowFixMe const originalValue = parent[key]; if (typeof originalValue === 'object' && originalValue !== value) { console.error( 'Only plain objects can be passed to client components from server components. ' + 'Objects with toJSON methods are not supported. Convert it manually ' + 'to a simple value before passing it to props. ' + 'Remove %s from these props: %s', describeKeyForErrorMessage(key), describeObjectForErrorMessage(parent), ); } } // Special Symbols switch (value) { case REACT_ELEMENT_TYPE: return '$'; case REACT_LAZY_TYPE: invariant( false, 'React Lazy Components are not yet supported on the server.', ); } // Resolve server components. while ( typeof value === 'object' && value !== null && value.$$typeof === REACT_ELEMENT_TYPE ) { // TODO: Concatenate keys of parents onto children. const element: React$Element = (value: any); try { // Attempt to render the server component. value = attemptResolveElement( element.type, element.key, element.ref, element.props, ); } catch (x) { if (typeof x === 'object' && x !== null && typeof x.then === 'function') { // Something suspended, we'll need to create a new segment and resolve it later. request.pendingChunks++; const newSegment = createSegment(request, value); const ping = newSegment.ping; x.then(ping, ping); return serializeByRefID(newSegment.id); } else { reportError(request, x); // Something errored. We'll still send everything we have up until this point. // We'll replace this element with a lazy reference that throws on the client // once it gets rendered. request.pendingChunks++; const errorId = request.nextChunkId++; emitErrorChunk(request, errorId, x); return serializeByRefID(errorId); } } } if (value === null) { return null; } if (typeof value === 'object') { if (isModuleReference(value)) { const moduleReference: ModuleReference = (value: any); const moduleKey: ModuleKey = getModuleKey(moduleReference); const writtenModules = request.writtenModules; const existingId = writtenModules.get(moduleKey); if (existingId !== undefined) { if (parent[0] === REACT_ELEMENT_TYPE && key === '1') { // If we're encoding the "type" of an element, we can refer // to that by a lazy reference instead of directly since React // knows how to deal with lazy values. This lets us suspend // on this component rather than its parent until the code has // loaded. return serializeByRefID(existingId); } return serializeByValueID(existingId); } try { const moduleMetaData: ModuleMetaData = resolveModuleMetaData( request.bundlerConfig, moduleReference, ); request.pendingChunks++; const moduleId = request.nextChunkId++; emitModuleChunk(request, moduleId, moduleMetaData); writtenModules.set(moduleKey, moduleId); if (parent[0] === REACT_ELEMENT_TYPE && key === '1') { // If we're encoding the "type" of an element, we can refer // to that by a lazy reference instead of directly since React // knows how to deal with lazy values. This lets us suspend // on this component rather than its parent until the code has // loaded. return serializeByRefID(moduleId); } return serializeByValueID(moduleId); } catch (x) { request.pendingChunks++; const errorId = request.nextChunkId++; emitErrorChunk(request, errorId, x); return serializeByValueID(errorId); } } if (__DEV__) { if (value !== null && !isArray(value)) { // Verify that this is a simple plain object. if (objectName(value) !== 'Object') { console.error( 'Only plain objects can be passed to client components from server components. ' + 'Built-ins like %s are not supported. ' + 'Remove %s from these props: %s', objectName(value), describeKeyForErrorMessage(key), describeObjectForErrorMessage(parent), ); } else if (!isSimpleObject(value)) { console.error( 'Only plain objects can be passed to client components from server components. ' + 'Classes or other objects with methods are not supported. ' + 'Remove %s from these props: %s', describeKeyForErrorMessage(key), 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 client components from server components. ' + 'Objects with symbol properties like %s are not supported. ' + 'Remove %s from these props: %s', symbols[0].description, describeKeyForErrorMessage(key), describeObjectForErrorMessage(parent, key), ); } } } } return value; } if (typeof value === 'string') { return escapeStringValue(value); } if ( typeof value === 'boolean' || typeof value === 'number' || typeof value === 'undefined' ) { return value; } if (typeof value === 'function') { if (/^on[A-Z]/.test(key)) { invariant( false, 'Event handlers cannot be passed to client component props. ' + 'Remove %s from these props if possible: %s\n' + 'If you need interactivity, consider converting part of this to a client component.', describeKeyForErrorMessage(key), describeObjectForErrorMessage(parent), ); } else { invariant( false, 'Functions cannot be passed directly to client components ' + "because they're not serializable. " + 'Remove %s (%s) from this object, or avoid the entire object: %s', describeKeyForErrorMessage(key), value.displayName || value.name || 'function', describeObjectForErrorMessage(parent), ); } } if (typeof value === 'symbol') { const writtenSymbols = request.writtenSymbols; const existingId = writtenSymbols.get(value); if (existingId !== undefined) { return serializeByValueID(existingId); } const name = value.description; invariant( Symbol.for(name) === value, 'Only global symbols received from Symbol.for(...) can be passed to client components. ' + 'The symbol Symbol.for(%s) cannot be found among global symbols. ' + 'Remove %s from this object, or avoid the entire object: %s', value.description, describeKeyForErrorMessage(key), describeObjectForErrorMessage(parent), ); request.pendingChunks++; const symbolId = request.nextChunkId++; emitSymbolChunk(request, symbolId, name); writtenSymbols.set(value, symbolId); return serializeByValueID(symbolId); } // $FlowFixMe: bigint isn't added to Flow yet. if (typeof value === 'bigint') { invariant( false, 'BigInt (%s) is not yet supported in client component props. ' + 'Remove %s from this object or use a plain number instead: %s', value, describeKeyForErrorMessage(key), describeObjectForErrorMessage(parent), ); } invariant( false, 'Type %s is not supported in client component props. ' + 'Remove %s from this object, or avoid the entire object: %s', typeof value, describeKeyForErrorMessage(key), describeObjectForErrorMessage(parent), ); } function reportError(request: Request, error: mixed): void { const onError = request.onError; onError(error); } function fatalError(request: Request, error: mixed): void { // This is called outside error handling code such as if an error happens in React internals. closeWithError(request.destination, error); } function emitErrorChunk(request: Request, id: number, error: mixed): void { // TODO: We should not leak error messages to the client in prod. // Give this an error code instead and log on the server. // We can serialize the error in DEV as a convenience. let message; let stack = ''; try { if (error instanceof Error) { message = '' + error.message; stack = '' + error.stack; } else { message = 'Error: ' + (error: any); } } catch (x) { message = 'An error occurred but serializing the error message failed.'; } const processedChunk = processErrorChunk(request, id, message, stack); request.completedErrorChunks.push(processedChunk); } function emitModuleChunk( request: Request, id: number, moduleMetaData: ModuleMetaData, ): void { const processedChunk = processModuleChunk(request, id, moduleMetaData); request.completedModuleChunks.push(processedChunk); } function emitSymbolChunk(request: Request, id: number, name: string): void { const processedChunk = processSymbolChunk(request, id, name); request.completedModuleChunks.push(processedChunk); } function retrySegment(request: Request, segment: Segment): void { try { let value = segment.model; while ( typeof value === 'object' && value !== null && value.$$typeof === REACT_ELEMENT_TYPE ) { // TODO: Concatenate keys of parents onto children. const element: React$Element = (value: any); // Attempt to render the server component. // Doing this here lets us reuse this same segment if the next component // also suspends. segment.model = value; value = attemptResolveElement( element.type, element.key, element.ref, element.props, ); } const processedChunk = processModelChunk(request, segment.id, value); request.completedJSONChunks.push(processedChunk); } catch (x) { if (typeof x === 'object' && x !== null && typeof x.then === 'function') { // Something suspended again, let's pick it back up later. const ping = segment.ping; x.then(ping, ping); return; } else { reportError(request, x); // This errored, we need to serialize this error to the emitErrorChunk(request, segment.id, x); } } } function performWork(request: Request): void { const prevDispatcher = ReactCurrentDispatcher.current; const prevCache = currentCache; ReactCurrentDispatcher.current = Dispatcher; currentCache = request.cache; try { const pingedSegments = request.pingedSegments; request.pingedSegments = []; for (let i = 0; i < pingedSegments.length; i++) { const segment = pingedSegments[i]; retrySegment(request, segment); } if (request.flowing) { flushCompletedChunks(request); } } catch (error) { reportError(request, error); fatalError(request, error); } finally { ReactCurrentDispatcher.current = prevDispatcher; currentCache = prevCache; } } let reentrant = false; function flushCompletedChunks(request: Request): void { if (reentrant) { return; } reentrant = true; const destination = request.destination; beginWriting(destination); try { // We emit module chunks first in the stream so that // they can be preloaded as early as possible. const moduleChunks = request.completedModuleChunks; let i = 0; for (; i < moduleChunks.length; i++) { request.pendingChunks--; const chunk = moduleChunks[i]; if (!writeChunk(destination, chunk)) { request.flowing = false; i++; break; } } moduleChunks.splice(0, i); // Next comes model data. const jsonChunks = request.completedJSONChunks; i = 0; for (; i < jsonChunks.length; i++) { request.pendingChunks--; const chunk = jsonChunks[i]; if (!writeChunk(destination, chunk)) { request.flowing = false; i++; break; } } jsonChunks.splice(0, i); // Finally, errors are sent. The idea is that it's ok to delay // any error messages and prioritize display of other parts of // the page. const errorChunks = request.completedErrorChunks; i = 0; for (; i < errorChunks.length; i++) { request.pendingChunks--; const chunk = errorChunks[i]; if (!writeChunk(destination, chunk)) { request.flowing = false; i++; break; } } errorChunks.splice(0, i); } finally { reentrant = false; completeWriting(destination); } flushBuffered(destination); if (request.pendingChunks === 0) { // We're done. close(destination); } } export function startWork(request: Request): void { request.flowing = true; scheduleWork(() => performWork(request)); } export function startFlowing(request: Request): void { request.flowing = true; try { flushCompletedChunks(request); } catch (error) { reportError(request, error); fatalError(request, error); } } function unsupportedHook(): void { invariant(false, 'This Hook is not supported in Server Components.'); } function unsupportedRefresh(): void { invariant( currentCache, 'Refreshing the cache is not supported in Server Components.', ); } let currentCache: Map | null = null; const Dispatcher: DispatcherType = { useMemo(nextCreate: () => T): T { return nextCreate(); }, useCallback(callback: T): T { return callback; }, useDebugValue(): void {}, useDeferredValue: (unsupportedHook: any), useTransition: (unsupportedHook: any), getCacheForType(resourceType: () => T): T { invariant( currentCache, 'Reading the cache is only supported while rendering.', ); let entry: T | void = (currentCache.get(resourceType): any); if (entry === undefined) { entry = resourceType(); // TODO: Warn if undefined? currentCache.set(resourceType, entry); } return entry; }, readContext: (unsupportedHook: any), useContext: (unsupportedHook: any), useReducer: (unsupportedHook: any), useRef: (unsupportedHook: any), useState: (unsupportedHook: any), useLayoutEffect: (unsupportedHook: any), useImperativeHandle: (unsupportedHook: any), useEffect: (unsupportedHook: any), useOpaqueIdentifier: (unsupportedHook: any), useMutableSource: (unsupportedHook: any), useCacheRefresh(): (?() => T, ?T) => void { return unsupportedRefresh; }, };