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

RobotWebTools / rclnodejs / 18974105082

31 Oct 2025 01:31PM UTC coverage: 82.753% (+0.7%) from 82.083%
18974105082

Pull #1318

github

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

998 of 1326 branches covered (75.26%)

Branch coverage included in aggregate %.

146 of 177 new or added lines in 3 files covered. (82.49%)

2 existing lines in 1 file now uncovered.

2260 of 2611 relevant lines covered (86.56%)

471.25 hits per line

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

85.54
/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 debug = require('debug')('rclnodejs:parameter_client');
26✔
23

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

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

50
    this.#node = node;
50✔
51
    this.#remoteNodeName = this.#normalizeNodeName(remoteNodeName);
50✔
52
    this.#timeout = options.timeout || 5000;
50✔
53
    this.#clients = new Map();
50✔
54
    this.#destroyed = false;
50✔
55

56
    debug(
50✔
57
      `ParameterClient created for remote node: ${this.#remoteNodeName} with timeout: ${this.#timeout}ms`
58
    );
59
  }
60

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

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

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

88
    return parameters[0];
17✔
89
  }
90

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

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

107
    const client = this.#getOrCreateClient('GetParameters');
22✔
108
    const request = { names };
22✔
109

110
    debug(
22✔
111
      `Getting ${names.length} parameter(s) from node ${this.#remoteNodeName}`
112
    );
113

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

119
    const parameters = [];
21✔
120
    for (let i = 0; i < names.length; i++) {
21✔
121
      const value = response.values[i];
25✔
122
      if (value.type !== ParameterType.PARAMETER_NOT_SET) {
25✔
123
        parameters.push(
23✔
124
          new Parameter(
125
            names[i],
126
            value.type,
127
            this.#deserializeParameterValue(value)
128
          )
129
        );
130
      }
131
    }
132

133
    debug(`Retrieved ${parameters.length} parameter(s)`);
21✔
134
    return parameters;
21✔
135
  }
136

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

150
    const results = await this.setParameters([{ name, value }], options);
8✔
151
    return results[0];
8✔
152
  }
153

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

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

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

178
    debug(
9✔
179
      `Setting ${parameters.length} parameter(s) on node ${this.#remoteNodeName}`
180
    );
181

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

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

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

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

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

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

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

225
    debug(
3✔
226
      `Listed ${response.result.names.length} parameter(s) and ${response.result.prefixes.length} prefix(es)`
227
    );
228

229
    return {
3✔
230
      names: response.result.names || [],
3!
231
      prefixes: response.result.prefixes || [],
3!
232
    };
233
  }
234

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

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

251
    const client = this.#getOrCreateClient('DescribeParameters');
2✔
252
    const request = { names };
2✔
253

254
    debug(
2✔
255
      `Describing ${names.length} parameter(s) on node ${this.#remoteNodeName}`
256
    );
257

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

263
    debug(`Described ${response.descriptors.length} parameter(s)`);
2✔
264
    return response.descriptors || [];
2!
265
  }
266

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

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

283
    const client = this.#getOrCreateClient('GetParameterTypes');
2✔
284
    const request = { names };
2✔
285

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

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

295
    return response.types || [];
2!
296
  }
297

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

306
    const client = this.#getOrCreateClient('GetParameters');
47✔
307
    return await client.waitForService(timeout);
47✔
308
  }
309

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

318
  /**
319
   * Destroy the parameter client and clean up all service clients.
320
   * @return {undefined}
321
   */
322
  destroy() {
323
    if (this.#destroyed) {
98✔
324
      return;
48✔
325
    }
326

327
    debug(`Destroying ParameterClient for node ${this.#remoteNodeName}`);
50✔
328

329
    for (const [serviceName, client] of this.#clients.entries()) {
50✔
330
      try {
63✔
331
        this.#node.destroyClient(client);
63✔
332
        debug(`Destroyed client for service: ${serviceName}`);
63✔
333
      } catch (error) {
NEW
334
        debug(`Error destroying client for service ${serviceName}:`, error);
×
335
      }
336
    }
337

338
    this.#clients.clear();
50✔
339
    this.#destroyed = true;
50✔
340

341
    debug('ParameterClient destroyed');
50✔
342
  }
343

344
  /**
345
   * Get or create a service client for the specified service type.
346
   * @private
347
   * @param {string} serviceType - The service type (e.g., 'GetParameters', 'SetParameters').
348
   * @return {Client} - The service client.
349
   */
350
  #getOrCreateClient(serviceType) {
351
    if (this.#clients.has(serviceType)) {
85✔
352
      return this.#clients.get(serviceType);
22✔
353
    }
354

355
    const serviceName = `/${this.#remoteNodeName}/${this.#toSnakeCase(serviceType)}`;
63✔
356
    const serviceInterface = `rcl_interfaces/srv/${serviceType}`;
63✔
357

358
    debug(`Creating client for service: ${serviceName}`);
63✔
359

360
    const client = this.#node.createClient(serviceInterface, serviceName);
63✔
361
    this.#clients.set(serviceType, client);
63✔
362

363
    return client;
63✔
364
  }
365

366
  /**
367
   * Serialize a JavaScript value to a ParameterValue message.
368
   * @private
369
   * @param {*} value - The value to serialize.
370
   * @return {object} - The ParameterValue message.
371
   */
372
  #serializeParameterValue(value) {
373
    const type = parameterTypeFromValue(value);
11✔
374

375
    const paramValue = {
11✔
376
      type,
377
      bool_value: false,
378
      integer_value: BigInt(0),
379
      double_value: 0.0,
380
      string_value: '',
381
      byte_array_value: [],
382
      bool_array_value: [],
383
      integer_array_value: [],
384
      double_array_value: [],
385
      string_array_value: [],
386
    };
387

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

423
    return paramValue;
11✔
424
  }
425

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

460
  /**
461
   * Normalize a node name by removing leading slash if present.
462
   * @private
463
   * @param {string} nodeName - The node name to normalize.
464
   * @return {string} - The normalized node name.
465
   */
466
  #normalizeNodeName(nodeName) {
467
    return nodeName.startsWith('/') ? nodeName.substring(1) : nodeName;
50✔
468
  }
469

470
  /**
471
   * Convert a service type name from PascalCase to snake_case.
472
   * @private
473
   * @param {string} name - The name to convert.
474
   * @return {string} - The snake_case name.
475
   */
476
  #toSnakeCase(name) {
477
    return name.replace(/[A-Z]/g, (letter, index) => {
63✔
478
      return index === 0 ? letter.toLowerCase() : '_' + letter.toLowerCase();
128✔
479
    });
480
  }
481

482
  /**
483
   * Check if the client has been destroyed and throw an error if it has.
484
   * @private
485
   * @throws {Error} If the client has been destroyed.
486
   */
487
  #checkNotDestroyed() {
488
    if (this.#destroyed) {
118✔
489
      throw new Error('ParameterClient has been destroyed');
1✔
490
    }
491
  }
492
}
493

494
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