rclnodejs 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/binding.gyp +2 -0
  2. package/index.js +93 -0
  3. package/lib/action/client.js +54 -1
  4. package/lib/client.js +66 -1
  5. package/lib/clock.js +178 -0
  6. package/lib/clock_change.js +49 -0
  7. package/lib/clock_event.js +88 -0
  8. package/lib/errors.js +50 -0
  9. package/lib/logging.js +78 -0
  10. package/lib/message_introspector.js +123 -0
  11. package/lib/message_validation.js +512 -0
  12. package/lib/node.js +133 -1
  13. package/lib/node_options.js +40 -1
  14. package/lib/observable_subscription.js +105 -0
  15. package/lib/publisher.js +56 -1
  16. package/lib/qos.js +57 -0
  17. package/lib/subscription.js +8 -0
  18. package/lib/timer.js +42 -0
  19. package/lib/validator.js +63 -7
  20. package/package.json +4 -2
  21. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  22. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  23. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  24. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  25. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  26. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  27. package/rosidl_gen/message_translator.js +0 -61
  28. package/scripts/config.js +1 -0
  29. package/src/addon.cpp +2 -0
  30. package/src/clock_event.cpp +268 -0
  31. package/src/clock_event.hpp +62 -0
  32. package/src/macros.h +2 -4
  33. package/src/rcl_action_server_bindings.cpp +21 -3
  34. package/src/rcl_bindings.cpp +59 -0
  35. package/src/rcl_context_bindings.cpp +5 -0
  36. package/src/rcl_graph_bindings.cpp +73 -0
  37. package/src/rcl_logging_bindings.cpp +158 -0
  38. package/src/rcl_node_bindings.cpp +14 -2
  39. package/src/rcl_publisher_bindings.cpp +12 -0
  40. package/src/rcl_service_bindings.cpp +7 -6
  41. package/src/rcl_subscription_bindings.cpp +51 -14
  42. package/src/rcl_time_point_bindings.cpp +135 -0
  43. package/src/rcl_timer_bindings.cpp +140 -0
  44. package/src/rcl_utilities.cpp +103 -2
  45. package/src/rcl_utilities.h +7 -1
  46. package/types/action_client.d.ts +27 -2
  47. package/types/base.d.ts +3 -0
  48. package/types/client.d.ts +29 -1
  49. package/types/clock.d.ts +86 -0
  50. package/types/clock_change.d.ts +27 -0
  51. package/types/clock_event.d.ts +51 -0
  52. package/types/errors.d.ts +49 -0
  53. package/types/index.d.ts +10 -0
  54. package/types/interfaces.d.ts +1 -1910
  55. package/types/logging.d.ts +32 -0
  56. package/types/message_introspector.d.ts +75 -0
  57. package/types/message_validation.d.ts +183 -0
  58. package/types/node.d.ts +67 -0
  59. package/types/node_options.d.ts +13 -0
  60. package/types/observable_subscription.d.ts +39 -0
  61. package/types/publisher.d.ts +28 -1
  62. package/types/qos.d.ts +18 -0
  63. package/types/subscription.d.ts +6 -0
  64. package/types/timer.d.ts +18 -0
  65. package/types/validator.d.ts +86 -0
package/binding.gyp CHANGED
@@ -26,6 +26,7 @@
26
26
  './src/rcl_action_server_bindings.cpp',
27
27
  './src/rcl_bindings.cpp',
28
28
  './src/rcl_client_bindings.cpp',
29
+ './src/clock_event.cpp',
29
30
  './src/rcl_context_bindings.cpp',
30
31
  './src/rcl_graph_bindings.cpp',
31
32
  './src/rcl_guard_condition_bindings.cpp',
@@ -67,6 +68,7 @@
67
68
  '-lrcl_action',
68
69
  '-lrcl_lifecycle',
69
70
  '-lrcutils',
71
+ '-lrcl_logging_interface',
70
72
  '-lrcl_yaml_param_parser',
71
73
  '-lrcpputils',
72
74
  '-lrmw',
package/index.js CHANGED
@@ -17,7 +17,9 @@
17
17
  const DistroUtils = require('./lib/distro.js');
18
18
  const RMWUtils = require('./lib/rmw.js');
19
19
  const { Clock, ROSClock } = require('./lib/clock.js');
20
+ const ClockEvent = require('./lib/clock_event.js');
20
21
  const ClockType = require('./lib/clock_type.js');
