Skip to content

Commit a9fe1a0

Browse files
committed
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.
1 parent 9bb44bd commit a9fe1a0

File tree

2 files changed

+310
-29
lines changed

2 files changed

+310
-29
lines changed

packages/react-server/src/ReactDOMServerFormatConfig.js

Lines changed: 274 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@ import {
1414
convertStringToBuffer,
1515
} from 'react-server/src/ReactServerStreamConfig';
1616

17+
import invariant from 'shared/invariant';
18+
19+
// Per response,
20+
export type ResponseState = {
21+
sentCompleteSegmentFunction: boolean,
22+
sentCompleteBoundaryFunction: boolean,
23+
sentClientRenderFunction: boolean,
24+
};
25+
26+
// Allows us to keep track of what we've already written so we can refer back to it.
27+
export function createResponseState(): ResponseState {
28+
return {
29+
sentCompleteSegmentFunction: false,
30+
sentCompleteBoundaryFunction: false,
31+
sentClientRenderFunction: false,
32+
};
33+
}
34+
1735
// This object is used to lazily reuse the ID of the first generated node, or assign one.
1836
// This is very specific to DOM where we can't assign an ID to.
1937
export type SuspenseBoundaryID = {
@@ -24,17 +42,45 @@ export function createSuspenseBoundaryID(): SuspenseBoundaryID {
2442
return {id: null};
2543
}
2644

27-
export function formatChunkAsString(type: string, props: Object): string {
28-
let str = '<' + type + '>';
29-
if (typeof props.children === 'string') {
30-
str += props.children;
31-
}
32-
str += '</' + type + '>';
33-
return str;
45+
function encodeHTMLIDAttribute(value: string): string {
46+
// TODO: This needs to be encoded for security purposes.
47+
return value;
3448
}
3549

36-
export function formatChunk(type: string, props: Object): Uint8Array {
37-
return convertStringToBuffer(formatChunkAsString(type, props));
50+
function encodeHTMLTextNode(text: string): string {
51+
// TOOD: This needs to be encoded for security purposes.
52+
return text;
53+
}
54+
55+
export function pushTextInstance(
56+
target: Array<Uint8Array>,
57+
text: string,
58+
): void {
59+
target.push(convertStringToBuffer(encodeHTMLTextNode(text)));
60+
}
61+
62+
const startTag1 = convertStringToBuffer('<');
63+
const startTag2 = convertStringToBuffer('>');
64+
65+
export function pushStartInstance(
66+
target: Array<Uint8Array>,
67+
type: string,
68+
props: Object,
69+
): void {
70+
// TODO: Figure out if it's self closing and everything else.
71+
target.push(startTag1, convertStringToBuffer(type), startTag2);
72+
}
73+
74+
const endTag1 = convertStringToBuffer('</');
75+
const endTag2 = convertStringToBuffer('>');
76+
77+
export function pushEndInstance(
78+
target: Array<Uint8Array>,
79+
type: string,
80+
props: Object,
81+
): void {
82+
// TODO: Figure out if it was self closing.
83+
target.push(endTag1, convertStringToBuffer(type), endTag2);
3884
}
3985

4086
// Structural Nodes
@@ -99,8 +145,227 @@ export function writeStartSegment(
99145
// TODO: Use the identifierPrefix option to make the prefix configurable.
100146
writeChunk(destination, startSegment2);
101147
const formattedID = convertStringToBuffer(id.toString(16));
148+
writeChunk(destination, formattedID);
102149
return writeChunk(destination, startSegment3);
103150
}
104151
export function writeEndSegment(destination: Destination): boolean {
105152
return writeChunk(destination, endSegment);
106153
}
154+
155+
// Instruction Set
156+
157+
// The following code is the source scripts that we then minify and inline below,
158+
// with renamed function names that we hope don't collide:
159+
160+
// const COMMENT_NODE = 8;
161+
// const SUSPENSE_START_DATA = '$';
162+
// const SUSPENSE_END_DATA = '/$';
163+
// const SUSPENSE_PENDING_START_DATA = '$?';
164+
// const SUSPENSE_FALLBACK_START_DATA = '$!';
165+
//
166+
// function clientRenderBoundary(suspenseBoundaryID) {
167+
// // Find the fallback's first element.
168+
// let suspenseNode = document.getElementById(suspenseBoundaryID);
169+
// if (!suspenseNode) {
170+
// // The user must have already navigated away from this tree.
171+
// // E.g. because the parent was hydrated.
172+
// return;
173+
// }
174+
// // Find the boundary around the fallback. This might include text nodes.
175+
// do {
176+
// suspenseNode = suspenseNode.previousSibling;
177+
// } while (
178+
// suspenseNode.nodeType !== COMMENT_NODE ||
179+
// suspenseNode.data !== SUSPENSE_PENDING_START_DATA
180+
// );
181+
// // Tag it to be client rendered.
182+
// suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
183+
// // Tell React to retry it if the parent already hydrated.
184+
// if (suspenseNode._reactRetry) {
185+
// suspenseNode._reactRetry();
186+
// }
187+
// }
188+
//
189+
// function completeBoundary(suspenseBoundaryID, contentID) {
190+
// // Find the fallback's first element.
191+
// let suspenseNode = document.getElementById(suspenseBoundaryID);
192+
// const contentNode = document.getElementById(contentID);
193+
// // We'll detach the content node so that regardless of what happens next we don't leave in the tree.
194+
// // This might also help by not causing recalcing each time we move a child from here to the target.
195+
// contentNode.parentNode.removeChild(contentNode);
196+
// if (!suspenseNode) {
197+
// // The user must have already navigated away from this tree.
198+
// // E.g. because the parent was hydrated. That's fine there's nothing to do
199+
// // but we have to make sure that we already deleted the container node.
200+
// return;
201+
// }
202+
// // Find the boundary around the fallback. This might include text nodes.
203+
// do {
204+
// suspenseNode = suspenseNode.previousSibling;
205+
// } while (
206+
// suspenseNode.nodeType !== COMMENT_NODE ||
207+
// suspenseNode.data !== SUSPENSE_PENDING_START_DATA
208+
// );
209+
//
210+
// // Clear all the existing children. This is complicated because
211+
// // there can be embedded Suspense boundaries in the fallback.
212+
// // This is similar to clearSuspenseBoundary in ReactDOMHostConfig.
213+
// // TOOD: We could avoid this if we never emitted suspense boundaries in fallback trees.
214+
// // They never hydrate anyway. However, currently we support incrementally loading the fallback.
215+
// const parentInstance = suspenseNode.parentNode;
216+
// let node = suspenseNode.nextSibling;
217+
// let depth = 0;
218+
// do {
219+
// if (node && node.nodeType === COMMENT_NODE) {
220+
// const data = node.data;
221+
// if (data === SUSPENSE_END_DATA) {
222+
// if (depth === 0) {
223+
// break;
224+
// } else {
225+
// depth--;
226+
// }
227+
// } else if (
228+
// data === SUSPENSE_START_DATA ||
229+
// data === SUSPENSE_PENDING_START_DATA ||
230+
// data === SUSPENSE_FALLBACK_START_DATA
231+
// ) {
232+
// depth++;
233+
// }
234+
// }
235+
//
236+
// const nextNode = node.nextSibling;
237+
// parentInstance.removeChild(node);
238+
// node = nextNode;
239+
// } while (node);
240+
//
241+
// const endOfBoundary = node;
242+
//
243+
// // Insert all the children from the contentNode between the start and end of suspense boundary.
244+
// while (contentNode.firstChild) {
245+
// parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
246+
// }
247+
248+
// suspenseNode.data = SUSPENSE_START_DATA;
249+
// if (suspenseNode._reactRetry) {
250+
// suspenseNode._reactRetry();
251+
// }
252+
// }
253+
//
254+
// function completeSegment(containerID, placeholderID) {
255+
// const segmentContainer = document.getElementById(containerID);
256+
// const placeholderNode = document.getElementById(placeholderID);
257+
// // We always expect both nodes to exist here because, while we might
258+
// // have navigated away from the main tree, we still expect the detached
259+
// // tree to exist.
260+
// segmentContainer.parentNode.removeChild(segmentContainer);
261+
// while (segmentContainer.firstChild) {
262+
// placeholderNode.parentNode.insertBefore(
263+
// segmentContainer.firstChild,
264+
// placeholderNode,
265+
// );
266+
// }
267+
// placeholderNode.parentNode.removeChild(placeholderNode);
268+
// }
269+
270+
const completeSegmentFunction =
271+
'function $RS(b,f){var a=document.getElementById(b),c=document.getElementById(f);for(a.parentNode.removeChild(a);a.firstChild;)c.parentNode.insertBefore(a.firstChild,c);c.parentNode.removeChild(c)}';
272+
const completeBoundaryFunction =
273+
'function $RC(b,f){var a=document.getElementById(b),c=document.getElementById(f);c.parentNode.removeChild(c);if(a){do a=a.previousSibling;while(8!==a.nodeType||"$?"!==a.data);var h=a.parentNode,d=a.nextSibling,g=0;do{if(d&&8===d.nodeType){var e=d.data;if("/$"===e)if(0===g)break;else g--;else"$"!==e&&"$?"!==e&&"$!"!==e||g++}e=d.nextSibling;h.removeChild(d);d=e}while(d);for(;c.firstChild;)h.insertBefore(c.firstChild,d);a.data="$";a._reactRetry&&a._reactRetry()}}';
274+
const clientRenderFunction =
275+
'function $RX(b){if(b=document.getElementById(b)){do b=b.previousSibling;while(8!==b.nodeType||"$?"!==b.data);b.data="$!";b._reactRetry&&b._reactRetry()}}';
276+
277+
const completeSegmentScript1Full = convertStringToBuffer(
278+
'<script>' + completeSegmentFunction + ';$RS("S:',
279+
);
280+
const completeSegmentScript1Partial = convertStringToBuffer('<script>$RS("S:');
281+
const completeSegmentScript2 = convertStringToBuffer('","P:');
282+
const completeSegmentScript3 = convertStringToBuffer('")</script>');
283+
284+
export function writeCompletedSegmentInstruction(
285+
destination: Destination,
286+
responseState: ResponseState,
287+
contentSegmentID: number,
288+
): boolean {
289+
if (responseState.sentCompleteSegmentFunction) {
290+
// The first time we write this, we'll need to include the full implementation.
291+
responseState.sentCompleteSegmentFunction = true;
292+
writeChunk(destination, completeSegmentScript1Full);
293+
} else {
294+
// Future calls can just reuse the same function.
295+
writeChunk(destination, completeSegmentScript1Partial);
296+
}
297+
// TODO: Use the identifierPrefix option to make the prefix configurable.
298+
const formattedID = convertStringToBuffer(contentSegmentID.toString(16));
299+
writeChunk(destination, formattedID);
300+
writeChunk(destination, completeSegmentScript2);
301+
writeChunk(destination, formattedID);
302+
return writeChunk(destination, completeSegmentScript3);
303+
}
304+
305+
const completeBoundaryScript1Full = convertStringToBuffer(
306+
'<script>' + completeBoundaryFunction + ';$RC("',
307+
);
308+
const completeBoundaryScript1Partial = convertStringToBuffer('<script>$RC("');
309+
const completeBoundaryScript2 = convertStringToBuffer('","S:');
310+
const completeBoundaryScript3 = convertStringToBuffer('")</script>');
311+
312+
export function writeCompletedBoundaryInstruction(
313+
destination: Destination,
314+
responseState: ResponseState,
315+
boundaryID: SuspenseBoundaryID,
316+
contentSegmentID: number,
317+
): boolean {
318+
if (responseState.sentCompleteBoundaryFunction) {
319+
// The first time we write this, we'll need to include the full implementation.
320+
responseState.sentCompleteBoundaryFunction = true;
321+
writeChunk(destination, completeBoundaryScript1Full);
322+
} else {
323+
// Future calls can just reuse the same function.
324+
writeChunk(destination, completeBoundaryScript1Partial);
325+
}
326+
// TODO: Use the identifierPrefix option to make the prefix configurable.
327+
invariant(
328+
boundaryID.id !== null,
329+
'An ID must have been assigned before we can complete the boundary.',
330+
);
331+
const formattedBoundaryID = convertStringToBuffer(
332+
encodeHTMLIDAttribute(boundaryID.id),
333+
);
334+
const formattedContentID = convertStringToBuffer(
335+
contentSegmentID.toString(16),
336+
);
337+
writeChunk(destination, formattedBoundaryID);
338+
writeChunk(destination, completeBoundaryScript2);
339+
writeChunk(destination, formattedContentID);
340+
return writeChunk(destination, completeBoundaryScript3);
341+
}
342+
343+
const clientRenderScript1Full = convertStringToBuffer(
344+
'<script>' + clientRenderFunction + ';$RX("',
345+
);
346+
const clientRenderScript1Partial = convertStringToBuffer('<script>$RX("');
347+
const clientRenderScript2 = convertStringToBuffer('")</script>');
348+
349+
export function writeClientRenderBoundaryInstruction(
350+
destination: Destination,
351+
responseState: ResponseState,
352+
boundaryID: SuspenseBoundaryID,
353+
): boolean {
354+
if (responseState.sentClientRenderFunction) {
355+
// The first time we write this, we'll need to include the full implementation.
356+
responseState.sentClientRenderFunction = true;
357+
writeChunk(destination, clientRenderScript1Full);
358+
} else {
359+
// Future calls can just reuse the same function.
360+
writeChunk(destination, clientRenderScript1Partial);
361+
}
362+
invariant(
363+
boundaryID.id !== null,
364+
'An ID must have been assigned before we can complete the boundary.',
365+
);
366+
const formattedBoundaryID = convertStringToBuffer(
367+
encodeHTMLIDAttribute(boundaryID.id),
368+
);
369+
writeChunk(destination, formattedBoundaryID);
370+
return writeChunk(destination, clientRenderScript2);
371+
}

0 commit comments

Comments
 (0)