@@ -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.
1937export 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}
104151export 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