/**
* 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 {Writable} from 'stream';
import {TextEncoder} from 'util';
import {createHash} from 'crypto';
interface MightBeFlushable {
flush?: () => void;
}
export type Destination = Writable & MightBeFlushable;
export type PrecomputedChunk = Uint8Array;
export opaque type Chunk = string;
export type BinaryChunk = Uint8Array;
export function scheduleWork(callback: () => void) {
setImmediate(callback);
}
export function flushBuffered(destination: Destination) {
// If we don't have any more data to send right now.
// Flush whatever is in the buffer to the wire.
if (typeof destination.flush === 'function') {
// By convention the Zlib streams provide a flush function for this purpose.
// For Express, compression middleware adds this method.
destination.flush();
}
}
const VIEW_SIZE = 2048;
let currentView = null;
let writtenBytes = 0;
let destinationHasCapacity = true;
export function beginWriting(destination: Destination) {
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
destinationHasCapacity = true;
}
function writeStringChunk(destination: Destination, stringChunk: string) {
if (stringChunk.length === 0) {
return;
}
// maximum possible view needed to encode entire string
if (stringChunk.length * 3 > VIEW_SIZE) {
if (writtenBytes > 0) {
writeToDestination(
destination,
((currentView: any): Uint8Array).subarray(0, writtenBytes),
);
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
}
writeToDestination(destination, textEncoder.encode(stringChunk));
return;
}
let target: Uint8Array = (currentView: any);
if (writtenBytes > 0) {
target = ((currentView: any): Uint8Array).subarray(writtenBytes);
}
const {read, written} = textEncoder.encodeInto(stringChunk, target);
writtenBytes += written;
if (read < stringChunk.length) {
writeToDestination(
destination,
(currentView: any).subarray(0, writtenBytes),
);
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = textEncoder.encodeInto(
stringChunk.slice(read),
(currentView: any),
).written;
}
if (writtenBytes === VIEW_SIZE) {
writeToDestination(destination, (currentView: any));
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
}
}
function writeViewChunk(
destination: Destination,
chunk: PrecomputedChunk | BinaryChunk,
) {
if (chunk.byteLength === 0) {
return;
}
if (chunk.byteLength > VIEW_SIZE) {
if (__DEV__) {
if (precomputedChunkSet && precomputedChunkSet.has(chunk)) {
console.error(
'A large precomputed chunk was passed to writeChunk without being copied.' +
' Large chunks get enqueued directly and are not copied. This is incompatible with precomputed chunks because you cannot enqueue the same precomputed chunk twice.' +
' Use "cloneChunk" to make a copy of this large precomputed chunk before writing it. This is a bug in React.',
);
}
}
// this chunk may overflow a single view which implies it was not
// one that is cached by the streaming renderer. We will enqueu
// it directly and expect it is not re-used
if (writtenBytes > 0) {
writeToDestination(
destination,
((currentView: any): Uint8Array).subarray(0, writtenBytes),
);
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
}
writeToDestination(destination, chunk);
return;
}
let bytesToWrite = chunk;
const allowableBytes = ((currentView: any): Uint8Array).length - writtenBytes;
if (allowableBytes < bytesToWrite.byteLength) {
// this chunk would overflow the current view. We enqueue a full view
// and start a new view with the remaining chunk
if (allowableBytes === 0) {
// the current view is already full, send it
writeToDestination(destination, (currentView: any));
} else {
// fill up the current view and apply the remaining chunk bytes
// to a new view.
((currentView: any): Uint8Array).set(
bytesToWrite.subarray(0, allowableBytes),
writtenBytes,
);
writtenBytes += allowableBytes;
writeToDestination(destination, (currentView: any));
bytesToWrite = bytesToWrite.subarray(allowableBytes);
}
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
}
((currentView: any): Uint8Array).set(bytesToWrite, writtenBytes);
writtenBytes += bytesToWrite.byteLength;
if (writtenBytes === VIEW_SIZE) {
writeToDestination(destination, (currentView: any));
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
}
}
export function writeChunk(
destination: Destination,
chunk: PrecomputedChunk | Chunk | BinaryChunk,
): void {
if (typeof chunk === 'string') {
writeStringChunk(destination, chunk);
} else {
writeViewChunk(destination, ((chunk: any): PrecomputedChunk | BinaryChunk));
}
}
function writeToDestination(
destination: Destination,
view: string | Uint8Array,
) {
const currentHasCapacity = destination.write(view);
destinationHasCapacity = destinationHasCapacity && currentHasCapacity;
}
export function writeChunkAndReturn(
destination: Destination,
chunk: PrecomputedChunk | Chunk,
): boolean {
writeChunk(destination, chunk);
return destinationHasCapacity;
}
export function completeWriting(destination: Destination) {
if (currentView && writtenBytes > 0) {
destination.write(currentView.subarray(0, writtenBytes));
}
currentView = null;
writtenBytes = 0;
destinationHasCapacity = true;
}
export function close(destination: Destination) {
destination.end();
}
const textEncoder = new TextEncoder();
export function stringToChunk(content: string): Chunk {
return content;
}
const precomputedChunkSet = __DEV__ ? new Set<PrecomputedChunk>() : null;
export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
const precomputedChunk = textEncoder.encode(content);
if (__DEV__) {
if (precomputedChunkSet) {
precomputedChunkSet.add(precomputedChunk);
}
}
return precomputedChunk;
}
export function typedArrayToBinaryChunk(
content: $ArrayBufferView,
): BinaryChunk {
// Convert any non-Uint8Array array to Uint8Array. We could avoid this for Uint8Arrays.
return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
}
export function clonePrecomputedChunk(
precomputedChunk: PrecomputedChunk,
): PrecomputedChunk {
return precomputedChunk.length > VIEW_SIZE
? precomputedChunk.slice()
: precomputedChunk;
}
export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
return typeof chunk === 'string'
? Buffer.byteLength(chunk, 'utf8')
: chunk.byteLength;
}
export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number {
return chunk.byteLength;
}
export function closeWithError(destination: Destination, error: mixed): void {
// $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
destination.destroy(error);
}
export function createFastHash(input: string): string | number {
const hash = createHash('md5');
hash.update(input);
return hash.digest('hex');
}