/** * 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} from 'shared/ReactTypes'; import type {LazyComponent} from 'react/src/ReactLazy'; import type { ModuleReference, ModuleMetaData, UninitializedModel, Response, BundlerConfig, } from './ReactFlightClientHostConfig'; import { resolveModuleReference, preloadModule, requireModule, parseModel, } from './ReactFlightClientHostConfig'; import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry'; export type JSONValue = | number | null | boolean | string | {+[key: string]: JSONValue} | $ReadOnlyArray; const PENDING = 'pending'; const BLOCKED = 'blocked'; const RESOLVED_MODEL = 'resolved_model'; const RESOLVED_MODULE = 'resolved_module'; const INITIALIZED = 'fulfilled'; const ERRORED = 'rejected'; type PendingChunk = { status: 'pending', value: null | Array<(T) => mixed>, reason: null | Array<(mixed) => mixed>, _response: Response, then(resolve: (T) => mixed, reject: (mixed) => mixed): void, }; type BlockedChunk = { status: 'blocked', value: null | Array<(T) => mixed>, reason: null | Array<(mixed) => mixed>, _response: Response, then(resolve: (T) => mixed, reject: (mixed) => mixed): void, }; type ResolvedModelChunk = { status: 'resolved_model', value: UninitializedModel, reason: null, _response: Response, then(resolve: (T) => mixed, reject: (mixed) => mixed): void, }; type ResolvedModuleChunk = { status: 'resolved_module', value: ModuleReference, reason: null, _response: Response, then(resolve: (T) => mixed, reject: (mixed) => mixed): void, }; type InitializedChunk = { status: 'fulfilled', value: T, reason: null, _response: Response, then(resolve: (T) => mixed, reject: (mixed) => mixed): void, }; type ErroredChunk = { status: 'rejected', value: null, reason: mixed, _response: Response, then(resolve: (T) => mixed, reject: (mixed) => mixed): void, }; type SomeChunk = | PendingChunk | BlockedChunk | ResolvedModelChunk | ResolvedModuleChunk | InitializedChunk | ErroredChunk; function Chunk(status: any, value: any, reason: any, response: Response) { this.status = status; this.value = value; this.reason = reason; this._response = response; } // We subclass Promise.prototype so that we get other methods like .catch Chunk.prototype = (Object.create(Promise.prototype): any); // TODO: This doesn't return a new Promise chain unlike the real .then Chunk.prototype.then = function( resolve: (value: T) => mixed, reject: (reason: mixed) => mixed, ) { const chunk: SomeChunk = this; // If we have resolved content, we try to initialize it first which // might put us back into one of the other states. switch (chunk.status) { case RESOLVED_MODEL: initializeModelChunk(chunk); break; case RESOLVED_MODULE: initializeModuleChunk(chunk); break; } // The status might have changed after initialization. switch (chunk.status) { case INITIALIZED: resolve(chunk.value); break; case PENDING: case BLOCKED: if (resolve) { if (chunk.value === null) { chunk.value = []; } chunk.value.push(resolve); } if (reject) { if (chunk.reason === null) { chunk.reason = []; } chunk.reason.push(reject); } break; default: reject(chunk.reason); break; } }; export type ResponseBase = { _bundlerConfig: BundlerConfig, _chunks: Map>, ... }; export type {Response}; function readChunk(chunk: SomeChunk): T { // If we have resolved content, we try to initialize it first which // might put us back into one of the other states. switch (chunk.status) { case RESOLVED_MODEL: initializeModelChunk(chunk); break; case RESOLVED_MODULE: initializeModuleChunk(chunk); break; } // The status might have changed after initialization. switch (chunk.status) { case INITIALIZED: return chunk.value; case PENDING: case BLOCKED: // eslint-disable-next-line no-throw-literal throw ((chunk: any): Thenable); default: throw chunk.reason; } } export function getRoot(response: Response): Thenable { const chunk = getChunk(response, 0); return (chunk: any); } function createPendingChunk(response: Response): PendingChunk { // $FlowFixMe Flow doesn't support functions as constructors return new Chunk(PENDING, null, null, response); } function createBlockedChunk(response: Response): BlockedChunk { // $FlowFixMe Flow doesn't support functions as constructors return new Chunk(BLOCKED, null, null, response); } function createErrorChunk( response: Response, error: ErrorWithDigest, ): ErroredChunk { // $FlowFixMe Flow doesn't support functions as constructors return new Chunk(ERRORED, null, error, response); } function createInitializedChunk( response: Response, value: T, ): InitializedChunk { // $FlowFixMe Flow doesn't support functions as constructors return new Chunk(INITIALIZED, value, null, response); } function wakeChunk(listeners: Array<(T) => mixed>, value: T): void { for (let i = 0; i < listeners.length; i++) { const listener = listeners[i]; listener(value); } } function wakeChunkIfInitialized( chunk: SomeChunk, resolveListeners: Array<(T) => mixed>, rejectListeners: null | Array<(mixed) => mixed>, ): void { switch (chunk.status) { case INITIALIZED: wakeChunk(resolveListeners, chunk.value); break; case PENDING: case BLOCKED: chunk.value = resolveListeners; chunk.reason = rejectListeners; break; case ERRORED: if (rejectListeners) { wakeChunk(rejectListeners, chunk.reason); } break; } } function triggerErrorOnChunk(chunk: SomeChunk, error: mixed): void { if (chunk.status !== PENDING && chunk.status !== BLOCKED) { // We already resolved. We didn't expect to see this. return; } const listeners = chunk.reason; const erroredChunk: ErroredChunk = (chunk: any); erroredChunk.status = ERRORED; erroredChunk.reason = error; if (listeners !== null) { wakeChunk(listeners, error); } } function createResolvedModelChunk( response: Response, value: UninitializedModel, ): ResolvedModelChunk { // $FlowFixMe Flow doesn't support functions as constructors return new Chunk(RESOLVED_MODEL, value, null, response); } function createResolvedModuleChunk( response: Response, value: ModuleReference, ): ResolvedModuleChunk { // $FlowFixMe Flow doesn't support functions as constructors return new Chunk(RESOLVED_MODULE, value, null, response); } function resolveModelChunk( chunk: SomeChunk, value: UninitializedModel, ): void { if (chunk.status !== PENDING) { // We already resolved. We didn't expect to see this. return; } const resolveListeners = chunk.value; const rejectListeners = chunk.reason; const resolvedChunk: ResolvedModelChunk = (chunk: any); resolvedChunk.status = RESOLVED_MODEL; resolvedChunk.value = value; if (resolveListeners !== null) { // This is unfortunate that we're reading this eagerly if // we already have listeners attached since they might no // longer be rendered or might not be the highest pri. initializeModelChunk(resolvedChunk); // The status might have changed after initialization. wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners); } } function resolveModuleChunk( chunk: SomeChunk, value: ModuleReference, ): void { if (chunk.status !== PENDING && chunk.status !== BLOCKED) { // We already resolved. We didn't expect to see this. return; } const resolveListeners = chunk.value; const rejectListeners = chunk.reason; const resolvedChunk: ResolvedModuleChunk = (chunk: any); resolvedChunk.status = RESOLVED_MODULE; resolvedChunk.value = value; if (resolveListeners !== null) { initializeModuleChunk(resolvedChunk); wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners); } } let initializingChunk: ResolvedModelChunk = (null: any); let initializingChunkBlockedModel: null | {deps: number, value: any} = null; function initializeModelChunk(chunk: ResolvedModelChunk): void { const prevChunk = initializingChunk; const prevBlocked = initializingChunkBlockedModel; initializingChunk = chunk; initializingChunkBlockedModel = null; try { const value: T = parseModel(chunk._response, chunk.value); if ( initializingChunkBlockedModel !== null && initializingChunkBlockedModel.deps > 0 ) { initializingChunkBlockedModel.value = value; // We discovered new dependencies on modules that are not yet resolved. // We have to go the BLOCKED state until they're resolved. const blockedChunk: BlockedChunk = (chunk: any); blockedChunk.status = BLOCKED; blockedChunk.value = null; blockedChunk.reason = null; } else { const initializedChunk: InitializedChunk = (chunk: any); initializedChunk.status = INITIALIZED; initializedChunk.value = value; } } catch (error) { const erroredChunk: ErroredChunk = (chunk: any); erroredChunk.status = ERRORED; erroredChunk.reason = error; } finally { initializingChunk = prevChunk; initializingChunkBlockedModel = prevBlocked; } } function initializeModuleChunk(chunk: ResolvedModuleChunk): void { try { const value: T = requireModule(chunk.value); const initializedChunk: InitializedChunk = (chunk: any); initializedChunk.status = INITIALIZED; initializedChunk.value = value; } catch (error) { const erroredChunk: ErroredChunk = (chunk: any); erroredChunk.status = ERRORED; erroredChunk.reason = error; } } // Report that any missing chunks in the model is now going to throw this // error upon read. Also notify any pending promises. export function reportGlobalError(response: Response, error: Error): void { response._chunks.forEach(chunk => { // If this chunk was already resolved or errored, it won't // trigger an error but if it wasn't then we need to // because we won't be getting any new data to resolve it. if (chunk.status === PENDING) { triggerErrorOnChunk(chunk, error); } }); } function createElement(type, key, props): React$Element { const element: any = { // This tag allows us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, // Built-in properties that belong on the element type: type, key: key, ref: null, props: props, // Record the component responsible for creating this element. _owner: null, }; if (__DEV__) { // We don't really need to add any of these but keeping them for good measure. // Unfortunately, _store is enumerable in jest matchers so for equality to // work, I need to keep it or make _store non-enumerable in the other file. element._store = {}; Object.defineProperty(element._store, 'validated', { configurable: false, enumerable: false, writable: true, value: true, // This element has already been validated on the server. }); Object.defineProperty(element, '_self', { configurable: false, enumerable: false, writable: false, value: null, }); Object.defineProperty(element, '_source', { configurable: false, enumerable: false, writable: false, value: null, }); } return element; } function createLazyChunkWrapper( chunk: SomeChunk, ): LazyComponent> { const lazyType: LazyComponent> = { $$typeof: REACT_LAZY_TYPE, _payload: chunk, _init: readChunk, }; return lazyType; } function getChunk(response: Response, id: number): SomeChunk { const chunks = response._chunks; let chunk = chunks.get(id); if (!chunk) { chunk = createPendingChunk(response); chunks.set(id, chunk); } return chunk; } function createModelResolver( chunk: SomeChunk, parentObject: Object, key: string, ) { let blocked; if (initializingChunkBlockedModel) { blocked = initializingChunkBlockedModel; blocked.deps++; } else { blocked = initializingChunkBlockedModel = { deps: 1, value: null, }; } return value => { parentObject[key] = value; blocked.deps--; if (blocked.deps === 0) { if (chunk.status !== BLOCKED) { return; } const resolveListeners = chunk.value; const initializedChunk: InitializedChunk = (chunk: any); initializedChunk.status = INITIALIZED; initializedChunk.value = blocked.value; if (resolveListeners !== null) { wakeChunk(resolveListeners, blocked.value); } } }; } function createModelReject(chunk: SomeChunk) { return error => triggerErrorOnChunk(chunk, error); } export function parseModelString( response: Response, parentObject: Object, key: string, value: string, ): any { switch (value[0]) { case '$': { if (value === '$') { return REACT_ELEMENT_TYPE; } else if (value[1] === '$' || value[1] === '@') { // This was an escaped string value. return value.substring(1); } else { const id = parseInt(value.substring(1), 16); const chunk = getChunk(response, id); switch (chunk.status) { case RESOLVED_MODEL: initializeModelChunk(chunk); break; case RESOLVED_MODULE: initializeModuleChunk(chunk); break; } // The status might have changed after initialization. switch (chunk.status) { case INITIALIZED: return chunk.value; case PENDING: case BLOCKED: const parentChunk = initializingChunk; chunk.then( createModelResolver(parentChunk, parentObject, key), createModelReject(parentChunk), ); return null; default: throw chunk.reason; } } } case '@': { const id = parseInt(value.substring(1), 16); const chunk = getChunk(response, id); // We create a React.lazy wrapper around any lazy values. // When passed into React, we'll know how to suspend on this. return createLazyChunkWrapper(chunk); } } return value; } export function parseModelTuple( response: Response, value: {+[key: string]: JSONValue} | $ReadOnlyArray, ): any { const tuple: [mixed, mixed, mixed, mixed] = (value: any); if (tuple[0] === REACT_ELEMENT_TYPE) { // TODO: Consider having React just directly accept these arrays as elements. // Or even change the ReactElement type to be an array. return createElement(tuple[1], tuple[2], tuple[3]); } return value; } export function createResponse(bundlerConfig: BundlerConfig): ResponseBase { const chunks: Map> = new Map(); const response = { _bundlerConfig: bundlerConfig, _chunks: chunks, }; return response; } export function resolveModel( response: Response, id: number, model: UninitializedModel, ): void { const chunks = response._chunks; const chunk = chunks.get(id); if (!chunk) { chunks.set(id, createResolvedModelChunk(response, model)); } else { resolveModelChunk(chunk, model); } } export function resolveProvider( response: Response, id: number, contextName: string, ): void { const chunks = response._chunks; chunks.set( id, createInitializedChunk( response, getOrCreateServerContext(contextName).Provider, ), ); } export function resolveModule( response: Response, id: number, model: UninitializedModel, ): void { const chunks = response._chunks; const chunk = chunks.get(id); const moduleMetaData: ModuleMetaData = parseModel(response, model); const moduleReference = resolveModuleReference( response._bundlerConfig, moduleMetaData, ); // TODO: Add an option to encode modules that are lazy loaded. // For now we preload all modules as early as possible since it's likely // that we'll need them. const promise = preloadModule(moduleReference); if (promise) { let blockedChunk: BlockedChunk; if (!chunk) { // Technically, we should just treat promise as the chunk in this // case. Because it'll just behave as any other promise. blockedChunk = createBlockedChunk(response); chunks.set(id, blockedChunk); } else { // This can't actually happen because we don't have any forward // references to modules. blockedChunk = (chunk: any); blockedChunk.status = BLOCKED; } promise.then( () => resolveModuleChunk(blockedChunk, moduleReference), error => triggerErrorOnChunk(blockedChunk, error), ); } else { if (!chunk) { chunks.set(id, createResolvedModuleChunk(response, moduleReference)); } else { // This can't actually happen because we don't have any forward // references to modules. resolveModuleChunk(chunk, moduleReference); } } } export function resolveSymbol( response: Response, id: number, name: string, ): void { const chunks = response._chunks; // We assume that we'll always emit the symbol before anything references it // to save a few bytes. chunks.set(id, createInitializedChunk(response, Symbol.for(name))); } type ErrorWithDigest = Error & {digest?: string}; export function resolveErrorProd( response: Response, id: number, digest: string, ): void { if (__DEV__) { // These errors should never make it into a build so we don't need to encode them in codes.json // eslint-disable-next-line react-internal/prod-error-codes throw new Error( 'resolveErrorProd should never be called in development mode. Use resolveErrorDev instead. This is a bug in React.', ); } const error = new Error( 'An error occurred in the Server Components render. The specific message is omitted in production' + ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' + ' may provide additional details about the nature of the error.', ); error.stack = 'Error: ' + error.message; (error: any).digest = digest; const errorWithDigest: ErrorWithDigest = (error: any); const chunks = response._chunks; const chunk = chunks.get(id); if (!chunk) { chunks.set(id, createErrorChunk(response, errorWithDigest)); } else { triggerErrorOnChunk(chunk, errorWithDigest); } } export function resolveErrorDev( response: Response, id: number, digest: string, message: string, stack: string, ): void { if (!__DEV__) { // These errors should never make it into a build so we don't need to encode them in codes.json // eslint-disable-next-line react-internal/prod-error-codes throw new Error( 'resolveErrorDev should never be called in production mode. Use resolveErrorProd instead. This is a bug in React.', ); } // eslint-disable-next-line react-internal/prod-error-codes const error = new Error( message || 'An error occurred in the Server Components render but no message was provided', ); error.stack = stack; (error: any).digest = digest; const errorWithDigest: ErrorWithDigest = (error: any); const chunks = response._chunks; const chunk = chunks.get(id); if (!chunk) { chunks.set(id, createErrorChunk(response, errorWithDigest)); } else { triggerErrorOnChunk(chunk, errorWithDigest); } } export function close(response: Response): void { // In case there are any remaining unresolved chunks, they won't // be resolved now. So we need to issue an error to those. // Ideally we should be able to early bail out if we kept a // ref count of pending chunks. reportGlobalError(response, new Error('Connection closed.')); }