Skip to content

Commit 3e58c4b

Browse files
authored
Add polyfill for AbortSignal.any() for Node.js <= 20.3.0 (#1322)
This PR adds a polyfill for `AbortSignal.any()` to support Node.js versions older than 20.3.0, where this method was introduced. The implementation enables the use of `AbortSignal.any()` for combining multiple abort signals in the async client functionality. ### Key Changes - Added a polyfill implementation for `AbortSignal.any()` in `lib/client.js` - Updated test to skip zero-timeout tests on Node.js versions < 18.20 where `AbortSignal.timeout(0)` throws errors - Modified CI workflow to test on Node.js 16.X for ARM64 builds to verify backward compatibility Fix: #1321
1 parent bceb9a6 commit 3e58c4b

File tree

3 files changed

+54
-4
lines changed

3 files changed

+54
-4
lines changed

.github/workflows/linux-arm64-build-and-test.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
strategy:
2121
fail-fast: false
2222
matrix:
23-
node-version: [24.X]
23+
node-version: [16.X]
2424
architecture: [arm64]
2525
ros_distribution:
2626
- humble
@@ -60,6 +60,3 @@ jobs:
6060
uname -a
6161
source /opt/ros/${{ matrix.ros_distribution }}/setup.bash
6262
npm i
63-
npm run lint
64-
npm test
65-
npm run clean

lib/client.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,51 @@ const {
2424
} = require('./errors.js');
2525
const debug = require('debug')('rclnodejs:client');
2626

27+
// Polyfill for AbortSignal.any() for Node.js <= 20.3.0
28+
// AbortSignal.any() was added in Node.js 20.3.0
29+
// See https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static
30+
if (!AbortSignal.any) {
31+
AbortSignal.any = function (signals) {
32+
// Filter out null/undefined values and validate inputs
33+
const validSignals = Array.isArray(signals)
34+
? signals.filter((signal) => signal != null)
35+
: [];
36+
37+
// If no valid signals, return a never-aborting signal
38+
if (validSignals.length === 0) {
39+
return new AbortController().signal;
40+
}
41+
42+
const controller = new AbortController();
43+
const listeners = [];
44+
45+
// Cleanup function to remove all event listeners
46+
const cleanup = () => {
47+
listeners.forEach(({ signal, listener }) => {
48+
signal.removeEventListener('abort', listener);
49+
});
50+
};
51+
52+
for (const signal of validSignals) {
53+
if (signal.aborted) {
54+
cleanup();
55+
controller.abort(signal.reason);
56+
return controller.signal;
57+
}
58+
59+
const listener = () => {
60+
cleanup();
61+
controller.abort(signal.reason);
62+
};
63+
64+
signal.addEventListener('abort', listener);
65+
listeners.push({ signal, listener });
66+
}
67+
68+
return controller.signal;
69+
};
70+
}
71+
2772
/**
2873
* @class - Class representing a Client in ROS
2974
* @hideconstructor

test/test-async-client.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,14 @@ describe('Client async functionality', function () {
260260
});
261261

262262
it('should handle zero and negative timeouts', async function () {
263+
// Skip this test on Node.js < 18.20.0: AbortSignal.timeout() was added in 17.3.0,
264+
// but support for zero timeout values was only fixed in
265+
// 18.20.0 (prior versions throw RangeError for AbortSignal.timeout(0))
266+
const [major, minor] = process.versions.node.split('.').map(Number);
267+
if (major < 18 || (major === 18 && minor < 20)) {
268+
this.skip();
269+
}
270+
263271
const request = { a: BigInt(1), b: BigInt(1) };
264272

265273
try {

0 commit comments

Comments
 (0)