Skip to content

Commit e8d5dea

Browse files
committed
Emit root strings or typed arrays without outlining
1 parent cecd166 commit e8d5dea

File tree

4 files changed

+207
-45
lines changed

4 files changed

+207
-45
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ import {
7070
export type {CallServerCallback, EncodeFormActionCallback};
7171

7272
interface FlightStreamController {
73-
enqueue(json: UninitializedModel): void;
73+
enqueueValue(value: any): void;
74+
enqueueModel(json: UninitializedModel): void;
7475
close(json: UninitializedModel): void;
7576
error(error: Error): void;
7677
}
@@ -381,6 +382,15 @@ function createInitializedBufferChunk(
381382
return new Chunk(INITIALIZED, value, null, response);
382383
}
383384

385+
function createInitializedIteratorResultChunk<T>(
386+
response: Response,
387+
value: T,
388+
done: boolean,
389+
): InitializedChunk<IteratorResult<T, T>> {
390+
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
391+
return new Chunk(INITIALIZED, {done: done, value: value}, null, response);
392+
}
393+
384394
function createInitializedStreamChunk<
385395
T: ReadableStream | $AsyncIterable<any, any, void>,
386396
>(
@@ -427,7 +437,7 @@ function resolveModelChunk<T>(
427437
// a stream chunk since any other row shouldn't have more than one entry.
428438
const streamChunk: InitializedStreamChunk<any> = (chunk: any);
429439
const controller = streamChunk.reason;
430-
controller.enqueue(value);
440+
controller.enqueueModel(value);
431441
}
432442
return;
433443
}
@@ -1034,8 +1044,17 @@ function resolveModel(
10341044

10351045
function resolveText(response: Response, id: number, text: string): void {
10361046
const chunks = response._chunks;
1037-
// We assume that we always reference large strings after they've been
1038-
// emitted.
1047+
if (enableFlightReadableStream) {
1048+
const chunk = chunks.get(id);
1049+
if (chunk && chunk.status !== PENDING) {
1050+
// If we get more data to an already resolved ID, we assume that it's
1051+
// a stream chunk since any other row shouldn't have more than one entry.
1052+
const streamChunk: InitializedStreamChunk<any> = (chunk: any);
1053+
const controller = streamChunk.reason;
1054+
controller.enqueueValue(text);
1055+
return;
1056+
}
1057+
}
10391058
chunks.set(id, createInitializedTextChunk(response, text));
10401059
}
10411060

@@ -1045,7 +1064,17 @@ function resolveBuffer(
10451064
buffer: $ArrayBufferView | ArrayBuffer,
10461065
): void {
10471066
const chunks = response._chunks;
1048-
// We assume that we always reference buffers after they've been emitted.
1067+
if (enableFlightReadableStream) {
1068+
const chunk = chunks.get(id);
1069+
if (chunk && chunk.status !== PENDING) {
1070+
// If we get more data to an already resolved ID, we assume that it's
1071+
// a stream chunk since any other row shouldn't have more than one entry.
1072+
const streamChunk: InitializedStreamChunk<any> = (chunk: any);
1073+
const controller = streamChunk.reason;
1074+
controller.enqueueValue(buffer);
1075+
return;
1076+
}
1077+
}
10491078
chunks.set(id, createInitializedBufferChunk(response, buffer));
10501079
}
10511080

@@ -1143,7 +1172,17 @@ function startReadableStream<T>(
11431172
});
11441173
let previousBlockedChunk: SomeChunk<T> | null = null;
11451174
const flightController = {
1146-
enqueue(json: UninitializedModel): void {
1175+
enqueueValue(value: T): void {
1176+
if (previousBlockedChunk === null) {
1177+
controller.enqueue(value);
1178+
} else {
1179+
// We're still waiting on a previous chunk so we can't enqueue quite yet.
1180+
previousBlockedChunk.then(function () {
1181+
controller.enqueue(value);
1182+
});
1183+
}
1184+
},
1185+
enqueueModel(json: UninitializedModel): void {
11471186
if (previousBlockedChunk === null) {
11481187
// If we're not blocked on any other chunks, we can try to eagerly initialize
11491188
// this as a fast-path to avoid awaiting them.
@@ -1236,7 +1275,30 @@ function startAsyncIterable<T>(
12361275
let closed = false;
12371276
let nextWriteIndex = 0;
12381277
const flightController = {
1239-
enqueue(value: UninitializedModel): void {
1278+
enqueueValue(value: T): void {
1279+
if (nextWriteIndex === buffer.length) {
1280+
buffer[nextWriteIndex] = createInitializedIteratorResultChunk(
1281+
response,
1282+
value,
1283+
false,
1284+
);
1285+
} else {
1286+
const chunk: PendingChunk<IteratorResult<T, T>> = (buffer[
1287+
nextWriteIndex
1288+
]: any);
1289+
const resolveListeners = chunk.value;
1290+
const rejectListeners = chunk.reason;
1291+
const initializedChunk: InitializedChunk<IteratorResult<T, T>> =
1292+
(chunk: any);
1293+
initializedChunk.status = INITIALIZED;
1294+
initializedChunk.value = {done: false, value: value};
1295+
if (resolveListeners !== null) {
1296+
wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
1297+
}
1298+
}
1299+
nextWriteIndex++;
1300+
},
1301+
enqueueModel(value: UninitializedModel): void {
12401302
if (nextWriteIndex === buffer.length) {
12411303
buffer[nextWriteIndex] = createResolvedIteratorResultChunk(
12421304
response,

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2305,7 +2305,6 @@ describe('ReactFlight', () => {
23052305
return {
23062306
async *[Symbol.asyncIterator]() {
23072307
yield <span>Who</span>;
2308-
yield ' ';
23092308
yield <span>dis?</span>;
23102309
resolve();
23112310
},
@@ -2386,7 +2385,8 @@ describe('ReactFlight', () => {
23862385

23872386
expect(ReactNoop).toMatchRenderedOutput(
23882387
<div>
2389-
<span>Who</span> <span>dis?</span>
2388+
<span>Who</span>
2389+
<span>dis?</span>
23902390
</div>,
23912391
);
23922392
});

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ describe('ReactFlightDOMEdge', () => {
615615
},
616616
);
617617

618-
expect(await readByteLength(stream2)).toBeLessThan(400);
618+
expect(await readByteLength(stream2)).toBeLessThan(300);
619619

620620
const streamedBuffers = [];
621621
const reader = result.getReader();
@@ -672,7 +672,7 @@ describe('ReactFlightDOMEdge', () => {
672672
},
673673
);
674674

675-
expect(await readByteLength(stream2)).toBeLessThan(400);
675+
expect(await readByteLength(stream2)).toBeLessThan(300);
676676

677677
const streamedBuffers = [];
678678
const reader = result.getReader({mode: 'byob'});

packages/react-server/src/ReactFlightServer.js

Lines changed: 134 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,13 +1411,9 @@ function serializeTemporaryReference(
14111411
}
14121412

14131413
function serializeLargeTextString(request: Request, text: string): string {
1414-
request.pendingChunks += 2;
1414+
request.pendingChunks++;
14151415
const textId = request.nextChunkId++;
1416-
const textChunk = stringToChunk(text);
1417-
const binaryLength = byteLengthOfChunk(textChunk);
1418-
const row = textId.toString(16) + ':T' + binaryLength.toString(16) + ',';
1419-
const headerChunk = stringToChunk(row);
1420-
request.completedRegularChunks.push(headerChunk, textChunk);
1416+
emitTextChunk(request, textId, text);
14211417
return serializeByValueID(textId);
14221418
}
14231419

@@ -1467,27 +1463,9 @@ function serializeTypedArray(
14671463
tag: string,
14681464
typedArray: $ArrayBufferView,
14691465
): string {
1470-
if (enableTaint) {
1471-
if (TaintRegistryByteLengths.has(typedArray.byteLength)) {
1472-
// If we have had any tainted values of this length, we check
1473-
// to see if these bytes matches any entries in the registry.
1474-
const tainted = TaintRegistryValues.get(
1475-
binaryToComparableString(typedArray),
1476-
);
1477-
if (tainted !== undefined) {
1478-
throwTaintViolation(tainted.message);
1479-
}
1480-
}
1481-
}
1482-
request.pendingChunks += 2;
1466+
request.pendingChunks++;
14831467
const bufferId = request.nextChunkId++;
1484-
// TODO: Convert to little endian if that's not the server default.
1485-
const binaryChunk = typedArrayToBinaryChunk(typedArray);
1486-
const binaryLength = byteLengthOfBinaryChunk(binaryChunk);
1487-
const row =
1488-
bufferId.toString(16) + ':' + tag + binaryLength.toString(16) + ',';
1489-
const headerChunk = stringToChunk(row);
1490-
request.completedRegularChunks.push(headerChunk, binaryChunk);
1468+
emitTypedArrayChunk(request, bufferId, tag, typedArray);
14911469
return serializeByValueID(bufferId);
14921470
}
14931471

@@ -2321,6 +2299,42 @@ function emitDebugChunk(
23212299
request.completedRegularChunks.push(processedChunk);
23222300
}
23232301

2302+
function emitTypedArrayChunk(
2303+
request: Request,
2304+
id: number,
2305+
tag: string,
2306+
typedArray: $ArrayBufferView,
2307+
): void {
2308+
if (enableTaint) {
2309+
if (TaintRegistryByteLengths.has(typedArray.byteLength)) {
2310+
// If we have had any tainted values of this length, we check
2311+
// to see if these bytes matches any entries in the registry.
2312+
const tainted = TaintRegistryValues.get(
2313+
binaryToComparableString(typedArray),
2314+
);
2315+
if (tainted !== undefined) {
2316+
throwTaintViolation(tainted.message);
2317+
}
2318+
}
2319+
}
2320+
request.pendingChunks++; // Extra chunk for the header.
2321+
// TODO: Convert to little endian if that's not the server default.
2322+
const binaryChunk = typedArrayToBinaryChunk(typedArray);
2323+
const binaryLength = byteLengthOfBinaryChunk(binaryChunk);
2324+
const row = id.toString(16) + ':' + tag + binaryLength.toString(16) + ',';
2325+
const headerChunk = stringToChunk(row);
2326+
request.completedRegularChunks.push(headerChunk, binaryChunk);
2327+
}
2328+
2329+
function emitTextChunk(request: Request, id: number, text: string): void {
2330+
request.pendingChunks++; // Extra chunk for the header.
2331+
const textChunk = stringToChunk(text);
2332+
const binaryLength = byteLengthOfChunk(textChunk);
2333+
const row = id.toString(16) + ':T' + binaryLength.toString(16) + ',';
2334+
const headerChunk = stringToChunk(row);
2335+
request.completedRegularChunks.push(headerChunk, textChunk);
2336+
}
2337+
23242338
function serializeEval(source: string): string {
23252339
if (!__DEV__) {
23262340
// These errors should never make it into a build so we don't need to encode them in codes.json
@@ -2681,6 +2695,96 @@ function forwardDebugInfo(
26812695
}
26822696
}
26832697

2698+
function emitChunk(
2699+
request: Request,
2700+
task: Task,
2701+
value: ReactClientValue,
2702+
): void {
2703+
const id = task.id;
2704+
// For certain types we have special types, we typically outlined them but
2705+
// we can emit them directly for this row instead of through an indirection.
2706+
if (typeof value === 'string') {
2707+
if (enableTaint) {
2708+
const tainted = TaintRegistryValues.get(value);
2709+
if (tainted !== undefined) {
2710+
throwTaintViolation(tainted.message);
2711+
}
2712+
}
2713+
emitTextChunk(request, id, value);
2714+
return;
2715+
}
2716+
if (enableBinaryFlight) {
2717+
if (value instanceof ArrayBuffer) {
2718+
emitTypedArrayChunk(request, id, 'A', new Uint8Array(value));
2719+
return;
2720+
}
2721+
if (value instanceof Int8Array) {
2722+
// char
2723+
emitTypedArrayChunk(request, id, 'O', value);
2724+
return;
2725+
}
2726+
if (value instanceof Uint8Array) {
2727+
// unsigned char
2728+
emitTypedArrayChunk(request, id, 'o', value);
2729+
return;
2730+
}
2731+
if (value instanceof Uint8ClampedArray) {
2732+
// unsigned clamped char
2733+
emitTypedArrayChunk(request, id, 'U', value);
2734+
return;
2735+
}
2736+
if (value instanceof Int16Array) {
2737+
// sort
2738+
emitTypedArrayChunk(request, id, 'S', value);
2739+
return;
2740+
}
2741+
if (value instanceof Uint16Array) {
2742+
// unsigned short
2743+
emitTypedArrayChunk(request, id, 's', value);
2744+
return;
2745+
}
2746+
if (value instanceof Int32Array) {
2747+
// long
2748+
emitTypedArrayChunk(request, id, 'L', value);
2749+
return;
2750+
}
2751+
if (value instanceof Uint32Array) {
2752+
// unsigned long
2753+
emitTypedArrayChunk(request, id, 'l', value);
2754+
return;
2755+
}
2756+
if (value instanceof Float32Array) {
2757+
// float
2758+
emitTypedArrayChunk(request, id, 'G', value);
2759+
return;
2760+
}
2761+
if (value instanceof Float64Array) {
2762+
// double
2763+
emitTypedArrayChunk(request, id, 'g', value);
2764+
return;
2765+
}
2766+
if (value instanceof BigInt64Array) {
2767+
// number
2768+
emitTypedArrayChunk(request, id, 'M', value);
2769+
return;
2770+
}
2771+
if (value instanceof BigUint64Array) {
2772+
// unsigned number
2773+
// We use "m" instead of "n" since JSON can start with "null"
2774+
emitTypedArrayChunk(request, id, 'm', value);
2775+
return;
2776+
}
2777+
if (value instanceof DataView) {
2778+
emitTypedArrayChunk(request, id, 'V', value);
2779+
return;
2780+
}
2781+
}
2782+
// For anything else we need to try to serialize it using JSON.
2783+
// $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
2784+
const json: string = stringify(value, task.toJSON);
2785+
emitModelChunk(request, task.id, json);
2786+
}
2787+
26842788
const emptyRoot = {};
26852789

26862790
function retryTask(request: Request, task: Task): void {
@@ -2725,19 +2829,17 @@ function retryTask(request: Request, task: Task): void {
27252829
task.keyPath = null;
27262830
task.implicitSlot = false;
27272831

2728-
let json: string;
27292832
if (typeof resolvedModel === 'object' && resolvedModel !== null) {
27302833
// Object might contain unresolved values like additional elements.
27312834
// This is simulating what the JSON loop would do if this was part of it.
2732-
// $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
2733-
json = stringify(resolvedModel, task.toJSON);
2835+
emitChunk(request, task, resolvedModel);
27342836
} else {
27352837
// If the value is a string, it means it's a terminal value and we already escaped it
27362838
// We don't need to escape it again so it's not passed the toJSON replacer.
27372839
// $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
2738-
json = stringify(resolvedModel);
2840+
const json: string = stringify(resolvedModel);
2841+
emitModelChunk(request, task.id, json);
27392842
}
2740-
emitModelChunk(request, task.id, json);
27412843

27422844
request.abortableTasks.delete(task);
27432845
task.status = COMPLETED;
@@ -2789,9 +2891,7 @@ function tryStreamTask(request: Request, task: Task): void {
27892891
debugID = null;
27902892
}
27912893
try {
2792-
// $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
2793-
const json: string = stringify(task.model, task.toJSON);
2794-
emitModelChunk(request, task.id, json);
2894+
emitChunk(request, task, task.model);
27952895
} finally {
27962896
if (__DEV__) {
27972897
debugID = prevDebugID;

0 commit comments

Comments
 (0)