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

RobotWebTools / rclnodejs / 22012287134

14 Feb 2026 05:53AM UTC coverage: 85.484% (-0.04%) from 85.524%
22012287134

push

github

minggangw
Support hyphenated ROSIDL subdirs (#1390)

This PR adds support for ROSIDL packages with hyphenated subdirectory names (e.g., `msg-common`, `msg-ros2`) by updating the subfolder extraction logic to normalize these to their base interface types (`msg`, `srv`, `action`).

**Changes:**
- Modified `getSubFolder()` to extract base interface type from hyphenated subdirectory names using `split('-')[0]`
- Added comprehensive test case using `mrpt_msgs` package to validate hyphenated subfolder support
- Refactored test setup in `test-native-loader.js` to avoid repeated module reloading
- Updated CI workflows to install `mrpt-msgs` package for testing
- Simplified process exit handling in test runner script

Fix: #1388

1380 of 1755 branches covered (78.63%)

Branch coverage included in aggregate %.

2860 of 3205 relevant lines covered (89.24%)

466.01 hits per line

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

84.21
/lib/lifecycle.js
1
// Copyright (c) 2020 Wayne Parrott. 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 LifecyclePublisher = require('./lifecycle_publisher.js');
26✔
19
const loader = require('./interface_loader.js');
26✔
20
const Context = require('./context.js');
26✔
21
const Node = require('./node.js');
26✔
22
const NodeOptions = require('./node_options.js');
26✔
23
const Service = require('./service.js');
26✔
24
const { ValidationError } = require('./errors.js');
26✔
25

26
const SHUTDOWN_TRANSITION_LABEL =
27
  rclnodejs.getLifecycleShutdownTransitionLabel();
26✔
28

29
// An instance of State message constructor used for accessing State
30
// state machine constants. This interface is lazy initialized at runtime.
31
let StateInterface;
32

33
// An instance of Transition message constructor used for accessing Transition
34
// state machine constants. This interface is lazy initialized at runtime.
35
let TransitionInterface;
36

37
function getStateInterface() {
38
  if (!StateInterface) {
8✔
39
    StateInterface = loader.loadInterface('lifecycle_msgs/msg/State');
1✔
40
  }
41
  return StateInterface;
8✔
42
}
43

44
function getTransitionInterface() {
45
  if (!TransitionInterface) {
51✔
46
    TransitionInterface = loader.loadInterface('lifecycle_msgs/msg/Transition');
1✔
47
  }
48
  return TransitionInterface;
51✔
49
}
50

51
/**
52
 * @typedef SerializedState
53
 * @type {object}
54
 * @property {number} id - code identifying the type of this state.
55
 * @property {string} label - readable name of this state.
56
 */
57

58
/**
59
 * The state of the lifecycle state model.
60
 */
61
class State {
62
  /**
63
   * Create a state.
64
   * @param {number} id - The id value.
65
   * @param {string} label - The label value.
66
   */
67
  constructor(id, label) {
68
    this.id = id;
92✔
69
    this.label = label;
92✔
70
  }
71

72
  /**
73
   * Create a State from a SerializedState
74
   * @param {SerializedState} aSerializedState - The state object.
75
   * @returns {State} The State converted from a SerializdState
76
   * @private
77
   */
78
  static fromSerializedState(aSerializedState) {
79
    return new State(aSerializedState.id, aSerializedState.label);
55✔
80
  }
81
}
82

83
/**
84
 * The intermediate state of the lifecycle state model during a state
85
 * transition.
86
 */
87
class Transition extends State {}
88

89
/**
90
 * Describes a state transition.
91
 */
92
class TransitionDescription {
93
  /**
94
   * Create a transition description.
95
   *
96
   * @param {Transition} transition - The transition
97
   * @param {State} startState - The initial
98
   * @param {State} goalState - The target state of a transition activity
99
   */
100
  constructor(transition, startState, goalState) {
101
    this.transition = transition;
5✔
102
    this.startState = startState;
5✔
103
    this.goalState = goalState;
5✔
104
  }
105
}
106

107
/**
108
 * The values returned by TransitionCallback.
109
 * @readonly
110
 * @enum {number}
111
 */
112
const CallbackReturnCode = {
26✔
113
  get SUCCESS() {
114
    return getTransitionInterface().TRANSITION_CALLBACK_SUCCESS;
23✔
115
  },
116
  get FAILURE() {
117
    return getTransitionInterface().TRANSITION_CALLBACK_FAILURE;
×
118
  },
119
  get ERROR() {
120
    return getTransitionInterface().TRANSITION_CALLBACK_ERROR;
16✔
121
  },
122
};
123
Object.freeze(CallbackReturnCode);
26✔
124

125
/**
126
 * A ValueHolder for a CallbackReturnCode.
127
 */
128
class CallbackReturnValue {
129
  /**
130
   * Creates a new instance.
131
   *
132
   * @param {number} [value=CallbackReturnCode.SUCCESS] - The value property.
133
   */
134
  constructor(value = CallbackReturnCode.SUCCESS) {
1✔
135
    this._value = value;
1✔
136
    this._errorMsg = null;
1✔
137
  }
138

139
  /**
140
   * Access the callbackReturnCode.
141
   * @returns {number} The CallbackReturnCode.
142
   */
143
  get value() {
144
    return this._value;
1✔
145
  }
146

147
  set value(value) {
148
    this._value = value;
1✔
149
  }
150

151
  /**
152
   * Access an optional error message when value is not SUCCESS.
153
   */
154
  get errorMsg() {
155
    return this._errorMsg;
×
156
  }
157

158
  /**
159
   * Assign the error message.
160
   * @param {string} msg - The error message.
161
   * @returns {unknown} void.
162
   */
163
  set errorMsg(msg) {
164
    this._errorMsg = msg;
×
165
  }
166

167
  /**
168
   * Overrides Object.valueOf() to return the 'value' property.
169
   * @returns {number} The property value.
170
   */
171
  valueOf() {
172
    return this.value;
×
173
  }
174

175
  /**
176
   * A predicate to test if the value is SUCCESS.
177
   * @returns {boolean} Return true if the value is SUCCESS; otherwise return false.
178
   */
179
  isSuccess() {
180
    return this.value === CallbackReturnCode.SUCCESS;
1✔
181
  }
182

183
  /**
184
   * A predicate to test if the value is FAILURE.
185
   * @returns {boolean} Return true if the value is FAILURE; otherwise return false.
186
   */
187
  isFailure() {
188
    return this.value === CallbackReturnCode.FAILURE;
×
189
  }
190

191
  /**
192
   * A predicate to test if the value is ERROR.
193
   * @returns {boolean} Return true if the value is ERROR; otherwise return false.
194
   */
195
  isError() {
196
    return this.value === CallbackReturnCode.ERROR;
×
197
  }
198

199
  /**
200
   * A predicate to test if an error message has been assigned.
201
   * @returns {boolean} Return true if an error message has been assigned; otherwise return false.
202
   */
203
  hasErrorMsg() {
204
    return !this.isSuccess() && this._errorMsg;
×
205
  }
206
}
207

208
/**
209
 * This callback is invoked when LifecycleNode is transitioning to a new state.
210
 * @callback TransitionCallback
211
 * @param {State} previousState - The previous node lifecycle state.
212
 * @return {CallbackReturnCode} - The result of the callback.
213
 *
214
 * @see [LifecycleNode.registerOnConfigure]{@link LifecycleNode#registerOnConfigure}
215
 * @see [LifecycleNode.registerOnCleanup]{@link LifecycleNode#registerOnCleanup}
216
 * @see [LifecycleNode.registerOnActivate]{@link LifecycleNode#registerOnActivate}
217
 * @see [LifecycleNode.registerOnDeactivate]{@link LifecycleNode#registerOnDeactivate}
218
 * @see [LifecycleNode.registerOnShutdown]{@link LifecycleNode#registerOnShutdown}
219
 * @see [LifecycleNode.registerOnError]{@link LifecycleNode#registerOnError}
220
 */
221

222
/**
223
 * A ROS2 managed Node that implements a well-defined life-cycle state model using the
224
 * {@link https://github.com/ros2/rcl/tree/master/rcl_lifecycle|ros2 client library (rcl) lifecyle api}.
225
 * @extends Node
226
 *
227
 * This class implments the ROS2 life-cycle state-machine defined by the
228
 * {@link https://github.com/ros2/rclcpp/tree/master/rclcpp_lifecycle}|ROS2 Managed Nodes Design}
229
 * and parallels the {@link https://github.com/ros2/rclcpp/tree/master/rclcpp_lifecycle|rclcpp lifecycle node }
230
 * implementation.
231
 *
232
 * The design consists of four primary lifecycle states:
233
 *   UNCONFIGURED
234
 *   INACTIVE
235
 *   ACTIVE
236
 *   FINALIZED.
237
 *
238
 * Transitioning between states is accomplished using an action api:
239
 *   configure()
240
 *   activate()
241
 *   deactivate(),
242
 *   cleanup()
243
 *   shutdown()
244
 *
245
 * During a state transition, the state-machine is in one of the
246
 * intermediate transitioning states:
247
 *   CONFIGURING
248
 *   ACTIVATING
249
 *   DEACTIVATING
250
 *   CLEANINGUP
251
 *   SHUTTING_DOWN
252
 *   ERROR_PROCESSING
253
 *
254
 * Messaging:
255
 * State changes are published on the '<node_name>/transition_event' topic.
256
 * Lifecycle service interfaces are also implemented.
257
 *
258
 * You can introduce your own state specific behaviors in the form of a
259
 * {@link TransitionCallback} functions that you register using:
260
 *   registerOnConfigure(cb)
261
 *   registerOnActivate(cb)
262
 *   registerOnDeactivate(cb)
263
 *   registerOnCleanup(cb)
264
 *   registerOnShutdown(cb)
265
 *   registerOnError(cb)
266
 */
267

268
class LifecycleNode extends Node {
269
  /**
270
   * Create a managed Node that implements a well-defined life-cycle state
271
   * model using the {@link https://github.com/ros2/rcl/tree/master/rcl_lifecycle|ros2 client library (rcl) lifecyle api}.
272
   * @param {string} nodeName - The name used to register in ROS.
273
   * @param {string} [namespace=''] - The namespace used in ROS.
274
   * @param {Context} [context=Context.defaultContext()] - The context to create the node in.
275
   * @param {NodeOptions} [options=NodeOptions.defaultOptions] - The options to configure the new node behavior.
276
   * @param {boolean} [enableCommunicationsInterface=true] - Enable lifecycle service interfaces, e.g., GetState.
277
   * @throws {Error} If the given context is not registered.
278
   */
279
  constructor(
280
    nodeName,
281
    namespace = '',
1✔
282
    context = Context.defaultContext(),
1✔
283
    options = NodeOptions.defaultOptions,
1✔
284
    enableCommunicationInterface = true
×
285
  ) {
286
    super(nodeName, namespace, context, options);
19✔
287
    this.init(enableCommunicationInterface);
19✔
288
  }
289

290
  init(enableCommunicationInterface) {
291
    // initialize native handle to rcl_lifecycle_state_machine
292
    this._stateMachineHandle = rclnodejs.createLifecycleStateMachine(
19✔
293
      this.handle,
294
      enableCommunicationInterface,
295
      this._clock.handle
296
    );
297

298
    // initialize Map<transitionId,TransitionCallback>
299
    this._callbackMap = new Map();
19✔
300

301
    if (!enableCommunicationInterface) {
19✔
302
      // do not create lifecycle services
303
      return;
1✔
304
    }
305

306
    // Setup and register the 4 native rcl lifecycle services thar are
307
    // part of _stateMachineHandle.
308
    let srvHandleObj = rclnodejs.getLifecycleSrvNameAndHandle(
18✔
309
      this._stateMachineHandle,
310
      'srv_get_state'
311
    );
312
    let service = new Service(
18✔
313
      this.handle,
314
      srvHandleObj.handle,
315
      srvHandleObj.name,
316
      loader.loadInterface('lifecycle_msgs/srv/GetState'),
317
      this._validateOptions(undefined),
318
      (request, response) => this._onGetState(request, response)
1✔
319
    );
320
    this._services.push(service);
18✔
321

322
    srvHandleObj = rclnodejs.getLifecycleSrvNameAndHandle(
18✔
323
      this._stateMachineHandle,
324
      'srv_get_available_states'
325
    );
326
    service = new Service(
18✔
327
      this.handle,
328
      srvHandleObj.handle,
329
      srvHandleObj.name,
330
      loader.loadInterface('lifecycle_msgs/srv/GetAvailableStates'),
331
      this._validateOptions(undefined),
332
      (request, response) => this._onGetAvailableStates(request, response)
1✔
333
    );
334
    this._services.push(service);
18✔
335

336
    srvHandleObj = rclnodejs.getLifecycleSrvNameAndHandle(
18✔
337
      this._stateMachineHandle,
338
      'srv_get_available_transitions'
339
    );
340
    service = new Service(
18✔
341
      this.handle,
342
      srvHandleObj.handle,
343
      srvHandleObj.name,
344
      loader.loadInterface('lifecycle_msgs/srv/GetAvailableTransitions'),
345
      this._validateOptions(undefined),
346
      (request, response) => this._onGetAvailableTransitions(request, response)
1✔
347
    );
348
    this._services.push(service);
18✔
349

350
    srvHandleObj = rclnodejs.getLifecycleSrvNameAndHandle(
18✔
351
      this._stateMachineHandle,
352
      'srv_change_state'
353
    );
354
    service = new Service(
18✔
355
      this.handle,
356
      srvHandleObj.handle,
357
      srvHandleObj.name,
358
      loader.loadInterface('lifecycle_msgs/srv/ChangeState'),
359
      this._validateOptions(undefined),
360
      (request, response) => this._onChangeState(request, response)
1✔
361
    );
362
    this._services.push(service);
18✔
363
    this.syncHandles();
18✔
364
  }
365

366
  /**
367
   * Create a LifecyclePublisher.
368
   * @param {function|string|object} typeClass - The ROS message class,
369
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
370
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
371
   * @param {string} topic - The name of the topic.
372
   * @param {object} options - The options argument used to parameterize the publisher.
373
   * @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
374
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the publisher, default: QoS.profileDefault.
375
   * @return {LifecyclePublisher} - An instance of LifecyclePublisher.
376
   */
377
  createLifecyclePublisher(typeClass, topic, options) {
378
    return this._createPublisher(typeClass, topic, options, LifecyclePublisher);
3✔
379
  }
380

381
  /**
382
   * Access the current lifecycle state.
383
   * @returns {State} The current state.
384
   */
385
  get currentState() {
386
    let currentStateObj = rclnodejs.getCurrentLifecycleState(
24✔
387
      this._stateMachineHandle
388
    );
389
    return State.fromSerializedState(currentStateObj);
24✔
390
  }
391

392
  /**
393
   * Retrieve all registered states of the state-machine.
394
   * @returns {State[]} The states.
395
   */
396
  get availableStates() {
397
    let stateObjs = rclnodejs.getLifecycleStates(this._stateMachineHandle);
2✔
398
    let states = stateObjs.map(
2✔
399
      (stateObj) => new State(stateObj.id, stateObj.label)
22✔
400
    );
401
    return states;
2✔
402
  }
403

404
  /**
405
   * Retrieve all registered transitions of the state-machine.
406
   *
407
   * @returns {TransitionDescription[]} The registered TransitionDescriptions.
408
   */
409
  get transitions() {
410
    let transitionObjs = rclnodejs.getLifecycleTransitions(
×
411
      this._stateMachineHandle
412
    );
413
    let transitions = transitionObjs.map((transitionDescObj) => {
×
414
      let transition = new Transition(
×
415
        transitionDescObj.transition.id,
416
        transitionDescObj.transition.label
417
      );
418
      let startState = new State(
×
419
        transitionDescObj.start_state.id,
420
        transitionDescObj.start_state.label
421
      );
422
      let goalState = new State(
×
423
        transitionDescObj.goal_state.id,
424
        transitionDescObj.goal_state.label
425
      );
426
      return new TransitionDescription(transition, startState, goalState);
×
427
    });
428
    return transitions;
×
429
  }
430

431
  /**
432
   * Retrieve the valid transitions available from the current state of the
433
   * state-machine.
434
   *
435
   * @returns {TransitionDescription[]} The available TransitionDescriptions.
436
   */
437
  get availableTransitions() {
438
    let transitionObjs = rclnodejs.getAvailableLifecycleTransitions(
2✔
439
      this._stateMachineHandle
440
    );
441
    let transitions = transitionObjs.map((transitionDescObj) => {
2✔
442
      let transition = new Transition(
5✔
443
        transitionDescObj.transition.id,
444
        transitionDescObj.transition.label
445
      );
446
      let startState = new State(
5✔
447
        transitionDescObj.start_state.id,
448
        transitionDescObj.start_state.label
449
      );
450
      let goalState = new State(
5✔
451
        transitionDescObj.goal_state.id,
452
        transitionDescObj.goal_state.label
453
      );
454
      return new TransitionDescription(transition, startState, goalState);
5✔
455
    });
456
    return transitions;
2✔
457
  }
458

459
  /**
460
   * Register a callback function to be invoked during the configure() action.
461
   * @param {TransitionCallback} cb - The callback function to invoke.
462
   * @returns {unknown} void.
463
   */
464
  registerOnConfigure(cb) {
465
    this._callbackMap.set(getStateInterface().TRANSITION_STATE_CONFIGURING, cb);
2✔
466
  }
467

468
  /**
469
   * Register a callback function to be invoked during the activate() action.
470
   * @param {TransitionCallback} cb - The callback function to invoke.
471
   * @returns {unknown} void.
472
   */
473
  registerOnActivate(cb) {
474
    this._callbackMap.set(getStateInterface().TRANSITION_STATE_ACTIVATING, cb);
1✔
475
  }
476

477
  /**
478
   * Register a callback function to be invoked during the deactivate() action.
479
   * @param {TransitionCallback} cb - The callback function to invoke.
480
   * @returns {unknown} void.
481
   */
482
  registerOnDeactivate(cb) {
483
    this._callbackMap.set(
1✔
484
      getStateInterface().TRANSITION_STATE_DEACTIVATING,
485
      cb
486
    );
487
  }
488

489
  /**
490
   * Register a callback function to be invoked during the cleanup() action.
491
   * @param {TransitionCallback} cb - The callback function to invoke.
492
   * @returns {unknown} void.
493
   */
494
  registerOnCleanup(cb) {
495
    this._callbackMap.set(getStateInterface().TRANSITION_STATE_CLEANINGUP, cb);
1✔
496
  }
497

498
  /**
499
   * Register a callback function to be invoked during the shutdown() action.
500
   * @param {TransitionCallback} cb - The callback function to invoke.
501
   * @returns {unknown} void
502
   */
503
  registerOnShutdown(cb) {
504
    this._callbackMap.set(
1✔
505
      getStateInterface().TRANSITION_STATE_SHUTTINGDOWN,
506
      cb
507
    );
508
  }
509

510
  /**
511
   * Register a callback function to be invoked when an error occurs during a
512
   * state transition.
513
   * @param {TransitionCallback} cb - The callback function to invoke.
514
   * @returns {unknown} void.
515
   */
516
  registerOnError(cb) {
517
    this._callbackMap.set(
2✔
518
      getStateInterface().TRANSITION_STATE_ERRORPROCESSING,
519
      cb
520
    );
521
  }
522

523
  /**
524
   * Initiate a transition from the UNCONFIGURED state to the INACTIVE state.
525
   * If an onConfigure callback has been registered it will be invoked.
526
   *
527
   * @param {CallbackReturnValue?} callbackReturnValue - value holder for the CallbackReturnCode returned from the callback.
528
   * @returns {State} The new state, should be INACTIVE.
529
   * @throws {Error} If transition is invalid for the current state.
530
   */
531
  configure(callbackReturnValue) {
532
    return this._changeState(
6✔
533
      getTransitionInterface().TRANSITION_CONFIGURE,
534
      callbackReturnValue
535
    );
536
  }
537

538
  /**
539
   * Initiate a transition from the INACTIVE state to the ACTIVE state.
540
   * If an onActivate callback has been registered it will be invoked.
541
   *
542
   * @param {CallbackReturnValue?} callbackReturnValue - value holder for the CallbackReturnCode returned from the callback.
543
   * @returns {State} The new state, should be ACTIVE.
544
   * @throws {Error} If transition is invalid for the current state.
545
   */
546
  activate(callbackReturnValue) {
547
    return this._changeState(
3✔
548
      getTransitionInterface().TRANSITION_ACTIVATE,
549
      callbackReturnValue
550
    );
551
  }
552

553
  /**
554
   * Initiate a transition from the ACTIVE state to the INACTIVE state.
555
   * If an onDeactivate callback has been registered it will be invoked.
556
   *
557
   * @param {CallbackReturnValue?} callbackReturnValue - value holder for the CallbackReturnCode returned from the callback.
558
   * @returns {State} The new state, should be INACTIVE.
559
   * @throws {Error} If transition is invalid for the current state.
560
   */
561
  deactivate(callbackReturnValue) {
562
    return this._changeState(
2✔
563
      getTransitionInterface().TRANSITION_DEACTIVATE,
564
      callbackReturnValue
565
    );
566
  }
567

568
  /**
569
   * Initiate a transition from the INACTIVE state to the UNCONFIGURED state.
570
   * If an onCleanup callback has been registered it will be invoked.
571
   *
572
   * @param {CallbackReturnValue?} callbackReturnValue - value holder for the CallbackReturnCode returned from the callback.
573
   * @returns {State} The new state, should be INACTIVE.
574
   * @throws {Error} If transition is invalid for the current state.
575
   */
576
  cleanup(callbackReturnValue) {
577
    return this._changeState(
2✔
578
      getTransitionInterface().TRANSITION_CLEANUP,
579
      callbackReturnValue
580
    );
581
  }
582

583
  /**
584
   * Initiate a transition to the FINALIZED state from any of the following
585
   * states: UNCONFIGURED, INACTIVE or ACTIVE state. If an onConfigure
586
   * callback has been registered it will be invoked.
587
   *
588
   * @param {CallbackReturnValue?} callbackReturnValue - value holder for the CallbackReturnCode returned from the callback.
589
   * @returns {State} The new state, should be FINALIZED.
590
   * @throws {Error} If transition is invalid for the current state.
591
   */
592
  shutdown(callbackReturnValue) {
593
    return this._changeState(SHUTDOWN_TRANSITION_LABEL, callbackReturnValue);
2✔
594
  }
595

596
  /**
597
   * Check if state machine is initialized.
598
   *
599
   * @returns {boolean} true if the state machine is initialized; otherwise false.
600
   */
601
  get isInitialized() {
602
    return rclnodejs.isInitialized(this._stateMachineHandle);
1✔
603
  }
604

605
  /**
606
   * Log the state machine data.
607
   *
608
   * @returns {undefined} void.
609
   */
610
  print() {
611
    rclnodejs.print(this._stateMachineHandle);
1✔
612
  }
613

614
  /**
615
   * The GetState service handler.
616
   * @param {Object} request - The GetState service request.
617
   * @param {Object} response - The GetState service response.
618
   * @returns {unknown} void.
619
   * @private
620
   */
621
  _onGetState(request, response) {
622
    let result = response.template;
1✔
623

624
    result.current_state = this.currentState;
1✔
625

626
    response.send(result);
1✔
627
  }
628

629
  /**
630
   * The GetAvailableStates service handler.
631
   * @param {Object} request - The GetAvailableStates service request.
632
   * @param {Object} response - The GetAvailableStates service response.
633
   * @returns {unknown} void.
634
   * @private
635
   */
636
  _onGetAvailableStates(request, response) {
637
    let result = response.template;
1✔
638

639
    result.available_states = this.availableStates;
1✔
640

641
    response.send(result);
1✔
642
  }
643

644
  /**
645
   * The GetAvailableTransitions service handler.
646
   * @param {Object} request - The GetAvailableTransitions service request
647
   * @param {Object} response - The GetAvailableTranactions service response.
648
   * @returns {unknown} void.
649
   */
650
  _onGetAvailableTransitions(request, response) {
651
    let result = response.template;
1✔
652

653
    result.available_transitions = this.availableTransitions;
1✔
654

655
    response.send(result);
1✔
656
  }
657

658
  /**
659
   * The ChangeState service handler.
660
   * @param {Object} request - The ChangeState service request.
661
   * @param {Object} response - The ChangeState service response
662
   * @returns {unknown} void.
663
   * @private
664
   */
665
  _onChangeState(request, response) {
666
    let result = response.template;
1✔
667

668
    let transitionId = request.transition.id;
1✔
669
    if (request.transition.label) {
1!
670
      let transitionObj = rclnodejs.getLifecycleTransitionByLabel(
1✔
671
        this._stateMachineHandle,
672
        request.transition.label
673
      );
674
      if (transitionObj.id) {
1!
675
        transitionId = transitionObj.id;
1✔
676
      } else {
677
        result.success = false;
×
678
        response.send(result);
×
679
        return;
×
680
      }
681
    }
682

683
    let callbackReturnValue = new CallbackReturnValue();
1✔
684
    this._changeState(transitionId, callbackReturnValue);
1✔
685

686
    result.success = callbackReturnValue.isSuccess();
1✔
687
    response.send(result);
1✔
688
  }
689

690
  /**
691
   * Transition to a new lifecycle state.
692
   * @param {number|string} transitionIdOrLabel - The id or label of the target transition.
693
   * @param {CallbackReturnValue} callbackReturnValue - An out parameter that holds the CallbackReturnCode.
694
   * @returns {State} The new state.
695
   * @throws {Error} If transition is invalid for the current state.
696
   * @private
697
   */
698
  _changeState(transitionIdOrLabel, callbackReturnValue) {
699
    let initialState = this.currentState;
15✔
700
    let newStateObj =
701
      typeof transitionIdOrLabel === 'number'
15✔
702
        ? rclnodejs.triggerLifecycleTransitionById(
703
            this._stateMachineHandle,
704
            transitionIdOrLabel
705
          )
706
        : rclnodejs.triggerLifecycleTransitionByLabel(
707
            this._stateMachineHandle,
708
            transitionIdOrLabel
709
          );
710

711
    if (!newStateObj) {
15!
712
      throw new ValidationError(
×
713
        `No transition available from state ${transitionIdOrLabel}`,
714
        {
715
          code: 'INVALID_TRANSITION',
716
          entityType: 'lifecycle node',
717
          details: { transitionIdOrLabel: transitionIdOrLabel },
718
        }
719
      );
720
    }
721

722
    let newState = State.fromSerializedState(newStateObj);
15✔
723

724
    let cbResult = this._executeCallback(newState, initialState);
15✔
725
    if (callbackReturnValue) callbackReturnValue.value = cbResult;
15✔
726

727
    let transitioningLabel = this._transitionId2Label(cbResult);
15✔
728
    newState = State.fromSerializedState(
15✔
729
      rclnodejs.triggerLifecycleTransitionByLabel(
730
        this._stateMachineHandle,
731
        transitioningLabel
732
      )
733
    );
734

735
    if (cbResult == CallbackReturnCode.ERROR) {
15✔
736
      cbResult = this._executeCallback(this.currentState, initialState);
1✔
737
      if (callbackReturnValue) callbackReturnValue.value = cbResult;
1!
738

739
      transitioningLabel = this._transitionId2Label(cbResult);
1✔
740
      newState = State.fromSerializedState(
1✔
741
        rclnodejs.triggerLifecycleTransitionByLabel(
742
          this._stateMachineHandle,
743
          transitioningLabel
744
        )
745
      );
746
    }
747

748
    return newState;
15✔
749
  }
750

751
  /**
752
   * Execute the callback function registered with a transition action,
753
   * e.g. registerOnConfigure(cb).
754
   * @param {State} state - The state to which the callback is
755
   * @param {State} prevState - The start state of the transition.
756
   * @returns {CallbackReturnCode} The callback return code.
757
   * @private
758
   */
759
  _executeCallback(state, prevState) {
760
    let result = CallbackReturnCode.SUCCESS;
16✔
761
    let callback = this._callbackMap.get(state.id);
16✔
762

763
    if (callback) {
16✔
764
      try {
7✔
765
        result = callback(prevState);
7✔
766
      } catch (err) {
767
        console.log('CB exception occured: ', err);
×
768
        result = CallbackReturnCode.ERROR;
×
769
      }
770
    }
771

772
    return result;
16✔
773
  }
774

775
  /**
776
   * Find the label for the transition with id == transitionId.
777
   * @param {number} transitionId - A transition id.
778
   * @returns {string} The label of the transition with id.
779
   * @private
780
   */
781
  _transitionId2Label(transitionId) {
782
    return rclnodejs.getLifecycleTransitionIdToLabel(transitionId);
16✔
783
  }
784
}
785

786
const Lifecycle = {
26✔
787
  CallbackReturnCode,
788
  CallbackReturnValue,
789
  LifecycleNode,
790
  State,
791
  Transition,
792
  TransitionDescription,
793
};
794

795
module.exports = Lifecycle;
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