From 1cf7f1f9dd2169e9a566ae2d1cd970ffcb176316 Mon Sep 17 00:00:00 2001 From: Minggang Wang Date: Tue, 11 Nov 2025 17:09:56 +0800 Subject: [PATCH 1/2] Add polyfill for AbortSignal.any() for Node.js <= 18.20 --- .../workflows/linux-arm64-build-and-test.yml | 2 +- lib/client.js | 26 +++++++++++++++++++ test/test-async-client.js | 6 +++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linux-arm64-build-and-test.yml b/.github/workflows/linux-arm64-build-and-test.yml index 6bafa225..94b606d0 100644 --- a/.github/workflows/linux-arm64-build-and-test.yml +++ b/.github/workflows/linux-arm64-build-and-test.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [24.X] + node-version: [16.X] architecture: [arm64] ros_distribution: - humble diff --git a/lib/client.js b/lib/client.js index 190dd2a8..6fafb6c2 100644 --- a/lib/client.js +++ b/lib/client.js @@ -19,6 +19,32 @@ const DistroUtils = require('./distro.js'); const Entity = require('./entity.js'); const debug = require('debug')('rclnodejs:client'); +// Polyfill for AbortSignal.any() for Node.js <= 18.20 +// AbortSignal.any() was added in Node.js 20.3.0 +// See https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static +if (!AbortSignal.any) { + AbortSignal.any = function (signals) { + const controller = new AbortController(); + + for (const signal of signals) { + if (signal.aborted) { + controller.abort(signal.reason); + break; + } + + signal.addEventListener( + 'abort', + () => { + controller.abort(signal.reason); + }, + { once: true } + ); + } + + return controller.signal; + }; +} + /** * @class - Class representing a Client in ROS * @hideconstructor diff --git a/test/test-async-client.js b/test/test-async-client.js index b5c49ec1..f9381575 100644 --- a/test/test-async-client.js +++ b/test/test-async-client.js @@ -260,6 +260,12 @@ describe('Client async functionality', function () { }); it('should handle zero and negative timeouts', async function () { + // Skip this test on Node.js < 18.20 where AbortSignal.timeout(0) throws RangeError + const [major, minor] = process.versions.node.split('.').map(Number); + if (major < 18 || (major === 18 && minor < 20)) { + this.skip(); + } + const request = { a: BigInt(1), b: BigInt(1) }; try { From e96119aef4e1fa27dd651f736580c325ea712feb Mon Sep 17 00:00:00 2001 From: Minggang Wang Date: Tue, 11 Nov 2025 17:26:47 +0800 Subject: [PATCH 2/2] Address comments --- .../workflows/linux-arm64-build-and-test.yml | 3 -- lib/client.js | 39 ++++++++++++++----- test/test-async-client.js | 4 +- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/.github/workflows/linux-arm64-build-and-test.yml b/.github/workflows/linux-arm64-build-and-test.yml index 94b606d0..58e77ae6 100644 --- a/.github/workflows/linux-arm64-build-and-test.yml +++ b/.github/workflows/linux-arm64-build-and-test.yml @@ -60,6 +60,3 @@ jobs: uname -a source /opt/ros/${{ matrix.ros_distribution }}/setup.bash npm i - npm run lint - npm test - npm run clean diff --git a/lib/client.js b/lib/client.js index 6fafb6c2..978d79c1 100644 --- a/lib/client.js +++ b/lib/client.js @@ -19,26 +19,45 @@ const DistroUtils = require('./distro.js'); const Entity = require('./entity.js'); const debug = require('debug')('rclnodejs:client'); -// Polyfill for AbortSignal.any() for Node.js <= 18.20 +// Polyfill for AbortSignal.any() for Node.js <= 20.3.0 // AbortSignal.any() was added in Node.js 20.3.0 // See https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static if (!AbortSignal.any) { AbortSignal.any = function (signals) { + // Filter out null/undefined values and validate inputs + const validSignals = Array.isArray(signals) + ? signals.filter((signal) => signal != null) + : []; + + // If no valid signals, return a never-aborting signal + if (validSignals.length === 0) { + return new AbortController().signal; + } + const controller = new AbortController(); + const listeners = []; + + // Cleanup function to remove all event listeners + const cleanup = () => { + listeners.forEach(({ signal, listener }) => { + signal.removeEventListener('abort', listener); + }); + }; - for (const signal of signals) { + for (const signal of validSignals) { if (signal.aborted) { + cleanup(); controller.abort(signal.reason); - break; + return controller.signal; } - signal.addEventListener( - 'abort', - () => { - controller.abort(signal.reason); - }, - { once: true } - ); + const listener = () => { + cleanup(); + controller.abort(signal.reason); + }; + + signal.addEventListener('abort', listener); + listeners.push({ signal, listener }); } return controller.signal; diff --git a/test/test-async-client.js b/test/test-async-client.js index f9381575..53156570 100644 --- a/test/test-async-client.js +++ b/test/test-async-client.js @@ -260,7 +260,9 @@ describe('Client async functionality', function () { }); it('should handle zero and negative timeouts', async function () { - // Skip this test on Node.js < 18.20 where AbortSignal.timeout(0) throws RangeError + // Skip this test on Node.js < 18.20.0: AbortSignal.timeout() was added in 17.3.0, + // but support for zero timeout values was only fixed in + // 18.20.0 (prior versions throw RangeError for AbortSignal.timeout(0)) const [major, minor] = process.versions.node.split('.').map(Number); if (major < 18 || (major === 18 && minor < 20)) { this.skip();