A tool for people curious about the React Server Components protocol

tighten up the code

-16
package-lock.json
··· 22 "react": "19.2.3", 23 "react-dom": "19.2.3", 24 "react-server-dom-webpack": "19.2.3", 25 - "text-encoding": "^0.7.0", 26 "web-streams-polyfill": "^4.2.0" 27 }, 28 "devDependencies": { ··· 33 "@types/node": "^25.0.2", 34 "@types/react": "^19.2.7", 35 "@types/react-dom": "^19.2.3", 36 - "@types/text-encoding": "^0.0.40", 37 "@vitejs/plugin-react": "^5.1.2", 38 "@vitest/browser": "^4.0.15", 39 "@vitest/browser-playwright": "^4.0.15", ··· 1876 "peerDependencies": { 1877 "@types/react": "^19.2.0" 1878 } 1879 - }, 1880 - "node_modules/@types/text-encoding": { 1881 - "version": "0.0.40", 1882 - "resolved": "https://registry.npmjs.org/@types/text-encoding/-/text-encoding-0.0.40.tgz", 1883 - "integrity": "sha512-dHzoIdwBfY7jcSTTt6XBkaeiuFQAQD7r/7aJySKDdHkYBCDOvs9jPVt4NYXuwBMn89PP6gSd29WubIS19wTiXg==", 1884 - "dev": true, 1885 - "license": "MIT" 1886 }, 1887 "node_modules/@typescript-eslint/eslint-plugin": { 1888 "version": "8.50.0", ··· 5924 "optional": true 5925 } 5926 } 5927 - }, 5928 - "node_modules/text-encoding": { 5929 - "version": "0.7.0", 5930 - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", 5931 - "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==", 5932 - "deprecated": "no longer maintained", 5933 - "license": "(Unlicense OR Apache-2.0)" 5934 }, 5935 "node_modules/through": { 5936 "version": "2.3.8",
··· 22 "react": "19.2.3", 23 "react-dom": "19.2.3", 24 "react-server-dom-webpack": "19.2.3", 25 "web-streams-polyfill": "^4.2.0" 26 }, 27 "devDependencies": { ··· 32 "@types/node": "^25.0.2", 33 "@types/react": "^19.2.7", 34 "@types/react-dom": "^19.2.3", 35 "@vitejs/plugin-react": "^5.1.2", 36 "@vitest/browser": "^4.0.15", 37 "@vitest/browser-playwright": "^4.0.15", ··· 1874 "peerDependencies": { 1875 "@types/react": "^19.2.0" 1876 } 1877 }, 1878 "node_modules/@typescript-eslint/eslint-plugin": { 1879 "version": "8.50.0", ··· 5915 "optional": true 5916 } 5917 } 5918 }, 5919 "node_modules/through": { 5920 "version": "2.3.8",
-2
package.json
··· 35 "react": "19.2.3", 36 "react-dom": "19.2.3", 37 "react-server-dom-webpack": "19.2.3", 38 - "text-encoding": "^0.7.0", 39 "web-streams-polyfill": "^4.2.0" 40 }, 41 "overrides": { ··· 49 "@types/node": "^25.0.2", 50 "@types/react": "^19.2.7", 51 "@types/react-dom": "^19.2.3", 52 - "@types/text-encoding": "^0.0.40", 53 "@vitejs/plugin-react": "^5.1.2", 54 "@vitest/browser": "^4.0.15", 55 "@vitest/browser-playwright": "^4.0.15",
··· 35 "react": "19.2.3", 36 "react-dom": "19.2.3", 37 "react-server-dom-webpack": "19.2.3", 38 "web-streams-polyfill": "^4.2.0" 39 }, 40 "overrides": { ··· 48 "@types/node": "^25.0.2", 49 "@types/react": "^19.2.7", 50 "@types/react-dom": "^19.2.3", 51 "@vitejs/plugin-react": "^5.1.2", 52 "@vitest/browser": "^4.0.15", 53 "@vitest/browser-playwright": "^4.0.15",
-16
src/client/byte-stream-polyfill.ts
··· 1 - // Safari doesn't implement ReadableByteStreamController. 2 - // The standard web-streams-polyfill only polyfills ReadableStream, not byte streams. 3 - // This adds the missing byte stream support. 4 - 5 - import { 6 - ReadableStream as PolyfillReadableStream, 7 - ReadableByteStreamController as PolyfillReadableByteStreamController, 8 - } from "web-streams-polyfill"; 9 - 10 - if (typeof globalThis.ReadableByteStreamController === "undefined") { 11 - // Safari doesn't have byte stream support - use the polyfill's ReadableStream 12 - // which includes full byte stream support 13 - globalThis.ReadableStream = PolyfillReadableStream as typeof ReadableStream; 14 - // @ts-expect-error: ReadableByteStreamController polyfill assignment 15 - globalThis.ReadableByteStreamController = PolyfillReadableByteStreamController; 16 - }
···
+2 -6
src/client/embed.tsx
··· 1 - // Must be first - shims webpack globals for react-server-dom-webpack 2 - import "./webpack-shim.ts"; 3 - 4 - import "./byte-stream-polyfill.ts"; 5 - import "web-streams-polyfill/polyfill"; 6 - import "text-encoding"; 7 8 import React, { useState, useEffect } from "react"; 9 import { createRoot } from "react-dom/client";
··· 1 + import "../shared/webpack-shim.ts"; 2 + import "../shared/polyfill.ts"; 3 4 import React, { useState, useEffect } from "react"; 5 import { createRoot } from "react-dom/client";
+2 -6
src/client/index.tsx
··· 1 - // Must be first - shims webpack globals for react-server-dom-webpack 2 - import "./webpack-shim.ts"; 3 - 4 - import "./byte-stream-polyfill.ts"; 5 - import "web-streams-polyfill/polyfill"; 6 - import "text-encoding"; 7 8 import { createRoot } from "react-dom/client"; 9 import { App } from "./ui/App.tsx";
··· 1 + import "../shared/webpack-shim.ts"; 2 + import "../shared/polyfill.ts"; 3 4 import { createRoot } from "react-dom/client"; 5 import { App } from "./ui/App.tsx";
-272
src/client/server-worker.ts
··· 1 - import workerUrl from "../server/worker.ts?rolldown-worker"; 2 - import type { ClientManifest } from "../shared/compiler.ts"; 3 - 4 - const randomUUID: () => string = 5 - crypto.randomUUID?.bind(crypto) ?? 6 - function (): string { 7 - const bytes = crypto.getRandomValues(new Uint8Array(16)); 8 - bytes[6] = (bytes[6]! & 0x0f) | 0x40; // version 4 9 - bytes[8] = (bytes[8]! & 0x3f) | 0x80; // variant 1 10 - const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")); 11 - return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`; 12 - }; 13 - 14 - type EncodedArgsFormData = { 15 - type: "formdata"; 16 - data: string; 17 - }; 18 - 19 - type EncodedArgsString = { 20 - type: "string"; 21 - data: string; 22 - }; 23 - 24 - type EncodedArgsTransfer = EncodedArgsFormData | EncodedArgsString; 25 - 26 - function serializeForTransfer(encoded: FormData | string): EncodedArgsTransfer { 27 - if (encoded instanceof FormData) { 28 - return { 29 - type: "formdata", 30 - data: new URLSearchParams(encoded as unknown as Record<string, string>).toString(), 31 - }; 32 - } 33 - return { type: "string", data: encoded }; 34 - } 35 - 36 - type WorkerReadyMessage = { 37 - type: "ready"; 38 - }; 39 - 40 - type WorkerStreamStartMessage = { 41 - type: "stream-start"; 42 - requestId: string; 43 - }; 44 - 45 - type WorkerStreamChunkMessage = { 46 - type: "stream-chunk"; 47 - requestId: string; 48 - chunk: Uint8Array; 49 - }; 50 - 51 - type WorkerStreamEndMessage = { 52 - type: "stream-end"; 53 - requestId: string; 54 - }; 55 - 56 - type WorkerStreamErrorMessage = { 57 - type: "stream-error"; 58 - requestId: string; 59 - error: { message: string }; 60 - }; 61 - 62 - type WorkerDeployedMessage = { 63 - type: "deployed"; 64 - requestId: string; 65 - }; 66 - 67 - type WorkerErrorMessage = { 68 - type: "error"; 69 - requestId: string; 70 - error: { message: string; stack?: string }; 71 - }; 72 - 73 - type WorkerMessage = 74 - | WorkerReadyMessage 75 - | WorkerStreamStartMessage 76 - | WorkerStreamChunkMessage 77 - | WorkerStreamEndMessage 78 - | WorkerStreamErrorMessage 79 - | WorkerDeployedMessage 80 - | WorkerErrorMessage; 81 - 82 - type PendingRequest = { 83 - resolve: (value: unknown) => void; 84 - reject: (error: Error) => void; 85 - }; 86 - 87 - export class ServerWorker { 88 - private worker: Worker; 89 - private pending: Map<string, PendingRequest> = new Map(); 90 - private streams: Map<string, ReadableStreamDefaultController<Uint8Array>> = new Map(); 91 - private readyPromise: Promise<void>; 92 - private readyResolve!: () => void; 93 - 94 - constructor() { 95 - this.worker = new Worker(workerUrl); 96 - this.readyPromise = new Promise((resolve) => { 97 - this.readyResolve = resolve; 98 - }); 99 - 100 - this.worker.onmessage = this.handleMessage.bind(this); 101 - this.worker.onerror = this.handleError.bind(this); 102 - } 103 - 104 - private handleMessage(event: MessageEvent<WorkerMessage>): void { 105 - const { type } = event.data; 106 - 107 - if (type === "ready") { 108 - this.readyResolve(); 109 - return; 110 - } 111 - 112 - if (type === "stream-start") { 113 - const { requestId } = event.data; 114 - const pending = this.pending.get(requestId); 115 - if (!pending) return; 116 - this.pending.delete(requestId); 117 - 118 - let controller: ReadableStreamDefaultController<Uint8Array>; 119 - const stream = new ReadableStream<Uint8Array>({ 120 - start: (c) => { 121 - controller = c; 122 - }, 123 - }); 124 - this.streams.set(requestId, controller!); 125 - pending.resolve(stream); 126 - return; 127 - } 128 - 129 - if (type === "stream-chunk") { 130 - const { requestId, chunk } = event.data; 131 - const controller = this.streams.get(requestId); 132 - if (controller) controller.enqueue(chunk); 133 - return; 134 - } 135 - 136 - if (type === "stream-end") { 137 - const { requestId } = event.data; 138 - const controller = this.streams.get(requestId); 139 - if (controller) { 140 - controller.close(); 141 - this.streams.delete(requestId); 142 - } 143 - return; 144 - } 145 - 146 - if (type === "stream-error") { 147 - const { requestId, error } = event.data; 148 - const controller = this.streams.get(requestId); 149 - if (controller) { 150 - controller.error(new Error(error.message)); 151 - this.streams.delete(requestId); 152 - } 153 - return; 154 - } 155 - 156 - if (type === "deployed") { 157 - const { requestId } = event.data as WorkerDeployedMessage; 158 - const pending = this.pending.get(requestId); 159 - if (!pending) { 160 - console.warn(`No pending request for ${requestId}`); 161 - return; 162 - } 163 - this.pending.delete(requestId); 164 - pending.resolve(undefined); 165 - return; 166 - } 167 - 168 - if (type === "error") { 169 - const { requestId, error } = event.data as WorkerErrorMessage; 170 - const pending = this.pending.get(requestId); 171 - if (!pending) { 172 - console.warn(`No pending request for ${requestId}`); 173 - return; 174 - } 175 - this.pending.delete(requestId); 176 - const err = new Error(error.message); 177 - if (error.stack) { 178 - err.stack = error.stack; 179 - } 180 - pending.reject(err); 181 - } 182 - } 183 - 184 - private handleError(event: ErrorEvent): void { 185 - const errorMsg = event.message || "Unknown worker error"; 186 - console.error(`Worker error: ${errorMsg}`); 187 - 188 - for (const [, pending] of this.pending) { 189 - pending.reject(new Error(`Worker error: ${errorMsg}`)); 190 - } 191 - this.pending.clear(); 192 - } 193 - 194 - async deploy({ 195 - compiledCode, 196 - manifest, 197 - actionNames, 198 - }: { 199 - compiledCode: string; 200 - manifest: ClientManifest; 201 - actionNames: string[]; 202 - }): Promise<void> { 203 - await this.readyPromise; 204 - const requestId = randomUUID(); 205 - 206 - return new Promise((resolve, reject) => { 207 - this.pending.set(requestId, { resolve: resolve as (value: unknown) => void, reject }); 208 - this.worker.postMessage({ 209 - type: "deploy", 210 - requestId, 211 - compiledCode, 212 - manifest, 213 - actionNames, 214 - }); 215 - }); 216 - } 217 - 218 - async render(): Promise<ReadableStream<Uint8Array>> { 219 - await this.readyPromise; 220 - const requestId = randomUUID(); 221 - 222 - return new Promise((resolve, reject) => { 223 - this.pending.set(requestId, { resolve: resolve as (value: unknown) => void, reject }); 224 - this.worker.postMessage({ type: "render", requestId }); 225 - }); 226 - } 227 - 228 - async callAction( 229 - actionId: string, 230 - encodedArgs: FormData | string, 231 - ): Promise<ReadableStream<Uint8Array>> { 232 - await this.readyPromise; 233 - const requestId = randomUUID(); 234 - 235 - return new Promise((resolve, reject) => { 236 - this.pending.set(requestId, { resolve: resolve as (value: unknown) => void, reject }); 237 - this.worker.postMessage({ 238 - type: "action", 239 - requestId, 240 - actionId, 241 - encodedArgs: serializeForTransfer(encodedArgs), 242 - }); 243 - }); 244 - } 245 - 246 - async callActionRaw(actionId: string, rawPayload: string): Promise<ReadableStream<Uint8Array>> { 247 - await this.readyPromise; 248 - const requestId = randomUUID(); 249 - 250 - return new Promise((resolve, reject) => { 251 - this.pending.set(requestId, { resolve: resolve as (value: unknown) => void, reject }); 252 - this.worker.postMessage({ 253 - type: "action", 254 - requestId, 255 - actionId, 256 - encodedArgs: { type: "formdata", data: rawPayload }, 257 - }); 258 - }); 259 - } 260 - 261 - terminate(): void { 262 - this.worker.terminate(); 263 - for (const [, pending] of this.pending) { 264 - pending.reject(new Error("Worker terminated")); 265 - } 266 - this.pending.clear(); 267 - for (const [, controller] of this.streams) { 268 - controller.error(new Error("Worker terminated")); 269 - } 270 - this.streams.clear(); 271 - } 272 - }
···
+21 -14
src/client/ui/Workspace.tsx
··· 7 evaluateClientModule, 8 type CallServerCallback, 9 } from "../runtime/index.ts"; 10 - import { ServerWorker } from "../server-worker.ts"; 11 import { 12 parseClientModule, 13 parseServerActions, ··· 35 }: WorkspaceProps): React.ReactElement { 36 const [serverCode, setServerCode] = useState(initialServerCode); 37 const [clientCode, setClientCode] = useState(initialClientCode); 38 - const [serverWorker] = useState(() => new ServerWorker()); 39 const [timeline] = useState(() => new Timeline()); 40 const [callServerRef] = useState<CallServerRef>({ current: null }); 41 42 const snapshot = useSyncExternalStore(timeline.subscribe, timeline.getSnapshot); ··· 67 68 const handleAddRawAction = useCallback( 69 async (actionName: string, rawPayload: string) => { 70 try { 71 - const responseRaw = await serverWorker.callActionRaw(actionName, rawPayload); 72 const streamOptions = callServerRef.current ? { callServer: callServerRef.current } : {}; 73 const stream = new SteppableStream(responseRaw, streamOptions); 74 await stream.waitForBuffer(); ··· 77 console.error("[raw action] Failed:", err); 78 } 79 }, 80 - [serverWorker, timeline, callServerRef], 81 ); 82 83 const compile = useCallback( 84 async (sCode: string, cCode: string) => { 85 try { 86 setError(null); 87 timeline.clear(); ··· 96 const compiledServer = compileToCommonJS(sCode); 97 setAvailableActions(actionNames); 98 99 - await serverWorker.deploy({ 100 - compiledCode: compiledServer, 101 - manifest, 102 - actionNames, 103 - }); 104 105 const callServer: CallServerCallback | null = 106 actionNames.length > 0 ··· 114 encodedArgs as unknown as Record<string, string>, 115 ).toString(); 116 117 - const responseRaw = await serverWorker.callAction(actionName, encodedArgs); 118 const stream = new SteppableStream(responseRaw, { 119 callServer: callServer as CallServerCallback, 120 }); ··· 126 127 callServerRef.current = callServer; 128 129 - const renderRaw = await serverWorker.render(); 130 const renderStreamOptions = callServer ? { callServer } : {}; 131 const renderStream = new SteppableStream(renderRaw, renderStreamOptions); 132 await renderStream.waitForBuffer(); ··· 140 setClientModuleReady(false); 141 } 142 }, 143 - [timeline, serverWorker, callServerRef], 144 ); 145 146 const handleReset = useCallback(() => { ··· 157 }, [serverCode, clientCode, compile]); 158 159 useEffect(() => { 160 - return () => serverWorker.terminate(); 161 - }, [serverWorker]); 162 163 return ( 164 <main>
··· 7 evaluateClientModule, 8 type CallServerCallback, 9 } from "../runtime/index.ts"; 10 + import { WorkerClient, encodeArgs } from "../worker-client.ts"; 11 import { 12 parseClientModule, 13 parseServerActions, ··· 35 }: WorkspaceProps): React.ReactElement { 36 const [serverCode, setServerCode] = useState(initialServerCode); 37 const [clientCode, setClientCode] = useState(initialClientCode); 38 const [timeline] = useState(() => new Timeline()); 39 + const [workerClient, setWorkerClient] = useState<WorkerClient | null>(null); 40 const [callServerRef] = useState<CallServerRef>({ current: null }); 41 42 const snapshot = useSyncExternalStore(timeline.subscribe, timeline.getSnapshot); ··· 67 68 const handleAddRawAction = useCallback( 69 async (actionName: string, rawPayload: string) => { 70 + if (!workerClient) throw new Error("Worker not initialized"); 71 try { 72 + const responseRaw = await workerClient.callAction(actionName, { 73 + type: "formdata", 74 + data: rawPayload, 75 + }); 76 const streamOptions = callServerRef.current ? { callServer: callServerRef.current } : {}; 77 const stream = new SteppableStream(responseRaw, streamOptions); 78 await stream.waitForBuffer(); ··· 81 console.error("[raw action] Failed:", err); 82 } 83 }, 84 + [workerClient, timeline, callServerRef], 85 ); 86 87 const compile = useCallback( 88 async (sCode: string, cCode: string) => { 89 + if (!workerClient) throw new Error("Worker not initialized"); 90 try { 91 setError(null); 92 timeline.clear(); ··· 101 const compiledServer = compileToCommonJS(sCode); 102 setAvailableActions(actionNames); 103 104 + await workerClient.deploy(compiledServer, manifest, actionNames); 105 106 const callServer: CallServerCallback | null = 107 actionNames.length > 0 ··· 115 encodedArgs as unknown as Record<string, string>, 116 ).toString(); 117 118 + const responseRaw = await workerClient.callAction( 119 + actionName, 120 + encodeArgs(encodedArgs), 121 + ); 122 const stream = new SteppableStream(responseRaw, { 123 callServer: callServer as CallServerCallback, 124 }); ··· 130 131 callServerRef.current = callServer; 132 133 + const renderRaw = await workerClient.render(); 134 const renderStreamOptions = callServer ? { callServer } : {}; 135 const renderStream = new SteppableStream(renderRaw, renderStreamOptions); 136 await renderStream.waitForBuffer(); ··· 144 setClientModuleReady(false); 145 } 146 }, 147 + [timeline, workerClient, callServerRef], 148 ); 149 150 const handleReset = useCallback(() => { ··· 161 }, [serverCode, clientCode, compile]); 162 163 useEffect(() => { 164 + const client = new WorkerClient(); 165 + // eslint-disable-next-line react-hooks/set-state-in-effect 166 + setWorkerClient(client); 167 + return () => client.terminate(); 168 + }, []); 169 170 return ( 171 <main>
-76
src/client/webpack-shim.ts
··· 1 - // Shim webpack globals for react-server-dom-webpack/client in browser context 2 - 3 - type ModuleFactory = { 4 - (module: { exports: unknown }): void; 5 - }; 6 - 7 - type WebpackRequire = { 8 - (moduleId: string): unknown; 9 - m: Record<string, ModuleFactory>; 10 - c: Record<string, { exports: unknown } | unknown>; 11 - d: (exports: object, definition: Record<string, () => unknown>) => void; 12 - r: (exports: object) => void; 13 - o: (obj: object, prop: string) => boolean; 14 - e: (chunkId: string) => Promise<void>; 15 - p: string; 16 - }; 17 - 18 - const clientModuleCache: Record<string, { exports: unknown }> = {}; 19 - const clientModuleFactories: Record<string, ModuleFactory> = {}; 20 - 21 - window.__webpack_module_cache__ = clientModuleCache; 22 - window.__webpack_modules__ = clientModuleFactories; 23 - 24 - const clientWebpackRequire: WebpackRequire = function (moduleId: string): unknown { 25 - const cached = clientModuleCache[moduleId]; 26 - if (cached) { 27 - return cached.exports ?? cached; 28 - } 29 - const factory = clientModuleFactories[moduleId]; 30 - if (factory) { 31 - const module: { exports: unknown } = { exports: {} }; 32 - factory(module); 33 - clientModuleCache[moduleId] = module; 34 - return module.exports; 35 - } 36 - throw new Error(`Module ${moduleId} not found in webpack shim`); 37 - } as WebpackRequire; 38 - 39 - clientWebpackRequire.m = clientModuleFactories; 40 - clientWebpackRequire.c = clientModuleCache; 41 - clientWebpackRequire.d = function ( 42 - exports: object, 43 - definition: Record<string, () => unknown>, 44 - ): void { 45 - for (const key in definition) { 46 - const getter = definition[key]; 47 - if ( 48 - getter && 49 - Object.prototype.hasOwnProperty.call(definition, key) && 50 - !Object.prototype.hasOwnProperty.call(exports, key) 51 - ) { 52 - Object.defineProperty(exports, key, { enumerable: true, get: getter }); 53 - } 54 - } 55 - }; 56 - clientWebpackRequire.r = function (exports: object): void { 57 - if (typeof Symbol !== "undefined" && Symbol.toStringTag) { 58 - Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); 59 - } 60 - Object.defineProperty(exports, "__esModule", { value: true }); 61 - }; 62 - clientWebpackRequire.o = function (obj: object, prop: string): boolean { 63 - return Object.prototype.hasOwnProperty.call(obj, prop); 64 - }; 65 - clientWebpackRequire.e = function (_chunkId: string): Promise<void> { 66 - return Promise.resolve(); 67 - }; 68 - clientWebpackRequire.p = "/"; 69 - 70 - window.__webpack_require__ = clientWebpackRequire; 71 - 72 - window.__webpack_chunk_load__ = function (_chunkId: string): Promise<void> { 73 - return Promise.resolve(); 74 - }; 75 - 76 - export {};
···
+109
src/client/worker-client.ts
···
··· 1 + import workerUrl from "../server/worker-server.ts?rolldown-worker"; 2 + import type { Response, EncodedArgs, Deploy, Render, CallAction } from "../server/worker-server.ts"; 3 + import type { ClientManifest } from "../shared/compiler.ts"; 4 + 5 + export type { EncodedArgs, ClientManifest }; 6 + 7 + export function encodeArgs(encoded: FormData | string): EncodedArgs { 8 + if (encoded instanceof FormData) { 9 + return { 10 + type: "formdata", 11 + data: new URLSearchParams(encoded as unknown as Record<string, string>).toString(), 12 + }; 13 + } 14 + return { type: "string", data: encoded }; 15 + } 16 + 17 + export class WorkerClient { 18 + private worker: Worker; 19 + private requests = new Map<string, ReadableStreamDefaultController<Uint8Array>>(); 20 + private readyPromise: Promise<void>; 21 + private readyResolve!: () => void; 22 + 23 + constructor() { 24 + this.worker = new Worker(workerUrl); 25 + this.readyPromise = new Promise((resolve) => { 26 + this.readyResolve = resolve; 27 + }); 28 + this.worker.onmessage = this.handleMessage.bind(this); 29 + this.worker.onerror = (e) => { 30 + const err = new Error(e.message || "Worker error"); 31 + for (const controller of this.requests.values()) { 32 + controller.error(err); 33 + } 34 + this.requests.clear(); 35 + }; 36 + } 37 + 38 + private handleMessage(event: MessageEvent<Response>): void { 39 + const msg = event.data; 40 + 41 + if (msg.type === "ready") { 42 + this.readyResolve(); 43 + return; 44 + } 45 + 46 + const controller = this.requests.get(msg.requestId); 47 + if (!controller) throw new Error(`Unknown request: ${msg.requestId}`); 48 + 49 + switch (msg.type) { 50 + case "next": 51 + controller.enqueue(msg.value); 52 + break; 53 + 54 + case "done": 55 + controller.close(); 56 + this.requests.delete(msg.requestId); 57 + break; 58 + 59 + case "throw": { 60 + const err = new Error(msg.error); 61 + if (msg.stack) { 62 + err.stack = msg.stack; 63 + } 64 + controller.error(err); 65 + this.requests.delete(msg.requestId); 66 + break; 67 + } 68 + } 69 + } 70 + 71 + private nextRequestId = 0; 72 + 73 + private request(body: Record<string, unknown>): ReadableStream<Uint8Array> { 74 + const requestId = String(this.nextRequestId++); 75 + let controller!: ReadableStreamDefaultController<Uint8Array>; 76 + const stream = new ReadableStream<Uint8Array>({ 77 + start: (c) => { 78 + controller = c; 79 + }, 80 + }); 81 + this.requests.set(requestId, controller); 82 + this.worker.postMessage({ ...body, requestId }); 83 + return stream; 84 + } 85 + 86 + terminate(): void { 87 + this.worker.terminate(); 88 + const err = new Error("Worker terminated"); 89 + for (const controller of this.requests.values()) { 90 + controller.error(err); 91 + } 92 + this.requests.clear(); 93 + } 94 + 95 + async deploy(...args: Parameters<Deploy>): Promise<ReturnType<Deploy>> { 96 + await this.readyPromise; 97 + return this.request({ method: "deploy", args }); 98 + } 99 + 100 + async render(...args: Parameters<Render>): Promise<ReturnType<Render>> { 101 + await this.readyPromise; 102 + return this.request({ method: "render", args }); 103 + } 104 + 105 + async callAction(...args: Parameters<CallAction>): ReturnType<CallAction> { 106 + await this.readyPromise; 107 + return this.request({ method: "action", args }); 108 + } 109 + }
-68
src/server/webpack-shim.ts
··· 1 - // Shim webpack globals for react-server-dom-webpack/server in worker context 2 - // Uses self instead of window since this runs in a Web Worker 3 - 4 - type WebpackRequire = { 5 - (moduleId: string): unknown; 6 - m: Record<string, (module: { exports: unknown }) => void>; 7 - c: Record<string, unknown>; 8 - d: (exports: object, definition: Record<string, () => unknown>) => void; 9 - r: (exports: object) => void; 10 - o: (obj: object, prop: string) => boolean; 11 - e: (chunkId: string) => Promise<void>; 12 - p: string; 13 - }; 14 - 15 - type WorkerSelf = DedicatedWorkerGlobalScope & { 16 - __webpack_require__: WebpackRequire; 17 - __webpack_chunk_load__: (chunkId: string) => Promise<void>; 18 - }; 19 - 20 - const workerSelf = self as unknown as WorkerSelf; 21 - 22 - const moduleCache: Record<string, unknown> = {}; 23 - 24 - const webpackRequire: WebpackRequire = function (moduleId: string): unknown { 25 - if (moduleCache[moduleId]) { 26 - return moduleCache[moduleId]; 27 - } 28 - throw new Error(`Module ${moduleId} not found in webpack shim`); 29 - } as WebpackRequire; 30 - 31 - webpackRequire.m = {}; 32 - webpackRequire.c = moduleCache; 33 - webpackRequire.d = function (exports: object, definition: Record<string, () => unknown>): void { 34 - for (const key in definition) { 35 - const getter = definition[key]; 36 - if ( 37 - getter && 38 - Object.prototype.hasOwnProperty.call(definition, key) && 39 - !Object.prototype.hasOwnProperty.call(exports, key) 40 - ) { 41 - Object.defineProperty(exports, key, { 42 - enumerable: true, 43 - get: getter, 44 - }); 45 - } 46 - } 47 - }; 48 - webpackRequire.r = function (exports: object): void { 49 - if (typeof Symbol !== "undefined" && Symbol.toStringTag) { 50 - Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); 51 - } 52 - Object.defineProperty(exports, "__esModule", { value: true }); 53 - }; 54 - webpackRequire.o = function (obj: object, prop: string): boolean { 55 - return Object.prototype.hasOwnProperty.call(obj, prop); 56 - }; 57 - webpackRequire.e = function (_chunkId: string): Promise<void> { 58 - return Promise.resolve(); 59 - }; 60 - webpackRequire.p = "/"; 61 - 62 - workerSelf.__webpack_require__ = webpackRequire; 63 - 64 - workerSelf.__webpack_chunk_load__ = function (_chunkId: string): Promise<void> { 65 - return Promise.resolve(); 66 - }; 67 - 68 - export {};
···
+168
src/server/worker-server.ts
···
··· 1 + // Server Worker - RSC server simulation 2 + 3 + import "../shared/webpack-shim.ts"; 4 + import "../shared/polyfill.ts"; 5 + 6 + import { 7 + renderToReadableStream, 8 + registerServerReference, 9 + createClientModuleProxy, 10 + decodeReply, 11 + } from "react-server-dom-webpack/server"; 12 + import React from "react"; 13 + import type { ClientManifest } from "../shared/compiler.ts"; 14 + 15 + declare const self: DedicatedWorkerGlobalScope; 16 + 17 + // --- Types --- 18 + 19 + export type EncodedArgs = { 20 + type: "formdata" | "string"; 21 + data: string; 22 + }; 23 + 24 + export 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 + 32 + type ServerModule = { 33 + default?: React.ComponentType | React.ReactNode; 34 + [key: string]: unknown; 35 + }; 36 + 37 + let deployed: { manifest: ClientManifest; module: ServerModule } | null = null; 38 + 39 + // --- Response helpers --- 40 + 41 + async 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 + 66 + function 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 + } 106 + export type Deploy = typeof deploy; 107 + 108 + function render(): ReadableStream<Uint8Array> { 109 + if (!deployed) throw new Error("No code deployed"); 110 + const App = deployed.module.default as React.ComponentType; 111 + return renderToReadableStream(React.createElement(App), deployed.manifest); 112 + } 113 + export type Render = typeof render; 114 + 115 + async function callAction( 116 + actionId: string, 117 + encodedArgs: EncodedArgs, 118 + ): Promise<ReadableStream<Uint8Array>> { 119 + if (!deployed) throw new Error("No code deployed"); 120 + if (!Object.hasOwn(deployed.module, actionId)) { 121 + throw new Error(`Action "${actionId}" not found`); 122 + } 123 + const actionFn = deployed.module[actionId] as Function; 124 + 125 + let body: FormData | string; 126 + if (encodedArgs.type === "formdata") { 127 + body = new FormData(); 128 + for (const [key, value] of new URLSearchParams(encodedArgs.data)) { 129 + body.append(key, value); 130 + } 131 + } else { 132 + body = encodedArgs.data; 133 + } 134 + 135 + const decoded = await decodeReply(body, {}); 136 + const args = Array.isArray(decoded) ? decoded : [decoded]; 137 + const result = await actionFn(...args); 138 + 139 + return renderToReadableStream(result, deployed.manifest); 140 + } 141 + export type CallAction = typeof callAction; 142 + 143 + // --- Message dispatch --- 144 + 145 + self.onmessage = ( 146 + event: MessageEvent< 147 + { requestId: string } & ( 148 + | { method: "deploy"; args: Parameters<Deploy> } 149 + | { method: "render"; args: Parameters<Render> } 150 + | { method: "action"; args: Parameters<CallAction> } 151 + ) 152 + >, 153 + ) => { 154 + const req = event.data; 155 + switch (req.method) { 156 + case "deploy": 157 + sendStream(req.requestId, () => deploy(...req.args)); 158 + break; 159 + case "render": 160 + sendStream(req.requestId, () => render(...req.args)); 161 + break; 162 + case "action": 163 + sendStream(req.requestId, () => callAction(...req.args)); 164 + break; 165 + } 166 + }; 167 + 168 + self.postMessage({ type: "ready" });
-273
src/server/worker.ts
··· 1 - // Server Worker - RSC server simulation 2 - // 3 - // Models a real server: deploy code once, then handle requests against it. 4 - // - `deploy`: Store compiled code, manifest, etc. (like deploying to production) 5 - // - `render`/`action`: Execute against deployed code 6 - 7 - import "./webpack-shim.ts"; 8 - import "../client/byte-stream-polyfill.ts"; 9 - import "text-encoding"; 10 - 11 - import { 12 - renderToReadableStream, 13 - registerServerReference, 14 - createClientModuleProxy, 15 - decodeReply, 16 - type ClientManifest, 17 - } from "react-server-dom-webpack/server"; 18 - import React from "react"; 19 - 20 - declare const self: DedicatedWorkerGlobalScope; 21 - 22 - type DeployMessage = { 23 - type: "deploy"; 24 - requestId: string; 25 - compiledCode: string; 26 - manifest: ClientManifest; 27 - actionNames: string[]; 28 - }; 29 - 30 - type RenderMessage = { 31 - type: "render"; 32 - requestId: string; 33 - }; 34 - 35 - type ActionMessage = { 36 - type: "action"; 37 - requestId: string; 38 - actionId: string; 39 - encodedArgs: EncodedArgs; 40 - }; 41 - 42 - type WorkerMessage = DeployMessage | RenderMessage | ActionMessage; 43 - 44 - type EncodedArgs = { 45 - type: "formdata" | "string"; 46 - data: string; 47 - }; 48 - 49 - type ErrorResponse = { 50 - type: "error"; 51 - requestId: string; 52 - error: { message: string; stack?: string }; 53 - }; 54 - 55 - type DeployedResponse = { 56 - type: "deployed"; 57 - requestId: string; 58 - }; 59 - 60 - type StreamStartResponse = { 61 - type: "stream-start"; 62 - requestId: string; 63 - }; 64 - 65 - type StreamChunkResponse = { 66 - type: "stream-chunk"; 67 - requestId: string; 68 - chunk: Uint8Array; 69 - }; 70 - 71 - type StreamEndResponse = { 72 - type: "stream-end"; 73 - requestId: string; 74 - }; 75 - 76 - type StreamErrorResponse = { 77 - type: "stream-error"; 78 - requestId: string; 79 - error: { message: string }; 80 - }; 81 - 82 - type ReadyResponse = { 83 - type: "ready"; 84 - }; 85 - 86 - type ServerModule = { 87 - default?: React.ComponentType | React.ReactNode; 88 - [key: string]: unknown; 89 - }; 90 - 91 - type DeployedState = { 92 - manifest: ClientManifest; 93 - serverModule: ServerModule; 94 - actionNames: string[]; 95 - }; 96 - 97 - let deployed: DeployedState | null = null; 98 - 99 - // Safari doesn't support transferable streams 100 - async function streamToMain(stream: ReadableStream<Uint8Array>, requestId: string): Promise<void> { 101 - const reader = stream.getReader(); 102 - try { 103 - while (true) { 104 - const { done, value } = await reader.read(); 105 - if (done) { 106 - self.postMessage({ type: "stream-end", requestId } satisfies StreamEndResponse); 107 - break; 108 - } 109 - self.postMessage({ 110 - type: "stream-chunk", 111 - requestId, 112 - chunk: value, 113 - } satisfies StreamChunkResponse); 114 - } 115 - } catch (err) { 116 - const error = err instanceof Error ? err : new Error(String(err)); 117 - self.postMessage({ 118 - type: "stream-error", 119 - requestId, 120 - error: { message: error.message }, 121 - } satisfies StreamErrorResponse); 122 - } 123 - } 124 - 125 - self.onmessage = async (event: MessageEvent<WorkerMessage>) => { 126 - const { type, requestId } = event.data; 127 - 128 - try { 129 - switch (type) { 130 - case "deploy": 131 - handleDeploy(event.data); 132 - break; 133 - case "render": 134 - await handleRender(event.data); 135 - break; 136 - case "action": 137 - await handleAction(event.data); 138 - break; 139 - default: { 140 - const _exhaustive: never = type; 141 - throw new Error(`Unknown message type: ${_exhaustive}`); 142 - } 143 - } 144 - } catch (error) { 145 - const err = error instanceof Error ? error : new Error(String(error)); 146 - const errorPayload: { message: string; stack?: string } = { message: err.message }; 147 - if (err.stack) { 148 - errorPayload.stack = err.stack; 149 - } 150 - self.postMessage({ 151 - type: "error", 152 - requestId, 153 - error: errorPayload, 154 - } satisfies ErrorResponse); 155 - } 156 - }; 157 - 158 - function handleDeploy({ compiledCode, manifest, actionNames, requestId }: DeployMessage): void { 159 - const clientModule = createClientModuleProxy("client"); 160 - const modules: Record<string, unknown> = { react: React, "./client": clientModule }; 161 - const serverModule = evalModule(compiledCode, modules, actionNames); 162 - 163 - deployed = { manifest, serverModule, actionNames }; 164 - 165 - self.postMessage({ type: "deployed", requestId } satisfies DeployedResponse); 166 - } 167 - 168 - function requireDeployed(): DeployedState { 169 - if (!deployed) throw new Error("No code deployed"); 170 - return deployed; 171 - } 172 - 173 - async function handleRender({ requestId }: RenderMessage): Promise<void> { 174 - const { manifest, serverModule } = requireDeployed(); 175 - 176 - const App = serverModule.default ?? serverModule; 177 - const element = 178 - typeof App === "function" 179 - ? React.createElement(App as React.ComponentType) 180 - : (App as React.ReactNode); 181 - 182 - const flightStream = renderToReadableStream(element, manifest, { 183 - onError: (error: unknown) => { 184 - if (error instanceof Error) return error.message; 185 - return String(error); 186 - }, 187 - }); 188 - 189 - self.postMessage({ type: "stream-start", requestId } satisfies StreamStartResponse); 190 - streamToMain(flightStream, requestId); 191 - } 192 - 193 - async function handleAction({ actionId, encodedArgs, requestId }: ActionMessage): Promise<void> { 194 - const { manifest, serverModule } = requireDeployed(); 195 - 196 - const actionFn = serverModule[actionId]; 197 - if (typeof actionFn !== "function") { 198 - throw new Error(`Action "${actionId}" not found`); 199 - } 200 - 201 - const toDecode = reconstructEncodedArgs(encodedArgs); 202 - const args = await decodeReply(toDecode, {}); 203 - const argsArray = Array.isArray(args) ? args : [args]; 204 - const result = (await (actionFn as (...args: unknown[]) => Promise<unknown>)( 205 - ...argsArray, 206 - )) as React.ReactNode; 207 - 208 - const flightStream = renderToReadableStream(result, manifest, { 209 - onError: (error: unknown) => { 210 - if (error instanceof Error) return error.message; 211 - return String(error); 212 - }, 213 - }); 214 - 215 - self.postMessage({ type: "stream-start", requestId } satisfies StreamStartResponse); 216 - streamToMain(flightStream, requestId); 217 - } 218 - 219 - function reconstructEncodedArgs(encodedArgs: EncodedArgs): FormData | string { 220 - if (encodedArgs.type === "formdata") { 221 - const formData = new FormData(); 222 - for (const [key, value] of new URLSearchParams(encodedArgs.data)) { 223 - formData.append(key, value); 224 - } 225 - return formData; 226 - } 227 - return encodedArgs.data; 228 - } 229 - 230 - function evalModule( 231 - code: string, 232 - modules: Record<string, unknown>, 233 - actionNames: string[] | undefined, 234 - ): ServerModule { 235 - let finalCode = code; 236 - if (actionNames && actionNames.length > 0) { 237 - finalCode += 238 - "\n" + 239 - actionNames 240 - .map( 241 - (name) => 242 - `__registerServerReference(${name}, "${name}", "${name}"); exports.${name} = ${name};`, 243 - ) 244 - .join("\n"); 245 - } 246 - 247 - const module: { exports: ServerModule } = { exports: {} }; 248 - const require = (id: string): unknown => { 249 - if (!modules[id]) throw new Error(`Module "${id}" not found`); 250 - return modules[id]; 251 - }; 252 - 253 - const fn = new Function( 254 - "module", 255 - "exports", 256 - "require", 257 - "React", 258 - "__registerServerReference", 259 - finalCode, 260 - ) as ( 261 - module: { exports: ServerModule }, 262 - exports: ServerModule, 263 - require: (id: string) => unknown, 264 - ReactLib: typeof React, 265 - registerServerRef: typeof registerServerReference, 266 - ) => void; 267 - 268 - fn(module, module.exports, require, React, registerServerReference); 269 - 270 - return module.exports; 271 - } 272 - 273 - self.postMessage({ type: "ready" } satisfies ReadyResponse);
···
+6
src/shared/polyfill.ts
···
··· 1 + import { ReadableStream as PolyfillReadableStream } from "web-streams-polyfill"; 2 + 3 + // Safari doesn't implement ReadableByteStreamController. 4 + if (typeof globalThis.ReadableByteStreamController === "undefined") { 5 + globalThis.ReadableStream = PolyfillReadableStream as typeof ReadableStream; 6 + }
+18
src/shared/webpack-shim.ts
···
··· 1 + // Minimal webpack shim for react-server-dom-webpack 2 + // Works in both browser (window) and worker (self) contexts via globalThis 3 + 4 + const g = globalThis as Record<string, unknown>; 5 + 6 + const moduleCache: Record<string, unknown> = {}; 7 + 8 + g.__webpack_module_cache__ = moduleCache; 9 + 10 + g.__webpack_require__ = function (moduleId: string): unknown { 11 + const cached = moduleCache[moduleId] as { exports?: unknown } | undefined; 12 + if (cached) return cached.exports ?? cached; 13 + throw new Error(`Module ${moduleId} not found`); 14 + }; 15 + 16 + g.__webpack_chunk_load__ = () => Promise.resolve(); 17 + 18 + export {};
-19
types/web-streams-polyfill.d.ts
··· 1 - // Type declarations for web-streams-polyfill specific exports 2 - 3 - declare module "web-streams-polyfill" { 4 - export const ReadableStream: { 5 - new <R = unknown>( 6 - underlyingSource?: UnderlyingSource<R>, 7 - strategy?: QueuingStrategy<R>, 8 - ): ReadableStream<R>; 9 - prototype: ReadableStream; 10 - }; 11 - // ReadableByteStreamController is an internal class, we type it loosely 12 - export const ReadableByteStreamController: unknown; 13 - export const WritableStream: typeof globalThis.WritableStream; 14 - export const TransformStream: typeof globalThis.TransformStream; 15 - } 16 - 17 - declare module "web-streams-polyfill/polyfill" { 18 - // Side-effect only import that polyfills globals 19 - }
···