From 92230ebd594ddc0fe82821b7ad4867c96c0e3e53 Mon Sep 17 00:00:00 2001 From: Kirill Zhumarin Date: Tue, 30 Jun 2026 00:56:43 +0300 Subject: [PATCH 1/3] fix: deliver poller events with MakeCallback --- src/poller.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/poller.cpp b/src/poller.cpp index 0b046926..0e393472 100644 --- a/src/poller.cpp +++ b/src/poller.cpp @@ -81,13 +81,13 @@ void Poller::onData(uv_poll_t* handle, int status, int events) { if (0 != status) { // fprintf(stdout, "OnData Error status=%s events=%d\n", uv_strerror(status), events); obj->_stop(); // doesn't matter if this errors - obj->callback.Call({Napi::Error::New(env, uv_strerror(status)).Value(), env.Undefined()}); + obj->callback.MakeCallback(obj->Value(), {Napi::Error::New(env, uv_strerror(status)).Value(), env.Undefined()}); } else { // fprintf(stdout, "OnData status=%d events=%d subscribed=%d\n", status, events, obj->events); // remove triggered events from the poll int newEvents = obj->events & ~events; obj->poll(env, newEvents); - obj->callback.Call({env.Null(), Napi::Number::New(env, events)}); + obj->callback.MakeCallback(obj->Value(), {env.Null(), Napi::Number::New(env, events)}); } } From 3fbc715b19b564fa9ccbf2a3c0cbfa8fb77ffbc5 Mon Sep 17 00:00:00 2001 From: DragonWork <97512+DragonWork@users.noreply.github.com> Date: Thu, 10 Sep 2026 02:12:15 +0200 Subject: [PATCH 2/3] fix: preserve native async contexts and scope Windows callbacks --- src/poller.cpp | 10 +++++++--- src/poller.h | 1 + src/serialport_win.cpp | 14 ++++++++------ src/serialport_win.h | 6 ++++-- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/poller.cpp b/src/poller.cpp index 0e393472..6a86af21 100644 --- a/src/poller.cpp +++ b/src/poller.cpp @@ -2,7 +2,8 @@ #include #include "./poller.h" -Poller::Poller (const Napi::CallbackInfo &info) : Napi::ObjectWrap(info) +Poller::Poller (const Napi::CallbackInfo &info) : Napi::ObjectWrap(info), + async_context(info.Env(), "serialport:Poller") { Napi::Env env = info.Env(); Napi::HandleScope scope(env); @@ -81,13 +82,16 @@ void Poller::onData(uv_poll_t* handle, int status, int events) { if (0 != status) { // fprintf(stdout, "OnData Error status=%s events=%d\n", uv_strerror(status), events); obj->_stop(); // doesn't matter if this errors - obj->callback.MakeCallback(obj->Value(), {Napi::Error::New(env, uv_strerror(status)).Value(), env.Undefined()}); + obj->callback.MakeCallback(env.Global(), + {Napi::Error::New(env, uv_strerror(status)).Value(), env.Undefined()}, obj->async_context); } else { // fprintf(stdout, "OnData status=%d events=%d subscribed=%d\n", status, events, obj->events); // remove triggered events from the poll int newEvents = obj->events & ~events; obj->poll(env, newEvents); - obj->callback.MakeCallback(obj->Value(), {env.Null(), Napi::Number::New(env, events)}); + // uv_poll invokes us outside Node's callback scopes. MakeCallback drains + // nextTick and Promise continuations before the event loop waits again. + obj->callback.MakeCallback(env.Global(), {env.Null(), Napi::Number::New(env, events)}, obj->async_context); } } diff --git a/src/poller.h b/src/poller.h index c15a2e9f..c9cfec22 100644 --- a/src/poller.h +++ b/src/poller.h @@ -17,6 +17,7 @@ class Poller : public Napi::ObjectWrap { int fd; uv_poll_t* poll_handle = nullptr; Napi::FunctionReference callback; + Napi::AsyncContext async_context; bool uv_poll_init_success = false; // can this be read off of poll_handle? diff --git a/src/serialport_win.cpp b/src/serialport_win.cpp index 7e79216a..7c2a177a 100644 --- a/src/serialport_win.cpp +++ b/src/serialport_win.cpp @@ -380,9 +380,9 @@ void EIO_AfterWrite(uv_async_t* req) { v8::Local argv[1]; if (baton->errorString[0]) { - baton->callback.Call({Napi::Error::New(env, baton->errorString).Value()}); + baton->callback.MakeCallback(env.Global(), {Napi::Error::New(env, baton->errorString).Value()}, baton->async_context); } else { - baton->callback.Call({env.Null()}); + baton->callback.MakeCallback(env.Global(), {env.Null()}, baton->async_context); } baton->buffer.Reset(); delete baton; @@ -414,7 +414,7 @@ Napi::Value Write(const Napi::CallbackInfo& info) { return env.Null(); } - WriteBaton* baton = new WriteBaton(); + WriteBaton* baton = new WriteBaton(env); baton->callback = Napi::Persistent(info[2].As()); baton->fd = fd; baton->buffer.Reset(buffer); @@ -552,9 +552,11 @@ void EIO_AfterRead(uv_async_t* req) { uv_close(reinterpret_cast(req), AsyncCloseCallback); if (baton->errorString[0]) { - baton->callback.Call({Napi::Error::New(env, baton->errorString).Value(), env.Undefined()}); + baton->callback.MakeCallback(env.Global(), + {Napi::Error::New(env, baton->errorString).Value(), env.Undefined()}, baton->async_context); } else { - baton->callback.Call({env.Null(), Napi::Number::New(env, static_cast(baton->bytesRead))}); + baton->callback.MakeCallback(env.Global(), + {env.Null(), Napi::Number::New(env, static_cast(baton->bytesRead))}, baton->async_context); } delete baton; } @@ -600,7 +602,7 @@ Napi::Value Read(const Napi::CallbackInfo& info) { Napi::TypeError::New(env, "Fifth argument must be a function").ThrowAsJavaScriptException(); return env.Null(); } - ReadBaton* baton = new ReadBaton(); + ReadBaton* baton = new ReadBaton(env); baton->callback = Napi::Persistent(info[4].As()); baton->fd = fd; baton->offset = offset; diff --git a/src/serialport_win.h b/src/serialport_win.h index b5357373..53dc9120 100644 --- a/src/serialport_win.h +++ b/src/serialport_win.h @@ -13,7 +13,7 @@ static inline HANDLE int2handle(int ptr) { } struct WriteBaton { - WriteBaton() : bufferData(), errorString() {} + explicit WriteBaton(Napi::Env env) : bufferData(), async_context(env, "serialport:Write"), errorString() {} int fd = 0; char* bufferData = nullptr; size_t bufferLength = 0; @@ -23,6 +23,7 @@ struct WriteBaton { bool complete = false; Napi::ObjectReference buffer; Napi::FunctionReference callback; + Napi::AsyncContext async_context; int result = 0; char errorString[ERROR_STRING_SIZE]; }; @@ -30,7 +31,7 @@ struct WriteBaton { Napi::Value Write(const Napi::CallbackInfo& info); struct ReadBaton { - ReadBaton() : errorString() {} + explicit ReadBaton(Napi::Env env) : async_context(env, "serialport:Read"), errorString() {} int fd = 0; char* bufferData = nullptr; size_t bufferLength = 0; @@ -39,6 +40,7 @@ struct ReadBaton { size_t offset = 0; void* hThread = nullptr; Napi::FunctionReference callback; + Napi::AsyncContext async_context; bool complete = false; char errorString[ERROR_STRING_SIZE]; }; From ecfdd8f2d7a040a4d1f5c3de0c98dd9b38762d37 Mon Sep 17 00:00:00 2001 From: DragonWork <97512+DragonWork@users.noreply.github.com> Date: Thu, 10 Sep 2026 21:01:22 +0200 Subject: [PATCH 3/3] build: publish patched bindings under DragonWork scope --- .github/workflows/{build.yml => publish.yml} | 38 +++++++++++++++----- README.md | 28 ++++++++++++++- package-lock.json | 8 ++--- package.json | 6 ++-- 4 files changed, 63 insertions(+), 17 deletions(-) rename .github/workflows/{build.yml => publish.yml} (74%) diff --git a/.github/workflows/build.yml b/.github/workflows/publish.yml similarity index 74% rename from .github/workflows/build.yml rename to .github/workflows/publish.yml index f86e9051..2ac90ac8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/publish.yml @@ -1,14 +1,16 @@ -name: Build +name: Publish on: - push: - branches: - - main - - beta + workflow_dispatch: + release: + types: [published] +permissions: + contents: read concurrency: group: build-${{ github.ref }} cancel-in-progress: false jobs: prebuild: + if: github.repository == 'DragonWork/bindings-cpp' strategy: matrix: include: @@ -107,6 +109,9 @@ jobs: needs: prebuild name: Release runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 @@ -116,10 +121,25 @@ jobs: path: prebuilds - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 + registry-url: https://registry.npmjs.org + - name: Check release version + if: github.event_name == 'release' + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: node -e "if (process.env.RELEASE_TAG !== 'v' + require('./package.json').version) throw new Error('Release tag must match package.json version')" - run: npm ci - run: npm run build - - run: npm run semantic-release + - run: npm run typecheck + - run: npm test env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_OPTIONS: --no-experimental-strip-types + - run: npm pack + - uses: actions/upload-artifact@v4 + with: + name: npm-package + path: dragonwork-serialport-bindings-cpp-*.tgz + if-no-files-found: error + - name: Publish to npm + if: github.event_name == 'release' + run: npm publish dragonwork-serialport-bindings-cpp-*.tgz --access public --tag latest diff --git a/README.md b/README.md index 1bf126aa..430f23cc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,30 @@ -# @serialport/bindings-cpp +# @dragonwork/serialport-bindings-cpp + +Temporary fork of `@serialport/bindings-cpp` with the native callback fixes from +[serialport/bindings-cpp#243](https://github.com/serialport/bindings-cpp/pull/243). +The upstream API is unchanged. Use an npm alias to keep existing imports: + +```json +"@serialport/bindings-cpp": "npm:@dragonwork/serialport-bindings-cpp@13.0.1-patch.1" +``` + +### Publishing this fork + +The **Publish** workflow rebuilds upstream's native prebuild matrix and packs it +with the compiled JavaScript. Running it manually only uploads the `npm-package` +artifact; it does not publish. + +For the first publication, own the `@dragonwork` scope on npm, run the workflow, +download and extract `npm-package`, then use `npm login` and +`npm publish dragonwork-serialport-bindings-cpp-13.0.1-patch.1.tgz --access public --tag latest`. + +In the npm package settings, add a trusted publisher for GitHub owner `DragonWork`, +repository `bindings-cpp`, workflow `publish.yml`, with direct `npm publish` +allowed. No npm token is needed in GitHub. Future GitHub releases publish through +OIDC; their tags must match `v` plus the version in `package.json` (also update +`package-lock.json`). Do not create a release for an already published version. + +## Upstream documentation [![Backers on Open Collective](https://opencollective.com/serialport/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/serialport/sponsors/badge.svg)](#sponsors) diff --git a/package-lock.json b/package-lock.json index 5a28174c..63d4db50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@serialport/bindings-cpp", - "version": "0.0.0-development", + "name": "@dragonwork/serialport-bindings-cpp", + "version": "13.0.1-patch.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@serialport/bindings-cpp", - "version": "0.0.0-development", + "name": "@dragonwork/serialport-bindings-cpp", + "version": "13.0.1-patch.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ea22bda2..29862b87 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "@serialport/bindings-cpp", + "name": "@dragonwork/serialport-bindings-cpp", "description": "SerialPort Hardware bindings for node serialport written in c++", - "version": "0.0.0-development", + "version": "13.0.1-patch.1", "main": "./dist/index.js", "types": "./dist/index.d.ts", "keywords": [ @@ -91,7 +91,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/serialport/bindings-cpp.git" + "url": "https://github.com/DragonWork/bindings-cpp.git" }, "funding": "https://opencollective.com/serialport/donate", "changelog": {