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