A tool for people curious about the React Server Components protocol rscexplorer.dev/
rsc react
at main 121 lines 3.7 kB view raw
1import { encodeReply } from "react-server-dom-webpack/client"; 2import { Timeline } from "./runtime/timeline.ts"; 3import { SteppableStream, registerClientModule, evaluateClientModule } from "./runtime/index.ts"; 4import { WorkerClient, encodeArgs, type EncodedArgs } from "./worker-client.ts"; 5import { 6 parseClientModule, 7 parseServerActions, 8 compileToCommonJS, 9 buildManifest, 10} from "../shared/compiler.ts"; 11 12export type SessionState = 13 | { status: "ready"; availableActions: string[] } 14 | { status: "error"; message: string }; 15 16let lastId = 0; 17 18const emptySnapshot = { 19 entries: [] as never[], 20 cursor: 0, 21 totalChunks: 0, 22 isAtStart: true, 23 isAtEnd: false, 24 isStreaming: false, 25}; 26 27export const loadingTimeline = { 28 subscribe: () => () => {}, 29 getSnapshot: () => emptySnapshot, 30 stepForward: () => {}, 31 skipToEntryEnd: () => {}, 32}; 33 34export class WorkspaceSession { 35 readonly timeline = new Timeline(); 36 readonly state: SessionState; 37 readonly id: number = lastId++; 38 private worker: WorkerClient; 39 40 private constructor(worker: WorkerClient, state: SessionState) { 41 this.worker = worker; 42 this.state = state; 43 } 44 45 static async create( 46 serverCode: string, 47 clientCode: string, 48 signal: AbortSignal, 49 ): Promise<WorkspaceSession> { 50 const worker = new WorkerClient(signal); 51 52 try { 53 const clientExports = await parseClientModule(clientCode); 54 const manifest = buildManifest("client", clientExports); 55 const compiledClient = await compileToCommonJS(clientCode); 56 const clientModule = evaluateClientModule(compiledClient); 57 registerClientModule("client", clientModule); 58 59 const actionNames = await parseServerActions(serverCode); 60 const compiledServer = await compileToCommonJS(serverCode); 61 62 await worker.deploy(compiledServer, manifest, actionNames); 63 const renderRaw = await worker.render(); 64 65 const session = new WorkspaceSession(worker, { 66 status: "ready", 67 availableActions: actionNames, 68 }); 69 70 const renderStream = new SteppableStream(renderRaw, { 71 callServer: session.callServer.bind(session), 72 }); 73 session.timeline.setRender(renderStream); 74 75 return session; 76 } catch (err) { 77 return new WorkspaceSession(worker, { 78 status: "error", 79 message: err instanceof Error ? err.message : String(err), 80 }); 81 } 82 } 83 84 private async runAction( 85 actionName: string, 86 args: EncodedArgs, 87 argsDisplay: string, 88 ): Promise<SteppableStream> { 89 let source: ReadableStream<Uint8Array>; 90 try { 91 source = await this.worker.callAction(actionName, args); 92 } catch (err) { 93 const error = err instanceof Error ? err : new Error(String(err)); 94 source = new ReadableStream({ start: (c) => c.error(error) }); 95 } 96 97 const stream = new SteppableStream(source, { 98 callServer: this.callServer.bind(this), 99 }); 100 this.timeline.addAction(actionName, argsDisplay, stream); 101 if (stream.error) { 102 throw stream.error; 103 } 104 return stream; 105 } 106 107 private async callServer(actionId: string, args: unknown[]): Promise<unknown> { 108 const actionName = actionId.split("#")[0] ?? actionId; 109 const encodedArgs = await encodeReply(args); 110 const argsDisplay = 111 typeof encodedArgs === "string" 112 ? `0=${encodedArgs}` 113 : new URLSearchParams(encodedArgs as unknown as Record<string, string>).toString(); 114 const stream = await this.runAction(actionName, encodeArgs(encodedArgs), argsDisplay); 115 return stream.flightPromise; 116 } 117 118 addRawAction = async (actionName: string, rawPayload: string): Promise<void> => { 119 await this.runAction(actionName, { type: "formdata", data: rawPayload }, rawPayload); 120 }; 121}