Skip to content

Commit 7c306aa

Browse files
feat: add promise-based service calls (#1311)
This PR implements promise-based service calls, allowing users to call services using async/await syntax instead of callbacks. This improves code readability and makes error handling more straightforward. Key changes: - Added promise support to service client methods - Maintained backward compatibility with callback-based API - Updated examples and documentation
1 parent 4d5bc66 commit 7c306aa

File tree

6 files changed

+607
-18
lines changed

6 files changed

+607
-18
lines changed

CONTRIBUTORS.md

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,55 @@
11
# rclnodejs contributors (sorted alphabetically)
22

33
- **[Alaa El Jawad](https://github.com/ejalaa12)**
4-
54
- Fix compatibility with ROS2 parameters array types
65
- Unit tests for all parameter types
76
- Handle concurrent ROS2 client calls, with unit tests
87

98
- **[Alex Mikhalev](https://github.com/amikhalev)**
10-
119
- Fix build for AMENT_PREFIX_PATH with multiple entries
1210

1311
- **[Felix Divo](https://github.com/felixdivo)**
14-
1512
- Code cleanup of index.js, tests cases & message generation
1613
- Improved shutdown behavior
1714
- Fixed compilation warnings
1815

1916
- **[Hanyia](https://github.com/hanyia)**
20-
2117
- Benchmark test script
2218

2319
- **[Ian McElroy](https://github.com/imcelroy)**
24-
2520
- Add descriptor namespace for all interfaces, rostsd_gen improvements
2621
- Fix compatibility with ROS2 parameters array types
2722
- Unit tests for all parameter types
2823
- Handle concurrent ROS2 client calls, with unit tests
2924

3025
- **[Kenny Yuan](https://github.com/kenny-y)**
31-
3226
- Message features: JS generation, typed arrays, plain JS object, compound msgs, many others...
3327
- npm publish scripts
3428
- Mac support
3529

3630
- **[Matt Richard](https://github.com/mattrichard)**
37-
3831
- ROS2 Actions
3932
- Guard conditions
4033
- Node utility methods (countPublishers/subscribers...)
4134
- TypeScript improvements
4235
- Node 12 compatibility
4336

4437
- **[Minggang Wang](https://github.com/minggangw)**
45-
4638
- Author, lead developer, maintainer
4739
- Core, CI
4840

49-
- **[Martins Mozeiko](https://github.com/martins-mozeiko)**
41+
- **[Mahmoud Alghalayini](https://github.com/mahmoud-ghalayini)**
42+
- JSON safe serialization improvements
43+
- Promise-based service calls implementation
5044

45+
- **[Martins Mozeiko](https://github.com/martins-mozeiko)**
5146
- QoS new/delete fix
5247

5348
- **[Qiuzhong](https://github.com/qiuzhong)**
54-
5549
- Test coverage for actions, topics, multi-array messages, cross platform, security
5650
- Converted from setTimeout to ROS2 Timer
5751

5852
- **[Teo Koon Peng](https://github.com/koonpeng)**
59-
6053
- TypeScript improvements
6154
- Added Client#waitForService
6255
- Code style improvements, e.g., Prettier formatting

example/services/README.md

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,63 @@ ROS 2 services provide a request-response communication pattern where clients se
4848
- Asynchronous request handling with callbacks
4949
- **Run Command**: `node example/services/client/client-example.js`
5050

51+
#### Async Service Client (`client/async-client-example.js`)
52+
53+
**Purpose**: Demonstrates modern async/await patterns for service communication, solving callback hell and providing cleaner error handling.
54+
55+
- **Service Type**: `example_interfaces/srv/AddTwoInts`
56+
- **Service Name**: `add_two_ints`
57+
- **Functionality**:
58+
- Multiple examples showing different async patterns
59+
- Simple async/await calls without callbacks
60+
- Timeout handling with configurable timeouts
61+
- Request cancellation using AbortController
62+
- Sequential and parallel service calls
63+
- Comprehensive error handling
64+
- **Features**:
65+
- **Modern JavaScript**: Clean async/await syntax instead of callback hell
66+
- **Timeout Support**: Built-in timeout with `options.timeout` (uses `AbortSignal.timeout()` internally)
67+
- **Cancellation**: Request cancellation using `AbortController` and `options.signal`
68+
- **Error Types**: Specific error types (`TimeoutError`, `AbortError`) for better error handling (async only)
69+
- **Backward Compatible**: Works alongside existing callback-based `sendRequest()`
70+
- **TypeScript Ready**: Full type safety with comprehensive TypeScript definitions
71+
- **Run Command**: `node example/services/client/async-client-example.js`
72+
73+
**Key API Differences**:
74+
75+
```javascript
76+
client.sendRequest(request, (response) => {
77+
console.log('Response:', response.sum);
78+
});
79+
80+
try {
81+
const response = await client.sendRequestAsync(request);
82+
83+
const response = await client.sendRequestAsync(request, { timeout: 5000 });
84+
85+
const controller = new AbortController();
86+
const response = await client.sendRequestAsync(request, {
87+
signal: controller.signal,
88+
});
89+
90+
const controller = new AbortController();
91+
const response = await client.sendRequestAsync(request, {
92+
timeout: 5000,
93+
signal: controller.signal,
94+
});
95+
96+
console.log('Response:', response.sum);
97+
} catch (error) {
98+
if (error.name === 'TimeoutError') {
99+
console.log('Request timed out');
100+
} else if (error.name === 'AbortError') {
101+
console.log('Request was cancelled');
102+
} else {
103+
console.error('Service error:', error.message);
104+
}
105+
}
106+
```
107+
51108
### GetMap Service
52109

53110
#### Service Server (`service/getmap-service-example.js`)
@@ -129,6 +186,41 @@ ROS 2 services provide a request-response communication pattern where clients se
129186
Result: object { sum: 79n }
130187
```
131188

189+
### Running the Async AddTwoInts Client Example
190+
191+
1. **Prerequisites**: Ensure ROS 2 is installed and sourced
192+
193+
2. **Start the Service Server**: Use the same service server as above:
194+
195+
```bash
196+
cd /path/to/rclnodejs
197+
node example/services/service/service-example.js
198+
```
199+
200+
3. **Start the Async Client**: In another terminal, run:
201+
202+
```bash
203+
cd /path/to/rclnodejs
204+
node example/services/client/async-client-example.js
205+
```
206+
207+
4. **Expected Output**:
208+
209+
**Service Server Terminal**: (Same as regular client)
210+
211+
```
212+
Incoming request: object { a: 42n, b: 37n }
213+
Sending response: object { sum: 79n }
214+
--
215+
```
216+
217+
**Async Client Terminal**:
218+
219+
```
220+
Sending: object { a: 42n, b: 37n }
221+
Result: object { sum: 79n }
222+
```
223+
132224
### Running the GetMap Service Example
133225

134226
1. **Prerequisites**: Ensure ROS 2 is installed and sourced
@@ -236,8 +328,11 @@ This script automatically starts the service, tests the client, and cleans up.
236328

237329
### Programming Patterns
238330

239-
- **Async/Await**: Modern JavaScript patterns for asynchronous operations
240-
- **Callback Handling**: Response processing using callback functions
331+
- **Modern Async/Await**: Clean Promise-based service calls with `sendRequestAsync()`
332+
- **Traditional Callbacks**: Response processing using callback functions with `sendRequest()`
333+
- **Error Handling**: Proper error handling with try/catch blocks and specific error types (async only)
334+
- **Timeout Management**: Built-in timeout support to prevent hanging requests (async only)
335+
- **Request Cancellation**: AbortController support for user-cancellable operations (async only)
241336
- **Resource Management**: Proper node shutdown and cleanup
242337
- **Data Analysis**: Processing and interpreting received data
243338
- **Visualization**: Converting data to human-readable formats
@@ -331,18 +426,23 @@ int8[] data
331426
### Common Issues
332427

333428
1. **Service Not Available**:
334-
335429
- Ensure the service server is running before starting the client
336430
- Check that both use the same service name (`add_two_ints`)
337431

338432
2. **Type Errors**:
339-
340433
- Ensure you're using `BigInt()` for integer values, not regular numbers
341434
- Use `response.template` to get the correct response structure
342435

343436
3. **Client Hangs**:
344437
- The client waits for service availability with a 1-second timeout
345438
- If the service isn't available, the client will log an error and shut down
439+
- For async clients, use timeout options: `client.sendRequestAsync(request, { timeout: 5000 })`
440+
441+
4. **Async/Await Issues** (applies only to `sendRequestAsync()`):
442+
- **Unhandled Promise Rejections**: Always use try/catch blocks around `sendRequestAsync()`
443+
- **Timeout Errors**: Handle `TimeoutError` specifically for timeout scenarios (async only)
444+
- **Cancelled Requests**: Handle `AbortError` when using AbortController cancellation (async only)
445+
- **Mixed Patterns**: You can use both `sendRequest()` and `sendRequestAsync()` in the same code
346446

347447
### Debugging Tips
348448

@@ -354,6 +454,10 @@ int8[] data
354454

355455
- Both examples use the standard rclnodejs initialization pattern
356456
- The service server runs continuously until manually terminated
357-
- The client performs a single request-response cycle then exits
457+
- The traditional client performs a single request-response cycle then exits
458+
- The async client demonstrates multiple patterns and then exits
459+
- **New async/await support**: Use `sendRequestAsync()` for modern Promise-based patterns
460+
- **Full backward compatibility**: Existing `sendRequest()` callback-based code continues to work unchanged
461+
- **TypeScript support**: Full type safety available for async methods
358462
- Service introspection is only available in ROS 2 Iron and later distributions
359463
- BigInt is required for integer message fields to maintain precision
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) 2025 Mahmoud Alghalayini. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const rclnodejs = require('../../../index.js');
18+
19+
async function main() {
20+
await rclnodejs.init();
21+
const node = rclnodejs.createNode('async_client_example_node');
22+
const client = node.createClient(
23+
'example_interfaces/srv/AddTwoInts',
24+
'add_two_ints'
25+
);
26+
27+
if (
28+
rclnodejs.DistroUtils.getDistroId() >
29+
rclnodejs.DistroUtils.getDistroId('humble')
30+
) {
31+
// To view service events use the following command:
32+
// ros2 topic echo "/add_two_ints/_service_event"
33+
client.configureIntrospection(
34+
node.getClock(),
35+
rclnodejs.QoS.profileSystemDefault,
36+
rclnodejs.ServiceIntrospectionStates.METADATA
37+
);
38+
}
39+
40+
const request = {
41+
a: BigInt(Math.floor(Math.random() * 100)),
42+
b: BigInt(Math.floor(Math.random() * 100)),
43+
};
44+
45+
let result = await client.waitForService(1000);
46+
if (!result) {
47+
console.log('Error: service not available');
48+
rclnodejs.shutdown();
49+
return;
50+
}
51+
52+
rclnodejs.spin(node);
53+
54+
console.log(`Sending: ${typeof request}`, request);
55+
56+
try {
57+
const response = await client.sendRequestAsync(request, { timeout: 5000 });
58+
console.log(`Result: ${typeof response}`, response);
59+
} catch (error) {
60+
console.log(`Error: ${error.message}`);
61+
} finally {
62+
rclnodejs.shutdown();
63+
}
64+
}
65+
66+
main();

0 commit comments

Comments
 (0)