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

RobotWebTools / rclnodejs / 19030223619

03 Nov 2025 09:41AM UTC coverage: 82.737% (+0.7%) from 82.083%
19030223619

Pull #1318

github

web-flow
Merge 14ed2c5f1 into 7c306aa60
Pull Request #1318: feat: add ParameterClient for external parameter access

997 of 1326 branches covered (75.19%)

Branch coverage included in aggregate %.

148 of 179 new or added lines in 3 files covered. (82.68%)

2 existing lines in 1 file now uncovered.

2262 of 2613 relevant lines covered (86.57%)

516.54 hits per line

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

85.66
/lib/parameter_client.js
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 {
18
  Parameter,
19
  ParameterType,
20
  parameterTypeFromValue,
21
} = require('./parameter.js');
26✔
22
const validator = require('./validator.js');
26✔
23
const debug = require('debug')('rclnodejs:parameter_client');
26✔
24

25
/**
26
 * @class - Class representing a Parameter Client for accessing parameters on remote nodes
27
 * @hideconstructor
28
 */
29
class ParameterClient {
30
  #node;
31
  #remoteNodeName;
32
  #timeout;
33
  #clients;
34
  #destroyed;
35

36
  /**
37
   * Create a ParameterClient instance.
38
   * @param {Node} node - The node to use for creating service clients.
39
   * @param {string} remoteNodeName - The name of the remote node whose parameters to access.
40
   * @param {object} [options] - Options for parameter client.
41
   * @param {number} [options.timeout=5000] - Default timeout in milliseconds for service calls.
42
   */
43
  constructor(node, remoteNodeName, options = {}) {
4✔
44
    if (!node) {
54✔
45
      throw new TypeError('Node is required');
1✔
46
    }
47
    if (!remoteNodeName || typeof remoteNodeName !== 'string') {
53✔
48
      throw new TypeError('Remote node name must be a non-empty string');
1✔
49
    }
50

51
    this.#node = node;
52✔
52
    this.#remoteNodeName = this.#normalizeNodeName(remoteNodeName);
52✔
53
    validator.validateNodeName(this.#remoteNodeName);
52✔
54

55
    this.#timeout = options.timeout || 5000;
51✔
56
    this.#clients = new Map();
51✔
57
    this.#destroyed = false;
51✔
58

59
    debug(
51✔
60
      `ParameterClient created for remote node: ${this.#remoteNodeName} with timeout: ${this.#timeout}ms`
61
    );
62
  }
63

64
  /**
65
   * Get the remote node name this client is connected to.
66
   * @return {string} - The remote node name.
67
   */
68
  get remoteNodeName() {
69
    return this.#remoteNodeName;
3✔
70
  }
71

72
  /**
73
   * Get a single parameter from the remote node.
74
   * @param {string} name - The name of the parameter to retrieve.
75
   * @param {object} [options] - Options for the service call.
76
   * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
77
   * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
78
   * @return {Promise<Parameter>} - Promise that resolves with the Parameter object.
79
   * @throws {Error} If the parameter is not found or service call fails.
80
   */
81
  async getParameter(name, options = {}) {
18✔
82
    this.#throwErrorIfClientDestroyed();
20✔
83

84
    const parameters = await this.getParameters([name], options);
19✔
85
    if (parameters.length === 0) {
19✔
86
      throw new Error(
1✔
87
        `Parameter '${name}' not found on node '${this.#remoteNodeName}'`
88
      );
89
    }
90

91
    return parameters[0];
18✔
92
  }
93

94
  /**
95
   * Get multiple parameters from the remote node.
96
   * @param {string[]} names - Array of parameter names to retrieve.
97
   * @param {object} [options] - Options for the service call.
98
   * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
99
   * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
100
   * @return {Promise<Parameter[]>} - Promise that resolves with an array of Parameter objects.
101
   * @throws {Error} If the service call fails.
102
   */
103
  async getParameters(names, options = {}) {
5✔
104
    this.#throwErrorIfClientDestroyed();
24✔
105

106
    if (!Array.isArray(names) || names.length === 0) {
24✔
107
      throw new TypeError('Names must be a non-empty array');
2✔
108
    }
109

110
    const client = this.#getOrCreateClient('GetParameters');
22✔
111
    const request = { names };
22✔
112

113
    debug(
22✔
114
      `Getting ${names.length} parameter(s) from node ${this.#remoteNodeName}`
115
    );
116

117
    const response = await client.sendRequestAsync(request, {
22✔
118
      timeout: options.timeout || this.#timeout,
43✔
119
      signal: options.signal,
120
    });
121

122
    const parameters = [];
22✔
123
    for (let i = 0; i < names.length; i++) {
22✔
124
      const value = response.values[i];
26✔
125
      if (value.type !== ParameterType.PARAMETER_NOT_SET) {
26✔
126
        parameters.push(
24✔
127
          new Parameter(
128
            names[i],
129
            value.type,
130
            this.#deserializeParameterValue(value)
131
          )
132
        );
133
      }
134
    }
135

136
    debug(`Retrieved ${parameters.length} parameter(s)`);
22✔
137
    return parameters;
22✔
138
  }
139

140
  /**
141
   * Set a single parameter on the remote node.
142
   * @param {string} name - The name of the parameter to set.
143
   * @param {*} value - The value to set. Type is automatically inferred.
144
   * @param {object} [options] - Options for the service call.
145
   * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
146
   * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
147
   * @return {Promise<object>} - Promise that resolves with the result {successful: boolean, reason: string}.
148
   * @throws {Error} If the service call fails.
149
   */
150
  async setParameter(name, value, options = {}) {
8✔
151
    this.#throwErrorIfClientDestroyed();
8✔
152

153
    const results = await this.setParameters([{ name, value }], options);
8✔
154
    return results[0];
8✔
155
  }
156

157
  /**
158
   * Set multiple parameters on the remote node.
159
   * @param {Array<{name: string, value: *}>} parameters - Array of parameter objects with name and value.
160
   * @param {object} [options] - Options for the service call.
161
   * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
162
   * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
163
   * @return {Promise<Array<{name: string, successful: boolean, reason: string}>>} - Promise that resolves with an array of results.
164
   * @throws {Error} If the service call fails.
165
   */
166
  async setParameters(parameters, options = {}) {
3✔
167
    this.#throwErrorIfClientDestroyed();
11✔
168

169
    if (!Array.isArray(parameters) || parameters.length === 0) {
11✔
170
      throw new TypeError('Parameters must be a non-empty array');
2✔
171
    }
172

173
    const client = this.#getOrCreateClient('SetParameters');
9✔
174
    const request = {
9✔
175
      parameters: parameters.map((param) => ({
11✔
176
        name: param.name,
177
        value: this.#serializeParameterValue(param.value),
178
      })),
179
    };
180

181
    debug(
9✔
182
      `Setting ${parameters.length} parameter(s) on node ${this.#remoteNodeName}`
183
    );
184

185
    const response = await client.sendRequestAsync(request, {
9✔
186
      timeout: options.timeout || this.#timeout,
18✔
187
      signal: options.signal,
188
    });
189

190
    const results = response.results.map((result, index) => ({
11✔
191
      name: parameters[index].name,
192
      successful: result.successful,
193
      reason: result.reason || '',
22✔
194
    }));
195

196
    debug(
9✔
197
      `Set ${results.filter((r) => r.successful).length}/${results.length} parameter(s) successfully`
11✔
198
    );
199
    return results;
9✔
200
  }
201

202
  /**
203
   * List all parameters available on the remote node.
204
   * @param {object} [options] - Options for listing parameters.
205
   * @param {string[]} [options.prefixes] - Optional array of parameter name prefixes to filter by.
206
   * @param {number} [options.depth=0] - Depth of parameter namespace to list (0 = unlimited).
207
   * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
208
   * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
209
   * @return {Promise<{names: string[], prefixes: string[]}>} - Promise that resolves with parameter names and prefixes.
210
   * @throws {Error} If the service call fails.
211
   */
212
  async listParameters(options = {}) {
1✔
213
    this.#throwErrorIfClientDestroyed();
3✔
214

215
    const client = this.#getOrCreateClient('ListParameters');
3✔
216
    const request = {
3✔
217
      prefixes: options.prefixes || [],
4✔
218
      depth: options.depth !== undefined ? BigInt(options.depth) : BigInt(0),
3✔
219
    };
220

221
    debug(`Listing parameters on node ${this.#remoteNodeName}`);
3✔
222

223
    const response = await client.sendRequestAsync(request, {
3✔
224
      timeout: options.timeout || this.#timeout,
6✔
225
      signal: options.signal,
226
    });
227

228
    debug(
3✔
229
      `Listed ${response.result.names.length} parameter(s) and ${response.result.prefixes.length} prefix(es)`
230
    );
231

232
    return {
3✔
233
      names: response.result.names || [],
3!
234
      prefixes: response.result.prefixes || [],
3!
235
    };
236
  }
237

238
  /**
239
   * Describe parameters on the remote node.
240
   * @param {string[]} names - Array of parameter names to describe.
241
   * @param {object} [options] - Options for the service call.
242
   * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
243
   * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
244
   * @return {Promise<Array<object>>} - Promise that resolves with an array of parameter descriptors.
245
   * @throws {Error} If the service call fails.
246
   */
247
  async describeParameters(names, options = {}) {
3✔
248
    this.#throwErrorIfClientDestroyed();
3✔
249

250
    if (!Array.isArray(names) || names.length === 0) {
3✔
251
      throw new TypeError('Names must be a non-empty array');
1✔
252
    }
253

254
    const client = this.#getOrCreateClient('DescribeParameters');
2✔
255
    const request = { names };
2✔
256

257
    debug(
2✔
258
      `Describing ${names.length} parameter(s) on node ${this.#remoteNodeName}`
259
    );
260

261
    const response = await client.sendRequestAsync(request, {
2✔
262
      timeout: options.timeout || this.#timeout,
4✔
263
      signal: options.signal,
264
    });
265

266
    debug(`Described ${response.descriptors.length} parameter(s)`);
2✔
267
    return response.descriptors || [];
2!
268
  }
269

270
  /**
271
   * Get the types of parameters on the remote node.
272
   * @param {string[]} names - Array of parameter names.
273
   * @param {object} [options] - Options for the service call.
274
   * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
275
   * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
276
   * @return {Promise<Array<number>>} - Promise that resolves with an array of parameter types.
277
   * @throws {Error} If the service call fails.
278
   */
279
  async getParameterTypes(names, options = {}) {
2✔
280
    this.#throwErrorIfClientDestroyed();
2✔
281

282
    if (!Array.isArray(names) || names.length === 0) {
2!
NEW
283
      throw new TypeError('Names must be a non-empty array');
×
284
    }
285

286
    const client = this.#getOrCreateClient('GetParameterTypes');
2✔
287
    const request = { names };
2✔
288

289
    debug(
2✔
290
      `Getting types for ${names.length} parameter(s) on node ${this.#remoteNodeName}`
291
    );
292

293
    const response = await client.sendRequestAsync(request, {
2✔
294
      timeout: options.timeout || this.#timeout,
4✔
295
      signal: options.signal,
296
    });
297

298
    return response.types || [];
2!
299
  }
300

301
  /**
302
   * Wait for the parameter services to be available on the remote node.
303
   * @param {number} [timeout] - Optional timeout in milliseconds.
304
   * @return {Promise<boolean>} - Promise that resolves to true if services are available.
305
   */
306
  async waitForService(timeout) {
307
    this.#throwErrorIfClientDestroyed();
48✔
308

309
    const client = this.#getOrCreateClient('GetParameters');
48✔
310
    return await client.waitForService(timeout);
48✔
311
  }
312

313
  /**
314
   * Check if the parameter client has been destroyed.
315
   * @return {boolean} - True if destroyed, false otherwise.
316
   */
317
  isDestroyed() {
318
    return this.#destroyed;
56✔
319
  }
320

321
  /**
322
   * Destroy the parameter client and clean up all service clients.
323
   * @return {undefined}
324
   */
325
  destroy() {
326
    if (this.#destroyed) {
100✔
327
      return;
49✔
328
    }
329

330
    debug(`Destroying ParameterClient for node ${this.#remoteNodeName}`);
51✔
331

332
    for (const [serviceType, client] of this.#clients.entries()) {
51✔
333
      try {
64✔
334
        this.#node.destroyClient(client);
64✔
335
        debug(`Destroyed client for service type: ${serviceType}`);
64✔
336
      } catch (error) {
NEW
337
        debug(
×
338
          `Error destroying client for service type ${serviceType}:`,
339
          error
340
        );
341
      }
342
    }
343

344
    this.#clients.clear();
51✔
345
    this.#destroyed = true;
51✔
346

347
    debug('ParameterClient destroyed');
51✔
348
  }
349

350
  /**
351
   * Get or create a service client for the specified service type.
352
   * @private
353
   * @param {string} serviceType - The service type (e.g., 'GetParameters', 'SetParameters').
354
   * @return {Client} - The service client.
355
   */
356
  #getOrCreateClient(serviceType) {
357
    if (this.#clients.has(serviceType)) {
86✔
358
      return this.#clients.get(serviceType);
22✔
359
    }
360

361
    const serviceName = `/${this.#remoteNodeName}/${this.#toSnakeCase(serviceType)}`;
64✔
362
    const serviceInterface = `rcl_interfaces/srv/${serviceType}`;
64✔
363

364
    debug(`Creating client for service: ${serviceName}`);
64✔
365

366
    const client = this.#node.createClient(serviceInterface, serviceName);
64✔
367
    this.#clients.set(serviceType, client);
64✔
368

369
    return client;
64✔
370
  }
371

372
  /**
373
   * Serialize a JavaScript value to a ParameterValue message.
374
   * @private
375
   * @param {*} value - The value to serialize.
376
   * @return {object} - The ParameterValue message.
377
   */
378
  #serializeParameterValue(value) {
379
    const type = parameterTypeFromValue(value);
11✔
380

381
    const paramValue = {
11✔
382
      type,
383
      bool_value: false,
384
      integer_value: BigInt(0),
385
      double_value: 0.0,
386
      string_value: '',
387
      byte_array_value: [],
388
      bool_array_value: [],
389
      integer_array_value: [],
390
      double_array_value: [],
391
      string_array_value: [],
392
    };
393

394
    switch (type) {
11!
395
      case ParameterType.PARAMETER_BOOL:
396
        paramValue.bool_value = value;
2✔
397
        break;
2✔
398
      case ParameterType.PARAMETER_INTEGER:
399
        paramValue.integer_value =
3✔
400
          typeof value === 'bigint' ? value : BigInt(value);
3!
401
        break;
3✔
402
      case ParameterType.PARAMETER_DOUBLE:
403
        paramValue.double_value = value;
2✔
404
        break;
2✔
405
      case ParameterType.PARAMETER_STRING:
406
        paramValue.string_value = value;
2✔
407
        break;
2✔
408
      case ParameterType.PARAMETER_BOOL_ARRAY:
NEW
409
        paramValue.bool_array_value = Array.from(value);
×
NEW
410
        break;
×
411
      case ParameterType.PARAMETER_INTEGER_ARRAY:
NEW
412
        paramValue.integer_array_value = Array.from(value).map((v) =>
×
NEW
413
          typeof v === 'bigint' ? v : BigInt(v)
×
414
        );
NEW
415
        break;
×
416
      case ParameterType.PARAMETER_DOUBLE_ARRAY:
NEW
417
        paramValue.double_array_value = Array.from(value);
×
NEW
418
        break;
×
419
      case ParameterType.PARAMETER_STRING_ARRAY:
NEW
420
        paramValue.string_array_value = Array.from(value);
×
NEW
421
        break;
×
422
      case ParameterType.PARAMETER_BYTE_ARRAY:
423
        paramValue.byte_array_value = Array.from(value).map((v) =>
2✔
424
          Math.trunc(v)
6✔
425
        );
426
        break;
2✔
427
    }
428

429
    return paramValue;
11✔
430
  }
