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 {Writable} from 'stream';
    
  11. 
    
  12. import {TextEncoder} from 'util';
    
  13. import {createHash} from 'crypto';
    
  14. 
    
  15. interface MightBeFlushable {
    
  16.   flush?: () => void;
    
  17. }
    
  18. 
    
  19. export type Destination = Writable & MightBeFlushable;
    
  20. 
    
  21. export type PrecomputedChunk = Uint8Array;
    
  22. export opaque type Chunk = string;
    
  23. export type BinaryChunk = Uint8Array;
    
  24. 
    
  25. export function scheduleWork(callback: () => void) {
    
  26.   setImmediate(callback);
    
  27. }
    
  28. 
    
  29. export function flushBuffered(destination: Destination) {
    
  30.   // If we don't have any more data to send right now.
    
  31.   // Flush whatever is in the buffer to the wire.
    
  32.   if (typeof destination.flush === 'function') {
    
  33.     // By convention the Zlib streams provide a flush function for this purpose.
    
  34.     // For Express, compression middleware adds this method.
    
  35.     destination.flush();
    
  36.   }
    
  37. }
    
  38. 
    
  39. const VIEW_SIZE = 2048;
    
  40. let currentView = null;
    
  41. let writtenBytes = 0;
    
  42. let destinationHasCapacity = true;
    
  43. 
    
  44. export function beginWriting(destination: Destination) {
    
  45.   currentView = new Uint8Array(VIEW_SIZE);
    
  46.   writtenBytes = 0;
    
  47.   destinationHasCapacity = true;
    
  48. }
    
  49. 
    
  50. function writeStringChunk(destination: Destination, stringChunk: string) {
    
  51.   if (stringChunk.length === 0) {
    
  52.     return;
    
  53.   }
    
  54.   // maximum possible view needed to encode entire string
    
  55.   if (stringChunk.length * 3 > VIEW_SIZE) {
    
  56.     if (writtenBytes > 0) {
    
  57.       writeToDestination(
    
  58.         destination,
    
  59.         ((currentView: any): Uint8Array).subarray(0, writtenBytes),
    
  60.       );
    
  61.       currentView = new Uint8Array(VIEW_SIZE);
    
  62.       writtenBytes = 0;
    
  63.     }
    
  64.     writeToDestination(destination, textEncoder.encode(stringChunk));
    
  65.     return;
    
  66.   }
    
  67. 
    
  68.   let target: Uint8Array = (currentView: any);
    
  69.   if (writtenBytes > 0) {
    
  70.     target = ((currentView: any): Uint8Array).subarray(writtenBytes);
    
  71.   }
    
  72.   const {read, written} = textEncoder.encodeInto(stringChunk, target);
    
  73.   writtenBytes += written;
    
  74. 
    
  75.   if (read < stringChunk.length) {
    
  76.     writeToDestination(
    
  77.       destination,
    
  78.       (currentView: any).subarray(0, writtenBytes),
    
  79.     );
    
  80.     currentView = new Uint8Array(VIEW_SIZE);
    
  81.     writtenBytes = textEncoder.encodeInto(
    
  82.       stringChunk.slice(read),
    
  83.       (currentView: any),
    
  84.     ).written;
    
  85.   }
    
  86. 
    
  87.   if (writtenBytes === VIEW_SIZE) {
    
  88.     writeToDestination(destination, (currentView: any));
    
  89.     currentView = new Uint8Array(VIEW_SIZE);
    
  90.     writtenBytes = 0;
    
  91.   }
    
  92. }
    
  93. 
    
  94. function writeViewChunk(
    
  95.   destination: Destination,
    
  96.   chunk: PrecomputedChunk | BinaryChunk,
    
  97. ) {
    
  98.   if (chunk.byteLength === 0) {
    
  99.     return;
    
  100.   }
    
  101.   if (chunk.byteLength > VIEW_SIZE) {
    
  102.     if (__DEV__) {
    
  103.       if (precomputedChunkSet && precomputedChunkSet.has(chunk)) {
    
  104.         console.error(
    
  105.           'A large precomputed chunk was passed to writeChunk without being copied.' +
    
  106.             ' Large chunks get enqueued directly and are not copied. This is incompatible with precomputed chunks because you cannot enqueue the same precomputed chunk twice.' +
    
  107.             ' Use "cloneChunk" to make a copy of this large precomputed chunk before writing it. This is a bug in React.',
    
  108.         );
    
  109.       }
    
  110.     }
    
  111.     // this chunk may overflow a single view which implies it was not
    
  112.     // one that is cached by the streaming renderer. We will enqueu
    
  113.     // it directly and expect it is not re-used
    
  114.     if (writtenBytes > 0) {
    
  115.       writeToDestination(
    
  116.         destination,
    
  117.         ((currentView: any): Uint8Array).subarray(0, writtenBytes),
    
  118.       );
    
  119.       currentView = new Uint8Array(VIEW_SIZE);
    
  120.       writtenBytes = 0;
    
  121.     }
    
  122.     writeToDestination(destination, chunk);
    
  123.     return;
    
  124.   }
    
  125. 
    
  126.   let bytesToWrite = chunk;
    
  127.   const allowableBytes = ((currentView: any): Uint8Array).length - writtenBytes;
    
  128.   if (allowableBytes < bytesToWrite.byteLength) {
    
  129.     // this chunk would overflow the current view. We enqueue a full view
    
  130.     // and start a new view with the remaining chunk
    
  131.     if (allowableBytes === 0) {
    
  132.       // the current view is already full, send it
    
  133.       writeToDestination(destination, (currentView: any));
    
  134.     } else {
    
  135.       // fill up the current view and apply the remaining chunk bytes
    
  136.       // to a new view.
    
  137.       ((currentView: any): Uint8Array).set(
    
  138.         bytesToWrite.subarray(0, allowableBytes),
    
  139.         writtenBytes,
    
  140.       );
    
  141.       writtenBytes += allowableBytes;
    
  142.       writeToDestination(destination, (currentView: any));
    
  143.       bytesToWrite = bytesToWrite.subarray(allowableBytes);
    
  144.     }
    
  145.     currentView = new Uint8Array(VIEW_SIZE);
    
  146.     writtenBytes = 0;
    
  147.   }
    
  148.   ((currentView: any): Uint8Array).set(bytesToWrite, writtenBytes);
    
  149.   writtenBytes += bytesToWrite.byteLength;
    
  150. 
    
  151.   if (writtenBytes === VIEW_SIZE) {
    
  152.     writeToDestination(destination, (currentView: any));
    
  153.     currentView = new Uint8Array(VIEW_SIZE);
    
  154.     writtenBytes = 0;
    
  155.   }
    
  156. }
    
  157. 
    
  158. export function writeChunk(
    
  159.   destination: Destination,
    
  160.   chunk: PrecomputedChunk | Chunk | BinaryChunk,
    
  161. ): void {
    
  162.   if (typeof chunk === 'string') {
    
  163.     writeStringChunk(destination, chunk);
    
  164.   } else {
    
  165.     writeViewChunk(destination, ((chunk: any): PrecomputedChunk | BinaryChunk));
    
  166.   }
    
  167. }
    
  168. 
    
  169. function writeToDestination(
    
  170.   destination: Destination,
    
  171.   view: string | Uint8Array,
    
  172. ) {
    
  173.   const currentHasCapacity = destination.write(view);
    
  174.   destinationHasCapacity = destinationHasCapacity && currentHasCapacity;
    
  175. }
    
  176. 
    
  177. export function writeChunkAndReturn(
    
  178.   destination: Destination,
    
  179.   chunk: PrecomputedChunk | Chunk,
    
  180. ): boolean {
    
  181.   writeChunk(destination, chunk);
    
  182.   return destinationHasCapacity;
    
  183. }
    
  184. 
    
  185. export function completeWriting(destination: Destination) {
    
  186.   if (currentView && writtenBytes > 0) {
    
  187.     destination.write(currentView.subarray(0, writtenBytes));
    
  188.   }
    
  189.   currentView = null;
    
  190.   writtenBytes = 0;
    
  191.   destinationHasCapacity = true;
    
  192. }
    
  193. 
    
  194. export function close(destination: Destination) {
    
  195.   destination.end();
    
  196. }
    
  197. 
    
  198. const textEncoder = new TextEncoder();
    
  199. 
    
  200. export function stringToChunk(content: string): Chunk {
    
  201.   return content;
    
  202. }
    
  203. 
    
  204. const precomputedChunkSet = __DEV__ ? new Set<PrecomputedChunk>() : null;
    
  205. 
    
  206. export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
    
  207.   const precomputedChunk = textEncoder.encode(content);
    
  208. 
    
  209.   if (__DEV__) {
    
  210.     if (precomputedChunkSet) {
    
  211.       precomputedChunkSet.add(precomputedChunk);
    
  212.     }
    
  213.   }
    
  214. 
    
  215.   return precomputedChunk;
    
  216. }
    
  217. 
    
  218. export function typedArrayToBinaryChunk(
    
  219.   content: $ArrayBufferView,
    
  220. ): BinaryChunk {
    
  221.   // Convert any non-Uint8Array array to Uint8Array. We could avoid this for Uint8Arrays.
    
  222.   return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
    
  223. }
    
  224. 
    
  225. export function clonePrecomputedChunk(
    
  226.   precomputedChunk: PrecomputedChunk,
    
  227. ): PrecomputedChunk {
    
  228.   return precomputedChunk.length > VIEW_SIZE
    
  229.     ? precomputedChunk.slice()
    
  230.     : precomputedChunk;
    
  231. }
    
  232. 
    
  233. export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
    
  234.   return typeof chunk === 'string'
    
  235.     ? Buffer.byteLength(chunk, 'utf8')
    
  236.     : chunk.byteLength;
    
  237. }
    
  238. 
    
  239. export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number {
    
  240.   return chunk.byteLength;
    
  241. }
    
  242. 
    
  243. export function closeWithError(destination: Destination, error: mixed): void {
    
  244.   // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
    
  245.   destination.destroy(error);
    
  246. }
    
  247. 
    
  248. export function createFastHash(input: string): string | number {
    
  249.   const hash = createHash('md5');
    
  250.   hash.update(input);
    
  251.   return hash.digest('hex');
    
  252. }