main
js 160 lines 3.8 KB
Raw
1 import {use, Suspense, useState, startTransition, Profiler} from 'react';
2 import ReactDOM from 'react-dom/client';
3 import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client';
4
5 // TODO: This should be a dependency of the App but we haven't implemented CSS in Node yet.
6 import './style.css';
7
8 function findSourceMapURL(fileName) {
9 return (
10 document.location.origin +
11 '/source-maps?name=' +
12 encodeURIComponent(fileName)
13 );
14 }
15
16 async function createWebSocketStream(url) {
17 const ws = new WebSocket(url);
18 ws.binaryType = 'arraybuffer';
19
20 await new Promise((resolve, reject) => {
21 ws.addEventListener('open', resolve, {once: true});
22 ws.addEventListener('error', reject, {once: true});
23 });
24
25 const writable = new WritableStream({
26 write(chunk) {
27 ws.send(chunk);
28 },
29 close() {
30 ws.close();
31 },
32 abort(reason) {
33 ws.close(1000, reason && String(reason));
34 },
35 });
36
37 const readable = new ReadableStream({
38 start(controller) {
39 ws.addEventListener('message', event => {
40 controller.enqueue(event.data);
41 });
42 ws.addEventListener('close', () => {
43 controller.close();
44 });
45 ws.addEventListener('error', err => {
46 controller.error(err);
47 });
48 },
49 });
50
51 return {readable, writable};
52 }
53
54 let updateRoot;
55 async function callServer(id, args) {
56 let response;
57 if (process.env.NODE_ENV === 'development') {
58 const requestId = crypto.randomUUID();
59 const debugChannel = await createWebSocketStream(
60 `ws://localhost:3001/debug-channel?id=${requestId}`
61 );
62 response = createFromFetch(
63 fetch('/', {
64 method: 'POST',
65 headers: {
66 Accept: 'text/x-component',
67 'rsc-action': id,
68 'rsc-request-id': requestId,
69 },
70 body: await encodeReply(args),
71 }),
72 {
73 callServer,
74 debugChannel,
75 findSourceMapURL,
76 }
77 );
78 } else {
79 response = createFromFetch(
80 fetch('/', {
81 method: 'POST',
82 headers: {
83 Accept: 'text/x-component',
84 'rsc-action': id,
85 },
86 body: await encodeReply(args),
87 }),
88 {
89 callServer,
90 findSourceMapURL,
91 }
92 );
93 }
94 const {returnValue, root} = await response;
95 // Refresh the tree with the new RSC payload.
96 startTransition(() => {
97 updateRoot(root);
98 });
99 return returnValue;
100 }
101
102 function Shell({data}) {
103 const [root, setRoot] = useState(data);
104 updateRoot = setRoot;
105 return root;
106 }
107
108 async function hydrateApp() {
109 let response;
110 if (process.env.NODE_ENV === 'development') {
111 const requestId = crypto.randomUUID();
112 const debugChannel = await createWebSocketStream(
113 `ws://localhost:3001/debug-channel?id=${requestId}`
114 );
115 response = createFromFetch(
116 fetch('/', {
117 headers: {
118 Accept: 'text/x-component',
119 'rsc-request-id': requestId,
120 },
121 }),
122 {
123 callServer,
124 debugChannel,
125 findSourceMapURL,
126 }
127 );
128 } else {
129 response = createFromFetch(
130 fetch('/', {
131 headers: {
132 Accept: 'text/x-component',
133 },
134 }),
135 {
136 callServer,
137 findSourceMapURL,
138 }
139 );
140 }
141 const {root, returnValue, formState} = await response;
142
143 ReactDOM.hydrateRoot(
144 document,
145 <Profiler id="root">
146 <Shell data={root} />
147 </Profiler>,
148 {
149 // TODO: This part doesn't actually work because the server only returns
150 // form state during the request that submitted the form. Which means it
151 // the state needs to be transported as part of the HTML stream. We intend
152 // to add a feature to Fizz for this, but for now it's up to the
153 // metaframework to implement correctly.
154 formState: formState,
155 }
156 );
157 }
158
159 // Remove this line to simulate MPA behavior
160 hydrateApp();