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

RobotWebTools / rclnodejs / 19071410104

04 Nov 2025 02:08PM UTC coverage: 83.072% (+0.4%) from 82.711%
19071410104

Pull #1320

github

web-flow
Merge 9cad4567e into 3ad842cc4
Pull Request #1320: feat: add structured error handling with class error hierarchy

1032 of 1365 branches covered (75.6%)

Branch coverage included in aggregate %.

161 of 239 new or added lines in 25 files covered. (67.36%)

29 existing lines in 1 file now uncovered.

2354 of 2711 relevant lines covered (86.83%)

459.93 hits per line

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

85.71
/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 {
23
  TypeValidationError,
24
  ParameterNotFoundError,
25
  OperationError,
26
} = require('./errors.js');
26✔
27
const validator = require('./validator.js');
26✔
28
const debug = require('debug')('rclnodejs:parameter_client');
26✔
29

30
/**
31
 * @class - Class representing a Parameter Client for accessing parameters on remote nodes
32
 * @hideconstructor
33
 */
34
class ParameterClient {
35
  #node;
36
  #remoteNodeName;
37
  #timeout;
38
  #clients;
39
  #destroyed;
40

41
  /**
42
   * Create a ParameterClient instance.
43
   * @param {Node} node - The node to use for creating service clients.
44
   * @param {string} remoteNodeName - The name of the remote node whose parameters to access.
45
   * @param {object} [options] - Options for parameter client.
46
   * @param {number} [options.timeout=5000] - Default timeout in milliseconds for service calls.
47
   */
48
  constructor(node, remoteNodeName, options = {}) {
4✔
49
    if (!node) {
54✔
50
      throw new TypeValidationError('node', node, 'Node instance');
1✔
51
    }
52
    if (!remoteNodeName || typeof remoteNodeName !== 'string') {
53✔
53
      throw new TypeValidationError(
1✔
54
        'remoteNodeName',
55
        remoteNodeName,
56
        'non-empty string'
57
      );
58
    }
59

60
    this.#node = node;
52✔
61
    this.#remoteNodeName = this.#normalizeNodeName(remoteNodeName);
52✔
62
    validator.validateNodeName(this.#remoteNodeName);
52✔
63

64
    this.#timeout = options.timeout || 5000;
51✔
65
    this.#clients = new Map();
51✔
66
    this.#destroyed = false;
51✔
67

68
    debug(
51✔
69
      `ParameterClient created for remote node: ${this.#remoteNodeName} with timeout: ${this.#timeout}ms`
70
    );
71
  }
72

73
  /**
74
   * Get the remote node name this client is connected to.
75
   * @return {string} - The remote node name.
76
   */
77
  get remoteNodeName() {
78
    return this.#remoteNodeName;
3✔
79
  }
80

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

93
    const parameters = await this.getParameters([name], options);
19✔
94
    if (parameters.length === 0) {
19✔
95
      throw new ParameterNotFoundError(name, this.#remoteNodeName);
1✔
96
    }
97

98
    return parameters[0];
18✔
99
  }
100

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

113
    if (!Array.isArray(names) || names.length === 0) {
24✔
114
      throw new TypeValidationError('names', names, 'non-empty array');
2✔
115
    }
116

117
    const client = this.#getOrCreateClient('GetParameters');
22✔
118
    const request = { names };
22✔
119

120
    debug(
22✔
121
      `Getting ${names.length} parameter(s) from node ${this.#remoteNodeName}`
122
    );
123

124
    const response = await client.sendRequestAsync(request, {
22✔
125
      timeout: options.timeout || this.#timeout,
43✔
126
      signal: options.signal,
127
    });
128

129
    const parameters = [];
22✔
130
    for (let i = 0; i < names.length; i++) {
22✔
131
      const value = response.values[i];
26✔
132
      if (value.type !== ParameterType.PARAMETER_NOT_SET) {
26✔
133
        parameters.push(
24✔
134
          new Parameter(
135
            names[i],
136
            value.type,
137
            this.#deserializeParameterValue(value)
138
          )
139
        );
140
      }
141
    }
142

143
    debug(`Retrieved ${parameters.length} parameter(s)`);
22✔
144
    return parameters;
22✔
145
  }
146

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

160
    const results = await this.setParameters([{ name, value }], options);
8✔
161
    return results[0];
8✔
162
  }
