A tool for people curious about the React Server Components protocol
rscexplorer.dev/
rsc
react
1import {
2 createFromReadableStream,
3 type CallServerCallback as ImportedCallServerCallback,
4} from "react-server-dom-webpack/client";
5import { parseRows, type ParsedRow } from "./flight-parser.ts";
6
7export type CallServerCallback = ImportedCallServerCallback;
8
9export interface Thenable<T> {
10 then<TResult1 = T, TResult2 = never>(
11 onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
12 onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
13 ): PromiseLike<TResult1 | TResult2>;
14}
15
16export interface SteppableStreamOptions {
17 callServer?: CallServerCallback;
18}
19
20const noop = () => {};
21
22function wrapParseError(err: unknown): Error {
23 const msg = err instanceof Error ? err.message : String(err);
24 return new Error(`RSC Explorer could not parse the React output into rows: ${msg}`);
25}
26
27interface Row {
28 display: string;
29 bytes: Uint8Array;
30 hexStart: number;
31}
32
33export class SteppableStream {
34 rows: Row[] = [];
35 done = false;
36 error: Error | null = null;
37 flightPromise: Thenable<unknown>;
38
39 private controller!: ReadableStreamDefaultController<Uint8Array>;
40 private releasedCount = 0;
41 private closed = false;
42 private yieldIndex = 0;
43 private ping = noop;
44 private decoder = new TextDecoder("utf-8", { fatal: false });
45
46 constructor(source: ReadableStream<Uint8Array>, options: SteppableStreamOptions = {}) {
47 const { callServer } = options;
48
49 const output = new ReadableStream<Uint8Array>({
50 start: (c) => {
51 this.controller = c;
52 },
53 });
54
55 const streamOptions = callServer ? { callServer } : {};
56 this.flightPromise = createFromReadableStream(output, streamOptions);
57 this.consumeSource(source);
58 }
59
60 release(count: number): void {
61 if (this.closed) return;
62
63 while (this.releasedCount < count && this.releasedCount < this.rows.length) {
64 this.controller.enqueue(this.rows[this.releasedCount]!.bytes);
65 this.releasedCount++;
66 }
67
68 this.maybeClose();
69 }
70
71 async *[Symbol.asyncIterator](): AsyncGenerator<string> {
72 while (true) {
73 while (this.yieldIndex < this.rows.length) {
74 yield this.rows[this.yieldIndex++]!.display;
75 }
76 if (this.error) throw this.error;
77 if (this.done) return;
78
79 await new Promise<void>((resolve) => {
80 this.ping = resolve;
81 });
82 this.ping = noop;
83 }
84 }
85
86 private async consumeSource(source: ReadableStream<Uint8Array>): Promise<void> {
87 const reader = source.getReader();
88 let buffer: Uint8Array = new Uint8Array(0);
89
90 try {
91 while (true) {
92 const { done, value } = await reader.read();
93 if (done) break;
94
95 const newBuffer = new Uint8Array(buffer.length + value.length);
96 newBuffer.set(buffer);
97 newBuffer.set(value, buffer.length);
98 buffer = newBuffer;
99
100 let result;
101 try {
102 result = parseRows(buffer, false);
103 } catch (err) {
104 throw wrapParseError(err);
105 }
106 for (const row of result.rows) {
107 const formatted = this.formatRow(row);
108 if (formatted) {
109 this.rows.push(formatted);
110 }
111 }
112 buffer = result.remainder;
113 this.ping();
114 }
115
116 if (buffer.length > 0) {
117 let result;
118 try {
119 result = parseRows(buffer, true);
120 } catch (err) {
121 throw wrapParseError(err);
122 }
123 for (const row of result.rows) {
124 const formatted = this.formatRow(row);
125 if (formatted) {
126 this.rows.push(formatted);
127 }
128 }
129 }
130 } catch (err) {
131 this.error = err instanceof Error ? err : new Error(String(err));
132 } finally {
133 this.done = true;
134 this.ping();
135 this.maybeClose();
136 }
137 }
138
139 private formatRow(parsed: ParsedRow): Row | null {
140 const { segment, raw } = parsed;
141
142 if (segment.type === "text") {
143 const headerLen = raw.length - segment.data.length - 1; // -1 for newline
144 const header = this.decoder.decode(raw.slice(0, headerLen));
145 const content = this.decoder.decode(segment.data);
146 const display = (header + content).trim();
147 if (!display) return null;
148 return { display, bytes: raw, hexStart: -1 };
149 }
150
151 const header = this.decoder.decode(raw.slice(0, raw.length - segment.data.length));
152 const maxPreview = 16;
153 const previewLen = Math.min(segment.data.length, maxPreview);
154 const hex = Array.from(segment.data.slice(0, previewLen))
155 .map((b) => b.toString(16).padStart(2, "0"))
156 .join(" ");
157 const ellipsis = segment.data.length > maxPreview ? "..." : "";
158 const display = header + hex + ellipsis;
159 if (!display.trim()) return null;
160 return { display, bytes: raw, hexStart: header.length };
161 }
162
163 private maybeClose(): void {
164 if (this.closed) return;
165 if (this.done && this.releasedCount >= this.rows.length) {
166 this.closed = true;
167 if (this.error) {
168 this.controller.error(this.error);
169 } else {
170 this.controller.close();
171 }
172 }
173 }
174}