Skip to content

Commit

Permalink
Basic Fizz Architecture (#20970)
Browse files Browse the repository at this point in the history
* Copy some infra structure patterns from Flight

* Basic data structures

* Move structural nodes and instruction commands to host config

* Move instruction command to host config

In the DOM this is implemented as script tags. The first time it's emitted
it includes the function. Future calls invoke the same function.

The side of the complete boundary function in particular is unfortunately
large.

* Implement Fizz Noop host configs

This is implemented not as a serialized protocol but by-passing the
serialization when possible and instead it's like a live tree being
built.

* Implement React Native host config

This is not wired up. I just need something for the flow types since
Flight and Fizz are both handled by the isServerSupported flag.

Might as well add something though.

The principle of this format is the same structure as for HTML but a
simpler binary format.

Each entry is a tag followed by some data and terminated by null.

* Check in error codes

* Comment
  • Loading branch information
sebmarkbage authored Mar 11, 2021
1 parent bd245c1 commit 10cc400
Show file tree
Hide file tree
Showing 8 changed files with 1,562 additions and 69 deletions.
6 changes: 0 additions & 6 deletions packages/react-noop-renderer/src/ReactNoopFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,6 @@ const ReactNoopFlightServer = ReactFlightServer({
convertStringToBuffer(content: string): Uint8Array {
return Buffer.from(content, 'utf8');
},
formatChunkAsString(type: string, props: Object): string {
return JSON.stringify({type, props});
},
formatChunk(type: string, props: Object): Uint8Array {
return Buffer.from(JSON.stringify({type, props}), 'utf8');
},
isModuleReference(reference: Object): boolean {
return reference.$$typeof === Symbol.for('react.module.reference');
},
Expand Down
192 changes: 183 additions & 9 deletions packages/react-noop-renderer/src/ReactNoopServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,206 @@

import ReactFizzServer from 'react-server';

type Destination = Array<string>;
type Instance = {|
type: string,
children: Array<Instance | TextInstance | SuspenseInstance>,
prop: any,
hidden: boolean,
|};

type TextInstance = {|
text: string,
hidden: boolean,
|};

type SuspenseInstance = {|
state: 'pending' | 'complete' | 'client-render',
children: Array<Instance | TextInstance | SuspenseInstance>,
|};

type Placeholder = {
parent: Instance | SuspenseInstance,
index: number,
};

type Segment = {
children: null | Instance | TextInstance | SuspenseInstance,
};

type Destination = {
root: null | Instance | TextInstance | SuspenseInstance,
placeholders: Map<number, Placeholder>,
segments: Map<number, Segment>,
stack: Array<Segment | Instance | SuspenseInstance>,
};

const POP = Buffer.from('/', 'utf8');

const ReactNoopServer = ReactFizzServer({
scheduleWork(callback: () => void) {
callback();
},
beginWriting(destination: Destination): void {},
writeChunk(destination: Destination, buffer: Uint8Array): void {
destination.push(JSON.parse(Buffer.from((buffer: any)).toString('utf8')));
const stack = destination.stack;
if (buffer === POP) {
stack.pop();
return;
}
// We assume one chunk is one instance.
const instance = JSON.parse(Buffer.from((buffer: any)).toString('utf8'));
if (stack.length === 0) {
destination.root = instance;
} else {
const parent = stack[stack.length - 1];
parent.children.push(instance);
}
stack.push(instance);
},
completeWriting(destination: Destination): void {},
close(destination: Destination): void {},
flushBuffered(destination: Destination): void {},
convertStringToBuffer(content: string): Uint8Array {
return Buffer.from(content, 'utf8');

createResponseState(): null {
return null;
},
formatChunkAsString(type: string, props: Object): string {
return JSON.stringify({type, props});
createSuspenseBoundaryID(): SuspenseInstance {
// The ID is a pointer to the boundary itself.
return {state: 'pending', children: []};
},

pushTextInstance(target: Array<Uint8Array>, text: string): void {
const textInstance: TextInstance = {
text,
hidden: false,
};
target.push(Buffer.from(JSON.stringify(textInstance), 'utf8'), POP);
},
pushStartInstance(
target: Array<Uint8Array>,
type: string,
props: Object,
): void {
const instance: Instance = {
type: type,
children: [],
prop: props.prop,
hidden: false,
};
target.push(Buffer.from(JSON.stringify(instance), 'utf8'));
},

pushEndInstance(
target: Array<Uint8Array>,
type: string,
props: Object,
): void {
target.push(POP);
},

writePlaceholder(destination: Destination, id: number): boolean {
const parent = destination.stack[destination.stack.length - 1];
destination.placeholders.set(id, {
parent: parent,
index: parent.children.length,
});
},
formatChunk(type: string, props: Object): Uint8Array {
return Buffer.from(JSON.stringify({type, props}), 'utf8');

writeStartCompletedSuspenseBoundary(
destination: Destination,
suspenseInstance: SuspenseInstance,
): boolean {
suspenseInstance.state = 'complete';
const parent = destination.stack[destination.stack.length - 1];
parent.children.push(suspenseInstance);
destination.stack.push(suspenseInstance);
},
writeStartPendingSuspenseBoundary(
destination: Destination,
suspenseInstance: SuspenseInstance,
): boolean {
suspenseInstance.state = 'pending';
const parent = destination.stack[destination.stack.length - 1];
parent.children.push(suspenseInstance);
destination.stack.push(suspenseInstance);
},
writeStartClientRenderedSuspenseBoundary(
destination: Destination,
suspenseInstance: SuspenseInstance,
): boolean {
suspenseInstance.state = 'client-render';
const parent = destination.stack[destination.stack.length - 1];
parent.children.push(suspenseInstance);
destination.stack.push(suspenseInstance);
},
writeEndSuspenseBoundary(destination: Destination): boolean {
destination.stack.pop();
},

writeStartSegment(destination: Destination, id: number): boolean {
const segment = {
children: [],
};
destination.segments.set(id, segment);
if (destination.stack.length > 0) {
throw new Error('Segments are only expected at the root of the stack.');
}
destination.stack.push(segment);
},
writeEndSegment(destination: Destination): boolean {
destination.stack.pop();
},

writeCompletedSegmentInstruction(
destination: Destination,
responseState: ResponseState,
contentSegmentID: number,
): boolean {
const segment = destination.segments.get(contentSegmentID);
if (!segment) {
throw new Error('Missing segment.');
}
const placeholder = destination.placeholders.get(contentSegmentID);
if (!placeholder) {
throw new Error('Missing placeholder.');
}
placeholder.parent.children.splice(
placeholder.index,
0,
...segment.children,
);
},

writeCompletedBoundaryInstruction(
destination: Destination,
responseState: ResponseState,
boundary: SuspenseInstance,
contentSegmentID: number,
): boolean {
const segment = destination.segments.get(contentSegmentID);
if (!segment) {
throw new Error('Missing segment.');
}
boundary.children = segment.children;
boundary.state = 'complete';
},

writeClientRenderBoundaryInstruction(
destination: Destination,
responseState: ResponseState,
boundary: SuspenseInstance,
): boolean {
boundary.status = 'client-render';
},
});

function render(children: React$Element<any>): Destination {
const destination: Destination = [];
const destination: Destination = {
root: null,
placeholders: new Map(),
segments: new Map(),
stack: [],
};
const request = ReactNoopServer.createRequest(children, destination);
ReactNoopServer.startWork(request);
return destination;
Expand Down
Loading

0 comments on commit 10cc400

Please sign in to comment.