A tool for people curious about the React Server Components protocol rscexplorer.dev/
rsc react
at main 186 lines 5.1 kB view raw
1// Server Worker - RSC server simulation 2 3import "../shared/webpack-shim.ts"; 4import { polyfillReady } from "../shared/polyfill.ts"; 5 6import { 7 renderToReadableStream, 8 registerServerReference, 9 createClientModuleProxy, 10 decodeReply, 11} from "react-server-dom-webpack/server"; 12import React from "react"; 13import type { ClientManifest } from "../shared/compiler.ts"; 14 15declare const self: DedicatedWorkerGlobalScope; 16 17// --- Types --- 18 19export type EncodedArgs = { 20 type: "formdata" | "string"; 21 data: string; 22}; 23 24export type Response = 25 | { type: "ready" } 26 | { type: "next"; requestId: string; value: Uint8Array } 27 | { type: "done"; requestId: string } 28 | { type: "throw"; requestId: string; error: string; stack?: string }; 29 30// --- State --- 31 32type ServerModule = { 33 default?: React.ComponentType | React.ReactNode; 34 [key: string]: unknown; 35}; 36 37let deployed: { manifest: ClientManifest; module: ServerModule } | null = null; 38 39// --- Response helpers --- 40 41async function sendStream( 42 requestId: string, 43 getStream: () => ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>, 44): Promise<void> { 45 try { 46 const stream = await getStream(); 47 const reader = stream.getReader(); 48 while (true) { 49 const { done, value } = await reader.read(); 50 if (done) break; 51 self.postMessage({ type: "next", requestId, value }); 52 } 53 self.postMessage({ type: "done", requestId }); 54 } catch (err) { 55 const error = err instanceof Error ? err : new Error(String(err)); 56 const msg: Response = { type: "throw", requestId, error: error.message }; 57 if (error.stack) { 58 msg.stack = error.stack; 59 } 60 self.postMessage(msg); 61 } 62} 63 64// --- RPC handlers --- 65 66function deploy( 67 compiledCode: string, 68 manifest: ClientManifest, 69 actionNames: string[], 70): ReadableStream<Uint8Array> { 71 const clientModule = createClientModuleProxy("client"); 72 const modules: Record<string, unknown> = { 73 react: React, 74 "./client": clientModule, 75 }; 76 77 let code = compiledCode; 78 if (actionNames.length > 0) { 79 code += 80 "\n" + 81 actionNames 82 .map( 83 (name) => 84 `__registerServerReference(${name}, "${name}", "${name}"); exports.${name} = ${name};`, 85 ) 86 .join("\n"); 87 } 88 89 const module: { exports: ServerModule } = { exports: {} }; 90 const require = (id: string): unknown => { 91 if (!modules[id]) throw new Error(`Module "${id}" not found`); 92 return modules[id]; 93 }; 94 95 new Function("module", "exports", "require", "React", "__registerServerReference", code)( 96 module, 97 module.exports, 98 require, 99 React, 100 registerServerReference, 101 ); 102 103 deployed = { manifest, module: module.exports }; 104 return new ReadableStream({ start: (c) => c.close() }); 105} 106export type Deploy = typeof deploy; 107 108const renderOptions = { 109 onError: () => "Switch to dev mode (top right) to see the full error.", 110}; 111 112function render(): ReadableStream<Uint8Array> { 113 if (!deployed) throw new Error("No code deployed"); 114 const App = deployed.module.default as React.ComponentType; 115 return renderToReadableStream(React.createElement(App), deployed.manifest, renderOptions); 116} 117export type Render = typeof render; 118 119async function callAction( 120 actionId: string, 121 encodedArgs: EncodedArgs, 122): Promise<ReadableStream<Uint8Array>> { 123 if (!deployed) throw new Error("No code deployed"); 124 if (!Object.hasOwn(deployed.module, actionId)) { 125 throw new Error(`Action "${actionId}" not found`); 126 } 127 const actionFn = deployed.module[actionId] as Function; 128 129 let body: FormData | string; 130 if (encodedArgs.type === "formdata") { 131 body = new FormData(); 132 for (const [key, value] of new URLSearchParams(encodedArgs.data)) { 133 body.append(key, value); 134 } 135 } else { 136 body = encodedArgs.data; 137 } 138 139 let decoded; 140 try { 141 decoded = await decodeReply(body, {}); 142 } catch (e: any) { 143 let message; 144 message = 145 "React couldn't parse the request payload. " + 146 "Try triggering a real action first and copying its payload format."; 147 if (e?.message !== "Connection closed.") { 148 message += "\n\n" + e.toString(); 149 } 150 throw new Error(message); 151 } 152 const args = Array.isArray(decoded) ? decoded : [decoded]; 153 const result = await actionFn(...args); 154 155 return renderToReadableStream(result, deployed.manifest, renderOptions); 156} 157export type CallAction = typeof callAction; 158 159// --- Message dispatch --- 160 161self.onmessage = ( 162 event: MessageEvent< 163 { requestId: string } & ( 164 | { method: "deploy"; args: Parameters<Deploy> } 165 | { method: "render"; args: Parameters<Render> } 166 | { method: "action"; args: Parameters<CallAction> } 167 ) 168 >, 169) => { 170 const req = event.data; 171 switch (req.method) { 172 case "deploy": 173 sendStream(req.requestId, () => deploy(...req.args)); 174 break; 175 case "render": 176 sendStream(req.requestId, () => render(...req.args)); 177 break; 178 case "action": 179 sendStream(req.requestId, () => callAction(...req.args)); 180 break; 181 } 182}; 183 184polyfillReady.then(() => { 185 self.postMessage({ type: "ready" }); 186});