A tool for people curious about the React Server Components protocol
rscexplorer.dev/
rsc
react
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});