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