163

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

176
    if (!Array.isArray(parameters) || parameters.length === 0) {
11✔
177
      throw new TypeValidationError(
2✔
178
        'parameters',
179
        parameters,
180
        'non-empty array'
181
      );
182
    }
183

184
    const client = this.#getOrCreateClient('SetParameters');
9✔
185
    const request = {
9✔
186
      parameters: parameters.map((param) => ({
11✔
187
        name: param.name,
188
        value: this.#serializeParameterValue(param.value),
189
      })),
190
    };
191

192
    debug(
9✔
193
      `Setting ${parameters.length} parameter(s) on node ${this.#remoteNodeName}`
194
    );
195

196
    const response = await client.sendRequestAsync(request, {
9✔
197
      timeout: options.timeout || this.#timeout,
18✔
198
      signal: options.signal,
199
    });
200

201
    const results = response.results.map((result, index) => ({
11✔
202
      name: parameters[index].name,
203
      successful: result.successful,
204
      reason: result.reason || '',
22✔
205
    }));
206

207
    debug(
9✔
208
      `Set ${results.filter((r) => r.successful).length}/${results.length} parameter(s) successfully`
11✔
209
    );
210
    return results;
9✔
211
  }
212

213
  /**
214
   * List all parameters available on the remote node.
215
   * @param {object} [options] - Options for listing parameters.
216
   * @param {string[]} [options.prefixes] - Optional array of parameter name prefixes to filter by.
217
   * @param {number} [options.depth=0] - Depth of parameter namespace to list (0 = unlimited).
218
   * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
219
   * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
220
   * @return {Promise<{names: string[], prefixes: string[]}>} - Promise that resolves with parameter names and prefixes.
221
   * @throws {Error} If the service call fails.
222
   */
223
  async listParameters(options = {}) {
1✔
224
    this.#throwErrorIfClientDestroyed();
3✔
225

226
    const client = this.#getOrCreateClient('ListParameters');
3✔
227
    const request = {
3✔
228
      prefixes: options.prefixes || [],
4✔
229
      depth: options.depth !== undefined ? BigInt(options.depth) : BigInt(0),
3✔
230
    };
231

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

234
    const response = await client.sendRequestAsync(request, {
3✔
235
      timeout: options.timeout || this.#timeout,
6✔
236
      signal: options.signal,
237
    });
238

239
    debug(
3✔
240
      `Listed ${response.result.names.length} parameter(s) and ${response.result.prefixes.length} prefix(es)`
241
    );
242

243
    return {
3✔
244
      names: response.result.names || [],
3!
245
      prefixes: response.result.prefixes || [],
3!
246
    };
247
  }
248

249
  /**
250
   * Describe parameters on the remote node.
251
   * @param {string[]} names - Array of parameter names to describe.
252
   * @param {object} [options] - Options for the service call.
253
   * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
254
   * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
255
   * @return {Promise<Array<object>>} - Promise that resolves with an array of parameter descriptors.
256
   * @throws {Error} If the service call fails.
257
   */
258
  async describeParameters(names, options = {}) {
3✔
259
    this.#throwErrorIfClientDestroyed();
3✔
260

261
    if (!Array.isArray(names) || names.length === 0) {
3✔
262
      throw new TypeValidationError('names', names, 'non-empty array');
1✔
263
    }
264

265
    const client = this.#getOrCreateClient('DescribeParameters');
2✔
266
    const request = { names };
2✔
267

268
    debug(
2✔
269
      `Describing ${names.length} parameter(s) on node ${this.#remoteNodeName}`
270
    );
271

272
    const response = await client.sendRequestAsync(request, {
2✔
273
      timeout: options.timeout || this.#timeout,
4✔
274
      signal: options.signal,
275
    });
276

277
    debug(`Described ${response.descriptors.length} parameter(s)`);
2✔
278
    return response.descriptors || [];
2!
279
  }
280

281
  /**
282
   * Get the types of parameters on the remote node.
283
   * @param {string[]} names - Array of parameter names.
284
   * @param {object} [options] - Options for the service call.
285
   * @param {number} [options.timeout] - Timeout in milliseconds for this specific call.
286
   * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
287
   * @return {Promise<Array<number>>} - Promise that resolves with an array of parameter types.
288
   * @throws {Error} If the service call fails.
289
   */
290
  async getParameterTypes(names, options = {}) {
2✔
291
    this.#throwErrorIfClientDestroyed();
2✔
292

293
    if (!Array.isArray(names) || names.length === 0) {
2!
NEW
294
      throw new TypeValidationError('names', names, 'non-empty array');
×
295
    }
296

297
    const client = this.#getOrCreateClient('GetParameterTypes');
2✔
298
    const request = { names };
2✔
299

300
    debug(
2✔
301
      `Getting types for ${names.length} parameter(s) on node ${this.#remoteNodeName}`
302
    );
303

304
    const response = await client.sendRequestAsync(request, {
2✔
305
      timeout: options.timeout || this.#timeout,
4✔
306
      signal: options.signal,
307
    });
308

309
    return response.types || [];
2!
310
  }
311

312
  /**
313
   * Wait for the parameter services to be available on the remote node.
314
   * @param {number} [timeout] - Optional timeout in milliseconds.
315
   * @return {Promise<boolean>} - Promise that resolves to true if services are available.
316
   */
317
  async waitForService(timeout) {
318
    this.#throwErrorIfClientDestroyed();
48✔
319

320
    const client = this.#getOrCreateClient('GetParameters');
48✔
321
    return await client.waitForService(timeout);
48✔
322
  }
323

324
  /**
325
   * Check if the parameter client has been destroyed.
326
   * @return {boolean} - True if destroyed, false otherwise.
327
   */
328
  isDestroyed() {
329
    return this.#destroyed;
56✔
330
  }
331

332
  /**
333
   * Destroy the parameter client and clean up all service clients.
334
   * @return {undefined}
335
   */
336
  destroy() {
337
    if (this.#destroyed) {
100✔
338
      return;
49✔
339
    }
340

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

343
    for (const [serviceType, client] of this.#clients.entries()) {
51✔
344
      try {
64✔
345
        this.#node.destroyClient(client);
64✔
346
        debug(`Destroyed client for service type: ${serviceType}`);
64✔
347
      } catch (error) {
348
        debug(
×
349
          `Error destroying client for service type ${serviceType}:`,
350
          error
351
        );
352
      }
353
    }
354

355
    this.#clients.clear();
51✔
356
    this.#destroyed = true;
51✔
357

358
    debug('ParameterClient destroyed');
51✔
359
  }
360

361
  /**
362
   * Get or create a service client for the specified service type.
363
   * @private
364
   * @param {string} serviceType - The service type (e.g., 'GetParameters', 'SetParameters').
365
   * @return {Client} - The service client.
366
   */
367
  #getOrCreateClient(serviceType) {
368
    if (this.#clients.has(serviceType)) {
86✔
369
      return this.#clients.get(serviceType);
22✔
370
    }
371

372
    const serviceName = `/${this.#remoteNodeName}/${this.#toSnakeCase(serviceType)}`;
64✔
373
    const serviceInterface = `rcl_interfaces/srv/${serviceType}`;
64✔
374

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

377
    const client = this.#node.createClient(serviceInterface, serviceName);
64✔
378
    this.#clients.set(serviceType, client);
64✔
379

380
    return client;
64✔
381
  }
382

383
  /**
384
   * Serialize a JavaScript value to a ParameterValue message.
385
   * @private
386
   * @param {*} value - The value to serialize.
387
   * @return {object} - The ParameterValue message.
388
   */
389
  #serializeParameterValue(value) {
390
    const type = parameterTypeFromValue(value);
11✔
391

392
    const paramValue = {
11✔
393
      type,
394
      bool_value: false,
395
      integer_value: BigInt(0),
396
      double_value: 0.0,
397
      string_value: '',
398
      byte_array_value: [],
399
      bool_array_value: [],
400
      integer_array_value: [],
401
      double_array_value: [],
402
      string_array_value: [],
403
    };
404

405
    switch (type) {
11!
406
      case ParameterType.PARAMETER_BOOL:
407
        paramValue.bool_value = value;
2✔
408
        break;
2✔
409
      case ParameterType.PARAMETER_INTEGER:
410
        paramValue.integer_value =
3✔
411
          typeof value === 'bigint' ? value : BigInt(value);
3!
412
        break;
3✔
413
      case ParameterType.PARAMETER_DOUBLE:
414
        paramValue.double_value = value;
2✔
415
        break;
2✔
416
      case ParameterType.PARAMETER_STRING:
417
        paramValue.string_value = value;
2✔
418
        break;
2✔
419
      case ParameterType.PARAMETER_BOOL_ARRAY:
420
        paramValue.bool_array_value = Array.from(value);
×
421
        break;
×
422
      case ParameterType.PARAMETER_INTEGER_ARRAY:
423
        paramValue.integer_array_value = Array.from(value).map((v) =>
×
424
          typeof v === 'bigint' ? v : BigInt(v)
×
425
        );
426
        break;
×
427
      case ParameterType.PARAMETER_DOUBLE_ARRAY:
428
        paramValue.double_array_value = Array.from(value);
×
429
        break;
×
430
      case ParameterType.PARAMETER_STRING_ARRAY:
431
        paramValue.string_array_value = Array.from(value);
×
432
        break;
×
433
      case ParameterType.PARAMETER_BYTE_ARRAY:
434
        paramValue.byte_array_value = Array.from(value).map((v) =>
2✔
435
          Math.trunc(v)
6✔
436
        );
437
        break;
2✔
438
    }
439

440
    return paramValue;
11✔
441
  }
442

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

477
  /**
478
   * Normalize a node name by removing leading slash if present.
479
   * @private
480
   * @param {string} nodeName - The node name to normalize.
481
   * @return {string} - The normalized node name.
482
   */
483
  #normalizeNodeName(nodeName) {
484
    return nodeName.startsWith('/') ? nodeName.substring(1) : nodeName;
52✔
485
  }
486

487
  /**
488
   * Convert a service type name from PascalCase to snake_case.
489
   * @private
490
   * @param {string} name - The name to convert.
491
   * @return {string} - The snake_case name.
492
   */
493
  #toSnakeCase(name) {
494
    return name.replace(/[A-Z]/g, (letter, index) => {
64✔
495
      return index === 0 ? letter.toLowerCase() : '_' + letter.toLowerCase();
130✔
496
    });
497
  }
498

499
  /**
500
   * Throws an error if the client has been destroyed.
501
   * @private
502
   * @throws {Error} If the client has been destroyed.
503
   */
504
  #throwErrorIfClientDestroyed() {
505
    if (this.#destroyed) {
119✔
506
      throw new OperationError('ParameterClient has been destroyed', {
1✔
507
        code: 'CLIENT_DESTROYED',
508
        entityType: 'parameter_client',
509
        entityName: this.#remoteNodeName,
510
      });
511
    }
512
  }
513
}
514

515
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