Refactor Flight Encoding (#26082)
This is just shifting around some encoding strategies for Flight in preparation for more types. ``` S1:"react.suspense" J2:["$", "$1", {children: "@3"}] J3:"Hello" ``` ``` 1:"$Sreact.suspense" 2:["$", "$1", {children: "$L3"}] 3:"Hello" ```
Sebastian Markbåge committed
Jan 31, 2023 at 12:41 UTC
977bccd24de2b062d2c114e6cf160d2bd9ed9493
10 files changed
+65
-154
packages/react-client/src/ReactFlightClient.js
+24
-47
@@ -201,14 +201,6 @@ function createErrorChunk<T>(
201
return new Chunk(ERRORED, null, error, response);
202
}
203
204
-function createInitializedChunk<T>(
205
- response: Response,
206
- value: T,
207
-): InitializedChunk<T> {
208
- // $FlowFixMe Flow doesn't support functions as constructors
209
- return new Chunk(INITIALIZED, value, null, response);
210
-}
211
-
204
function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void {
205
for (let i = 0; i < listeners.length; i++) {
206
const listener = listeners[i];
@@ -483,14 +475,32 @@ export function parseModelString(
475
key: string,
476
value: string,
477
): any {
486
- switch (value[0]) {
487
- case '$': {
488
- if (value === '$') {
489
- return REACT_ELEMENT_TYPE;
490
- } else if (value[1] === '$' || value[1] === '@') {
478
+ if (value[0] === '$') {
479
+ if (value === '$') {
480
+ // A very common symbol.
481
+ return REACT_ELEMENT_TYPE;
482
+ }
483
+ switch (value[1]) {
484
+ case '$': {
485
// This was an escaped string value.
486
return value.substring(1);
493
- } else {
487
+ }
488
+ case 'L': {
489
+ // Lazy node
490
+ const id = parseInt(value.substring(2), 16);
491
+ const chunk = getChunk(response, id);
492
+ // We create a React.lazy wrapper around any lazy values.
493
+ // When passed into React, we'll know how to suspend on this.
494
+ return createLazyChunkWrapper(chunk);
495
+ }
496
+ case 'S': {
497
+ return Symbol.for(value.substring(2));
498
+ }
499
+ case 'P': {
500
+ return getOrCreateServerContext(value.substring(2)).Provider;
501
+ }
502
+ default: {
503
+ // We assume that anything else is a reference ID.
504
const id = parseInt(value.substring(1), 16);
505
const chunk = getChunk(response, id);
506
switch (chunk.status) {
@@ -518,13 +528,6 @@ export function parseModelString(
528
}
529
}
530
}
521
- case '@': {
522
- const id = parseInt(value.substring(1), 16);
523
- const chunk = getChunk(response, id);
524
- // We create a React.lazy wrapper around any lazy values.
525
- // When passed into React, we'll know how to suspend on this.
526
- return createLazyChunkWrapper(chunk);
527
- }
531
}
532
return value;
533
}
@@ -566,21 +569,6 @@ export function resolveModel(
569
}
570
}
571
569
-export function resolveProvider(
570
- response: Response,
571
- id: number,
572
- contextName: string,
573
-): void {
574
- const chunks = response._chunks;
575
- chunks.set(
576
- id,
577
- createInitializedChunk(
578
- response,
579
- getOrCreateServerContext(contextName).Provider,
580
- ),
581
- );
582
-}
583
-
572
export function resolveModule(
573
response: Response,
574
id: number,
@@ -626,17 +614,6 @@ export function resolveModule(
614
}
615
}
616
629
-export function resolveSymbol(
630
- response: Response,
631
- id: number,
632
- name: string,
633
-): void {
634
- const chunks = response._chunks;
635
- // We assume that we'll always emit the symbol before anything references it
636
- // to save a few bytes.
637
- chunks.set(id, createInitializedChunk(response, Symbol.for(name)));
638
-}
639
-
617
type ErrorWithDigest = Error & {digest?: string};
618
export function resolveErrorProd(
619
response: Response,
packages/react-client/src/ReactFlightClientStream.js
+9
-24
@@ -14,8 +14,6 @@ import type {BundlerConfig} from './ReactFlightClientHostConfig';
14
import {
15
resolveModule,
16
resolveModel,
17
- resolveProvider,
18
- resolveSymbol,
17
resolveErrorProd,
18
resolveErrorDev,
19
createResponse as createResponseBase,
@@ -36,33 +34,20 @@ function processFullRow(response: Response, row: string): void {
34
if (row === '') {
35
return;
36
}
39
- const tag = row[0];
37
+ const colon = row.indexOf(':', 0);
38
+ const id = parseInt(row.substring(0, colon), 16);
39
+ const tag = row[colon + 1];
40
// When tags that are not text are added, check them here before
41
// parsing the row as text.
42
// switch (tag) {
43
// }
44
- const colon = row.indexOf(':', 1);
45
- const id = parseInt(row.substring(1, colon), 16);
46
- const text = row.substring(colon + 1);
44
switch (tag) {
48
- case 'J': {
49
- resolveModel(response, id, text);
50
- return;
51
- }
52
- case 'M': {
53
- resolveModule(response, id, text);
54
- return;
55
- }
56
- case 'P': {
57
- resolveProvider(response, id, text);
58
- return;
59
- }
60
- case 'S': {
61
- resolveSymbol(response, id, JSON.parse(text));
45
+ case 'I': {
46
+ resolveModule(response, id, row.substring(colon + 2));
47
return;
48
}
49
case 'E': {
65
- const errorInfo = JSON.parse(text);
50
+ const errorInfo = JSON.parse(row.substring(colon + 2));
51
if (__DEV__) {
52
resolveErrorDev(
53
response,
@@ -77,9 +62,9 @@ function processFullRow(response: Response, row: string): void {
62
return;
63
}
64
default: {
80
- throw new Error(
81
- "Error parsing the data. It's probably an error code or network corruption.",
82
- );
65
+ // We assume anything else is JSON.
66
+ resolveModel(response, id, row.substring(colon + 1));
67
+ return;
68
}
69
}
70
}
packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js
+2
-6
@@ -15,7 +15,6 @@ import {
15
createResponse,
16
resolveModel,
17
resolveModule,
18
- resolveSymbol,
18
resolveErrorDev,
19
resolveErrorProd,
20
close,
@@ -25,15 +24,12 @@ import {
24
export {createResponse, close, getRoot};
25
26
export function resolveRow(response: Response, chunk: RowEncoding): void {
28
- if (chunk[0] === 'J') {
27
+ if (chunk[0] === 'O') {
28
// $FlowFixMe unable to refine on array indices
29
resolveModel(response, chunk[1], chunk[2]);
31
- } else if (chunk[0] === 'M') {
30
+ } else if (chunk[0] === 'I') {
31
// $FlowFixMe unable to refine on array indices
32
resolveModule(response, chunk[1], chunk[2]);
34
- } else if (chunk[0] === 'S') {
35
- // $FlowFixMe: Flow doesn't support disjoint unions on tuples.
36
- resolveSymbol(response, chunk[1], chunk[2]);
33
} else {
34
if (__DEV__) {
35
resolveErrorDev(
packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js
+2
-2
@@ -18,8 +18,8 @@ export type JSONValue =
18
| $ReadOnlyArray<JSONValue>;
19
20
export type RowEncoding =
21
- | ['J', number, JSONValue]
22
- | ['M', number, ModuleMetaData]
21
+ | ['O', number, JSONValue]
22
+ | ['I', number, ModuleMetaData]
23
| ['P', number, string]
24
| ['S', number, string]
25
| [
packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js
+3
-19
@@ -151,7 +151,7 @@ export function processModelChunk(
151
): Chunk {
152
// $FlowFixMe no good way to define an empty exact object
153
const json = convertModelToJSON(request, {}, '', model);
154
- return ['J', id, json];
154
+ return ['O', id, json];
155
}
156
157
export function processReferenceChunk(
@@ -159,7 +159,7 @@ export function processReferenceChunk(
159
id: number,
160
reference: string,
161
): Chunk {
162
- return ['J', id, reference];
162
+ return ['O', id, reference];
163
}
164
165
export function processModuleChunk(
@@ -168,23 +168,7 @@ export function processModuleChunk(
168
moduleMetaData: ModuleMetaData,
169
): Chunk {
170
// The moduleMetaData is already a JSON serializable value.
171
- return ['M', id, moduleMetaData];
172
-}
173
-
174
-export function processProviderChunk(
175
- request: Request,
176
- id: number,
177
- contextName: string,
178
-): Chunk {
179
- return ['P', id, contextName];
180
-}
181
-
182
-export function processSymbolChunk(
183
- request: Request,
184
- id: number,
185
- name: string,
186
-): Chunk {
187
- return ['S', id, name];
171
+ return ['I', id, moduleMetaData];
172
}
173
174
export function scheduleWork(callback: () => void) {
packages/react-server-native-relay/src/ReactFlightNativeRelayClient.js
+2
-6
@@ -15,7 +15,6 @@ import {
15
createResponse,
16
resolveModel,
17
resolveModule,
18
- resolveSymbol,
18
resolveErrorDev,
19
resolveErrorProd,
20
close,
@@ -25,15 +24,12 @@ import {
24
export {createResponse, close, getRoot};
25
26
export function resolveRow(response: Response, chunk: RowEncoding): void {
28
- if (chunk[0] === 'J') {
27
+ if (chunk[0] === 'O') {
28
// $FlowFixMe `Chunk` doesn't flow into `JSONValue` because of the `E` row type.
29
resolveModel(response, chunk[1], chunk[2]);
31
- } else if (chunk[0] === 'M') {
30
+ } else if (chunk[0] === 'I') {
31
// $FlowFixMe `Chunk` doesn't flow into `JSONValue` because of the `E` row type.
32
resolveModule(response, chunk[1], chunk[2]);
34
- } else if (chunk[0] === 'S') {
35
- // $FlowFixMe: Flow doesn't support disjoint unions on tuples.
36
- resolveSymbol(response, chunk[1], chunk[2]);
33
} else {
34
if (__DEV__) {
35
resolveErrorDev(
packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js
+2
-2
@@ -18,8 +18,8 @@ export type JSONValue =
18
| Array<JSONValue>;
19
20
export type RowEncoding =
21
- | ['J', number, JSONValue]
22
- | ['M', number, ModuleMetaData]
21
+ | ['O', number, JSONValue]
22
+ | ['I', number, ModuleMetaData]
23
| ['P', number, string]
24
| ['S', number, string]
25
| [
packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js
+3
-19
@@ -146,7 +146,7 @@ export function processModelChunk(
146
): Chunk {
147
// $FlowFixMe no good way to define an empty exact object
148
const json = convertModelToJSON(request, {}, '', model);
149
- return ['J', id, json];
149
+ return ['O', id, json];
150
}
151
152
export function processReferenceChunk(
@@ -154,7 +154,7 @@ export function processReferenceChunk(
154
id: number,
155
reference: string,
156
): Chunk {
157
- return ['J', id, reference];
157
+ return ['O', id, reference];
158
}
159
160
export function processModuleChunk(
@@ -163,23 +163,7 @@ export function processModuleChunk(
163
moduleMetaData: ModuleMetaData,
164
): Chunk {
165
// The moduleMetaData is already a JSON serializable value.
166
- return ['M', id, moduleMetaData];
167
-}
168
-
169
-export function processProviderChunk(
170
- request: Request,
171
- id: number,
172
- contextName: string,
173
-): Chunk {
174
- return ['P', id, contextName];
175
-}
176
-
177
-export function processSymbolChunk(
178
- request: Request,
179
- id: number,
180
- name: string,
181
-): Chunk {
182
- return ['S', id, name];
166
+ return ['I', id, moduleMetaData];
167
}
168
169
export function scheduleWork(callback: () => void) {
packages/react-server/src/ReactFlightServer.js
+14
-6
@@ -38,8 +38,6 @@ import {
38
closeWithError,
39
processModelChunk,
40
processModuleChunk,
41
- processProviderChunk,
42
- processSymbolChunk,
41
processErrorChunkProd,
42
processErrorChunkDev,
43
processReferenceChunk,
@@ -417,7 +415,15 @@ function serializeByValueID(id: number): string {
415
}
416
417
function serializeByRefID(id: number): string {
420
- return '@' + id.toString(16);
418
+ return '$L' + id.toString(16);
419
+}
420
+
421
+function serializeSymbolReference(name: string): string {
422
+ return '$S' + name;
423
+}
424
+
425
+function serializeProviderReference(name: string): string {
426
+ return '$P' + name;
427
}
428
429
function serializeClientReference(
@@ -473,7 +479,7 @@ function serializeClientReference(
479
}
480
481
function escapeStringValue(value: string): string {
476
- if (value[0] === '$' || value[0] === '@') {
482
+ if (value[0] === '$') {
483
// We need to escape $ or @ prefixed strings since we use those to encode
484
// references to IDs and as special symbol values.
485
return '$' + value;
@@ -1110,7 +1116,8 @@ function emitModuleChunk(
1116
}
1117
1118
function emitSymbolChunk(request: Request, id: number, name: string): void {
1113
- const processedChunk = processSymbolChunk(request, id, name);
1119
+ const symbolReference = serializeSymbolReference(name);
1120
+ const processedChunk = processReferenceChunk(request, id, symbolReference);
1121
request.completedModuleChunks.push(processedChunk);
1122
}
1123
@@ -1119,7 +1126,8 @@ function emitProviderChunk(
1126
id: number,
1127
contextName: string,
1128
): void {
1122
- const processedChunk = processProviderChunk(request, id, contextName);
1129
+ const contextReference = serializeProviderReference(contextName);
1130
+ const processedChunk = processReferenceChunk(request, id, contextReference);
1131
request.completedJSONChunks.push(processedChunk);
1132
}
1133
packages/react-server/src/ReactFlightServerConfigStream.js
+4
-23
@@ -80,7 +80,7 @@ export {
80
const stringify = JSON.stringify;
81
82
function serializeRowHeader(tag: string, id: number) {
83
- return tag + id.toString(16) + ':';
83
+ return id.toString(16) + ':' + tag;
84
}
85
86
export function processErrorChunkProd(
@@ -127,7 +127,7 @@ export function processModelChunk(
127
model: ReactModel,
128
): Chunk {
129
const json: string = stringify(model, request.toJSON);
130
- const row = serializeRowHeader('J', id) + json + '\n';
130
+ const row = id.toString(16) + ':' + json + '\n';
131
return stringToChunk(row);
132
}
133
@@ -137,7 +137,7 @@ export function processReferenceChunk(
137
reference: string,
138
): Chunk {
139
const json = stringify(reference);
140
- const row = serializeRowHeader('J', id) + json + '\n';
140
+ const row = id.toString(16) + ':' + json + '\n';
141
return stringToChunk(row);
142
}
143
@@ -147,26 +147,7 @@ export function processModuleChunk(
147
moduleMetaData: ReactModel,
148
): Chunk {
149
const json: string = stringify(moduleMetaData);
150
- const row = serializeRowHeader('M', id) + json + '\n';
151
- return stringToChunk(row);
152
-}
153
-
154
-export function processProviderChunk(
155
- request: Request,
156
- id: number,
157
- contextName: string,
158
-): Chunk {
159
- const row = serializeRowHeader('P', id) + contextName + '\n';
160
- return stringToChunk(row);
161
-}
162
-
163
-export function processSymbolChunk(
164
- request: Request,
165
- id: number,
166
- name: string,
167
-): Chunk {
168
- const json = stringify(name);
169
- const row = serializeRowHeader('S', id) + json + '\n';
150
+ const row = serializeRowHeader('I', id) + json + '\n';
151
return stringToChunk(row);
152
}
153