• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RobotWebTools / rclnodejs / 19957465401

05 Dec 2025 08:39AM UTC coverage: 80.404% (-2.3%) from 82.751%
19957465401

Pull #1341

github

web-flow
Merge e3710d3b9 into faaa2d4b5
Pull Request #1341: Enhance Message Validation

1212 of 1681 branches covered (72.1%)

Branch coverage included in aggregate %.

115 of 186 new or added lines in 5 files covered. (61.83%)

13 existing lines in 3 files now uncovered.

2612 of 3075 relevant lines covered (84.94%)

460.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.25
/lib/client.js
1
// Copyright (c) 2017 Intel Corporation. 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('./native_loader.js');
26✔
18
const DistroUtils = require('./distro.js');
26✔
19
const Entity = require('./entity.js');
26✔
20
const {
21
  TypeValidationError,
22
  TimeoutError,
23
  AbortError,
24
} = require('./errors.js');
26✔
25
const { assertValidMessage } = require('./message_validation.js');
26✔
26
const debug = require('debug')('rclnodejs:client');
26✔
27

28
// Polyfill for AbortSignal.any() for Node.js <= 20.3.0
29
// AbortSignal.any() was added in Node.js 20.3.0
30
// See https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static
31
if (!AbortSignal.any) {
26!
32
  AbortSignal.any = function (signals) {
×
33
    // Filter out null/undefined values and validate inputs
34
    const validSignals = Array.isArray(signals)
×
35
      ? signals.filter((signal) => signal != null)
×
36
      : [];
37

38
    // If no valid signals, return a never-aborting signal
39
    if (validSignals.length === 0) {
×
40
      return new AbortController().signal;
×
41
    }
42

43
    const controller = new AbortController();
×
44
    const listeners = [];
×
45

46
    // Cleanup function to remove all event listeners
47
    const cleanup = () => {
×
48
      listeners.forEach(({ signal, listener }) => {
×
49
        signal.removeEventListener('abort', listener);
×
50
      });
51
    };
52

53
    for (const signal of validSignals) {
×
54
      if (signal.aborted) {
×
55
        cleanup();
×
56
        controller.abort(signal.reason);
×
57
        return controller.signal;
×
58
      }
59

60
      const listener = () => {
×
61
        cleanup();
×
62
        controller.abort(signal.reason);
×
63
      };
64

65
      signal.addEventListener('abort', listener);
×
66
      listeners.push({ signal, listener });
×
67
    }
68

69
    return controller.signal;
×
70
  };
71
}
72

73
/**
74
 * @class - Class representing a Client in ROS
75
 * @hideconstructor
76
 */
77