431

432
  /**
433
   * Deserialize a ParameterValue message to a JavaScript value.
434
   * @private
435
   * @param {object} paramValue - The ParameterValue message.
436
   * @return {*} - The deserialized value.
437
   */
438
  #deserializeParameterValue(paramValue) {
439
    switch (paramValue.type) {
24!
440
      case ParameterType.PARAMETER_BOOL:
441
        return paramValue.bool_value;
4✔
442
      case ParameterType.PARAMETER_INTEGER:
443
        return paramValue.integer_value;
5✔
444
      case ParameterType.PARAMETER_DOUBLE:
445
        return paramValue.double_value;
3✔
446
      case ParameterType.PARAMETER_STRING:
447
        return paramValue.string_value;
6✔
448
      case ParameterType.PARAMETER_BYTE_ARRAY:
449
        return Array.from(paramValue.byte_array_value || []);
3!
450
      case ParameterType.PARAMETER_BOOL_ARRAY:
NEW
451
        return Array.from(paramValue.bool_array_value || []);
×
452
      case ParameterType.PARAMETER_INTEGER_ARRAY:
453
        return Array.from(paramValue.integer_array_value || []).map((v) =>
1!
454
          typeof v === 'bigint' ? v : BigInt(v)
3!
455
        );
456
      case ParameterType.PARAMETER_DOUBLE_ARRAY:
457
        return Array.from(paramValue.double_array_value || []);
1!
458
      case ParameterType.PARAMETER_STRING_ARRAY:
459
        return Array.from(paramValue.string_array_value || []);
1!
460
      case ParameterType.PARAMETER_NOT_SET:
461
      default:
NEW
462
        return null;
×
463
    }
464
  }
465

466
  /**
467
   * Normalize a node name by removing leading slash if present.
468
   * @private
469
   * @param {string} nodeName - The node name to normalize.
470
   * @return {string} - The normalized node name.
471
   */
472
  #normalizeNodeName(nodeName) {
473
    return nodeName.startsWith('/') ? nodeName.substring(1) : nodeName;
52✔
474
  }
475

476
  /**
477
   * Convert a service type name from PascalCase to snake_case.
478
   * @private
479
   * @param {string} name - The name to convert.
480
   * @return {string} - The snake_case name.
481
   */
482
  #toSnakeCase(name) {
483
    return name.replace(/[A-Z]/g, (letter, index) => {
64✔
484
      return index === 0 ? letter.toLowerCase() : '_' + letter.toLowerCase();
130✔
485
    });
486
  }
487

488
  /**
489
   * Throws an error if the client has been destroyed.
490
   * @private
491
   * @throws {Error} If the client has been destroyed.
492
   */
493
  #throwErrorIfClientDestroyed() {
494
    if (this.#destroyed) {
119✔
495
      throw new Error('ParameterClient has been destroyed');
1✔
496
    }
497
  }
498
}
499

500
module.exports = ParameterClient;
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