main
js 251 lines 7.35 KB
Raw
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 const scheduleMicrotask = queueMicrotask;
30
31 export function flushBuffered(destination: Destination) {
32 // If we don't have any more data to send right now.
33 // Flush whatever is in the buffer to the wire.
34 if (typeof destination.flush === 'function') {
35 // By convention the Zlib streams provide a flush function for this purpose.
36 // For Express, compression middleware adds this method.
37 destination.flush();
38 }
39 }
40
41 // Chunks larger than VIEW_SIZE are written directly, without copying into the
42 // internal view buffer. This must be at least half of Node's internal Buffer
43 // pool size (8192) to avoid corrupting the pool when using
44 // renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
45 const VIEW_SIZE = 4096;
46 let currentView = null;
47 let writtenBytes = 0;
48 let destinationHasCapacity = true;
49
50 export function beginWriting(destination: Destination) {
51 currentView = new Uint8Array(VIEW_SIZE);
52 writtenBytes = 0;
53 destinationHasCapacity = true;
54 }
55
56 function writeStringChunk(destination: Destination, stringChunk: string) {
57 if (stringChunk.length === 0) {
58 return;
59 }
60 // maximum possible view needed to encode entire string
61 if (stringChunk.length * 3 > VIEW_SIZE) {
62 if (writtenBytes > 0) {
63 writeToDestination(
64 destination,
65 (currentView as any as Uint8Array).subarray(0, writtenBytes),
66 );
67 currentView = new Uint8Array(VIEW_SIZE);
68 writtenBytes = 0;
69 }
70 // Write the raw string chunk and let the consumer handle the encoding.
71 writeToDestination(destination, stringChunk);
72 return;
73 }
74
75 let target: Uint8Array = currentView as any;
76 if (writtenBytes > 0) {
77 target = (currentView as any as Uint8Array).subarray(writtenBytes);
78 }
79 const {read, written} = textEncoder.encodeInto(stringChunk, target);
80 writtenBytes += written;
81
82 if (read < stringChunk.length) {
83 writeToDestination(
84 destination,
85 (currentView as any).subarray(0, writtenBytes),
86 );
87 currentView = new Uint8Array(VIEW_SIZE);
88 writtenBytes = textEncoder.encodeInto(
89 stringChunk.slice(read),
90 currentView as any,
91 ).written;
92 }
93
94 if (writtenBytes === VIEW_SIZE) {
95 writeToDestination(destination, currentView as any);
96 currentView = new Uint8Array(VIEW_SIZE);
97 writtenBytes = 0;
98 }
99 }
100
101 function writeViewChunk(
102 destination: Destination,
103 chunk: PrecomputedChunk | BinaryChunk,
104 ) {
105 if (chunk.byteLength === 0) {
106 return;
107 }
108 if (chunk.byteLength > VIEW_SIZE) {
109 // this chunk may overflow a single view which implies it was not
110 // one that is cached by the streaming renderer. We will enqueu
111 // it directly and expect it is not re-used
112 if (writtenBytes > 0) {
113 writeToDestination(
114 destination,
115 (currentView as any as Uint8Array).subarray(0, writtenBytes),
116 );
117 currentView = new Uint8Array(VIEW_SIZE);
118 writtenBytes = 0;
119 }
120 writeToDestination(destination, chunk);
121 return;
122 }
123
124 let bytesToWrite = chunk;
125 const allowableBytes =
126 (currentView as any as Uint8Array).length - writtenBytes;
127 if (allowableBytes < bytesToWrite.byteLength) {
128 // this chunk would overflow the current view. We enqueue a full view
129 // and start a new view with the remaining chunk
130 if (allowableBytes === 0) {
131 // the current view is already full, send it
132 writeToDestination(destination, currentView as any);
133 } else {
134 // fill up the current view and apply the remaining chunk bytes
135 // to a new view.
136 (currentView as any as Uint8Array).set(
137 bytesToWrite.subarray(0, allowableBytes),
138 writtenBytes,
139 );
140 writtenBytes += allowableBytes;
141 writeToDestination(destination, currentView as any);
142 bytesToWrite = bytesToWrite.subarray(allowableBytes);
143 }
144 currentView = new Uint8Array(VIEW_SIZE);
145 writtenBytes = 0;
146 }
147 (currentView as any as Uint8Array).set(bytesToWrite, writtenBytes);
148 writtenBytes += bytesToWrite.byteLength;
149
150 if (writtenBytes === VIEW_SIZE) {
151 writeToDestination(destination, currentView as any);
152 currentView = new Uint8Array(VIEW_SIZE);
153 writtenBytes = 0;
154 }
155 }
156
157 export function writeChunk(
158 destination: Destination,
159 chunk: PrecomputedChunk | Chunk | BinaryChunk,
160 ): void {
161 if (typeof chunk === 'string') {
162 writeStringChunk(destination, chunk);
163 } else {
164 writeViewChunk(destination, chunk as any as PrecomputedChunk | BinaryChunk);
165 }
166 }
167
168 function writeToDestination(
169 destination: Destination,
170 view: string | Uint8Array,
171 ) {
172 const currentHasCapacity = destination.write(view);
173 destinationHasCapacity = destinationHasCapacity && currentHasCapacity;
174 }
175
176 export function writeChunkAndReturn(
177 destination: Destination,
178 chunk: PrecomputedChunk | Chunk,
179 ): boolean {
180 writeChunk(destination, chunk);
181 return destinationHasCapacity;
182 }
183
184 export function completeWriting(destination: Destination) {
185 if (currentView && writtenBytes > 0) {
186 destination.write(currentView.subarray(0, writtenBytes));
187 }
188 currentView = null;
189 writtenBytes = 0;
190 destinationHasCapacity = true;
191 }
192
193 export function close(destination: Destination) {
194 destination.end();
195 }
196
197 export const textEncoder: TextEncoder = new TextEncoder();
198
199 export function stringToChunk(content: string): Chunk {
200 return content;
201 }
202
203 export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
204 const precomputedChunk = textEncoder.encode(content);
205
206 if (__DEV__) {
207 if (precomputedChunk.byteLength > VIEW_SIZE) {
208 console.error(
209 'precomputed chunks must be smaller than the view size configured for this host. This is a bug in React.',
210 );
211 }
212 }
213
214 return precomputedChunk;
215 }
216
217 export function typedArrayToBinaryChunk(
218 content: $ArrayBufferView,
219 ): BinaryChunk {
220 // Convert any non-Uint8Array array to Uint8Array. We could avoid this for Uint8Arrays.
221 return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
222 }
223
224 export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
225 return typeof chunk === 'string'
226 ? Buffer.byteLength(chunk, 'utf8')
227 : chunk.byteLength;
228 }
229
230 export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number {
231 return chunk.byteLength;
232 }
233
234 export function closeWithError(destination: Destination, error: mixed): void {
235 // $FlowFixMe[incompatible-type]: This is an Error object or the destination accepts other types.
236 destination.destroy(error);
237 }
238
239 export function createFastHash(input: string): string | number {
240 const hash = createHash('md5');
241 hash.update(input);
242 return hash.digest('hex');
243 }
244
245 export function readAsDataURL(blob: Blob): Promise<string> {
246 return blob.arrayBuffer().then(arrayBuffer => {
247 const encoded = Buffer.from(arrayBuffer).toString('base64');
248 const mimeType = blob.type || 'application/octet-stream';
249 return 'data:' + mimeType + ';base64,' + encoded;
250 });
251 }