main
js 193 lines 6.33 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 export type Destination = ReadableStreamController;
11
12 export type PrecomputedChunk = Uint8Array;
13 export opaque type Chunk = Uint8Array;
14 export type BinaryChunk = Uint8Array;
15
16 function handleErrorInNextTick(error: any) {
17 setTimeout(() => {
18 throw error;
19 });
20 }
21
22 const LocalPromise = Promise;
23
24 export const scheduleMicrotask: (callback: () => void) => void =
25 typeof queueMicrotask === 'function'
26 ? queueMicrotask
27 : callback => {
28 LocalPromise.resolve(null).then(callback).catch(handleErrorInNextTick);
29 };
30
31 export function scheduleWork(callback: () => void) {
32 setTimeout(callback, 0);
33 }
34
35 export function flushBuffered(destination: Destination) {
36 // WHATWG Streams do not yet have a way to flush the underlying
37 // transform streams. https://github.com/whatwg/streams/issues/960
38 }
39
40 // Chunks larger than VIEW_SIZE are written directly, without copying into the
41 // internal view buffer. This must be at least half of Node's internal Buffer
42 // pool size (8192) to avoid corrupting the pool when using
43 // renderToReadableStream, which uses a byte stream that detaches ArrayBuffers.
44 const VIEW_SIZE = 4096;
45 let currentView = null;
46 let writtenBytes = 0;
47
48 export function beginWriting(destination: Destination) {
49 currentView = new Uint8Array(VIEW_SIZE);
50 writtenBytes = 0;
51 }
52
53 export function writeChunk(
54 destination: Destination,
55 chunk: PrecomputedChunk | Chunk | BinaryChunk,
56 ): void {
57 if (chunk.byteLength === 0) {
58 return;
59 }
60
61 if (chunk.byteLength > VIEW_SIZE) {
62 // this chunk may overflow a single view which implies it was not
63 // one that is cached by the streaming renderer. We will enqueu
64 // it directly and expect it is not re-used
65 if (writtenBytes > 0) {
66 destination.enqueue(
67 new Uint8Array(
68 (currentView as any as Uint8Array).buffer,
69 0,
70 writtenBytes,
71 ),
72 );
73 currentView = new Uint8Array(VIEW_SIZE);
74 writtenBytes = 0;
75 }
76 destination.enqueue(chunk);
77 return;
78 }
79
80 let bytesToWrite = chunk;
81 const allowableBytes =
82 (currentView as any as Uint8Array).length - writtenBytes;
83 if (allowableBytes < bytesToWrite.byteLength) {
84 // this chunk would overflow the current view. We enqueue a full view
85 // and start a new view with the remaining chunk
86 if (allowableBytes === 0) {
87 // the current view is already full, send it
88 destination.enqueue(currentView);
89 } else {
90 // fill up the current view and apply the remaining chunk bytes
91 // to a new view.
92 (currentView as any as Uint8Array).set(
93 bytesToWrite.subarray(0, allowableBytes),
94 writtenBytes,
95 );
96 // writtenBytes += allowableBytes; // this can be skipped because we are going to immediately reset the view
97 destination.enqueue(currentView);
98 bytesToWrite = bytesToWrite.subarray(allowableBytes);
99 }
100 currentView = new Uint8Array(VIEW_SIZE);
101 writtenBytes = 0;
102 }
103 (currentView as any as Uint8Array).set(bytesToWrite, writtenBytes);
104 writtenBytes += bytesToWrite.byteLength;
105 }
106
107 export function writeChunkAndReturn(
108 destination: Destination,
109 chunk: PrecomputedChunk | Chunk | BinaryChunk,
110 ): boolean {
111 writeChunk(destination, chunk);
112 // in web streams there is no backpressure so we can alwas write more
113 return true;
114 }
115
116 export function completeWriting(destination: Destination) {
117 if (currentView && writtenBytes > 0) {
118 destination.enqueue(new Uint8Array(currentView.buffer, 0, writtenBytes));
119 currentView = null;
120 writtenBytes = 0;
121 }
122 }
123
124 export function close(destination: Destination) {
125 destination.close();
126 }
127
128 const textEncoder = new TextEncoder();
129
130 export function stringToChunk(content: string): Chunk {
131 return textEncoder.encode(content);
132 }
133
134 export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
135 const precomputedChunk = textEncoder.encode(content);
136
137 if (__DEV__) {
138 if (precomputedChunk.byteLength > VIEW_SIZE) {
139 console.error(
140 'precomputed chunks must be smaller than the view size configured for this host. This is a bug in React.',
141 );
142 }
143 }
144
145 return precomputedChunk;
146 }
147
148 export function typedArrayToBinaryChunk(
149 content: $ArrayBufferView,
150 ): BinaryChunk {
151 // Convert any non-Uint8Array array to Uint8Array. We could avoid this for Uint8Arrays.
152 // If we passed through this straight to enqueue we wouldn't have to convert it but since
153 // we need to copy the buffer in that case, we need to convert it to copy it.
154 // When we copy it into another array using set() it needs to be a Uint8Array.
155 return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
156 }
157
158 export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
159 return chunk.byteLength;
160 }
161
162 export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number {
163 return chunk.byteLength;
164 }
165
166 export function closeWithError(destination: Destination, error: mixed): void {
167 // $FlowFixMe[method-unbinding]
168 if (typeof destination.error === 'function') {
169 // $FlowFixMe[incompatible-type]: This is an Error object or the destination accepts other types.
170 destination.error(error);
171 } else {
172 // Earlier implementations doesn't support this method. In that environment you're
173 // supposed to throw from a promise returned but we don't return a promise in our
174 // approach. We could fork this implementation but this is environment is an edge
175 // case to begin with. It's even less common to run this in an older environment.
176 // Even then, this is not where errors are supposed to happen and they get reported
177 // to a global callback in addition to this anyway. So it's fine just to close this.
178 destination.close();
179 }
180 }
181
182 export {createFastHashJS as createFastHash} from 'react-server/src/createFastHashJS';
183
184 export function readAsDataURL(blob: Blob): Promise<string> {
185 return blob.arrayBuffer().then(arrayBuffer => {
186 const encoded =
187 typeof Buffer === 'function' && typeof Buffer.from === 'function'
188 ? Buffer.from(arrayBuffer).toString('base64')
189 : btoa(String.fromCharCode.apply(String, new Uint8Array(arrayBuffer)));
190 const mimeType = blob.type || 'application/octet-stream';
191 return 'data:' + mimeType + ';base64,' + encoded;
192 });
193 }