A tool for people curious about the React Server Components protocol rscexplorer.dev/
rsc react
at main 174 lines 5.1 kB view raw
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}