main
js 241 lines 6.73 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 // Fork of ReactServerStreamConfigNode for the FB Flight server.
11 // Avoids importing from 'util' and 'crypto'. Uses the global TextEncoder
12 // and stubs createFastHash.
13
14 import type {Writable} from 'stream';
15
16 import type {TextEncoder as TextEncoderType} from 'util';
17
18 interface MightBeFlushable {
19 flush?: () => void;
20 }
21
22 export type Destination = Writable & MightBeFlushable;
23
24 export type PrecomputedChunk = Uint8Array;
25 export opaque type Chunk = string;
26 export type BinaryChunk = Uint8Array;
27
28 export function scheduleWork(callback: () => void) {
29 setImmediate(callback);
30 }
31
32 export function scheduleMicrotask(callback: () => void) {
33 Promise.resolve().then(callback);
34 }
35
36 export function flushBuffered(destination: Destination) {
37 // If we don't have any more data to send right now.
38 // Flush whatever is in the buffer to the wire.
39 if (typeof destination.flush === 'function') {
40 // By convention the Zlib streams provide a flush function for this purpose.
41 // For Express, compression middleware adds this method.
42 destination.flush();
43 }
44 }
45
46 const VIEW_SIZE = 4096;
47 let currentView = null;
48 let writtenBytes = 0;
49 let destinationHasCapacity = true;
50
51 export function beginWriting(destination: Destination) {
52 currentView = new Uint8Array(VIEW_SIZE);
53 writtenBytes = 0;
54 destinationHasCapacity = true;
55 }
56
57 function writeStringChunk(destination: Destination, stringChunk: string) {
58 if (stringChunk.length === 0) {
59 return;
60 }
61 // maximum possible view needed to encode entire string
62 if (stringChunk.length * 3 > VIEW_SIZE) {
63 if (writtenBytes > 0) {
64 writeToDestination(
65 destination,
66 (currentView as any as Uint8Array).subarray(0, writtenBytes),
67 );
68 currentView = new Uint8Array(VIEW_SIZE);
69 writtenBytes = 0;
70 }
71 // Write the raw string chunk and let the consumer handle the encoding.
72 writeToDestination(destination, stringChunk);
73 return;
74 }
75
76 let target: Uint8Array = currentView as any;
77 if (writtenBytes > 0) {
78 target = (currentView as any as Uint8Array).subarray(writtenBytes);
79 }
80 const {read, written} = textEncoder.encodeInto(stringChunk, target);
81 writtenBytes += written;
82
83 if (read < stringChunk.length) {
84 writeToDestination(
85 destination,
86 (currentView as any).subarray(0, writtenBytes),
87 );
88 currentView = new Uint8Array(VIEW_SIZE);
89 writtenBytes = textEncoder.encodeInto(
90 stringChunk.slice(read),
91 currentView as any,
92 ).written;
93 }
94
95 if (writtenBytes === VIEW_SIZE) {
96 writeToDestination(destination, currentView as any);
97 currentView = new Uint8Array(VIEW_SIZE);
98 writtenBytes = 0;
99 }
100 }
101
102 function writeViewChunk(
103 destination: Destination,
104 chunk: PrecomputedChunk | BinaryChunk,
105 ) {
106 if (chunk.byteLength === 0) {
107 return;
108 }
109 if (chunk.byteLength > VIEW_SIZE) {
110 if (writtenBytes > 0) {
111 writeToDestination(
112 destination,
113 (currentView as any as Uint8Array).subarray(0, writtenBytes),
114 );
115 currentView = new Uint8Array(VIEW_SIZE);
116 writtenBytes = 0;
117 }
118 writeToDestination(destination, chunk);
119 return;
120 }
121
122 let bytesToWrite = chunk;
123 const allowableBytes =
124 (currentView as any as Uint8Array).length - writtenBytes;
125 if (allowableBytes < bytesToWrite.byteLength) {
126 if (allowableBytes === 0) {
127 writeToDestination(destination, currentView as any);
128 } else {
129 (currentView as any as Uint8Array).set(
130 bytesToWrite.subarray(0, allowableBytes),
131 writtenBytes,
132 );
133 writtenBytes += allowableBytes;
134 writeToDestination(destination, currentView as any);
135 bytesToWrite = bytesToWrite.subarray(allowableBytes);
136 }
137 currentView = new Uint8Array(VIEW_SIZE);
138 writtenBytes = 0;
139 }
140 (currentView as any as Uint8Array).set(bytesToWrite, writtenBytes);
141 writtenBytes += bytesToWrite.byteLength;
142
143 if (writtenBytes === VIEW_SIZE) {
144 writeToDestination(destination, currentView as any);
145 currentView = new Uint8Array(VIEW_SIZE);
146 writtenBytes = 0;
147 }
148 }
149
150 export function writeChunk(
151 destination: Destination,
152 chunk: PrecomputedChunk | Chunk | BinaryChunk,
153 ): void {
154 if (typeof chunk === 'string') {
155 writeStringChunk(destination, chunk);
156 } else {
157 writeViewChunk(destination, chunk as any as PrecomputedChunk | BinaryChunk);
158 }
159 }
160
161 function writeToDestination(
162 destination: Destination,
163 view: string | Uint8Array,
164 ) {
165 const currentHasCapacity = destination.write(view);
166 destinationHasCapacity = destinationHasCapacity && currentHasCapacity;
167 }
168
169 export function writeChunkAndReturn(
170 destination: Destination,
171 chunk: PrecomputedChunk | Chunk,
172 ): boolean {
173 writeChunk(destination, chunk);
174 return destinationHasCapacity;
175 }
176
177 export function completeWriting(destination: Destination) {
178 if (currentView && writtenBytes > 0) {
179 destination.write(currentView.subarray(0, writtenBytes));
180 }
181 currentView = null;
182 writtenBytes = 0;
183 destinationHasCapacity = true;
184 }
185
186 export function close(destination: Destination) {
187 destination.end();
188 }
189
190 export const textEncoder: TextEncoderType = new TextEncoder() as any;
191
192 export function stringToChunk(content: string): Chunk {
193 return content;
194 }
195
196 export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
197 const precomputedChunk = textEncoder.encode(content);
198
199 if (__DEV__) {
200 if (precomputedChunk.byteLength > VIEW_SIZE) {
201 console.error(
202 'precomputed chunks must be smaller than the view size configured for this host. This is a bug in React.',
203 );
204 }
205 }
206
207 return precomputedChunk;
208 }
209
210 export function typedArrayToBinaryChunk(
211 content: $ArrayBufferView,
212 ): BinaryChunk {
213 return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
214 }
215
216 export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
217 return typeof chunk === 'string'
218 ? Buffer.byteLength(chunk, 'utf8')
219 : chunk.byteLength;
220 }
221
222 export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number {
223 return chunk.byteLength;
224 }
225
226 export function closeWithError(destination: Destination, error: mixed): void {
227 // $FlowFixMe[incompatible-type]: This is an Error object or the destination accepts other types.
228 destination.destroy(error);
229 }
230
231 export function createFastHash(_input: string): string | number {
232 throw new Error('createFastHash is not supported in this environment.');
233 }
234
235 export function readAsDataURL(blob: Blob): Promise<string> {
236 return blob.arrayBuffer().then(arrayBuffer => {
237 const encoded = Buffer.from(arrayBuffer).toString('base64');
238 const mimeType = blob.type || 'application/octet-stream';
239 return 'data:' + mimeType + ';base64,' + encoded;
240 });
241 }