Skip to content

[docs-infra] Use length-prefixed binary framing for SocketServer IPC#1309

Draft
JCQuintas wants to merge 1 commit into
masterfrom
jcquintas/fix-socketServer-large-payload
Draft

[docs-infra] Use length-prefixed binary framing for SocketServer IPC#1309
JCQuintas wants to merge 1 commit into
masterfrom
jcquintas/fix-socketServer-large-payload

Conversation

@JCQuintas
Copy link
Copy Markdown
Member

Summary

Replaces the NDJSON framing between SocketServer and SocketClient with a length-prefixed binary format using v8.serialize / v8.deserialize.

Why: JSON.stringify crashed with RangeError: Invalid string length on payloads whose UTF-8 size exceeds Node's ~500 MB string cap. mui-x DataGridProps (131 props, fully-expanded generic chain) reliably reproduced this and took down the worker pool. The client-side decoder also paid O(n) UTF-8 cost per chunk via buffer += chunk.

What:

  • New socketFraming.ts with encodeFrame / FrameDecoder (4-byte BE length prefix + v8.serialize body).
  • SocketServer.handleConnection / sendResponse and SocketClient.handleData / sendRequest switched to the new codec.
  • 8 unit tests: round-trip primitives, Map/BigInt/Uint8Array, multi-frame, byte-by-byte partial delivery, 200 MB payload.

v8.serialize is the same structured-clone used internally by worker_threads.postMessage, so payload shape guarantees are unchanged.

The `SocketServer` ↔ `SocketClient` protocol used newline-delimited
JSON: each message was `JSON.stringify(message) + '\n'`, and the
receiver buffered incoming bytes as a JS string and split on '\n'
before `JSON.parse`ing. Two linked failure modes on large consumer
projects:

1. `JSON.stringify` threw `RangeError: Invalid string length` once a
   payload's UTF-8 representation exceeded Node's ~500 MB string cap.
   Reproducible on mui-x `DataGridProps` (131 direct props with a
   fully expanded generic chain) — the worker pool crashed with

       RangeError: Invalid string length
           at JSON.stringify (<anonymous>)
           at SocketServer.sendResponse (.../socketServer.mjs:168:26)

   and the validate CLI terminated reporting "0 files updated" after
   the workers died.

2. The client-side decoder concatenated each chunk via
   `this.buffer += data.toString()`. That is O(n) UTF-8 decoding + O(n)
   string re-allocation per chunk, which adds up to O(n²) on payloads
   delivered in many small TCP segments, and shares the same string
   length ceiling as the encoder.

Fix: replace NDJSON with length-prefixed binary frames using
`v8.serialize` / `v8.deserialize`. That's the same structured-clone
algorithm `worker_threads.postMessage` uses internally; it has no
UTF-8 string-length ceiling (max frame body is 2^32 − 1 bytes ≈ 4 GB),
preserves binary structures faithfully, and is faster than JSON on
deeply nested objects.

Wire format:

    [ 4-byte big-endian uint32 body length ][ body bytes ]

Changes:

* New `socketFraming.ts` module exposing `encodeFrame(message): Buffer`
  and a stateful `FrameDecoder` that handles partial frames, multiple
  frames delivered in one chunk, and a single frame split across many
  chunks (including one byte at a time). The decoder compacts its
  internal buffer whenever a complete frame is emitted — no O(n²)
  re-concat on large payloads.
* `socketServer.ts`: data handler routes chunks through `FrameDecoder`;
  `sendResponse` encodes via `encodeFrame`. The previous band-aid
  try/catch around `JSON.stringify` is dropped — `v8.serialize` only
  fails on structured-clone-incompatible shapes (functions, host
  objects), which would be a coding bug not a size issue, and those
  are still caught and reported as a recoverable per-request error.
* `socketClient.ts`: symmetric changes on the client side. The
  `buffer = ''` string state is replaced with a `FrameDecoder`
  instance.
* `socketFraming.test.ts`: round-trip tests for primitives, non-JSON
  types (`Map`, `BigInt`, `Uint8Array`), multiple frames per chunk,
  partial frames split byte-by-byte, a trailing partial frame
  buffered across two pushes, and a 200 MB payload (far above any
  realistic JSON.stringify ceiling).

Verified end-to-end on mui-x: the `docs-infra validate --types`
command processes 290+ generated `types.*.ts` entries (including
`DataGrid`, `DataGridPro`, `DataGridPremium`, and all of the main
`@mui/x-*` component exports) without a single
`RangeError: Invalid string length`. `pnpm test` for
`@mui/internal-docs-infra` stays green; 8 new tests added.
@code-infra-dashboard
Copy link
Copy Markdown

code-infra-dashboard Bot commented Apr 14, 2026

Deploy preview

https://deploy-preview-1309--mui-internal.netlify.app/

Performance

Total duration: 15.77 ms -0.37 ms(-2.3%) | Renders: 4 (+0) | Paint: 71.04 ms +2.05 ms(+3.0%)

Test Duration Renders
DataGrid mount with paint timing 1.98 ms 🔺+0.07 ms(+3.6%) 1 (+0)
HeavyList mount 9.59 ms ▼-0.36 ms(-3.6%) 1 (+0)
Counter click 4.20 ms ▼-0.08 ms(-1.9%) 2 (+0)

Details of benchmark changes

Bundle size

Bundle Parsed size Gzip size
@base-ui/react 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@zannager zannager added the scope: docs-infra Involves the docs-infra product (https://www.notion.so/mui-org/b9f676062eb94747b6768209f7751305). label May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: docs-infra Involves the docs-infra product (https://www.notion.so/mui-org/b9f676062eb94747b6768209f7751305).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants