A tool for people curious about the React Server Components protocol rscexplorer.dev/
rsc react
37
fork

Configure Feed

Select the types of activity you want to include in your feed.

initial binary implementation

+489 -38
+2 -1
README.md
··· 25 25 - [Error Handling](https://rscexplorer.dev/?s=errors) 26 26 - [Client Reference](https://rscexplorer.dev/?s=clientref) 27 27 - [Bound Actions](https://rscexplorer.dev/?s=bound) 28 + - [Binary Data](https://rscexplorer.dev/?s=binary) 28 29 - [Kitchen Sink](https://rscexplorer.dev/?s=kitchensink) 29 30 - [CVE-2025-55182](https://rscexplorer.dev/?s=cve) 30 31 ··· 39 40 - The Server part runs in a worker. 40 41 - We try to approximate a real RSC environment as much as we can (while staying in browser). 41 42 - No dependencies on React internals. We use `react-server-dom-webpack` and shim the Webpack runtime. 42 - - No dependencies on the protocol format. We display it, but treat it as an implementation detail of React. 43 + - Minimal dependencies on the protocol format. We break the stream into rows, but treat them as an implementation detail of React. 43 44 - Only end-to-end tests. 44 45 45 46 This is fully vibecoded but heavily steered so individual pieces may be weird or suboptimal.
+18 -10
package-lock.json
··· 3219 3219 "version": "1.7.0", 3220 3220 "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", 3221 3221 "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", 3222 + "dev": true, 3222 3223 "license": "MIT" 3223 3224 }, 3224 3225 "node_modules/es-object-atoms": { ··· 6350 6351 } 6351 6352 }, 6352 6353 "node_modules/watchpack": { 6353 - "version": "2.4.4", 6354 - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", 6355 - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", 6354 + "version": "2.5.0", 6355 + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.0.tgz", 6356 + "integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==", 6356 6357 "license": "MIT", 6357 6358 "peer": true, 6358 6359 "dependencies": { ··· 6378 6379 } 6379 6380 }, 6380 6381 "node_modules/webpack": { 6381 - "version": "5.103.0", 6382 - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz", 6383 - "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", 6382 + "version": "5.104.1", 6383 + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", 6384 + "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", 6384 6385 "license": "MIT", 6385 6386 "peer": true, 6386 6387 "dependencies": { ··· 6392 6393 "@webassemblyjs/wasm-parser": "^1.14.1", 6393 6394 "acorn": "^8.15.0", 6394 6395 "acorn-import-phases": "^1.0.3", 6395 - "browserslist": "^4.26.3", 6396 + "browserslist": "^4.28.1", 6396 6397 "chrome-trace-event": "^1.0.2", 6397 - "enhanced-resolve": "^5.17.3", 6398 - "es-module-lexer": "^1.2.1", 6398 + "enhanced-resolve": "^5.17.4", 6399 + "es-module-lexer": "^2.0.0", 6399 6400 "eslint-scope": "5.1.1", 6400 6401 "events": "^3.2.0", 6401 6402 "glob-to-regexp": "^0.4.1", ··· 6406 6407 "neo-async": "^2.6.2", 6407 6408 "schema-utils": "^4.3.3", 6408 6409 "tapable": "^2.3.0", 6409 - "terser-webpack-plugin": "^5.3.11", 6410 + "terser-webpack-plugin": "^5.3.16", 6410 6411 "watchpack": "^2.4.4", 6411 6412 "webpack-sources": "^3.3.3" 6412 6413 }, ··· 6434 6435 "engines": { 6435 6436 "node": ">=10.13.0" 6436 6437 } 6438 + }, 6439 + "node_modules/webpack/node_modules/es-module-lexer": { 6440 + "version": "2.0.0", 6441 + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", 6442 + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", 6443 + "license": "MIT", 6444 + "peer": true 6437 6445 }, 6438 6446 "node_modules/which": { 6439 6447 "version": "2.0.2",
+8 -2
scripts/build-version.js
··· 7 7 8 8 const REACT_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"]; 9 9 10 - function run(cmd, opts = {}) { 10 + function run(cmd) { 11 11 console.log(`\n> ${cmd}`); 12 - execSync(cmd, { stdio: "inherit", ...opts }); 12 + execSync(cmd, { stdio: "inherit" }); 13 13 } 14 14 15 15 function getLatestVersion() { ··· 22 22 run(`npm install ${packages} --no-save`); 23 23 } 24 24 25 + function testVersion(version) { 26 + console.log(`\n--- Running tests for React ${version} ---`); 27 + run("npm test"); 28 + } 29 + 25 30 function buildForVersion(version, outDir) { 26 31 console.log(`\n========================================`); 27 32 console.log(`Building React ${version} (dev + prod) → ${outDir || `dist/${version}`}`); 28 33 console.log(`========================================`); 29 34 30 35 installReactVersion(version); 36 + testVersion(version); 31 37 32 38 const dir = outDir || `dist/${version}`; 33 39 // Base path for assets (e.g., /19.1.0/ or / for root)
+163
src/client/runtime/flight-parser.ts
··· 1 + // React Flight Protocol Row Parser 2 + // 3 + // This is a framing parser that splits the RSC byte stream into discrete rows. 4 + // It does NOT interpret row contents, it only determines row boundaries. 5 + // (We want to keep knowledge of implementation details as little as possible.) 6 + // 7 + // Two framing modes based on tag byte after "ID:": 8 + // - Binary framing: ID:TAG + HEX_LENGTH + "," + BINARY_DATA (no terminator) 9 + // - Text framing: ID:TAG + DATA + "\n" (newline terminated) 10 + // 11 + // The parser must know which tags use binary framing to correctly find row 12 + // boundaries. If a binary tag is missing from BINARY_TAGS, the parser will 13 + // treat it as text, scan for newline, and corrupt subsequent parsing. 14 + 15 + // Check if byte is a hex digit (0-9, a-f, A-F) 16 + function isHexDigit(b: number): boolean { 17 + return (b >= 0x30 && b <= 0x39) || (b >= 0x61 && b <= 0x66) || (b >= 0x41 && b <= 0x46); 18 + } 19 + 20 + // Parse hex digit to value (0-15) 21 + function hexValue(b: number): number { 22 + if (b >= 0x30 && b <= 0x39) return b - 0x30; // 0-9 23 + if (b >= 0x61 && b <= 0x66) return b - 0x57; // a-f 24 + return b - 0x37; // A-F 25 + } 26 + 27 + // Tags that use binary (length-prefixed) framing 28 + const BINARY_TAGS = new Set([ 29 + 0x54, // T - long text 30 + 0x41, // A - ArrayBuffer 31 + 0x4f, // O - Int8Array 32 + 0x6f, // o - Uint8Array 33 + 0x55, // U - Uint8ClampedArray 34 + 0x53, // S - Int16Array 35 + 0x73, // s - Uint16Array 36 + 0x4c, // L - Int32Array 37 + 0x6c, // l - Uint32Array 38 + 0x47, // G - Float32Array 39 + 0x67, // g - Float64Array 40 + 0x4d, // M - BigInt64Array 41 + 0x6d, // m - BigUint64Array 42 + 0x56, // V - DataView 43 + 0x62, // b - byte stream chunk 44 + ]); 45 + 46 + export type RowSegment = { type: "text" | "binary"; data: Uint8Array }; 47 + 48 + export interface ParsedRow { 49 + id: string; 50 + segment: RowSegment; 51 + raw: Uint8Array; 52 + } 53 + 54 + export interface ParseResult { 55 + rows: ParsedRow[]; 56 + remainder: Uint8Array; 57 + } 58 + 59 + export function parseRows(buffer: Uint8Array, final: boolean = false): ParseResult { 60 + const rows: ParsedRow[] = []; 61 + let i = 0; 62 + 63 + while (i < buffer.length) { 64 + const rowStart = i; 65 + 66 + // Row ID: hex digits until ':' 67 + while (i < buffer.length && buffer[i] !== 0x3a) { 68 + const b = buffer[i]!; 69 + if (!isHexDigit(b)) { 70 + throw new Error(`Expected hex digit in row ID, got 0x${b.toString(16)}`); 71 + } 72 + i++; 73 + } 74 + if (i >= buffer.length) { 75 + if (final) { 76 + throw new Error(`Truncated row ID at end of stream`); 77 + } 78 + return { rows, remainder: buffer.slice(rowStart) }; 79 + } 80 + 81 + const id = decodeAscii(buffer, rowStart, i); 82 + // buffer[i] is guaranteed to be colon here (while loop exit condition) 83 + i++; 84 + 85 + if (i >= buffer.length) { 86 + if (final) { 87 + throw new Error(`Row ${id} truncated after colon`); 88 + } 89 + return { rows, remainder: buffer.slice(rowStart) }; 90 + } 91 + 92 + const tag = buffer[i]!; 93 + 94 + if (BINARY_TAGS.has(tag)) { 95 + // Binary framing: TAG + HEX_LENGTH + "," + DATA 96 + i++; 97 + 98 + let length = 0; 99 + while (i < buffer.length && buffer[i] !== 0x2c) { 100 + const b = buffer[i]!; 101 + if (!isHexDigit(b)) { 102 + throw new Error( 103 + `Expected hex digit in binary length for row ${id}, got 0x${b.toString(16)}`, 104 + ); 105 + } 106 + length = (length << 4) | hexValue(b); 107 + i++; 108 + } 109 + 110 + if (i >= buffer.length) { 111 + if (final) { 112 + throw new Error(`Row ${id} truncated in binary length`); 113 + } 114 + return { rows, remainder: buffer.slice(rowStart) }; 115 + } 116 + // buffer[i] is guaranteed to be comma here (while loop exit condition) 117 + i++; 118 + 119 + if (i + length > buffer.length) { 120 + if (final) { 121 + throw new Error( 122 + `Row ${id} truncated in binary data (need ${length} bytes, have ${buffer.length - i})`, 123 + ); 124 + } 125 + return { rows, remainder: buffer.slice(rowStart) }; 126 + } 127 + 128 + const data = buffer.slice(i, i + length); 129 + const raw = buffer.slice(rowStart, i + length); 130 + rows.push({ id, segment: { type: "binary", data }, raw }); 131 + i += length; 132 + } else { 133 + // Text framing: scan for newline 134 + const contentStart = i + 1; // after the tag byte 135 + while (i < buffer.length && buffer[i] !== 0x0a) { 136 + i++; 137 + } 138 + 139 + if (i >= buffer.length) { 140 + if (!final) { 141 + // Incomplete row, wait for more data 142 + return { rows, remainder: buffer.slice(rowStart) }; 143 + } 144 + throw new Error(`Text row ${id} missing trailing newline at end of stream`); 145 + } else { 146 + const data = buffer.slice(contentStart, i); 147 + const raw = buffer.slice(rowStart, i + 1); 148 + rows.push({ id, segment: { type: "text", data }, raw }); 149 + i++; 150 + } 151 + } 152 + } 153 + 154 + return { rows, remainder: new Uint8Array(0) }; 155 + } 156 + 157 + function decodeAscii(buffer: Uint8Array, start: number, end: number): string { 158 + let s = ""; 159 + for (let i = start; i < end; i++) { 160 + s += String.fromCharCode(buffer[i]!); 161 + } 162 + return s; 163 + }
+2 -1
src/client/runtime/index.ts
··· 5 5 type Thenable, 6 6 type CallServerCallback, 7 7 } from "./steppable-stream.ts"; 8 - export { Timeline, type EntryView, type TimelineSnapshot } from "./timeline.ts"; 8 + export { Timeline, type EntryView, type RowView, type TimelineSnapshot } from "./timeline.ts"; 9 + export { parseRows, type ParsedRow, type ParseResult, type RowSegment } from "./flight-parser.ts";
+69 -15
src/client/runtime/steppable-stream.ts
··· 2 2 createFromReadableStream, 3 3 type CallServerCallback as ImportedCallServerCallback, 4 4 } from "react-server-dom-webpack/client"; 5 + import { parseRows, type ParsedRow } from "./flight-parser.ts"; 5 6 6 7 export type CallServerCallback = ImportedCallServerCallback; 7 8 ··· 17 18 } 18 19 19 20 const noop = () => {}; 20 - const encoder = new TextEncoder(); 21 + 22 + function 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 + 27 + interface Row { 28 + display: string; 29 + bytes: Uint8Array; 30 + hexStart: number; 31 + } 21 32 22 33 export class SteppableStream { 23 - rows: string[] = []; 34 + rows: Row[] = []; 24 35 done = false; 25 36 error: Error | null = null; 26 37 flightPromise: Thenable<unknown>; ··· 30 41 private closed = false; 31 42 private yieldIndex = 0; 32 43 private ping = noop; 44 + private decoder = new TextDecoder("utf-8", { fatal: false }); 33 45 34 46 constructor(source: ReadableStream<Uint8Array>, options: SteppableStreamOptions = {}) { 35 47 const { callServer } = options; ··· 49 61 if (this.closed) return; 50 62 51 63 while (this.releasedCount < count && this.releasedCount < this.rows.length) { 52 - this.controller.enqueue(encoder.encode(this.rows[this.releasedCount]! + "\n")); 64 + this.controller.enqueue(this.rows[this.releasedCount]!.bytes); 53 65 this.releasedCount++; 54 66 } 55 67 ··· 59 71 async *[Symbol.asyncIterator](): AsyncGenerator<string> { 60 72 while (true) { 61 73 while (this.yieldIndex < this.rows.length) { 62 - yield this.rows[this.yieldIndex++]!; 74 + yield this.rows[this.yieldIndex++]!.display; 63 75 } 64 76 if (this.error) throw this.error; 65 77 if (this.done) return; ··· 73 85 74 86 private async consumeSource(source: ReadableStream<Uint8Array>): Promise<void> { 75 87 const reader = source.getReader(); 76 - const decoder = new TextDecoder(); 77 - let partial = ""; 88 + let buffer: Uint8Array = new Uint8Array(0); 78 89 79 90 try { 80 91 while (true) { 81 92 const { done, value } = await reader.read(); 82 93 if (done) break; 83 94 84 - partial += decoder.decode(value, { stream: true }); 85 - const lines = partial.split("\n"); 86 - partial = lines.pop() ?? ""; 95 + const newBuffer = new Uint8Array(buffer.length + value.length); 96 + newBuffer.set(buffer); 97 + newBuffer.set(value, buffer.length); 98 + buffer = newBuffer; 87 99 88 - for (const line of lines) { 89 - if (line.trim()) { 90 - this.rows.push(line); 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); 91 110 } 92 111 } 112 + buffer = result.remainder; 93 113 this.ping(); 94 114 } 95 115 96 - partial += decoder.decode(); 97 - if (partial.trim()) { 98 - this.rows.push(partial); 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 + } 99 129 } 100 130 } catch (err) { 101 131 this.error = err instanceof Error ? err : new Error(String(err)); ··· 104 134 this.ping(); 105 135 this.maybeClose(); 106 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 }; 107 161 } 108 162 109 163 private maybeClose(): void {
+7 -2
src/client/runtime/timeline.ts
··· 4 4 | { type: "render"; stream: SteppableStream } 5 5 | { type: "action"; name: string; args: string; stream: SteppableStream }; 6 6 7 + export type RowView = { 8 + display: string; 9 + hexStart: number; 10 + }; 11 + 7 12 export type EntryView = { 8 13 type: "render" | "action"; 9 14 name?: string; 10 15 args?: string; 11 - rows: readonly string[]; 16 + rows: readonly RowView[]; 12 17 flightPromise: Thenable<unknown> | undefined; 13 18 error: Error | null; 14 19 chunkStart: number; ··· 58 63 const chunkCount = stream.rows.length; 59 64 const chunkEnd = chunkStart + chunkCount; 60 65 const base = { 61 - rows: stream.rows, 66 + rows: stream.rows.map((r) => ({ display: r.display, hexStart: r.hexStart })), 62 67 flightPromise: stream.flightPromise, 63 68 error: stream.error, 64 69 chunkStart,
+65 -2
src/client/samples.ts
··· 417 417 ) 418 418 }`, 419 419 }, 420 + binary: { 421 + name: "Binary Data", 422 + server: `import { BinaryDisplay } from './client' 423 + 424 + export default function App() { 425 + const buffer = new ArrayBuffer(4) 426 + new Uint8Array(buffer).set([0xCA, 0xFE, 0xBA, 0xBE]) 427 + 428 + return ( 429 + <div> 430 + <h1>Binary Data</h1> 431 + <BinaryDisplay 432 + arrayBuffer={buffer} 433 + int8={new Int8Array([0x7F, -1, -128])} 434 + uint8={new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF])} 435 + uint8Clamped={new Uint8ClampedArray([0, 128, 255])} 436 + int16={new Int16Array([0x7FFF, -1])} 437 + uint16={new Uint16Array([0xFFFF, 0x1234])} 438 + int32={new Int32Array([0x12345678, -1])} 439 + uint32={new Uint32Array([0xDEADBEEF])} 440 + float32={new Float32Array([Math.PI])} 441 + float64={new Float64Array([Math.PI])} 442 + bigInt64={new BigInt64Array([0x123456789ABCDEFn, -1n])} 443 + bigUint64={new BigUint64Array([0xFEDCBA9876543210n])} 444 + dataView={new DataView(buffer)} 445 + /> 446 + </div> 447 + ) 448 + }`, 449 + client: `'use client' 450 + 451 + function formatBytes(arr) { 452 + return Array.from(new Uint8Array(arr.buffer || arr)).map(b => '0x' + b.toString(16).padStart(2, '0')).join(', ') 453 + } 454 + 455 + export function BinaryDisplay(props) { 456 + return ( 457 + <div style={{ fontSize: 12, fontFamily: 'monospace' }}> 458 + <div>ArrayBuffer: [{formatBytes(props.arrayBuffer)}]</div> 459 + <div>Int8Array: [{props.int8.join(', ')}]</div> 460 + <div>Uint8Array: [{formatBytes(props.uint8)}]</div> 461 + <div>Uint8ClampedArray: [{props.uint8Clamped.join(', ')}]</div> 462 + <div>Int16Array: [{props.int16.join(', ')}]</div> 463 + <div>Uint16Array: [{props.uint16.join(', ')}]</div> 464 + <div>Int32Array: [{props.int32.map(n => '0x' + (n >>> 0).toString(16)).join(', ')}]</div> 465 + <div>Uint32Array: [{props.uint32.map(n => '0x' + n.toString(16)).join(', ')}]</div> 466 + <div>Float32Array: [{props.float32.join(', ')}]</div> 467 + <div>Float64Array: [{props.float64.join(', ')}]</div> 468 + <div>BigInt64Array: [{props.bigInt64.map(n => n.toString()).join(', ')}]</div> 469 + <div>BigUint64Array: [{props.bigUint64.map(n => '0x' + n.toString(16)).join(', ')}]</div> 470 + <div>DataView: [{formatBytes(props.dataView)}]</div> 471 + </div> 472 + ) 473 + }`, 474 + }, 420 475 kitchensink: { 421 476 name: "Kitchen Sink", 422 - server: `// Kitchen Sink - All RSC Protocol Types 423 - import { Suspense } from 'react' 477 + server: `import { Suspense } from 'react' 424 478 import { DataDisplay } from './client' 425 479 426 480 export default function App() { ··· 465 519 date: new Date("2024-01-15T12:00:00.000Z"), 466 520 bigint: BigInt("12345678901234567890"), 467 521 symbol: Symbol.for("mySymbol"), 522 + }, 523 + 524 + // Binary / TypedArrays 525 + binary: { 526 + uint8: new Uint8Array([1, 2, 3, 4, 5]), 527 + int32: new Int32Array([-1, 0, 2147483647]), 528 + float64: new Float64Array([3.14159, 2.71828]), 468 529 }, 469 530 470 531 // Collections ··· 573 634 if (v instanceof Set) return 'Set(' + v.size + ')' 574 635 if (v instanceof FormData) return 'FormData' 575 636 if (v instanceof Blob) return 'Blob(' + v.size + ')' 637 + if (v instanceof ArrayBuffer) return 'ArrayBuffer(' + v.byteLength + ')' 638 + if (ArrayBuffer.isView(v)) return v.constructor.name + '(' + v.length + ')' 576 639 if (Array.isArray(v)) return '[' + v.length + ' items]' 577 640 if (typeof v === 'object') return '{...}' 578 641 return String(v)
+4
src/client/ui/FlightLog.css
··· 237 237 opacity: 0.4; 238 238 } 239 239 240 + .FlightLog-hexBytes { 241 + opacity: 0.7; 242 + } 243 + 240 244 .FlightLog-tree { 241 245 flex: 1; 242 246 min-width: 0;
+13 -5
src/client/ui/FlightLog.tsx
··· 1 1 import React, { useState, useRef, useEffect, useTransition } from "react"; 2 2 import { FlightTreeView } from "./TreeView.tsx"; 3 3 import { Select } from "./Select.tsx"; 4 - import type { EntryView } from "../runtime/index.ts"; 4 + import type { EntryView, RowView } from "../runtime/index.ts"; 5 5 import "./FlightLog.css"; 6 6 7 - function escapeHtml(str: string): string { 8 - return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); 7 + function formatRow(row: RowView): React.ReactNode { 8 + if (row.hexStart < 0) { 9 + return row.display; 10 + } 11 + return ( 12 + <> 13 + {row.display.slice(0, row.hexStart)} 14 + <span className="FlightLog-hexBytes">{row.display.slice(row.hexStart)}</span> 15 + </> 16 + ); 9 17 } 10 18 11 19 type RenderLogViewProps = { ··· 42 50 <div className="FlightLog-renderView-split"> 43 51 <div className="FlightLog-linesWrapper"> 44 52 <pre className="FlightLog-lines"> 45 - {rows.map((line, i) => { 53 + {rows.map((row, i) => { 46 54 const isCurrent = i === nextLineIndex; 47 55 return ( 48 56 <span ··· 52 60 data-testid="flight-line" 53 61 aria-current={isCurrent ? "step" : undefined} 54 62 > 55 - {escapeHtml(line)} 63 + {formatRow(row)} 56 64 </span> 57 65 ); 58 66 })}
+3
src/client/ui/TreeView.tsx
··· 231 231 ); 232 232 } 233 233 234 + if (value instanceof DataView) { 235 + return <span className="TreeView-collection">DataView({value.byteLength} bytes)</span>; 236 + } 234 237 if (ArrayBuffer.isView(value)) { 235 238 const name = value.constructor.name; 236 239 const arr = value as Uint8Array;
+43
tests/binary.spec.ts
··· 1 + import { test, expect, beforeAll, afterAll, afterEach } from "vitest"; 2 + import { createHelpers, launchBrowser, type TestHelpers } from "./helpers.ts"; 3 + import type { Browser, Page } from "playwright"; 4 + 5 + let browser: Browser; 6 + let page: Page; 7 + let h: TestHelpers; 8 + 9 + beforeAll(async () => { 10 + browser = await launchBrowser(); 11 + page = await browser.newPage(); 12 + h = createHelpers(page); 13 + }); 14 + 15 + afterAll(async () => { 16 + await browser.close(); 17 + }); 18 + 19 + afterEach(async () => { 20 + await h.checkNoRemainingSteps(); 21 + }); 22 + 23 + test("binary sample - TypedArray serialization", async () => { 24 + await h.load("binary"); 25 + await h.stepAll(); 26 + 27 + // Verify the preview shows decoded values for various types 28 + expect(await h.preview("ArrayBuffer:")).toContain("0xca"); 29 + expect(await h.preview("Int32Array:")).toContain("305419896"); // 0x12345678 30 + expect(await h.preview("Float64Array:")).toContain("3.14159"); 31 + }); 32 + 33 + test("binary sample - rows show hex bytes", async () => { 34 + await h.load("binary"); 35 + await h.stepAll(); 36 + 37 + const rows = await h.getRows(); 38 + 39 + // Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]) 40 + expect(rows.some((r) => r.text && /de ad be ef/i.test(r.text))).toBe(true); 41 + // ArrayBuffer with 0xCA, 0xFE, 0xBA, 0xBE 42 + expect(rows.some((r) => r.text && /ca fe ba be/i.test(r.text))).toBe(true); 43 + });
+78
tests/binarytags.spec.ts
··· 1 + import { test, expect, beforeAll, afterAll, afterEach } from "vitest"; 2 + import { createHelpers, launchBrowser, type TestHelpers } from "./helpers.ts"; 3 + import type { Browser, Page } from "playwright"; 4 + 5 + const BINARY_MARKER_CASES = [ 6 + "arrayBuffer: buffer", 7 + "int8: new Int8Array(marker)", 8 + "uint8: new Uint8Array(marker)", 9 + "uint8Clamped: new Uint8ClampedArray(marker)", 10 + "int16: new Int16Array(new Uint8Array(marker).buffer)", 11 + "uint16: new Uint16Array(new Uint8Array(marker).buffer)", 12 + "int32: new Int32Array(new Uint8Array(marker).buffer)", 13 + "uint32: new Uint32Array(new Uint8Array(marker).buffer)", 14 + "float32: new Float32Array(new Uint8Array(marker).buffer)", 15 + "float64: new Float64Array(new Uint8Array([...marker, ...marker]).buffer)", 16 + "bigInt64: new BigInt64Array(new Uint8Array([...marker, ...marker]).buffer)", 17 + "bigUint64: new BigUint64Array(new Uint8Array([...marker, ...marker]).buffer)", 18 + "dataView: new DataView(buffer)", 19 + `byteStream: new ReadableStream({ 20 + type: 'bytes', 21 + start(controller) { 22 + controller.enqueue(new Uint8Array(marker)) 23 + controller.close() 24 + } 25 + })`, 26 + ]; 27 + 28 + const BINARY_TAGS_SERVER = `export default function App() { 29 + const marker = [0xAB, 0xCD, 0xAB, 0xCD] 30 + const buffer = new ArrayBuffer(4) 31 + new Uint8Array(buffer).set(marker) 32 + 33 + return { 34 + ${BINARY_MARKER_CASES.join(",\n ")}, 35 + longText: 'test'.repeat(300), 36 + } 37 + }`; 38 + 39 + const BINARY_TAGS_CLIENT = `'use client'`; 40 + 41 + let browser: Browser; 42 + let page: Page; 43 + let h: TestHelpers; 44 + 45 + beforeAll(async () => { 46 + browser = await launchBrowser(); 47 + page = await browser.newPage(); 48 + h = createHelpers(page); 49 + }); 50 + 51 + afterAll(async () => { 52 + await browser.close(); 53 + }); 54 + 55 + afterEach(async () => { 56 + await h.checkNoRemainingSteps(); 57 + }); 58 + 59 + test("all binary tags produce marker rows", async () => { 60 + await h.loadCode(BINARY_TAGS_SERVER, BINARY_TAGS_CLIENT); 61 + await h.stepAll(); 62 + 63 + const rows = await h.getRows(); 64 + 65 + // Count rows containing "ab cd ab cd" (binary marker) 66 + const binaryMarkerRows = rows.filter((r) => r.text && /ab cd ab cd/i.test(r.text)); 67 + 68 + // Count rows containing "74 65 73 74" (ASCII for "test") 69 + const textRows = rows.filter((r) => r.text && /74 65 73 74/.test(r.text)); 70 + 71 + // If any binary tag is missing from the parser, the stream will be corrupted 72 + expect( 73 + binaryMarkerRows.length, 74 + `Expected ${BINARY_MARKER_CASES.length} binary marker rows, got ${binaryMarkerRows.length}`, 75 + ).toBe(BINARY_MARKER_CASES.length); 76 + 77 + expect(textRows.length, `Expected 1 Text row, got ${textRows.length}`).toBe(1); 78 + });
+14
tests/kitchensink.spec.ts
··· 74 74 bigint: 12345678901234567890n, 75 75 symbol: Symbol(mySymbol) 76 76 }, 77 + binary: { 78 + uint8: Uint8Array(5) [1, 2, 3, 4, 5], 79 + int32: Int32Array(3) [-1, 0, 2147483647], 80 + float64: Float64Array(2) [3.14159, 2.71828] 81 + }, 77 82 collections: { 78 83 map: Map(2) { 79 84 "a" => 1, ··· 175 180 bigint: 12345678901234567890n, 176 181 symbol: Symbol(mySymbol) 177 182 }, 183 + binary: { 184 + uint8: Uint8Array(5) [1, 2, 3, 4, 5], 185 + int32: Int32Array(3) [-1, 0, 2147483647], 186 + float64: Float64Array(2) [3.14159, 2.71828] 187 + }, 178 188 collections: { 179 189 map: Map(2) { 180 190 "a" => 1, ··· 262 272 date: 2024-01-15T12:00:00.000Z 263 273 bigint: 12345678901234567890n 264 274 symbol: Symbol(mySymbol) 275 + binary 276 + uint8: Uint8Array(5) 277 + int32: Int32Array(3) 278 + float64: Float64Array(2) 265 279 collections 266 280 map: Map(2) 267 281 set: Set(3)