78
class Client extends Entity {
79
  constructor(handle, nodeHandle, serviceName, typeClass, options) {
80
    super(handle, typeClass, options);
159✔
81
    this._nodeHandle = nodeHandle;
159✔
82
    this._serviceName = serviceName;
159✔
83
    this._sequenceNumberToCallbackMap = new Map();
159✔
84
    this._validateRequests = options.validateRequests || false;
159✔
85
    this._validationOptions = options.validationOptions || {
159✔
86
      strict: true,
87
      checkTypes: true,
88
    };
89
  }
90

91
  /**
92
   * Enable or disable request validation for this client
93
   * @param {boolean} enabled - Whether to validate requests before sending
94
   * @param {object} [options] - Validation options
95
   * @param {boolean} [options.strict=true] - Throw on unknown fields
96
   * @param {boolean} [options.checkTypes=true] - Validate field types
97
   */
98
  setValidation(enabled, options = {}) {
×
NEW
99
    this._validateRequests = enabled;
×
NEW
100
    if (options && Object.keys(options).length > 0) {
×
NEW
101
      this._validationOptions = { ...this._validationOptions, ...options };
×
102
    }
103
  }
104

105
  /**
106
   * Check if request validation is enabled for this client
107
   * @returns {boolean} True if validation is enabled
108
   */
109
  get validationEnabled() {
NEW
110
    return this._validateRequests;
×
111
  }
112

113
  /**
114
   * This callback is called when a response is sent back from service
115
   * @callback ResponseCallback
116
   * @param {Object} response - The response sent from the service
117
   * @see [Client.sendRequest]{@link Client#sendRequest}
118
   * @see [Node.createService]{@link Node#createService}
119
   * @see {@link Client}
120
   * @see {@link Service}
121
   */
122

123
  /**
124
   * Send the request and will be notified asynchronously if receiving the response.
125
   * @param {object} request - The request to be submitted.
126
   * @param {ResponseCallback} callback - Thc callback function for receiving the server response.
127
   * @param {object} [options] - Send options
128
   * @param {boolean} [options.validate] - Override validateRequests setting for this call
129
   * @return {undefined}
130
   * @throws {MessageValidationError} If validation is enabled and request is invalid
131
   * @see {@link ResponseCallback}
132
   */
133
  sendRequest(request, callback, options = {}) {
36✔
134
    if (typeof callback !== 'function') {
36!
UNCOV
135
      throw new TypeValidationError('callback', callback, 'function', {
×
136
        entityType: 'service',
137
        entityName: this._serviceName,
138
      });
139
    }
140

141
    const shouldValidate =
142
      options.validate !== undefined
36!
143
        ? options.validate
144
        : this._validateRequests;
145

146
    if (shouldValidate && !(request instanceof this._typeClass.Request)) {
36!
NEW
147
      assertValidMessage(
×
148
        request,
149
        this._typeClass.Request,
150
        this._validationOptions
151
      );
152
    }
153

154
    let requestToSend =
155
      request instanceof this._typeClass.Request
36✔
156
        ? request
157
        : new this._typeClass.Request(request);
158

159
    let rawRequest = requestToSend.serialize();
34✔
160
    let sequenceNumber = rclnodejs.sendRequest(this._handle, rawRequest);
34✔
161
    debug(`Client has sent a ${this._serviceName} request.`);
34✔
162
    this._sequenceNumberToCallbackMap.set(sequenceNumber, callback);
34✔
163
  }
164

165
  /**
166
   * Send the request and return a Promise that resolves with the response.
167
   * @param {object} request - The request to be submitted.
168
   * @param {object} [options] - Optional parameters for the request.
169
   * @param {number} [options.timeout] - Timeout in milliseconds for the request.
170
   * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
171
   * @param {boolean} [options.validate] - Override validateRequests setting for this call
172
   * @return {Promise<object>} Promise that resolves with the service response.
173
   * @throws {module:rclnodejs.TimeoutError} If the request times out (when options.timeout is exceeded).
174
   * @throws {module:rclnodejs.AbortError} If the request is manually aborted (via options.signal).
175
   * @throws {module:rclnodejs.MessageValidationError} If validation is enabled and request is invalid.
176
   * @throws {Error} If the request fails for other reasons.
177
   */
178
  sendRequestAsync(request, options = {}) {
6✔
179
    return new Promise((resolve, reject) => {
61✔
180
      let sequenceNumber = null;
61✔
181
      let isResolved = false;
61✔
182
      let isTimeout = false;
61✔
183

184
      const cleanup = () => {
61✔
185
        if (sequenceNumber !== null) {
59✔
186
          this._sequenceNumberToCallbackMap.delete(sequenceNumber);
58✔
187
        }
188
        isResolved = true;
59✔
189
      };
190

191
      let effectiveSignal = options.signal;
61✔
192

193
      if (options.timeout !== undefined && options.timeout >= 0) {
61✔
194
        const timeoutSignal = AbortSignal.timeout(options.timeout);
51✔
195

196
        timeoutSignal.addEventListener('abort', () => {
51✔
197
          isTimeout = true;
51✔
198
        });
199

200
        if (options.signal) {
51✔
201
          effectiveSignal = AbortSignal.any([options.signal, timeoutSignal]);
3✔
202
        } else {
203
          effectiveSignal = timeoutSignal;
48✔
204
        }
205
      }
206

207
      if (effectiveSignal) {
61✔
208
        if (effectiveSignal.aborted) {
53✔
209
          const error = isTimeout
2!
210
            ? new TimeoutError('Service request', options.timeout, {
211
                entityType: 'service',
212
                entityName: this._serviceName,
213
              })
214
            : new AbortError('Service request', undefined, {
215
                entityType: 'service',
216
                entityName: this._serviceName,
217
              });
218
          reject(error);
2✔
219
          return;
2✔
220
        }
221

222
        effectiveSignal.addEventListener('abort', () => {
51✔
223
          if (!isResolved) {
51✔
224
            cleanup();
8✔
225
            const error = isTimeout
8✔
226
              ? new TimeoutError('Service request', options.timeout, {
227
                  entityType: 'service',
228
                  entityName: this._serviceName,
229
                })
230
              : new AbortError('Service request', undefined, {
231
                  entityType: 'service',
232
                  entityName: this._serviceName,
233
                });
234
            reject(error);
8✔
235
          }
236
        });
237
      }
238

239
      try {
59✔
240
        const shouldValidate =
241
          options.validate !== undefined
59!
242
            ? options.validate
243
            : this._validateRequests;
244

245
        if (shouldValidate && !(request instanceof this._typeClass.Request)) {
59!
NEW
246
          assertValidMessage(
×
247
            request,
248
            this._typeClass.Request,
249
            this._validationOptions
250
          );
251
        }
252

253
        let requestToSend =
254
          request instanceof this._typeClass.Request
59!
255
            ? request
256
            : new this._typeClass.Request(request);
257

258
        let rawRequest = requestToSend.serialize();
58✔
259
        sequenceNumber = rclnodejs.sendRequest(this._handle, rawRequest);
58✔
260

261
        debug(`Client has sent a ${this._serviceName} request (async).`);
58✔
262

263
        this._sequenceNumberToCallbackMap.set(sequenceNumber, (response) => {
58✔
264
          if (!isResolved) {
50!
265
            cleanup();
50✔
266
            resolve(response);
50✔
267
          }
268
        });
269
      } catch (error) {
270
        cleanup();
1✔
271
        reject(error);
1✔
272
      }
273
    });
274
  }
275

276
  processResponse(sequenceNumber, response) {
277
    if (this._sequenceNumberToCallbackMap.has(sequenceNumber)) {
86✔
278
      debug(`Client has received ${this._serviceName} response from service.`);
83✔
279
      let callback = this._sequenceNumberToCallbackMap.get(sequenceNumber);
83✔
280
      this._sequenceNumberToCallbackMap.delete(sequenceNumber);
83✔
281
      callback(response.toPlainObject(this.typedArrayEnabled));
83✔
282
    } else {
283
      debug(
3✔
284
        `Client has received an unexpected ${this._serviceName} with sequence number ${sequenceNumber}.`
285
      );
286
    }
287
  }
288

289
  /**
290
   * Checks if the service is available.
291
   * @return {boolean} true if the service is available.
292
   */
293
  isServiceServerAvailable() {
294
    return rclnodejs.serviceServerIsAvailable(this._nodeHandle, this.handle);
147✔
295
  }
296

297
  /**
298
   * Wait until the service server is available or a timeout is reached. This
299
   * function polls for the service state so it may not return as soon as the
300
   * service is available.
301
   * @param {number} timeout The maximum amount of time to wait for, if timeout
302
   * is `undefined` or `< 0`, this will wait indefinitely.
303
   * @return {Promise<boolean>} true if the service is available.
304
   */
305
  async waitForService(timeout = undefined) {
9✔
306
    let deadline = Infinity;
115✔
307
    if (timeout !== undefined && timeout >= 0) {
115✔
308
      deadline = Date.now() + timeout;
106✔
309
    }
310
    let waitMs = 5;
115✔
311
    let serviceAvailable = this.isServiceServerAvailable();
115✔
312
    while (!serviceAvailable && Date.now() < deadline) {
115✔
313
      waitMs *= 2;
32✔
314
      waitMs = Math.min(waitMs, 1000);
32✔
315
      if (timeout !== undefined && timeout >= -1) {
32✔
316
        waitMs = Math.min(waitMs, deadline - Date.now());
28✔
317
      }
318
      await new Promise((resolve) => setTimeout(resolve, waitMs));
32✔
319
      serviceAvailable = this.isServiceServerAvailable();
32✔
320
    }
321
    return serviceAvailable;
115✔
322
  }
323

324
  static createClient(nodeHandle, serviceName, typeClass, options) {
325
    let type = typeClass.type();
169✔
326
    let handle = rclnodejs.createClient(
169✔
327
      nodeHandle,
328
      serviceName,
329
      type.interfaceName,
330
      type.pkgName,
331
      options.qos
332
    );
333
    return new Client(handle, nodeHandle, serviceName, typeClass, options);
159✔
334
  }
335

336
  /**
337
   * @type {string}
338
   */
339
  get serviceName() {
340
    return rclnodejs.getClientServiceName(this._handle);
3✔
341
  }
342

343
  /**
344
   * Configure introspection.
345
   * @param {Clock} clock - Clock to use for service event timestamps
346
   * @param {QoS} qos - QoSProfile for the service event publisher
347
   * @param {ServiceIntrospectionState} introspectionState - State to set introspection to
348
   */
349
  configureIntrospection(clock, qos, introspectionState) {
350
    if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
5!
UNCOV
351
      console.warn(
×
352
        'Service introspection is not supported by this version of ROS 2'
353
      );
UNCOV
354
      return;
×
355
    }
356

357
    let type = this.typeClass.type();
5✔
358
    rclnodejs.configureClientIntrospection(
5✔
359
      this.handle,
360
      this._nodeHandle,
361
      clock.handle,
362
      type.interfaceName,
363
      type.pkgName,
364
      qos,
365
      introspectionState
366
    );
367
  }
368

369
  /**
370
   * Get the logger name for this client.
371
   * @returns {string} The logger name for this client.
372
   */
373
  get loggerName() {
374
    return rclnodejs.getNodeLoggerName(this._nodeHandle);
1✔
375
  }
376
}
377

378
module.exports = Client;
26✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc