Skip to content

Work around serialport callback stalls on Node 26.4+ #16

Description

@tfo01hyp

The Node 26 issue is understood and the temporary vscope workaround is implemented. I’d include this in today’s patch release.

Root cause

The first bad version is exactly Node 26.4.0:

@serialport/bindings-cpp invokes JavaScript from custom uv_async_t/uv_poll_t callbacks using FunctionReference::Call, which maps to napi_call_function. For custom asynchronous operations, Node documents that addons should establish an async context and use napi_make_callback; that also drains queued promises and process.nextTick work. Node-API documentation

Node’s optimization removed an incidental callback-scope drain that serialport was unknowingly relying on. Therefore:

  • The Node change exposed the problem.
  • The proper upstream fix belongs primarily in @serialport/bindings-cpp.
  • This explains why 0.1.1 worked: it was the Node runtime change, not vscope’s protocol or firmware changes.

Platform scope

This is not Windows-only:

So the defensible conclusion is “Windows and Linux confirmed; macOS currently unconfirmed/not reproduced,” rather than either Windows-only or universally all platforms.

Release workaround

client.ts now starts one 16 ms timer only on Node 26.4+:

  • It is unref()’d, so it cannot keep vscope alive.
  • It covers daemon startup and all serial chunks.
  • Node 24/25 and Node 26.0–26.3 remain untouched.
  • Cost: 62.5 lightweight callbacks per second on affected Node versions. That is acceptable as a clearly temporary workaround.

A request-scoped timer was initially considered, but it did not cover the daemon-startup stall. Moving the response timeout also proved unsuitable: split serial frames could take roughly 470 ms and narrowly race the 500 ms timeout.

I also fixed the Windows release gate in package.json by removing the hard-coded /dev/null redirects.

Validation

Under Node 26.5, with COM4 at 312500 baud, 8N1, DTR/RTS enabled:

  • Packaged daemon started without a preload.
  • Health and MCP initialization passed.
  • COM4 discovery and device connection passed.
  • Device identified as GIC0_LLC1.
  • Run → trigger → halted/valid capture passed.
  • Downloaded and persisted 2,000 samples × 5 channels.
  • HTTP sample readback returned 40,000 bytes in the expected format.
  • Smoke snapshot was deleted afterward.
  • Full pnpm run check exited successfully: lint, diagnostics, typechecks, 107 tests, runtime/UI builds, release assets, formatting and CLI smoke checks.
  • pnpm pack --dry-run passed.

Mise is restored to Node 26.5, and node resolves only through the mise shim. No vscope server was left running.

For upstream, I’d add the bisection and Node commit to issue #3148, then propose changing the raw Windows EIO_AfterRead/EIO_AfterWrite and POSIX Poller::onData callbacks to use Napi::AsyncContext plus MakeCallback. The two local modifications are uncommitted and ready for your patch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions