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

RobotWebTools / rclnodejs / 27193208698

09 Jun 2026 08:17AM UTC coverage: 91.22% (+5.7%) from 85.523%
27193208698

push

github

web-flow
Phase 2: convert lib/, index.js and tests to native ES modules (#1530)

**Overview:** 189 files changed, +957 / −1162. Converts `lib/`, `index.js`, `bin/`, `rosocket/` and the full test suite to native ES modules; keeps build/codegen tooling as `.cjs`; updates CI to ROS 2 Lyrical (Ubuntu 26.04); and fixes a native-addon debug-build compile error.

### Core ESM conversion
- **`index.js`** — converted to ESM entrypoint (`import`/`export`), module wiring reworked.
- **`lib/**`** — all library modules converted from `require`/`module.exports` to `import`/`export` (node, client, service, publisher, subscription, action/*, clock*, parameter*, logging*, runtime/*, serialization, time*, timer, qos*, utils, validator, native_loader, etc.).
- **`bin/rclnodejs-web.js`**, **`rosocket/index.js`**, **`rosocket/cli.js`** — converted to ESM.

### ESM/CJS boundary fixes (from Copilot review)
- **`lib/native_loader.js`** — load the CommonJS `bindings` helper via `createRequire`'s `require('bindings')` instead of `import bindings from 'bindings'`, preserving CJS caller context for addon resolution.
- **`lib/type_description_service.js`** — corrected sibling import from `'../lib/parameter.js'` to `'./parameter.js'`.

### CommonJS files kept as `.cjs`
- **`rosidl_gen/*.cjs`** (deallocator, idl_generator, index, primitive_types, templates/message-template), **`rostsd_gen/index.cjs`**, **`scripts/ros_distro.cjs`**, **`scripts/run_test.cjs`** — build/codegen tooling stays CJS (loads ESM helpers via supported `require(esm)` / `.default`).
- **`third_party/ref-napi/`** — `lib/ref.js` and `package.json` tweaks for module resolution.

### Tooling / config
- **`package.json`** — `"type": "module"` and script adjustments.
- **`eslint.config.mjs`** — ESM lint rules.
- **`.c8rc.json`** added, **`.nycrc.yml`** removed — switch coverage from nyc to c8.

### CI
- **`.github/workflows/linux-x64-asan-test.yml`**, **`.github/workflows/linux-x64-build-and-test.yml`** — migrated from ROS 2 ... (continued)

2044 of 2402 branches covered (85.1%)

Branch coverage included in aggregate %.

369 of 370 new or added lines in 65 files covered. (99.73%)

942 existing lines in 47 files now uncovered.

16688 of 18133 relevant lines covered (92.03%)

228.85 hits per line

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

91.34
/lib/parameter_event_handler.js
1
// Copyright (c) 2026, The Robot Web Tools Contributors
27✔
2
//
27✔
3
// Licensed under the Apache License, Version 2.0 (the "License");
27✔
4
// you may not use this file except in compliance with the License.
27✔
5
// You may obtain a copy of the License at
27✔
6
//
27✔
7
//     http://www.apache.org/licenses/LICENSE-2.0
27✔
8
//
27✔
9
// Unless required by applicable law or agreed to in writing, software
27✔
10
// distributed under the License is distributed on an "AS IS" BASIS,
27✔
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
27✔
12
// See the License for the specific language governing permissions and
27✔
13
// limitations under the License.
27✔
14

27✔
15
import { TypeValidationError, OperationError } from './errors.js';
27✔
16
import { normalizeNodeName } from './utils.js';
27✔
17
import validator from './validator.js';
27✔
18
import createDebug from 'debug';
27✔
19
const debug = createDebug('rclnodejs:parameter_event_handler');
27✔
20

27✔
21
const PARAMETER_EVENT_MSG_TYPE = 'rcl_interfaces/msg/ParameterEvent';
27✔
22
const PARAMETER_EVENT_TOPIC = '/parameter_events';
27✔
23

27✔
24
/**
27✔
25
 * @class ParameterCallbackHandle
27✔
26
 * Opaque handle returned when adding a parameter callback.
27✔
27
 * Used to remove the callback later.
27✔
28
 */
27✔
29
class ParameterCallbackHandle {
27✔
30
  /**
27✔
31
   * @param {string} parameterName - The parameter name
27✔
32
   * @param {string} nodeName - The fully qualified node name
27✔
33
   * @param {Function} callback - The callback function
27✔
34
   * @hideconstructor
27✔
35
   */
27✔
36
  constructor(parameterName, nodeName, callback) {
27✔
37
    this.parameterName = parameterName;
7✔
38
    this.nodeName = nodeName;
7✔
39
    this.callback = callback;
7✔
40
  }
7✔
41
}
27✔
42

27✔
43
/**
27✔
44
 * @class ParameterEventCallbackHandle
27✔
45
 * Opaque handle returned when adding a parameter event callback.
27✔
46
 * Used to remove the callback later.
27✔
47
 */
27✔
48
class ParameterEventCallbackHandle {
27✔
49
  /**
27✔
50
   * @param {Function} callback - The callback function
27✔
51
   * @hideconstructor
27✔
52
   */
27✔
53
  constructor(callback) {
27✔
54
    this.callback = callback;
45✔
55
  }
45✔
56
}
27✔
57

27✔
58
/**
27✔
59
 * @class ParameterEventHandler
27✔
60
 *
27✔
61
 * Monitors and responds to parameter changes on any node in the ROS 2 graph
27✔
62
 * by subscribing to the `/parameter_events` topic.
27✔
63
 *
27✔
64
 * Unlike {@link ParameterWatcher}, which is tied to a single remote node and
27✔
65
 * requires waiting for that node's parameter services, ParameterEventHandler
27✔
66
 * responds to parameter events from any node without needing service availability.
27✔
67
 *
27✔
68
 * Two types of callbacks are supported:
27✔
69
 * - **Parameter callbacks**: fired when a specific parameter on a specific node
27✔
70
 *   is added or changed (new_parameters + changed_parameters).
27✔
71
 *   Note: deleted parameters are not dispatched to parameter callbacks;
27✔
72
 *   use event callbacks to observe deletions.
27✔
73
 * - **Event callbacks**: fired for every ParameterEvent message received,
27✔
74
 *   including deletions.
27✔
75
 *
27✔
76
 * @example
27✔
77
 * const handler = node.createParameterEventHandler();
27✔
78
 *
27✔
79
 * // Watch a specific parameter on a specific node
27✔
80
 * const handle = handler.addParameterCallback(
27✔
81
 *   'my_param',
27✔
82
 *   '/my_node',
27✔
83
 *   (parameter) => {
27✔
84
 *     console.log(`Parameter changed: ${parameter.name} = ${parameter.value}`);
27✔
85
 *   }
27✔
86
 * );
27✔
87
 *
27✔
88
 * // Watch all parameter events
27✔
89
 * const eventHandle = handler.addParameterEventCallback((event) => {
27✔
90
 *   console.log(`Event from node: ${event.node}`);
27✔
91
 * });
27✔
92
 *
27✔
93
 * // Remove callbacks when done
27✔
94
 * handler.removeParameterCallback(handle);
27✔
95
 * handler.removeParameterEventCallback(eventHandle);
27✔
96
 *
27✔
97
 * // Destroy when no longer needed
27✔
98
 * handler.destroy();
27✔
99
 */
27✔
100
class ParameterEventHandler {
27✔
101
  #node;
27✔
102
  #subscription;
68✔
103
  #parameterCallbacks; // Map<string, ParameterCallbackHandle[]> keyed by "paramName\0nodeName"
68✔
104
  #eventCallbacks; // ParameterEventCallbackHandle[]
68✔
105
  #destroyed;
27✔
106

27✔
107
  /**
27✔
108
   * Create a ParameterEventHandler.
27✔
109
   *
27✔
110
   * @param {object} node - The rclnodejs Node used to create the subscription
27✔
111
   * @param {object} [options] - Options
27✔
112
   * @param {object} [options.qos] - QoS profile for the parameter_events subscription
27✔
113
   */
27✔
114
  constructor(node, options = {}) {
27✔
115
    if (!node || typeof node.createSubscription !== 'function') {
68✔
116
      throw new TypeValidationError('node', node, 'Node instance', {
2✔
117
        entityType: 'parameter event handler',
2✔
118
      });
2✔
119
    }
2✔
120

66✔
121
    if (
66✔
122
      options !== undefined &&
66✔
123
      options !== null &&
68✔
124
      typeof options !== 'object'
66✔
125
    ) {
68!
126
      throw new TypeValidationError('options', options, 'object or undefined', {
×
UNCOV
127
        entityType: 'parameter event handler',
×
UNCOV
128
      });
×
UNCOV
129
    }
×
130

66✔
131
    const opts = options || {};
68!
132

68✔
133
    this.#node = node;
68✔
134
    this.#parameterCallbacks = new Map();
68✔
135
    this.#eventCallbacks = [];
68✔
136
    this.#destroyed = false;
68✔
137

68✔
138
    const subscriptionOptions = opts.qos ? { qos: opts.qos } : undefined;
68!
139

68✔
140
    this.#subscription = node.createSubscription(
68✔
141
      PARAMETER_EVENT_MSG_TYPE,
68✔
142
      PARAMETER_EVENT_TOPIC,
68✔
143
      subscriptionOptions,
68✔
144
      (event) => this.#handleEvent(event)
68✔
145
    );
68✔
146

68✔
147
    debug('Created ParameterEventHandler on node=%s', node.name());
68✔
148
  }
68✔
149

27✔
150
  /**
27✔
151
   * Add a callback for a specific parameter on a specific node.
27✔
152
   *
27✔
153
   * The callback is invoked whenever the named parameter is added or changed
27✔
154
   * on the specified node. The callback receives the parameter message object
27✔
155
   * (rcl_interfaces/msg/Parameter) with `name` and `value` fields.
27✔
156
   *
27✔
157
   * @param {string} parameterName - Name of the parameter to monitor
27✔
158
   * @param {string} nodeName - Fully qualified name of the node (e.g., '/my_node')
27✔
159
   * @param {Function} callback - Called with (parameter) when the parameter changes
27✔
160
   * @returns {ParameterCallbackHandle} Handle for removing this callback later
27✔
161
   * @throws {Error} If the handler has been destroyed
27✔
162
   * @throws {TypeError} If arguments are invalid
27✔
163
   */
27✔
164
  addParameterCallback(parameterName, nodeName, callback) {
27✔
165
    this.#checkNotDestroyed();
11✔
166

11✔
167
    if (typeof parameterName !== 'string' || parameterName.trim() === '') {
11✔
168
      throw new TypeValidationError(
1✔
169
        'parameterName',
1✔
170
        parameterName,
1✔
171
        'non-empty string',
1✔
172
        { entityType: 'parameter event handler' }
1✔
173
      );
1✔
174
    }
1✔
175

9✔
176
    if (typeof nodeName !== 'string' || nodeName.trim() === '') {
11✔
177
      throw new TypeValidationError('nodeName', nodeName, 'non-empty string', {
1✔
178
        entityType: 'parameter event handler',
1✔
179
      });
1✔
180
    }
1✔
181

8✔
182
    if (typeof callback !== 'function') {
11✔
183
      throw new TypeValidationError('callback', callback, 'function', {
1✔
184
        entityType: 'parameter event handler',
1✔
185
      });
1✔
186
    }
1✔
187

7✔
188
    const resolvedNodeName = normalizeNodeName(nodeName);
7✔
189
    const resolvedParamName = parameterName.trim();
7✔
190
    const handle = new ParameterCallbackHandle(
7✔
191
      resolvedParamName,
7✔
192
      resolvedNodeName,
7✔
193
      callback
7✔
194
    );
7✔
195
    const key = this.#makeKey(resolvedParamName, resolvedNodeName);
7✔
196

7✔
197
    if (!this.#parameterCallbacks.has(key)) {
7✔
198
      this.#parameterCallbacks.set(key, []);
7✔
199
    }
7✔
200

7✔
201
    // Insert at front (FILO order, matching rclpy behavior)
7✔
202
    this.#parameterCallbacks.get(key).unshift(handle);
7✔
203

7✔
204
    debug(
7✔
205
      'Added parameter callback: param=%s node=%s',
7✔
206
      resolvedParamName,
7✔
207
      resolvedNodeName
7✔
208
    );
7✔
209

7✔
210
    return handle;
7✔
211
  }
11✔
212

27✔
213
  /**
27✔
214
   * Configure which node parameter events will be received.
27✔
215
   *
27✔
216
   * If nodeNames is omitted or empty, the current node filter is cleared.
27✔
217
   * When a filter is active, parameter and event callbacks only receive
27✔
218
   * events from the specified nodes.
27✔
219
   *
27✔
220
   * @param {string[]} [nodeNames] - Node names to filter parameter events from.
27✔
221
   *   Relative names are resolved against the handler node namespace.
27✔
222
   * @returns {boolean} True if the filter is active or was successfully cleared.
27✔
223
   */
27✔
224
  configureNodesFilter(nodeNames) {
27✔
225
    this.#checkNotDestroyed();
10✔
226

10✔
227
    if (nodeNames === undefined || nodeNames === null) {
10✔
228
      this.#subscription.clearContentFilter();
1✔
229
      return !this.#subscription.hasContentFilter();
1✔
230
    }
1✔
231

9✔
232
    if (!Array.isArray(nodeNames)) {
10✔
233
      throw new TypeValidationError('nodeNames', nodeNames, 'string[]', {
1✔
234
        entityType: 'parameter event handler',
1✔
235
      });
1✔
236
    }
1✔
237

8✔
238
    if (nodeNames.length === 0) {
10✔
239
      this.#subscription.clearContentFilter();
1✔
240
      return !this.#subscription.hasContentFilter();
1✔
241
    }
1✔
242

7✔
243
    const resolvedNodeNames = nodeNames.map((nodeName, index) => {
7✔
244
      if (typeof nodeName !== 'string' || nodeName.trim() === '') {
9✔
245
        throw new TypeValidationError(
2✔
246
          `nodeNames[${index}]`,
2✔
247
          nodeName,
2✔
248
          'non-empty string',
2✔
249
          {
2✔
250
            entityType: 'parameter event handler',
2✔
251
          }
2✔
252
        );
2✔
253
      }
2✔
254

7✔
255
      const resolvedNodeName = this.#resolvePath(nodeName.trim());
7✔
256
      this.#validateFullyQualifiedNodePath(resolvedNodeName);
7✔
257
      return resolvedNodeName;
7✔
258
    });
7✔
259

7✔
260
    const contentFilter = {
7✔
261
      expression: resolvedNodeNames
7✔
262
        .map((_, index) => `node = %${index}`)
7✔
263
        .join(' OR '),
7✔
264
      parameters: resolvedNodeNames.map((nodeName) => `'${nodeName}'`),
7✔
265
    };
7✔
266

7✔
267
    this.#subscription.setContentFilter(contentFilter);
7✔
268
    return this.#subscription.hasContentFilter();
7✔
269
  }
10✔
270

27✔
271
  /**
27✔
272
   * Remove a previously added parameter callback.
27✔
273
   *
27✔
274
   * @param {ParameterCallbackHandle} handle - The handle returned by addParameterCallback
27✔
275
   * @throws {Error} If the handle is not found or handler is destroyed
27✔
276
   */
27✔
277
  removeParameterCallback(handle) {
27✔
278
    this.#checkNotDestroyed();
4✔
279

4✔
280
    if (!(handle instanceof ParameterCallbackHandle)) {
4!
281
      throw new TypeValidationError(
×
UNCOV
282
        'handle',
×
UNCOV
283
        handle,
×
UNCOV
284
        'ParameterCallbackHandle',
×
UNCOV
285
        { entityType: 'parameter event handler' }
×
UNCOV
286
      );
×
UNCOV
287
    }
×
288

4✔
289
    const key = this.#makeKey(handle.parameterName, handle.nodeName);
4✔
290
    const callbacks = this.#parameterCallbacks.get(key);
4✔
291

4✔
292
    if (!callbacks) {
4✔
293
      throw new OperationError(
1✔
294
        `No callbacks registered for parameter '${handle.parameterName}' on node '${handle.nodeName}'`,
1✔
295
        { entityType: 'parameter event handler' }
1✔
296
      );
1✔
297
    }
1✔
298

3✔
299
    const index = callbacks.indexOf(handle);
3✔
300
    if (index === -1) {
4!
301
      throw new OperationError("Callback doesn't exist", {
×
UNCOV
302
        entityType: 'parameter event handler',
×
UNCOV
303
      });
×
UNCOV
304
    }
×
305

3✔
306
    callbacks.splice(index, 1);
3✔
307

3✔
308
    if (callbacks.length === 0) {
3✔
309
      this.#parameterCallbacks.delete(key);
3✔
310
    }
3✔
311

3✔
312
    debug(
3✔
313
      'Removed parameter callback: param=%s node=%s',
3✔
314
      handle.parameterName,
3✔
315
      handle.nodeName
3✔
316
    );
3✔
317
  }
4✔
318

27✔
319
  /**
27✔
320
   * Add a callback that is invoked for every parameter event.
27✔
321
   *
27✔
322
   * The callback receives the full ParameterEvent message
27✔
323
   * (rcl_interfaces/msg/ParameterEvent) with `node`, `new_parameters`,
27✔
324
   * `changed_parameters`, and `deleted_parameters` fields.
27✔
325
   *
27✔
326
   * @param {Function} callback - Called with (event) for every ParameterEvent
27✔
327
   * @returns {ParameterEventCallbackHandle} Handle for removing this callback later
27✔
328
   * @throws {Error} If the handler has been destroyed
27✔
329
   * @throws {TypeError} If callback is not a function
27✔
330
   */
27✔
331
  addParameterEventCallback(callback) {
27✔
332
    this.#checkNotDestroyed();
47✔
333

47✔
334
    if (typeof callback !== 'function') {
47✔
335
      throw new TypeValidationError('callback', callback, 'function', {
1✔
336
        entityType: 'parameter event handler',
1✔
337
      });
1✔
338
    }
1✔
339

45✔
340
    const handle = new ParameterEventCallbackHandle(callback);
45✔
341

45✔
342
    // Insert at front (FILO order)
45✔
343
    this.#eventCallbacks.unshift(handle);
45✔
344

45✔
345
    debug('Added parameter event callback');
45✔
346

45✔
347
    return handle;
45✔
348
  }
47✔
349

27✔
350
  /**
27✔
351
   * Remove a previously added parameter event callback.
27✔
352
   *
27✔
353
   * @param {ParameterEventCallbackHandle} handle - The handle returned by addParameterEventCallback
27✔
354
   * @throws {Error} If the handle is not found or handler is destroyed
27✔
355
   */
27✔
356
  removeParameterEventCallback(handle) {
27✔
357
    this.#checkNotDestroyed();
3✔
358

3✔
359
    if (!(handle instanceof ParameterEventCallbackHandle)) {
3!
360
      throw new TypeValidationError(
×
UNCOV
361
        'handle',
×
UNCOV
362
        handle,
×
UNCOV
363
        'ParameterEventCallbackHandle',
×
UNCOV
364
        { entityType: 'parameter event handler' }
×
UNCOV
365
      );
×
UNCOV
366
    }
×
367

3✔
368
    const index = this.#eventCallbacks.indexOf(handle);
3✔
369
    if (index === -1) {
3✔
370
      throw new OperationError("Callback doesn't exist", {
1✔
371
        entityType: 'parameter event handler',
1✔
372
      });
1✔
373
    }
1✔
374

2✔
375
    this.#eventCallbacks.splice(index, 1);
2✔
376

2✔
377
    debug('Removed parameter event callback');
2✔
378
  }
3✔
379

27✔
380
  /**
27✔
381
   * Check if the handler has been destroyed.
27✔
382
   *
27✔
383
   * @returns {boolean} True if destroyed
27✔
384
   */
27✔
385
  isDestroyed() {
27✔
386
    return this.#destroyed;
29✔
387
  }
29✔
388

27✔
389
  /**
27✔
390
   * Destroy the handler and clean up resources.
27✔
391
   * Removes the subscription and clears all callbacks.
27✔
392
   */
27✔
393
  destroy() {
27✔
394
    if (this.#destroyed) {
84✔
395
      return;
18✔
396
    }
18✔
397

66✔
398
    debug('Destroying ParameterEventHandler');
66✔
399

66✔
400
    if (this.#subscription) {
66✔
401
      try {
66✔
402
        this.#node.destroySubscription(this.#subscription);
66✔
403
      } catch (error) {
66!
404
        debug('Error destroying subscription: %s', error.message);
×
UNCOV
405
      }
×
406
      this.#subscription = null;
66✔
407
    }
66✔
408

66✔
409
    this.#parameterCallbacks.clear();
66✔
410
    this.#eventCallbacks.length = 0;
66✔
411
    this.#destroyed = true;
66✔
412
  }
84✔
413

27✔
414
  /**
27✔
415
   * Get a specific parameter from a ParameterEvent message.
27✔
416
   *
27✔
417
   * @param {object} event - A ParameterEvent message
27✔
418
   * @param {string} parameterName - The parameter name to look for
27✔
419
   * @param {string} nodeName - The node name to match
27✔
420
   * @returns {object|null} The matching parameter message, or null
27✔
421
   * @static
27✔
422
   */
27✔
423
  static getParameterFromEvent(event, parameterName, nodeName) {
27✔
424
    const resolvedNodeName = normalizeNodeName(nodeName);
2✔
425
    const resolvedParamName = (parameterName || '').trim();
2!
426

2✔
427
    if (normalizeNodeName(event.node) !== resolvedNodeName) {
2✔
428
      return null;
1✔
429
    }
1✔
430

1✔
431
    const allParams = [
1✔
432
      ...(event.new_parameters || []),
2!
433
      ...(event.changed_parameters || []),
2!
434
    ];
2✔
435

2✔
436
    for (const param of allParams) {
2✔
437
      if (param.name === resolvedParamName) {
2✔
438
        return param;
1✔
439
      }
1✔
440
    }
2✔
UNCOV
441

×
442
    return null;
×
443
  }
2✔
444

27✔
445
  /**
27✔
446
   * Get all parameters from a ParameterEvent message (new + changed).
27✔
447
   *
27✔
448
   * @param {object} event - A ParameterEvent message
27✔
449
   * @returns {object[]} Array of parameter messages
27✔
450
   * @static
27✔
451
   */
27✔
452
  static getParametersFromEvent(event) {
27✔
453
    return [
1✔
454
      ...(event.new_parameters || []),
1!
455
      ...(event.changed_parameters || []),
1!
456
    ];
1✔
457
  }
1✔
458

27✔
459
  /**
27✔
460
   * Handle incoming parameter event.
27✔
461
   * @private
27✔
462
   */
27✔
463
  #handleEvent(event) {
27✔
464
    const eventNodeName = normalizeNodeName(event.node);
18✔
465

18✔
466
    // Dispatch parameter-specific callbacks by iterating event params
18✔
467
    // and doing direct Map lookups (O(event_params) instead of O(registered_callbacks))
18✔
468
    const allParams = [
18✔
469
      ...(event.new_parameters || []),
18!
470
      ...(event.changed_parameters || []),
18!
471
    ];
18✔
472

18✔
473
    for (const parameter of allParams) {
18✔
474
      const key = this.#makeKey(parameter.name, eventNodeName);
18✔
475
      const callbacks = this.#parameterCallbacks.get(key);
18✔
476

18✔
477
      if (callbacks) {
18✔
478
        for (const handle of callbacks.slice()) {
1✔
479
          try {
1✔
480
            handle.callback(parameter);
1✔
481
          } catch (err) {
1!
482
            debug(
×
UNCOV
483
              'Error in parameter callback for %s on %s: %s',
×
UNCOV
484
              parameter.name,
×
UNCOV
485
              eventNodeName,
×
UNCOV
486
              err.message
×
UNCOV
487
            );
×
UNCOV
488
          }
×
489
        }
1✔
490
      }
1✔
491
    }
18✔
492

18✔
493
    // Dispatch event-level callbacks
18✔
494
    for (const handle of this.#eventCallbacks.slice()) {
18✔
495
      try {
14✔
496
        handle.callback(event);
14✔
497
      } catch (err) {
14!
498
        debug('Error in parameter event callback: %s', err.message);
×
UNCOV
499
      }
×
500
    }
14✔
501
  }
18✔
502

27✔
503
  /**
27✔
504
   * Create a map key from parameter name and node name.
27✔
505
   * @private
27✔
506
   */
27✔
507
  #makeKey(paramName, nodeName) {
27✔
508
    return `${paramName}\0${nodeName}`;
29✔
509
  }
29✔
510

27✔
511
  /**
27✔
512
   * Resolve a node path to the fully qualified name used in ParameterEvent.node.
27✔
513
   * @private
27✔
514
   */
27✔
515
  #resolvePath(nodePath) {
27✔
516
    // Absolute node paths are already rooted. Relative names are resolved
7✔
517
    // against the handler node namespace before building the content filter.
7✔
518
    const unresolvedPath = nodePath.startsWith('/')
7✔
519
      ? nodePath
7✔
520
      : `${this.#node.namespace().replace(/\/+$/, '')}/${nodePath}`;
7✔
521

7✔
522
    // Collapse repeated separators for inputs like '/ns//node/' or 'nested//node'.
7✔
523
    const resolvedPath = unresolvedPath.replace(/\/+/g, '/');
7✔
524

7✔
525
    // Preserve the root namespace as '/' and strip trailing slashes everywhere
7✔
526
    // else so the filter matches the canonical ParameterEvent.node format.
7✔
527
    if (resolvedPath === '/') {
7!
528
      return resolvedPath;
×
UNCOV
529
    }
×
530

7✔
531
    return resolvedPath.replace(/\/+$/, '');
7✔
532
  }
7✔
533

27✔
534
  /**
27✔
535
   * Validate a fully qualified node path before using it in a content filter.
27✔
536
   * @private
27✔
537
   */
27✔
538
  #validateFullyQualifiedNodePath(nodePath) {
27✔
539
    const normalizedPath =
7✔
540
      nodePath.length > 1 ? nodePath.replace(/\/+$/, '') : nodePath;
7!
541
    const separatorIndex = normalizedPath.lastIndexOf('/');
7✔
542
    const nodeNamespace =
7✔
543
      separatorIndex === 0 ? '/' : normalizedPath.slice(0, separatorIndex);
7✔
544
    const nodeName = normalizedPath.slice(separatorIndex + 1);
7✔
545

7✔
546
    validator.validateNamespace(nodeNamespace);
7✔
547
    validator.validateNodeName(nodeName);
7✔
548
  }
7✔
549

27✔
550
  /**
27✔
551
   * Check if the handler has been destroyed and throw if so.
27✔
552
   * @private
27✔
553
   */
27✔
554
  #checkNotDestroyed() {
27✔
555
    if (this.#destroyed) {
75✔
556
      throw new OperationError('ParameterEventHandler has been destroyed', {
2✔
557
        entityType: 'parameter event handler',
2✔
558
      });
2✔
559
    }
2✔
560
  }
75✔
561
}
27✔
562

27✔
563
export default ParameterEventHandler;
27✔
564
export { ParameterCallbackHandle, ParameterEventCallbackHandle };
27✔
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