Skip to content

Commit

Permalink
Refactor Flight Encoding (#26082)
Browse files Browse the repository at this point in the history
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"
```
  • Loading branch information
sebmarkbage authored Jan 31, 2023
1 parent 8b9ac81 commit 977bccd
Show file tree
Hide file tree
Showing 10 changed files with 65 additions and 154 deletions.
71 changes: 24 additions & 47 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,14 +201,6 @@ function createErrorChunk<T>(
return new Chunk(ERRORED, null, error, response);
}

function createInitializedChunk<T>(
response: Response,
value: T,
): InitializedChunk<T> {
// $FlowFixMe Flow doesn't support functions as constructors
return new Chunk(INITIALIZED, value, null, response);
}

function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void {
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
Expand Down Expand Up @@ -483,14 +475,32 @@ export function parseModelString(
key: string,
value: string,
): any {
switch (value[0]) {
case '$': {
if (value === '$') {
return REACT_ELEMENT_TYPE;
} else if (value[1] === '$' || value[1] === '@') {
if (value[0] === '$') {
if (value === '$') {
// A very common symbol.
return REACT_ELEMENT_TYPE;
}
switch (value[1]) {
case '$': {
// This was an escaped string value.
return value.substring(1);
} else {
}
case 'L': {
// Lazy node
const id = parseInt(value.substring(2), 16);
const chunk = getChunk(response, id);
// We create a React.lazy wrapper around any lazy values.
// When passed into React, we'll know how to suspend on this.
return createLazyChunkWrapper(chunk);
}
case 'S': {
return Symbol.for(value.substring(2));
}
case 'P': {
return getOrCreateServerContext(value.substring(2)).Provider;
}
default: {
// We assume that anything else is a reference ID.
const id = parseInt(value.substring(1), 16);
const chunk = getChunk(response, id);
switch (chunk.status) {
Expand Down Expand Up @@ -518,13 +528,6 @@ export function parseModelString(
}
}
}
case '@': {
const id = parseInt(value.substring(1), 16);
const chunk = getChunk(response, id);
// We create a React.lazy wrapper around any lazy values.
// When passed into React, we'll know how to suspend on this.
return createLazyChunkWrapper(chunk);
}
}
return value;
}
Expand Down Expand Up @@ -566,21 +569,6 @@ export function resolveModel(
}
}

export function resolveProvider(
response: Response,
id: number,
contextName: string,
): void {
const chunks = response._chunks;
chunks.set(
id,
createInitializedChunk(
response,
getOrCreateServerContext(contextName).Provider,
),
);
}

export function resolveModule(
response: Response,
id: number,
Expand Down Expand Up @@ -626,17 +614,6 @@ export function resolveModule(
}
}

export function resolveSymbol(
response: Response,
id: number,
name: string,
): void {
const chunks = response._chunks;
// We assume that we'll always emit the symbol before anything references it
// to save a few bytes.
chunks.set(id, createInitializedChunk(response, Symbol.for(name)));
}

type ErrorWithDigest = Error & {digest?: string};
export function resolveErrorProd(
response: Response,
Expand Down
33 changes: 9 additions & 24 deletions packages/react-client/src/ReactFlightClientStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import type {BundlerConfig} from './ReactFlightClientHostConfig';
import {
resolveModule,
resolveModel,
resolveProvider,
resolveSymbol,
resolveErrorProd,
resolveErrorDev,
createResponse as createResponseBase,
Expand All @@ -36,33 +34,20 @@ function processFullRow(response: Response, row: string): void {
if (row === '') {
return;
}
const tag = row[0];
const colon = row.indexOf(':', 0);
const id = parseInt(row.substring(0, colon), 16);
const tag = row[colon + 1];
// When tags that are not text are added, check them here before
// parsing the row as text.
// switch (tag) {
// }
const colon = row.indexOf(':', 1);
const id = parseInt(row.substring(1, colon), 16);
const text = row.substring(colon + 1);
switch (tag) {
case 'J': {
resolveModel(response, id, text);
return;
}
case 'M': {
resolveModule(response, id, text);
return;
}
case 'P': {
resolveProvider(response, id, text);
return;
}
case 'S': {
resolveSymbol(response, id, JSON.parse(text));
case 'I': {
resolveModule(response, id, row.substring(colon + 2));
return;
}
case 'E': {
const errorInfo = JSON.parse(text);
const errorInfo = JSON.parse(row.substring(colon + 2));
if (__DEV__) {
resolveErrorDev(
response,
Expand All @@ -77,9 +62,9 @@ function processFullRow(response: Response, row: string): void {
return;
}
default: {
throw new Error(
"Error parsing the data. It's probably an error code or network corruption.",
);
// We assume anything else is JSON.
resolveModel(response, id, row.substring(colon + 1));
return;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
createResponse,
resolveModel,
resolveModule,
resolveSymbol,
resolveErrorDev,
resolveErrorProd,
close,
Expand All @@ -25,15 +24,12 @@ import {
export {createResponse, close, getRoot};

export function resolveRow(response: Response, chunk: RowEncoding): void {
if (chunk[0] === 'J') {
if (chunk[0] === 'O') {
// $FlowFixMe unable to refine on array indices
resolveModel(response, chunk[1], chunk[2]);
} else if (chunk[0] === 'M') {
} else if (chunk[0] === 'I') {
// $FlowFixMe unable to refine on array indices
resolveModule(response, chunk[1], chunk[2]);
} else if (chunk[0] === 'S') {
// $FlowFixMe: Flow doesn't support disjoint unions on tuples.
resolveSymbol(response, chunk[1], chunk[2]);
} else {
if (__DEV__) {
resolveErrorDev(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export type JSONValue =
| $ReadOnlyArray<JSONValue>;

export type RowEncoding =
| ['J', number, JSONValue]
| ['M', number, ModuleMetaData]
| ['O', number, JSONValue]
| ['I', number, ModuleMetaData]
| ['P', number, string]
| ['S', number, string]
| [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,15 @@ export function processModelChunk(
): Chunk {
// $FlowFixMe no good way to define an empty exact object
const json = convertModelToJSON(request, {}, '', model);
return ['J', id, json];
return ['O', id, json];
}

export function processReferenceChunk(
request: Request,
id: number,
reference: string,
): Chunk {
return ['J', id, reference];
return ['O', id, reference];
}

export function processModuleChunk(
Expand All @@ -168,23 +168,7 @@ export function processModuleChunk(
moduleMetaData: ModuleMetaData,
): Chunk {
// The moduleMetaData is already a JSON serializable value.
return ['M', id, moduleMetaData];
}

export function processProviderChunk(
request: Request,
id: number,
contextName: string,
): Chunk {
return ['P', id, contextName];
}

export function processSymbolChunk(
request: Request,
id: number,
name: string,
): Chunk {
return ['S', id, name];
return ['I', id, moduleMetaData];
}

export function scheduleWork(callback: () => void) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
createResponse,
resolveModel,
resolveModule,
resolveSymbol,
resolveErrorDev,
resolveErrorProd,
close,
Expand All @@ -25,15 +24,12 @@ import {
export {createResponse, close, getRoot};

export function resolveRow(response: Response, chunk: RowEncoding): void {
if (chunk[0] === 'J') {
if (chunk[0] === 'O') {
// $FlowFixMe `Chunk` doesn't flow into `JSONValue` because of the `E` row type.
resolveModel(response, chunk[1], chunk[2]);
} else if (chunk[0] === 'M') {
} else if (chunk[0] === 'I') {
// $FlowFixMe `Chunk` doesn't flow into `JSONValue` because of the `E` row type.
resolveModule(response, chunk[1], chunk[2]);
} else if (chunk[0] === 'S') {
// $FlowFixMe: Flow doesn't support disjoint unions on tuples.
resolveSymbol(response, chunk[1], chunk[2]);
} else {
if (__DEV__) {
resolveErrorDev(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export type JSONValue =
| Array<JSONValue>;

export type RowEncoding =
| ['J', number, JSONValue]
| ['M', number, ModuleMetaData]
| ['O', number, JSONValue]
| ['I', number, ModuleMetaData]
| ['P', number, string]
| ['S', number, string]
| [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,15 @@ export function processModelChunk(
): Chunk {
// $FlowFixMe no good way to define an empty exact object
const json = convertModelToJSON(request, {}, '', model);
return ['J', id, json];
return ['O', id, json];
}

export function processReferenceChunk(
request: Request,
id: number,
reference: string,
): Chunk {
return ['J', id, reference];
return ['O', id, reference];
}

export function processModuleChunk(
Expand All @@ -163,23 +163,7 @@ export function processModuleChunk(
moduleMetaData: ModuleMetaData,
): Chunk {
// The moduleMetaData is already a JSON serializable value.
return ['M', id, moduleMetaData];
}

export function processProviderChunk(
request: Request,
id: number,
contextName: string,
): Chunk {
return ['P', id, contextName];
}

export function processSymbolChunk(
request: Request,
id: number,
name: string,
): Chunk {
return ['S', id, name];
return ['I', id, moduleMetaData];
}

export function scheduleWork(callback: () => void) {
Expand Down
20 changes: 14 additions & 6 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ import {
closeWithError,
processModelChunk,
processModuleChunk,
processProviderChunk,
processSymbolChunk,
processErrorChunkProd,
processErrorChunkDev,
processReferenceChunk,
Expand Down Expand Up @@ -417,7 +415,15 @@ function serializeByValueID(id: number): string {
}

function serializeByRefID(id: number): string {
return '@' + id.toString(16);
return '$L' + id.toString(16);
}

function serializeSymbolReference(name: string): string {
return '$S' + name;
}

function serializeProviderReference(name: string): string {
return '$P' + name;
}

function serializeClientReference(
Expand Down Expand Up @@ -473,7 +479,7 @@ function serializeClientReference(
}

function escapeStringValue(value: string): string {
if (value[0] === '$' || value[0] === '@') {
if (value[0] === '$') {
// We need to escape $ or @ prefixed strings since we use those to encode
// references to IDs and as special symbol values.
return '$' + value;
Expand Down Expand Up @@ -1110,7 +1116,8 @@ function emitModuleChunk(
}

function emitSymbolChunk(request: Request, id: number, name: string): void {
const processedChunk = processSymbolChunk(request, id, name);
const symbolReference = serializeSymbolReference(name);
const processedChunk = processReferenceChunk(request, id, symbolReference);
request.completedModuleChunks.push(processedChunk);
}

Expand All @@ -1119,7 +1126,8 @@ function emitProviderChunk(
id: number,
contextName: string,
): void {
const processedChunk = processProviderChunk(request, id, contextName);
const contextReference = serializeProviderReference(contextName);
const processedChunk = processReferenceChunk(request, id, contextReference);
request.completedJSONChunks.push(processedChunk);
}

Expand Down
Loading

0 comments on commit 977bccd

Please sign in to comment.