22
+ const ClockChange = require('./lib/clock_change.js');
21
23
  const { compareVersions } = require('./lib/utils.js');
22
24
  const Context = require('./lib/context.js');
23
25
  const debug = require('debug')('rclnodejs');
@@ -61,7 +63,18 @@ const {
61
63
  const ParameterClient = require('./lib/parameter_client.js');
62
64
  const errors = require('./lib/errors.js');
63
65
  const ParameterWatcher = require('./lib/parameter_watcher.js');
66
+ const MessageIntrospector = require('./lib/message_introspector.js');
67
+ const ObservableSubscription = require('./lib/observable_subscription.js');
64
68
  const { spawn } = require('child_process');
69
+ const {
70
+ ValidationProblem,
71
+ getMessageSchema,
72
+ getFieldNames,
73
+ getFieldType,
74
+ validateMessage,
75
+ assertValidMessage,
76
+ createMessageValidator,
77
+ } = require('./lib/message_validation.js');
65
78
 
66
79
  /**
67
80
  * Get the version of the generator that was used for the currently present interfaces.
@@ -178,9 +191,15 @@ let rcl = {
178
191
  /** {@link Clock} class */
179
192
  Clock: Clock,
180
193
 
194
+ /** {@link ClockEvent} class */
195
+ ClockEvent: ClockEvent,
196
+
181
197
  /** {@link ClockType} enum */
182
198
  ClockType: ClockType,
183
199
 
200
+ /** {@link ClockChange} enum */
201
+ ClockChange: ClockChange,
202
+
184
203
  /** {@link Context} class */
185
204
  Context: Context,
186
205
 
@@ -226,6 +245,9 @@ let rcl = {
226
245
  /** {@link ParameterWatcher} class */
227
246
  ParameterWatcher: ParameterWatcher,
228
247
 
248
+ /** {@link ObservableSubscription} class */
249
+ ObservableSubscription: ObservableSubscription,
250
+
229
251
  /** {@link QoS} class */
230
252
  QoS: QoS,
231
253
 
@@ -386,6 +408,21 @@ let rcl = {
386
408
  _rosVersionChecked = true;
387
409
  },
388
410
 
411
+ /**
412
+ * Remove ROS-specific arguments from the given argument list.
413
+ * @param {string[]} argv - The argument list to process.
414
+ * @return {string[]} The argument list with ROS arguments removed.
415
+ */
416
+ removeROSArgs(argv) {
417
+ if (!Array.isArray(argv)) {
418
+ throw new TypeError('argv must be an array.');
419
+ }
420
+ if (!argv.every((argument) => typeof argument === 'string')) {
421
+ throw new TypeError('argv elements must be strings (and not null).');
422
+ }
423
+ return rclnodejs.removeROSArgs(argv);
424
+ },
425
+
389
426
  /**
390
427
  * Start detection and processing of units of work.
391
428
  * @param {Node} node - The node to be spun up.
@@ -566,6 +603,57 @@ let rcl = {
566
603
  */
567
604
  toJSONString: toJSONString,
568
605
 
606
+ /** {@link ValidationProblem} - Enum of validation problem types */
607
+ ValidationProblem: ValidationProblem,
608
+
609
+ /**
610
+ * Get the schema definition for a message type
611
+ * @param {function|string|object} typeClass - Message type class or identifier
612
+ * @returns {object|null} Schema definition with fields and constants
613
+ */
614
+ getMessageSchema: getMessageSchema,
615
+
616
+ /**
617
+ * Get field names for a message type
618
+ * @param {function|string|object} typeClass - Message type class or identifier
619
+ * @returns {string[]} Array of field names
620
+ */
621
+ getFieldNames: getFieldNames,
622
+
623
+ /**
624
+ * Get type information for a specific field
625
+ * @param {function|string|object} typeClass - Message type class or identifier
626
+ * @param {string} fieldName - Name of the field
627
+ * @returns {object|null} Field type information or null if not found
628
+ */
629
+ getFieldType: getFieldType,
630
+
631
+ /**
632
+ * Validate a message object against its schema
633
+ * @param {object} obj - Plain object to validate
634
+ * @param {function|string|object} typeClass - Message type class or identifier
635
+ * @param {object} [options] - Validation options
636
+ * @returns {{valid: boolean, issues: Array<object>}} Validation result
637
+ */
638
+ validateMessage: validateMessage,
639
+
640
+ /**
641
+ * Validate a message and throw if invalid
642
+ * @param {object} obj - Plain object to validate
643
+ * @param {function|string|object} typeClass - Message type class or identifier
644
+ * @param {object} [options] - Validation options
645
+ * @throws {MessageValidationError} If validation fails
646
+ */
647
+ assertValidMessage: assertValidMessage,
648
+
649
+ /**
650
+ * Create a validator function for a specific message type
651
+ * @param {function|string|object} typeClass - Message type class or identifier
652
+ * @param {object} [defaultOptions] - Default validation options
653
+ * @returns {function} Validator function
654
+ */
655
+ createMessageValidator: createMessageValidator,
656
+
569
657
  // Error classes for structured error handling
570
658
  /** {@link RclNodeError} - Base error class for all rclnodejs errors */
571
659
  RclNodeError: errors.RclNodeError,
@@ -576,6 +664,8 @@ let rcl = {
576
664
  TypeValidationError: errors.TypeValidationError,
577
665
  /** {@link RangeValidationError} - Range/value validation error */
578
666
  RangeValidationError: errors.RangeValidationError,
667
+ /** {@link MessageValidationError} - Message structure/type validation error */
668
+ MessageValidationError: errors.MessageValidationError,
579
669
  /** {@link NameValidationError} - ROS name validation error */
580
670
  NameValidationError: errors.NameValidationError,
581
671
 
@@ -644,3 +734,6 @@ const Lifecycle = require('./lib/lifecycle.js');
644
734
 
645
735
  /** Lifecycle namespace */
646
736
  rcl.lifecycle = Lifecycle;
737
+
738
+ /** {@link MessageIntrospector} class */
739
+ rcl.MessageIntrospector = MessageIntrospector;
@@ -28,6 +28,7 @@ const {
28
28
  ActionError,
29
29
  OperationError,
30
30
  } = require('../errors.js');
31
+ const { assertValidMessage } = require('../message_validation.js');
31
32
 
32
33
  /**
33
34
  * @class - ROS Action client.
@@ -80,6 +81,12 @@ class ActionClient extends Entity {
80
81
  this._options.qos.statusSubQosProfile =
81
82
  this._options.qos.statusSubQosProfile || QoS.profileActionStatusDefault;
82
83
 
84
+ this._validateGoals = this._options.validateGoals || false;
85
+ this._validationOptions = this._options.validationOptions || {
86
+ strict: true,
87
+ checkTypes: true,
88
+ };
89
+
83
90
  let type = this.typeClass.type();
84
91
 
85
92
  this._handle = rclnodejs.actionCreateClient(
@@ -200,6 +207,34 @@ class ActionClient extends Entity {
200
207
  }
201
208
  }
202
209
 
210
+ /**
211
+ * Whether goals will be validated before sending.
212
+ * @type {boolean}
213
+ */
214
+ get willValidateGoal() {
215
+ return this._validateGoals;
216
+ }
217
+
218
+ /**
219
+ * Enable or disable goal validation for this action client.
220
+ * @param {boolean} value - Whether to validate goals
221
+ */
222
+ set willValidateGoal(value) {
223
+ this._validateGoals = value;
224
+ }
225
+
226
+ /**
227
+ * Set validation options for this action client.
228
+ * @param {object} options - Validation options
229
+ * @param {boolean} [options.strict=true] - Throw on unknown fields
230
+ * @param {boolean} [options.checkTypes=true] - Validate field types
231
+ */
232
+ setValidation(options) {
233
+ if (options && Object.keys(options).length > 0) {
234
+ this._validationOptions = { ...this._validationOptions, ...options };
235
+ }
236
+ }
237
+
203
238
  /**
204
239
  * Send a goal and wait for the goal ACK asynchronously.
205
240
  *
@@ -209,9 +244,27 @@ class ActionClient extends Entity {
209
244
  * @param {object} goal - The goal request.
210
245
  * @param {function} feedbackCallback - Callback function for feedback associated with the goal.
211
246
  * @param {object} goalUuid - Universally unique identifier for the goal. If None, then a random UUID is generated.
247
+ * @param {object} [options] - Send options
248
+ * @param {boolean} [options.validate] - Override validateGoals setting for this call
212
249
  * @returns {Promise} - A Promise to a goal handle that resolves when the goal request has been accepted or rejected.
250
+ * @throws {MessageValidationError} If validation is enabled and goal is invalid
213
251
  */
214
- sendGoal(goal, feedbackCallback, goalUuid) {
252
+ sendGoal(goal, feedbackCallback, goalUuid, options = {}) {
253
+ const shouldValidate =
254
+ options.validate !== undefined ? options.validate : this._validateGoals;
255
+
256
+ if (shouldValidate) {
257
+ const GoalType = this._typeClass.impl.SendGoalService.Request;
258
+ const goalInstance = new GoalType();
259
+ if (goalInstance.goal && goalInstance.goal.constructor) {
260
+ assertValidMessage(
261
+ goal,
262
+ goalInstance.goal.constructor,
263
+ this._validationOptions
264
+ );
265
+ }
266
+ }
267
+
215
268
  let request = new this._typeClass.impl.SendGoalService.Request();
216
269
  request['goal_id'] = goalUuid || ActionUuid.randomMessage();
217
270
  request.goal = goal;
package/lib/client.js CHANGED
@@ -22,6 +22,7 @@ const {
22
22
  TimeoutError,
23
23
  AbortError,
24
24
  } = require('./errors.js');
25
+ const { assertValidMessage } = require('./message_validation.js');
25
26
  const debug = require('debug')('rclnodejs:client');
26
27
 
27
28
  // Polyfill for AbortSignal.any() for Node.js <= 20.3.0
@@ -80,6 +81,39 @@ class Client extends Entity {
80
81
  this._nodeHandle = nodeHandle;
81
82
  this._serviceName = serviceName;
82
83
  this._sequenceNumberToCallbackMap = new Map();
84
+ this._validateRequests = options.validateRequests || false;
85
+ this._validationOptions = options.validationOptions || {
86
+ strict: true,
87
+ checkTypes: true,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Whether requests will be validated before sending.
93
+ * @type {boolean}
94
+ */
95
+ get willValidateRequest() {
96
+ return this._validateRequests;
97
+ }
98
+
99
+ /**
100
+ * Enable or disable request validation for this client.
101
+ * @param {boolean} value - Whether to validate requests
102
+ */
103
+ set willValidateRequest(value) {
104
+ this._validateRequests = value;
105
+ }
106
+
107
+ /**
108
+ * Set validation options for this client.
109
+ * @param {object} options - Validation options
110
+ * @param {boolean} [options.strict=true] - Throw on unknown fields
111
+ * @param {boolean} [options.checkTypes=true] - Validate field types
112
+ */
113
+ setValidation(options) {
114
+ if (options && Object.keys(options).length > 0) {
115
+ this._validationOptions = { ...this._validationOptions, ...options };
116
+ }
83
117
  }
84
118
 
85
119
  /**
@@ -96,10 +130,13 @@ class Client extends Entity {
96
130
  * Send the request and will be notified asynchronously if receiving the response.
97
131
  * @param {object} request - The request to be submitted.
98
132
  * @param {ResponseCallback} callback - Thc callback function for receiving the server response.
133
+ * @param {object} [options] - Send options
134
+ * @param {boolean} [options.validate] - Override validateRequests setting for this call
99
135
  * @return {undefined}
136
+ * @throws {MessageValidationError} If validation is enabled and request is invalid
100
137
  * @see {@link ResponseCallback}
101
138
  */
102
- sendRequest(request, callback) {
139
+ sendRequest(request, callback, options = {}) {
103
140
  if (typeof callback !== 'function') {
104
141
  throw new TypeValidationError('callback', callback, 'function', {
105
142
  entityType: 'service',
@@ -107,6 +144,19 @@ class Client extends Entity {
107
144
  });
108
145
  }
109
146
 
147
+ const shouldValidate =
148
+ options.validate !== undefined
149
+ ? options.validate
150
+ : this._validateRequests;
151
+
152
+ if (shouldValidate && !(request instanceof this._typeClass.Request)) {
153
+ assertValidMessage(
154
+ request,
155
+ this._typeClass.Request,
156
+ this._validationOptions
157
+ );
158
+ }
159
+
110
160
  let requestToSend =
111
161
  request instanceof this._typeClass.Request
112
162
  ? request
@@ -124,9 +174,11 @@ class Client extends Entity {
124
174
  * @param {object} [options] - Optional parameters for the request.
125
175
  * @param {number} [options.timeout] - Timeout in milliseconds for the request.
126
176
  * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
177
+ * @param {boolean} [options.validate] - Override validateRequests setting for this call
127
178
  * @return {Promise<object>} Promise that resolves with the service response.
128
179
  * @throws {module:rclnodejs.TimeoutError} If the request times out (when options.timeout is exceeded).
129
180
  * @throws {module:rclnodejs.AbortError} If the request is manually aborted (via options.signal).
181
+ * @throws {module:rclnodejs.MessageValidationError} If validation is enabled and request is invalid.
130
182
  * @throws {Error} If the request fails for other reasons.
131
183
  */
132
184
  sendRequestAsync(request, options = {}) {
@@ -191,6 +243,19 @@ class Client extends Entity {
191
243
  }
192
244
 
193
245
  try {
246
+ const shouldValidate =
247
+ options.validate !== undefined
248
+ ? options.validate
249
+ : this._validateRequests;
250
+
251
+ if (shouldValidate && !(request instanceof this._typeClass.Request)) {
252
+ assertValidMessage(
253
+ request,
254
+ this._typeClass.Request,
255
+ this._validationOptions
256
+ );
257
+ }
258
+
194
259
  let requestToSend =
195
260
  request instanceof this._typeClass.Request
196
261
  ? request
package/lib/clock.js CHANGED
@@ -17,6 +17,9 @@
17
17
  const rclnodejs = require('./native_loader.js');
18
18
  const Time = require('./time.js');
19
19
  const ClockType = require('./clock_type.js');
20
+ const ClockChange = require('./clock_change.js');
21
+ const Context = require('./context.js');
22
+ const ClockEvent = require('./clock_event.js');
20
23
  const { TypeValidationError } = require('./errors.js');
21
24
 
22
25
  /**
@@ -48,6 +51,71 @@ class Clock {
48
51
  return this._clockType;
49
52
  }
50
53
 
54
+ /**
55
+ * Add a clock callback.
56
+ * @param {object} callbackObject - The object containing callback methods.
57
+ * @param {Function} [callbackObject._pre_callback] - Optional callback invoked before a time jump. Takes no arguments.
58
+ * @param {Function} [callbackObject._post_callback] - Optional callback invoked after a time jump.
59
+ * Receives a jumpInfo object with properties:
60
+ * - clock_change {number}: Type of clock change that occurred.
61
+ * - delta {bigint}: Time delta in nanoseconds.
62
+ * @param {boolean} onClockChange - Whether to call the callback on clock change.
63
+ * @param {bigint} minForward - Minimum forward jump in nanoseconds to trigger the callback.
64
+ * @param {bigint} minBackward - Minimum backward jump in nanoseconds to trigger the callback.
65
+ */
66
+ addClockCallback(callbackObject, onClockChange, minForward, minBackward) {
67
+ if (typeof callbackObject !== 'object' || callbackObject === null) {
68
+ throw new TypeValidationError(
69
+ 'callbackObject',
70
+ callbackObject,
71
+ 'object',
72
+ {
73
+ entityType: 'clock',
74
+ }
75
+ );
76
+ }
77
+ if (typeof onClockChange !== 'boolean') {
78
+ throw new TypeValidationError('onClockChange', onClockChange, 'boolean', {
79
+ entityType: 'clock',
80
+ });
81
+ }
82
+ if (typeof minForward !== 'bigint') {
83
+ throw new TypeValidationError('minForward', minForward, 'bigint', {
84
+ entityType: 'clock',
85
+ });
86
+ }
87
+ if (typeof minBackward !== 'bigint') {
88
+ throw new TypeValidationError('minBackward', minBackward, 'bigint', {
89
+ entityType: 'clock',
90
+ });
91
+ }
92
+ rclnodejs.clockAddJumpCallback(
93
+ this._handle,
94
+ callbackObject,
95
+ onClockChange,
96
+ minForward,
97
+ minBackward
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Remove a clock callback.
103
+ * @param {object} callbackObject - The callback object that was previously registered with addClockCallback().
104
+ */
105
+ removeClockCallback(callbackObject) {
106
+ if (typeof callbackObject !== 'object' || callbackObject === null) {
107
+ throw new TypeValidationError(
108
+ 'callbackObject',
109
+ callbackObject,
110
+ 'object',
111
+ {
112
+ entityType: 'clock',
113
+ }
114
+ );
115
+ }
116
+ rclnodejs.clockRemoveJumpCallback(this._handle, callbackObject);
117
+ }
118
+
51
119
  /**
52
120
  * Return the current time.
53
121
  * @return {Time} Return the current time.
@@ -56,6 +124,116 @@ class Clock {
56
124
  const nowInNanosec = rclnodejs.clockGetNow(this._handle);
57
125
  return new Time(0n, nowInNanosec, this._clockType);
58
126
  }
127
+
128
+ /**
129
+ * Sleep until a specific time is reached on this Clock.
130
+ *
131
+ * When using a ROSClock, this may sleep forever if the TimeSource is misconfigured
132
+ * and the context is never shut down. ROS time being activated or deactivated causes
133
+ * this function to cease sleeping and return false.
134
+ *
135
+ * @param {Time} until - Time at which this function should stop sleeping.
136
+ * @param {Context} [context=null] - Context whose validity will be checked while sleeping.
137
+ * If context is null, then the default context is used. If the context is found to be
138
+ * shut down before or by the time the wait completes, the returned promise resolves to false.
139
+ * @return {Promise<boolean>} Promise that resolves to true if 'until' was reached without
140
+ * detecting a time jump or a shut down context, or false otherwise (for example, if a time
141
+ * jump occurred or the context is no longer valid when the wait completes).
142
+ * @throws {TypeError} if until is specified for a different type of clock than this one.
143
+ * @throws {Error} if context has not been initialized or is shutdown.
144
+ */
145
+ async sleepUntil(until, context = null) {
146
+ if (!(until instanceof Time)) {
147
+ throw new TypeValidationError('until', until, 'Time', {
148
+ entityType: 'clock',
149
+ });
150
+ }
151
+
152
+ if (!context) {
153
+ context = Context.defaultContext();
154
+ }
155
+
156
+ if (!context.isValid()) {
157
+ throw new Error('Context is not initialized or has been shut down');
158
+ }
159
+
160
+ if (until.clockType !== this._clockType) {
161
+ throw new TypeError(
162
+ "until's clock type does not match this clock's type"
163
+ );
164
+ }
165
+
166
+ const event = new ClockEvent();
167
+ let timeSourceChanged = false;
168
+
169
+ // Callback to wake when time jumps and is past target time
170
+ const callbackObject = {
171
+ _pre_callback: () => {
172
+ // Optional pre-callback - no action needed
173
+ },
174
+ _post_callback: (jumpInfo) => {
175
+ // ROS time being activated or deactivated changes the epoch,
176
+ // so sleep time loses its meaning
177
+ timeSourceChanged =
178
+ timeSourceChanged ||
179
+ jumpInfo.clock_change === ClockChange.ROS_TIME_ACTIVATED ||
180
+ jumpInfo.clock_change === ClockChange.ROS_TIME_DEACTIVATED;
181
+
182
+ if (timeSourceChanged || this.now().nanoseconds >= until.nanoseconds) {
183
+ event.set();
184
+ }
185
+ },
186
+ };
187
+
188
+ // Setup jump callback with minimal forward threshold
189
+ this.addClockCallback(
190
+ callbackObject,
191
+ true, // onClockChange
192
+ 1n, // minForward (1 nanosecond - any forward jump triggers)
193
+ 0n // minBackward (0 disables backward threshold)
194
+ );
195
+
196
+ try {
197
+ // Wait based on clock type
198
+ if (this._clockType === ClockType.SYSTEM_TIME) {
199
+ await event.waitUntilSystem(this, until.nanoseconds);
200
+ } else if (this._clockType === ClockType.STEADY_TIME) {
201
+ await event.waitUntilSteady(this, until.nanoseconds);
202
+ } else if (this._clockType === ClockType.ROS_TIME) {
203
+ await event.waitUntilRos(this, until.nanoseconds);
204
+ }
205
+ } finally {
206
+ // Always clean up callback
207
+ this.removeClockCallback(callbackObject);
208
+ }
209
+
210
+ if (!context.isValid() || timeSourceChanged) {
211
+ return false;
212
+ }
213
+
214
+ return this.now().nanoseconds >= until.nanoseconds;
215
+ }
216
+
217
+ /**
218
+ * Sleep for a specified duration.
219
+ *
220
+ * Equivalent to: clock.sleepUntil(clock.now() + duration, context)
221
+ *
222
+ * When using a ROSClock, this may sleep forever if the TimeSource is misconfigured
223
+ * and the context is never shut down. ROS time being activated or deactivated causes
224
+ * this function to cease sleeping and return false.
225
+ *
226
+ * @param {Duration} duration - Duration of time to sleep for.
227
+ * @param {Context} [context=null] - Context which when shut down will cause this sleep to wake early.
228
+ * If context is null, then the default context is used.
229
+ * @return {Promise<boolean>} Promise that resolves to true if the full duration was slept,
230
+ * or false if it woke for another reason.
231
+ * @throws {Error} if context has not been initialized or is shutdown.
232
+ */
233
+ async sleepFor(duration, context = null) {
234
+ const until = this.now().add(duration);
235
+ return this.sleepUntil(until, context);
236
+ }
59
237
  }
60
238
 
61
239
  /**
@@ -0,0 +1,49 @@
1
+ // Copyright (c) 2025 The Robot Web Tools Contributors. 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
+ /**
18
+ * Enum for ClockChange
19
+ * Represents the type of clock change that occurred during a time jump.
20
+ * @readonly
21
+ * @enum {number}
22
+ */
23
+ const ClockChange = {
24
+ /**
25
+ * The source before and after the jump is ROS_TIME.
26
+ * @member {number}
27
+ */
28
+ ROS_TIME_NO_CHANGE: 1,
29
+
30
+ /**
31
+ * The source switched to ROS_TIME from SYSTEM_TIME.
32
+ * @member {number}
33
+ */
34
+ ROS_TIME_ACTIVATED: 2,
35
+
36
+ /**
37
+ * The source switched to SYSTEM_TIME from ROS_TIME.
38
+ * @member {number}
39
+ */
40
+ ROS_TIME_DEACTIVATED: 3,
41
+
42
+ /**
43
+ * The source before and after the jump is SYSTEM_TIME.
44
+ * @member {number}
45
+ */
46
+ SYSTEM_TIME_NO_CHANGE: 4,
47
+ };
48
+
49
+ module.exports = ClockChange;
@@ -0,0 +1,88 @@
1
+ // Copyright (c) 2025, The Robot Web Tools Contributors
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');
18
+
19
+ /**
20
+ * @class - Class representing a ClockEvent in ROS
21
+ */
22
+ class ClockEvent {
23
+ constructor() {
24
+ this._handle = rclnodejs.createClockEvent();
25
+ }
26
+
27
+ /**
28
+ * Wait until a time specified by a steady clock.
29
+ * @param {Clock} clock - The clock to use for time synchronization.
30
+ * @param {bigint} until - The time to wait until.
31
+ * @return {Promise<void>} - A promise that resolves when the time is reached.
32
+ */
33
+ async waitUntilSteady(clock, until) {
34
+ return rclnodejs.clockEventWaitUntilSteady(
35
+ this._handle,
36
+ clock.handle,
37
+ until
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Wait until a time specified by a system clock.
43
+ * @param {Clock} clock - The clock to use for time synchronization.
44
+ * @param {bigint} until - The time to wait until.
45
+ * @return {Promise<void>} - A promise that resolves when the time is reached.
46
+ */
47
+ async waitUntilSystem(clock, until) {
48
+ return rclnodejs.clockEventWaitUntilSystem(
49
+ this._handle,
50
+ clock.handle,
51
+ until
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Wait until a time specified by a ROS clock.
57
+ * @param {Clock} clock - The clock to use for time synchronization.
58
+ * @param {bigint} until - The time to wait until.
59
+ * @return {Promise<void>} - A promise that resolves when the time is reached.
60
+ */
61
+ async waitUntilRos(clock, until) {
62
+ return rclnodejs.clockEventWaitUntilRos(this._handle, clock.handle, until);
63
+ }
64
+
65
+ /**
66
+ * Indicate if the ClockEvent is set.
67
+ * @return {boolean} - True if the ClockEvent is set.
68
+ */
69
+ isSet() {
70
+ return rclnodejs.clockEventIsSet(this._handle);
71
+ }
72
+
73
+ /**
74
+ * Set the event.
75
+ */
76
+ set() {
77
+ rclnodejs.clockEventSet(this._handle);
78
+ }
79
+
80
+ /**
81
+ * Clear the event.
82
+ */
83
+ clear() {
84
+ rclnodejs.clockEventClear(this._handle);
85
+ }
86
+ }
87
+
88
+ module.exports = ClockEvent;