1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. import type {Thenable} from 'shared/ReactTypes';
    
  11. 
    
  12. // The server acts as a Client of itself when resolving Server References.
    
  13. // That's why we import the Client configuration from the Server.
    
  14. // Everything is aliased as their Server equivalence for clarity.
    
  15. import type {
    
  16.   ServerReferenceId,
    
  17.   ServerManifest,
    
  18.   ClientReference as ServerReference,
    
  19. } from 'react-client/src/ReactFlightClientConfig';
    
  20. 
    
  21. import {
    
  22.   resolveServerReference,
    
  23.   preloadModule,
    
  24.   requireModule,
    
  25. } from 'react-client/src/ReactFlightClientConfig';
    
  26. 
    
  27. export type JSONValue =
    
  28.   | number
    
  29.   | null
    
  30.   | boolean
    
  31.   | string
    
  32.   | {+[key: string]: JSONValue}
    
  33.   | $ReadOnlyArray<JSONValue>;
    
  34. 
    
  35. const PENDING = 'pending';
    
  36. const BLOCKED = 'blocked';
    
  37. const RESOLVED_MODEL = 'resolved_model';
    
  38. const INITIALIZED = 'fulfilled';
    
  39. const ERRORED = 'rejected';
    
  40. 
    
  41. type PendingChunk<T> = {
    
  42.   status: 'pending',
    
  43.   value: null | Array<(T) => mixed>,
    
  44.   reason: null | Array<(mixed) => mixed>,
    
  45.   _response: Response,
    
  46.   then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
    
  47. };
    
  48. type BlockedChunk<T> = {
    
  49.   status: 'blocked',
    
  50.   value: null | Array<(T) => mixed>,
    
  51.   reason: null | Array<(mixed) => mixed>,
    
  52.   _response: Response,
    
  53.   then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
    
  54. };
    
  55. type ResolvedModelChunk<T> = {
    
  56.   status: 'resolved_model',
    
  57.   value: string,
    
  58.   reason: null,
    
  59.   _response: Response,
    
  60.   then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
    
  61. };
    
  62. type InitializedChunk<T> = {
    
  63.   status: 'fulfilled',
    
  64.   value: T,
    
  65.   reason: null,
    
  66.   _response: Response,
    
  67.   then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
    
  68. };
    
  69. type ErroredChunk<T> = {
    
  70.   status: 'rejected',
    
  71.   value: null,
    
  72.   reason: mixed,
    
  73.   _response: Response,
    
  74.   then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
    
  75. };
    
  76. type SomeChunk<T> =
    
  77.   | PendingChunk<T>
    
  78.   | BlockedChunk<T>
    
  79.   | ResolvedModelChunk<T>
    
  80.   | InitializedChunk<T>
    
  81.   | ErroredChunk<T>;
    
  82. 
    
  83. // $FlowFixMe[missing-this-annot]
    
  84. function Chunk(status: any, value: any, reason: any, response: Response) {
    
  85.   this.status = status;
    
  86.   this.value = value;
    
  87.   this.reason = reason;
    
  88.   this._response = response;
    
  89. }
    
  90. // We subclass Promise.prototype so that we get other methods like .catch
    
  91. Chunk.prototype = (Object.create(Promise.prototype): any);
    
  92. // TODO: This doesn't return a new Promise chain unlike the real .then
    
  93. Chunk.prototype.then = function <T>(
    
  94.   this: SomeChunk<T>,
    
  95.   resolve: (value: T) => mixed,
    
  96.   reject: (reason: mixed) => mixed,
    
  97. ) {
    
  98.   const chunk: SomeChunk<T> = this;
    
  99.   // If we have resolved content, we try to initialize it first which
    
  100.   // might put us back into one of the other states.
    
  101.   switch (chunk.status) {
    
  102.     case RESOLVED_MODEL:
    
  103.       initializeModelChunk(chunk);
    
  104.       break;
    
  105.   }
    
  106.   // The status might have changed after initialization.
    
  107.   switch (chunk.status) {
    
  108.     case INITIALIZED:
    
  109.       resolve(chunk.value);
    
  110.       break;
    
  111.     case PENDING:
    
  112.     case BLOCKED:
    
  113.       if (resolve) {
    
  114.         if (chunk.value === null) {
    
  115.           chunk.value = ([]: Array<(T) => mixed>);
    
  116.         }
    
  117.         chunk.value.push(resolve);
    
  118.       }
    
  119.       if (reject) {
    
  120.         if (chunk.reason === null) {
    
  121.           chunk.reason = ([]: Array<(mixed) => mixed>);
    
  122.         }
    
  123.         chunk.reason.push(reject);
    
  124.       }
    
  125.       break;
    
  126.     default:
    
  127.       reject(chunk.reason);
    
  128.       break;
    
  129.   }
    
  130. };
    
  131. 
    
  132. export type Response = {
    
  133.   _bundlerConfig: ServerManifest,
    
  134.   _prefix: string,
    
  135.   _formData: FormData,
    
  136.   _chunks: Map<number, SomeChunk<any>>,
    
  137.   _fromJSON: (key: string, value: JSONValue) => any,
    
  138. };
    
  139. 
    
  140. export function getRoot<T>(response: Response): Thenable<T> {
    
  141.   const chunk = getChunk(response, 0);
    
  142.   return (chunk: any);
    
  143. }
    
  144. 
    
  145. function createPendingChunk<T>(response: Response): PendingChunk<T> {
    
  146.   // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
    
  147.   return new Chunk(PENDING, null, null, response);
    
  148. }
    
  149. 
    
  150. function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void {
    
  151.   for (let i = 0; i < listeners.length; i++) {
    
  152.     const listener = listeners[i];
    
  153.     listener(value);
    
  154.   }
    
  155. }
    
  156. 
    
  157. function wakeChunkIfInitialized<T>(
    
  158.   chunk: SomeChunk<T>,
    
  159.   resolveListeners: Array<(T) => mixed>,
    
  160.   rejectListeners: null | Array<(mixed) => mixed>,
    
  161. ): void {
    
  162.   switch (chunk.status) {
    
  163.     case INITIALIZED:
    
  164.       wakeChunk(resolveListeners, chunk.value);
    
  165.       break;
    
  166.     case PENDING:
    
  167.     case BLOCKED:
    
  168.       chunk.value = resolveListeners;
    
  169.       chunk.reason = rejectListeners;
    
  170.       break;
    
  171.     case ERRORED:
    
  172.       if (rejectListeners) {
    
  173.         wakeChunk(rejectListeners, chunk.reason);
    
  174.       }
    
  175.       break;
    
  176.   }
    
  177. }
    
  178. 
    
  179. function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: mixed): void {
    
  180.   if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
    
  181.     // We already resolved. We didn't expect to see this.
    
  182.     return;
    
  183.   }
    
  184.   const listeners = chunk.reason;
    
  185.   const erroredChunk: ErroredChunk<T> = (chunk: any);
    
  186.   erroredChunk.status = ERRORED;
    
  187.   erroredChunk.reason = error;
    
  188.   if (listeners !== null) {
    
  189.     wakeChunk(listeners, error);
    
  190.   }
    
  191. }
    
  192. 
    
  193. function createResolvedModelChunk<T>(
    
  194.   response: Response,
    
  195.   value: string,
    
  196. ): ResolvedModelChunk<T> {
    
  197.   // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
    
  198.   return new Chunk(RESOLVED_MODEL, value, null, response);
    
  199. }
    
  200. 
    
  201. function resolveModelChunk<T>(chunk: SomeChunk<T>, value: string): void {
    
  202.   if (chunk.status !== PENDING) {
    
  203.     // We already resolved. We didn't expect to see this.
    
  204.     return;
    
  205.   }
    
  206.   const resolveListeners = chunk.value;
    
  207.   const rejectListeners = chunk.reason;
    
  208.   const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
    
  209.   resolvedChunk.status = RESOLVED_MODEL;
    
  210.   resolvedChunk.value = value;
    
  211.   if (resolveListeners !== null) {
    
  212.     // This is unfortunate that we're reading this eagerly if
    
  213.     // we already have listeners attached since they might no
    
  214.     // longer be rendered or might not be the highest pri.
    
  215.     initializeModelChunk(resolvedChunk);
    
  216.     // The status might have changed after initialization.
    
  217.     wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
    
  218.   }
    
  219. }
    
  220. 
    
  221. function bindArgs(fn: any, args: any) {
    
  222.   return fn.bind.apply(fn, [null].concat(args));
    
  223. }
    
  224. 
    
  225. function loadServerReference<T>(
    
  226.   response: Response,
    
  227.   id: ServerReferenceId,
    
  228.   bound: null | Thenable<Array<any>>,
    
  229.   parentChunk: SomeChunk<T>,
    
  230.   parentObject: Object,
    
  231.   key: string,
    
  232. ): T {
    
  233.   const serverReference: ServerReference<T> =
    
  234.     resolveServerReference<$FlowFixMe>(response._bundlerConfig, id);
    
  235.   // We expect most servers to not really need this because you'd just have all
    
  236.   // the relevant modules already loaded but it allows for lazy loading of code
    
  237.   // if needed.
    
  238.   const preloadPromise = preloadModule(serverReference);
    
  239.   let promise: Promise<T>;
    
  240.   if (bound) {
    
  241.     promise = Promise.all([(bound: any), preloadPromise]).then(
    
  242.       ([args]: Array<any>) => bindArgs(requireModule(serverReference), args),
    
  243.     );
    
  244.   } else {
    
  245.     if (preloadPromise) {
    
  246.       promise = Promise.resolve(preloadPromise).then(() =>
    
  247.         requireModule(serverReference),
    
  248.       );
    
  249.     } else {
    
  250.       // Synchronously available
    
  251.       return requireModule(serverReference);
    
  252.     }
    
  253.   }
    
  254.   promise.then(
    
  255.     createModelResolver(parentChunk, parentObject, key),
    
  256.     createModelReject(parentChunk),
    
  257.   );
    
  258.   // We need a placeholder value that will be replaced later.
    
  259.   return (null: any);
    
  260. }
    
  261. 
    
  262. let initializingChunk: ResolvedModelChunk<any> = (null: any);
    
  263. let initializingChunkBlockedModel: null | {deps: number, value: any} = null;
    
  264. function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
    
  265.   const prevChunk = initializingChunk;
    
  266.   const prevBlocked = initializingChunkBlockedModel;
    
  267.   initializingChunk = chunk;
    
  268.   initializingChunkBlockedModel = null;
    
  269.   try {
    
  270.     const value: T = JSON.parse(chunk.value, chunk._response._fromJSON);
    
  271.     if (
    
  272.       initializingChunkBlockedModel !== null &&
    
  273.       initializingChunkBlockedModel.deps > 0
    
  274.     ) {
    
  275.       initializingChunkBlockedModel.value = value;
    
  276.       // We discovered new dependencies on modules that are not yet resolved.
    
  277.       // We have to go the BLOCKED state until they're resolved.
    
  278.       const blockedChunk: BlockedChunk<T> = (chunk: any);
    
  279.       blockedChunk.status = BLOCKED;
    
  280.       blockedChunk.value = null;
    
  281.       blockedChunk.reason = null;
    
  282.     } else {
    
  283.       const initializedChunk: InitializedChunk<T> = (chunk: any);
    
  284.       initializedChunk.status = INITIALIZED;
    
  285.       initializedChunk.value = value;
    
  286.     }
    
  287.   } catch (error) {
    
  288.     const erroredChunk: ErroredChunk<T> = (chunk: any);
    
  289.     erroredChunk.status = ERRORED;
    
  290.     erroredChunk.reason = error;
    
  291.   } finally {
    
  292.     initializingChunk = prevChunk;
    
  293.     initializingChunkBlockedModel = prevBlocked;
    
  294.   }
    
  295. }
    
  296. 
    
  297. // Report that any missing chunks in the model is now going to throw this
    
  298. // error upon read. Also notify any pending promises.
    
  299. export function reportGlobalError(response: Response, error: Error): void {
    
  300.   response._chunks.forEach(chunk => {
    
  301.     // If this chunk was already resolved or errored, it won't
    
  302.     // trigger an error but if it wasn't then we need to
    
  303.     // because we won't be getting any new data to resolve it.
    
  304.     if (chunk.status === PENDING) {
    
  305.       triggerErrorOnChunk(chunk, error);
    
  306.     }
    
  307.   });
    
  308. }
    
  309. 
    
  310. function getChunk(response: Response, id: number): SomeChunk<any> {
    
  311.   const chunks = response._chunks;
    
  312.   let chunk = chunks.get(id);
    
  313.   if (!chunk) {
    
  314.     const prefix = response._prefix;
    
  315.     const key = prefix + id;
    
  316.     // Check if we have this field in the backing store already.
    
  317.     const backingEntry = response._formData.get(key);
    
  318.     if (backingEntry != null) {
    
  319.       // We assume that this is a string entry for now.
    
  320.       chunk = createResolvedModelChunk(response, (backingEntry: any));
    
  321.     } else {
    
  322.       // We're still waiting on this entry to stream in.
    
  323.       chunk = createPendingChunk(response);
    
  324.     }
    
  325.     chunks.set(id, chunk);
    
  326.   }
    
  327.   return chunk;
    
  328. }
    
  329. 
    
  330. function createModelResolver<T>(
    
  331.   chunk: SomeChunk<T>,
    
  332.   parentObject: Object,
    
  333.   key: string,
    
  334. ): (value: any) => void {
    
  335.   let blocked;
    
  336.   if (initializingChunkBlockedModel) {
    
  337.     blocked = initializingChunkBlockedModel;
    
  338.     blocked.deps++;
    
  339.   } else {
    
  340.     blocked = initializingChunkBlockedModel = {
    
  341.       deps: 1,
    
  342.       value: null,
    
  343.     };
    
  344.   }
    
  345.   return value => {
    
  346.     parentObject[key] = value;
    
  347.     blocked.deps--;
    
  348.     if (blocked.deps === 0) {
    
  349.       if (chunk.status !== BLOCKED) {
    
  350.         return;
    
  351.       }
    
  352.       const resolveListeners = chunk.value;
    
  353.       const initializedChunk: InitializedChunk<T> = (chunk: any);
    
  354.       initializedChunk.status = INITIALIZED;
    
  355.       initializedChunk.value = blocked.value;
    
  356.       if (resolveListeners !== null) {
    
  357.         wakeChunk(resolveListeners, blocked.value);
    
  358.       }
    
  359.     }
    
  360.   };
    
  361. }
    
  362. 
    
  363. function createModelReject<T>(chunk: SomeChunk<T>): (error: mixed) => void {
    
  364.   return (error: mixed) => triggerErrorOnChunk(chunk, error);
    
  365. }
    
  366. 
    
  367. function getOutlinedModel(response: Response, id: number): any {
    
  368.   const chunk = getChunk(response, id);
    
  369.   if (chunk.status === RESOLVED_MODEL) {
    
  370.     initializeModelChunk(chunk);
    
  371.   }
    
  372.   if (chunk.status !== INITIALIZED) {
    
  373.     // We know that this is emitted earlier so otherwise it's an error.
    
  374.     throw chunk.reason;
    
  375.   }
    
  376.   return chunk.value;
    
  377. }
    
  378. 
    
  379. function parseModelString(
    
  380.   response: Response,
    
  381.   parentObject: Object,
    
  382.   key: string,
    
  383.   value: string,
    
  384. ): any {
    
  385.   if (value[0] === '$') {
    
  386.     switch (value[1]) {
    
  387.       case '$': {
    
  388.         // This was an escaped string value.
    
  389.         return value.slice(1);
    
  390.       }
    
  391.       case '@': {
    
  392.         // Promise
    
  393.         const id = parseInt(value.slice(2), 16);
    
  394.         const chunk = getChunk(response, id);
    
  395.         return chunk;
    
  396.       }
    
  397.       case 'S': {
    
  398.         // Symbol
    
  399.         return Symbol.for(value.slice(2));
    
  400.       }
    
  401.       case 'F': {
    
  402.         // Server Reference
    
  403.         const id = parseInt(value.slice(2), 16);
    
  404.         // TODO: Just encode this in the reference inline instead of as a model.
    
  405.         const metaData: {id: ServerReferenceId, bound: Thenable<Array<any>>} =
    
  406.           getOutlinedModel(response, id);
    
  407.         return loadServerReference(
    
  408.           response,
    
  409.           metaData.id,
    
  410.           metaData.bound,
    
  411.           initializingChunk,
    
  412.           parentObject,
    
  413.           key,
    
  414.         );
    
  415.       }
    
  416.       case 'Q': {
    
  417.         // Map
    
  418.         const id = parseInt(value.slice(2), 16);
    
  419.         const data = getOutlinedModel(response, id);
    
  420.         return new Map(data);
    
  421.       }
    
  422.       case 'W': {
    
  423.         // Set
    
  424.         const id = parseInt(value.slice(2), 16);
    
  425.         const data = getOutlinedModel(response, id);
    
  426.         return new Set(data);
    
  427.       }
    
  428.       case 'K': {
    
  429.         // FormData
    
  430.         const stringId = value.slice(2);
    
  431.         const formPrefix = response._prefix + stringId + '_';
    
  432.         const data = new FormData();
    
  433.         const backingFormData = response._formData;
    
  434.         // We assume that the reference to FormData always comes after each
    
  435.         // entry that it references so we can assume they all exist in the
    
  436.         // backing store already.
    
  437.         // $FlowFixMe[prop-missing] FormData has forEach on it.
    
  438.         backingFormData.forEach((entry: File | string, entryKey: string) => {
    
  439.           if (entryKey.startsWith(formPrefix)) {
    
  440.             data.append(entryKey.slice(formPrefix.length), entry);
    
  441.           }
    
  442.         });
    
  443.         return data;
    
  444.       }
    
  445.       case 'I': {
    
  446.         // $Infinity
    
  447.         return Infinity;
    
  448.       }
    
  449.       case '-': {
    
  450.         // $-0 or $-Infinity
    
  451.         if (value === '$-0') {
    
  452.           return -0;
    
  453.         } else {
    
  454.           return -Infinity;
    
  455.         }
    
  456.       }
    
  457.       case 'N': {
    
  458.         // $NaN
    
  459.         return NaN;
    
  460.       }
    
  461.       case 'u': {
    
  462.         // matches "$undefined"
    
  463.         // Special encoding for `undefined` which can't be serialized as JSON otherwise.
    
  464.         return undefined;
    
  465.       }
    
  466.       case 'D': {
    
  467.         // Date
    
  468.         return new Date(Date.parse(value.slice(2)));
    
  469.       }
    
  470.       case 'n': {
    
  471.         // BigInt
    
  472.         return BigInt(value.slice(2));
    
  473.       }
    
  474.       default: {
    
  475.         // We assume that anything else is a reference ID.
    
  476.         const id = parseInt(value.slice(1), 16);
    
  477.         const chunk = getChunk(response, id);
    
  478.         switch (chunk.status) {
    
  479.           case RESOLVED_MODEL:
    
  480.             initializeModelChunk(chunk);
    
  481.             break;
    
  482.         }
    
  483.         // The status might have changed after initialization.
    
  484.         switch (chunk.status) {
    
  485.           case INITIALIZED:
    
  486.             return chunk.value;
    
  487.           case PENDING:
    
  488.           case BLOCKED:
    
  489.             const parentChunk = initializingChunk;
    
  490.             chunk.then(
    
  491.               createModelResolver(parentChunk, parentObject, key),
    
  492.               createModelReject(parentChunk),
    
  493.             );
    
  494.             return null;
    
  495.           default:
    
  496.             throw chunk.reason;
    
  497.         }
    
  498.       }
    
  499.     }
    
  500.   }
    
  501.   return value;
    
  502. }
    
  503. 
    
  504. export function createResponse(
    
  505.   bundlerConfig: ServerManifest,
    
  506.   formFieldPrefix: string,
    
  507.   backingFormData?: FormData = new FormData(),
    
  508. ): Response {
    
  509.   const chunks: Map<number, SomeChunk<any>> = new Map();
    
  510.   const response: Response = {
    
  511.     _bundlerConfig: bundlerConfig,
    
  512.     _prefix: formFieldPrefix,
    
  513.     _formData: backingFormData,
    
  514.     _chunks: chunks,
    
  515.     _fromJSON: function (this: any, key: string, value: JSONValue) {
    
  516.       if (typeof value === 'string') {
    
  517.         // We can't use .bind here because we need the "this" value.
    
  518.         return parseModelString(response, this, key, value);
    
  519.       }
    
  520.       return value;
    
  521.     },
    
  522.   };
    
  523.   return response;
    
  524. }
    
  525. 
    
  526. export function resolveField(
    
  527.   response: Response,
    
  528.   key: string,
    
  529.   value: string,
    
  530. ): void {
    
  531.   // Add this field to the backing store.
    
  532.   response._formData.append(key, value);
    
  533.   const prefix = response._prefix;
    
  534.   if (key.startsWith(prefix)) {
    
  535.     const chunks = response._chunks;
    
  536.     const id = +key.slice(prefix.length);
    
  537.     const chunk = chunks.get(id);
    
  538.     if (chunk) {
    
  539.       // We were waiting on this key so now we can resolve it.
    
  540.       resolveModelChunk(chunk, value);
    
  541.     }
    
  542.   }
    
  543. }
    
  544. 
    
  545. export function resolveFile(response: Response, key: string, file: File): void {
    
  546.   // Add this field to the backing store.
    
  547.   response._formData.append(key, file);
    
  548. }
    
  549. 
    
  550. export opaque type FileHandle = {
    
  551.   chunks: Array<Uint8Array>,
    
  552.   filename: string,
    
  553.   mime: string,
    
  554. };
    
  555. 
    
  556. export function resolveFileInfo(
    
  557.   response: Response,
    
  558.   key: string,
    
  559.   filename: string,
    
  560.   mime: string,
    
  561. ): FileHandle {
    
  562.   return {
    
  563.     chunks: [],
    
  564.     filename,
    
  565.     mime,
    
  566.   };
    
  567. }
    
  568. 
    
  569. export function resolveFileChunk(
    
  570.   response: Response,
    
  571.   handle: FileHandle,
    
  572.   chunk: Uint8Array,
    
  573. ): void {
    
  574.   handle.chunks.push(chunk);
    
  575. }
    
  576. 
    
  577. export function resolveFileComplete(
    
  578.   response: Response,
    
  579.   key: string,
    
  580.   handle: FileHandle,
    
  581. ): void {
    
  582.   // Add this file to the backing store.
    
  583.   // Node.js doesn't expose a global File constructor so we need to use
    
  584.   // the append() form that takes the file name as the third argument,
    
  585.   // to create a File object.
    
  586.   const blob = new Blob(handle.chunks, {type: handle.mime});
    
  587.   response._formData.append(key, blob, handle.filename);
    
  588. }
    
  589. 
    
  590. export function close(response: Response): void {
    
  591.   // In case there are any remaining unresolved chunks, they won't
    
  592.   // be resolved now. So we need to issue an error to those.
    
  593.   // Ideally we should be able to early bail out if we kept a
    
  594.   // ref count of pending chunks.
    
  595.   reportGlobalError(response, new Error('Connection closed.'));
    
  596. }