rclnodejs 1.6.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 (84) hide show
  1. package/binding.gyp +2 -0
  2. package/index.js +152 -0
  3. package/lib/action/client.js +109 -10
  4. package/lib/action/deferred.js +8 -2
  5. package/lib/action/server.js +10 -1
  6. package/lib/action/uuid.js +4 -1
  7. package/lib/client.js +218 -4
  8. package/lib/clock.js +182 -1
  9. package/lib/clock_change.js +49 -0
  10. package/lib/clock_event.js +88 -0
  11. package/lib/context.js +12 -2
  12. package/lib/duration.js +37 -12
  13. package/lib/errors.js +621 -0
  14. package/lib/event_handler.js +21 -4
  15. package/lib/interface_loader.js +52 -12
  16. package/lib/lifecycle.js +8 -2
  17. package/lib/logging.js +90 -3
  18. package/lib/message_introspector.js +123 -0
  19. package/lib/message_serialization.js +10 -2
  20. package/lib/message_validation.js +512 -0
  21. package/lib/native_loader.js +9 -4
  22. package/lib/node.js +403 -50
  23. package/lib/node_options.js +40 -1
  24. package/lib/observable_subscription.js +105 -0
  25. package/lib/parameter.js +172 -35
  26. package/lib/parameter_client.js +506 -0
  27. package/lib/parameter_watcher.js +309 -0
  28. package/lib/publisher.js +56 -1
  29. package/lib/qos.js +79 -5
  30. package/lib/rate.js +6 -1
  31. package/lib/serialization.js +7 -2
  32. package/lib/subscription.js +8 -0
  33. package/lib/time.js +136 -21
  34. package/lib/time_source.js +13 -4
  35. package/lib/timer.js +42 -0
  36. package/lib/utils.js +27 -1
  37. package/lib/validator.js +74 -19
  38. package/package.json +4 -2
  39. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  40. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  41. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  42. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  43. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  44. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  45. package/rosidl_gen/message_translator.js +0 -61
  46. package/scripts/config.js +1 -0
  47. package/src/addon.cpp +2 -0
  48. package/src/clock_event.cpp +268 -0
  49. package/src/clock_event.hpp +62 -0
  50. package/src/macros.h +2 -4
  51. package/src/rcl_action_server_bindings.cpp +21 -3
  52. package/src/rcl_bindings.cpp +59 -0
  53. package/src/rcl_context_bindings.cpp +5 -0
  54. package/src/rcl_graph_bindings.cpp +73 -0
  55. package/src/rcl_logging_bindings.cpp +158 -0
  56. package/src/rcl_node_bindings.cpp +14 -2
  57. package/src/rcl_publisher_bindings.cpp +12 -0
  58. package/src/rcl_service_bindings.cpp +7 -6
  59. package/src/rcl_subscription_bindings.cpp +51 -14
  60. package/src/rcl_time_point_bindings.cpp +135 -0
  61. package/src/rcl_timer_bindings.cpp +140 -0
  62. package/src/rcl_utilities.cpp +103 -2
  63. package/src/rcl_utilities.h +7 -1
  64. package/types/action_client.d.ts +27 -2
  65. package/types/base.d.ts +6 -0
  66. package/types/client.d.ts +65 -1
  67. package/types/clock.d.ts +86 -0
  68. package/types/clock_change.d.ts +27 -0
  69. package/types/clock_event.d.ts +51 -0
  70. package/types/errors.d.ts +496 -0
  71. package/types/index.d.ts +10 -0
  72. package/types/logging.d.ts +32 -0
  73. package/types/message_introspector.d.ts +75 -0
  74. package/types/message_validation.d.ts +183 -0
  75. package/types/node.d.ts +107 -0
  76. package/types/node_options.d.ts +13 -0
  77. package/types/observable_subscription.d.ts +39 -0
  78. package/types/parameter_client.d.ts +252 -0
  79. package/types/parameter_watcher.d.ts +104 -0
  80. package/types/publisher.d.ts +28 -1
  81. package/types/qos.d.ts +18 -0
  82. package/types/subscription.d.ts +6 -0
  83. package/types/timer.d.ts +18 -0
  84. package/types/validator.d.ts +86 -0
@@ -0,0 +1,512 @@
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 { MessageValidationError, TypeValidationError } = require('./errors.js');
18
+ const interfaceLoader = require('./interface_loader.js');
19
+
20
+ /**
21
+ * Validation issue problem types
22
+ * @enum {string}
23
+ */
24
+ const ValidationProblem = {
25
+ /** Field exists in object but not in message schema */
26
+ UNKNOWN_FIELD: 'UNKNOWN_FIELD',
27
+ /** Field type doesn't match expected type */
28
+ TYPE_MISMATCH: 'TYPE_MISMATCH',
29
+ /** Required field is missing */
30
+ MISSING_FIELD: 'MISSING_FIELD',
31
+ /** Array length constraint violated */
32
+ ARRAY_LENGTH: 'ARRAY_LENGTH',
33
+ /** Value is out of valid range */
34
+ OUT_OF_RANGE: 'OUT_OF_RANGE',
35
+ /** Nested message validation failed */
36
+ NESTED_ERROR: 'NESTED_ERROR',
37
+ };
38
+
39
+ /**
40
+ * Map ROS primitive types to JavaScript types
41
+ */
42
+ const PRIMITIVE_TYPE_MAP = {
43
+ bool: 'boolean',
44
+ int8: 'number',
45
+ uint8: 'number',
46
+ int16: 'number',
47
+ uint16: 'number',
48
+ int32: 'number',
49
+ uint32: 'number',
50
+ int64: 'bigint',
51
+ uint64: 'bigint',
52
+ float32: 'number',
53
+ float64: 'number',
54
+ char: 'number',
55
+ byte: 'number',
56
+ string: 'string',
57
+ wstring: 'string',
58
+ };
59
+
60
+ /**
61
+ * Check if value is a TypedArray
62
+ * @param {any} value - Value to check
63
+ * @returns {boolean} True if TypedArray
64
+ */
65
+ function isTypedArray(value) {
66
+ return ArrayBuffer.isView(value) && !(value instanceof DataView);
67
+ }
68
+
69
+ /**
70
+ * Get the JavaScript type string for a value
71
+ * @param {any} value - Value to get type of
72
+ * @returns {string} Type description
73
+ */
74
+ function getValueType(value) {
75
+ if (value === null) return 'null';
76
+ if (value === undefined) return 'undefined';
77
+ if (Array.isArray(value)) return 'array';
78
+ if (isTypedArray(value)) return 'TypedArray';
79
+ return typeof value;
80
+ }
81
+
82
+ /**
83
+ * Resolve a type class from various input formats
84
+ * @param {string|object|function} typeClass - Type identifier
85
+ * @returns {function|null} The resolved type class or null
86
+ */
87
+ function resolveTypeClass(typeClass) {
88
+ if (typeof typeClass === 'function') {
89
+ return typeClass;
90
+ }
91
+
92
+ try {
93
+ return interfaceLoader.loadInterface(typeClass);
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Get message type string from type class
101
+ * @param {function} typeClass - Message type class
102
+ * @returns {string} Message type string (e.g., 'std_msgs/msg/String')
103
+ */
104
+ function getMessageTypeString(typeClass) {
105
+ if (typeof typeClass.type === 'function') {
106
+ const t = typeClass.type();
107
+ return `${t.pkgName}/${t.subFolder}/${t.interfaceName}`;
108
+ }
109
+ return 'unknown';
110
+ }
111
+
112
+ /**
113
+ * Get the schema definition for a message type
114
+ * @param {function|string|object} typeClass - Message type class or identifier
115
+ * @returns {object|null} Schema definition with fields and constants, or null if not found
116
+ * @example
117
+ * const schema = getMessageSchema(StringClass);
118
+ * // Returns: {
119
+ * // fields: [{name: 'data', type: {type: 'string', isPrimitiveType: true, ...}}],
120
+ * // constants: [],
121
+ * // messageType: 'std_msgs/msg/String'
122
+ * // }
123
+ */
124
+ function getMessageSchema(typeClass) {
125
+ const resolved = resolveTypeClass(typeClass);
126
+ if (!resolved || !resolved.ROSMessageDef) {
127
+ return null;
128
+ }
129
+
130
+ const def = resolved.ROSMessageDef;
131
+ return {
132
+ fields: def.fields || [],
133
+ constants: def.constants || [],
134
+ messageType: getMessageTypeString(resolved),
135
+ baseType: def.baseType,
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Get field names for a message type
141
+ * @param {function|string|object} typeClass - Message type class or identifier
142
+ * @returns {string[]} Array of field names
143
+ */
144
+ function getFieldNames(typeClass) {
145
+ const schema = getMessageSchema(typeClass);
146
+ if (!schema) return [];
147
+ return schema.fields.map((f) => f.name);
148
+ }
149
+
150
+ /**
151
+ * Get type information for a specific field
152
+ * @param {function|string|object} typeClass - Message type class or identifier
153
+ * @param {string} fieldName - Name of the field
154
+ * @returns {object|null} Field type information or null if not found
155
+ */
156
+ function getFieldType(typeClass, fieldName) {
157
+ const schema = getMessageSchema(typeClass);
158
+ if (!schema) return null;
159
+
160
+ const field = schema.fields.find((f) => f.name === fieldName);
161
+ return field ? field.type : null;
162
+ }
163
+
164
+ /**
165
+ * Validate a primitive value against its expected type
166
+ * @param {any} value - Value to validate
167
+ * @param {object} fieldType - Field type definition
168
+ * @returns {object|null} Validation issue or null if valid
169
+ */
170
+ function validatePrimitiveValue(value, fieldType) {
171
+ const expectedJsType = PRIMITIVE_TYPE_MAP[fieldType.type];
172
+ const actualType = typeof value;
173
+
174
+ if (!expectedJsType) {
175
+ return null; // Unknown primitive type, skip validation
176
+ }
177
+
178
+ // Allow number for bigint fields (will be converted)
179
+ if (expectedJsType === 'bigint' && actualType === 'number') {
180
+ return null;
181
+ }
182
+
183
+ if (actualType !== expectedJsType) {
184
+ return {
185
+ problem: ValidationProblem.TYPE_MISMATCH,
186
+ expected: expectedJsType,
187
+ received: actualType,
188
+ };
189
+ }
190
+
191
+ return null;
192
+ }
193
+
194
+ /**
195
+ * Validate array constraints
196
+ * @param {any} value - Array value to validate
197
+ * @param {object} fieldType - Field type definition
198
+ * @returns {object|null} Validation issue or null if valid
199
+ */
200
+ function validateArrayConstraints(value, fieldType) {
201
+ if (!Array.isArray(value) && !isTypedArray(value)) {
202
+ return {
203
+ problem: ValidationProblem.TYPE_MISMATCH,
204
+ expected: 'array',
205
+ received: getValueType(value),
206
+ };
207
+ }
208
+
209
+ const length = value.length;
210
+
211
+ // Fixed size array
212
+ if (fieldType.isFixedSizeArray && length !== fieldType.arraySize) {
213
+ return {
214
+ problem: ValidationProblem.ARRAY_LENGTH,
215
+ expected: `exactly ${fieldType.arraySize} elements`,
216
+ received: `${length} elements`,
217
+ };
218
+ }
219
+
220
+ // Upper bound array
221
+ if (fieldType.isUpperBound && length > fieldType.arraySize) {
222
+ return {
223
+ problem: ValidationProblem.ARRAY_LENGTH,
224
+ expected: `at most ${fieldType.arraySize} elements`,
225
+ received: `${length} elements`,
226
+ };
227
+ }
228
+
229
+ return null;
230
+ }
231
+
232
+ /**
233
+ * Validate a message object against its schema
234
+ * @param {object} obj - Plain object to validate
235
+ * @param {function|string|object} typeClass - Message type class or identifier
236
+ * @param {object} [options] - Validation options
237
+ * @param {boolean} [options.strict=false] - If true, unknown fields cause validation failure
238
+ * @param {boolean} [options.checkTypes=true] - If true, validate field types
239
+ * @param {boolean} [options.checkRequired=false] - If true, check for missing fields
240
+ * @param {string} [options.path=''] - Current path for nested validation (internal use)
241
+ * @returns {{valid: boolean, issues: Array<object>}} Validation result
242
+ */
243
+ function validateMessage(obj, typeClass, options = {}) {
244
+ const {
245
+ strict = false,
246
+ checkTypes = true,
247
+ checkRequired = false,
248
+ path = '',
249
+ } = options;
250
+
251
+ const issues = [];
252
+ const resolved = resolveTypeClass(typeClass);
253
+
254
+ if (!resolved) {
255
+ issues.push({
256
+ field: path || '(root)',
257
+ problem: 'INVALID_TYPE_CLASS',
258
+ expected: 'valid message type class',
259
+ received: typeof typeClass,
260
+ });
261
+ return { valid: false, issues };
262
+ }
263
+
264
+ const schema = getMessageSchema(resolved);
265
+ if (!schema) {
266
+ issues.push({
267
+ field: path || '(root)',
268
+ problem: 'NO_SCHEMA',
269
+ expected: 'message with ROSMessageDef',
270
+ received: 'class without schema',
271
+ });
272
+ return { valid: false, issues };
273
+ }
274
+
275
+ if (obj === null || obj === undefined) {
276
+ issues.push({
277
+ field: path || '(root)',
278
+ problem: ValidationProblem.TYPE_MISMATCH,
279
+ expected: 'object',
280
+ received: String(obj),
281
+ });
282
+ return { valid: false, issues };
283
+ }
284
+
285
+ const type = typeof obj;
286
+ if (
287
+ type === 'string' ||
288
+ type === 'number' ||
289
+ type === 'boolean' ||
290
+ type === 'bigint'
291
+ ) {
292
+ if (schema.fields.length === 1 && schema.fields[0].name === 'data') {
293
+ const fieldType = schema.fields[0].type;
294
+ if (checkTypes && fieldType.isPrimitiveType) {
295
+ const typeIssue = validatePrimitiveValue(obj, fieldType);
296
+ if (typeIssue) {
297
+ issues.push({
298
+ field: path ? `${path}.data` : 'data',
299
+ ...typeIssue,
300
+ });
301
+ }
302
+ }
303
+ return { valid: issues.length === 0, issues };
304
+ }
305
+ }
306
+
307
+ if (type !== 'object') {
308
+ issues.push({
309
+ field: path || '(root)',
310
+ problem: ValidationProblem.TYPE_MISMATCH,
311
+ expected: 'object',
312
+ received: type,
313
+ });
314
+ return { valid: false, issues };
315
+ }
316
+
317
+ const fieldNames = new Set(schema.fields.map((f) => f.name));
318
+ const objKeys = Object.keys(obj);
319
+
320
+ if (strict) {
321
+ for (const key of objKeys) {
322
+ if (!fieldNames.has(key)) {
323
+ issues.push({
324
+ field: path ? `${path}.${key}` : key,
325
+ problem: ValidationProblem.UNKNOWN_FIELD,
326
+ });
327
+ }
328
+ }
329
+ }
330
+
331
+ for (const field of schema.fields) {
332
+ const fieldPath = path ? `${path}.${field.name}` : field.name;
333
+ const value = obj[field.name];
334
+ const fieldType = field.type;
335
+
336
+ if (field.name.startsWith('_')) continue;
337
+
338
+ if (value === undefined) {
339
+ if (checkRequired) {
340
+ issues.push({
341
+ field: fieldPath,
342
+ problem: ValidationProblem.MISSING_FIELD,
343
+ expected: fieldType.type,
344
+ });
345
+ }
346
+ continue;
347
+ }
348
+
349
+ if (fieldType.isArray) {
350
+ const arrayIssue = validateArrayConstraints(value, fieldType);
351
+ if (arrayIssue) {
352
+ issues.push({ field: fieldPath, ...arrayIssue });
353
+ continue;
354
+ }
355
+
356
+ if (checkTypes && Array.isArray(value) && value.length > 0) {
357
+ if (fieldType.isPrimitiveType) {
358
+ for (let i = 0; i < value.length; i++) {
359
+ const elemIssue = validatePrimitiveValue(value[i], fieldType);
360
+ if (elemIssue) {
361
+ issues.push({
362
+ field: `${fieldPath}[${i}]`,
363
+ ...elemIssue,
364
+ });
365
+ }
366
+ }
367
+ } else {
368
+ for (let i = 0; i < value.length; i++) {
369
+ const nestedResult = validateMessage(
370
+ value[i],
371
+ getNestedTypeClass(resolved, field.name),
372
+ {
373
+ strict,
374
+ checkTypes,
375
+ checkRequired,
376
+ path: `${fieldPath}[${i}]`,
377
+ }
378
+ );
379
+ if (!nestedResult.valid) {
380
+ issues.push(...nestedResult.issues);
381
+ }
382
+ }
383
+ }
384
+ }
385
+ } else if (fieldType.isPrimitiveType) {
386
+ if (checkTypes) {
387
+ const typeIssue = validatePrimitiveValue(value, fieldType);
388
+ if (typeIssue) {
389
+ issues.push({ field: fieldPath, ...typeIssue });
390
+ }
391
+ }
392
+ } else {
393
+ if (value !== null && typeof value === 'object') {
394
+ const nestedTypeClass = getNestedTypeClass(resolved, field.name);
395
+ if (nestedTypeClass) {
396
+ const nestedResult = validateMessage(value, nestedTypeClass, {
397
+ strict,
398
+ checkTypes,
399
+ checkRequired,
400
+ path: fieldPath,
401
+ });
402
+ if (!nestedResult.valid) {
403
+ issues.push(...nestedResult.issues);
404
+ }
405
+ }
406
+ } else if (checkTypes && value !== null) {
407
+ issues.push({
408
+ field: fieldPath,
409
+ problem: ValidationProblem.TYPE_MISMATCH,
410
+ expected: 'object',
411
+ received: getValueType(value),
412
+ });
413
+ }
414
+ }
415
+ }
416
+
417
+ return { valid: issues.length === 0, issues };
418
+ }
419
+
420
+ /**
421
+ * Get the type class for a nested field
422
+ * @param {function} parentTypeClass - Parent message type class
423
+ * @param {string} fieldName - Field name
424
+ * @returns {function|null} Nested type class or null
425
+ */
426
+ function getNestedTypeClass(parentTypeClass, fieldName) {
427
+ try {
428
+ const instance = new parentTypeClass();
429
+ const fieldValue = instance[fieldName];
430
+
431
+ if (
432
+ fieldValue &&
433
+ fieldValue.constructor &&
434
+ fieldValue.constructor.ROSMessageDef
435
+ ) {
436
+ return fieldValue.constructor;
437
+ }
438
+
439
+ if (
440
+ fieldValue &&
441
+ fieldValue.classType &&
442
+ fieldValue.classType.elementType
443
+ ) {
444
+ return fieldValue.classType.elementType;
445
+ }
446
+ } catch {
447
+ const schema = getMessageSchema(parentTypeClass);
448
+ if (schema) {
449
+ const field = schema.fields.find((f) => f.name === fieldName);
450
+ if (field && !field.type.isPrimitiveType) {
451
+ const typeName = `${field.type.pkgName}/msg/${field.type.type}`;
452
+ return resolveTypeClass(typeName);
453
+ }
454
+ }
455
+ }
456
+ return null;
457
+ }
458
+
459
+ /**
460
+ * Validate a message and throw if invalid
461
+ * @param {object} obj - Plain object to validate
462
+ * @param {function|string|object} typeClass - Message type class or identifier
463
+ * @param {object} [options] - Validation options (same as validateMessage)
464
+ * @throws {MessageValidationError} If validation fails
465
+ * @returns {void}
466
+ */
467
+ function assertValidMessage(obj, typeClass, options = {}) {
468
+ const result = validateMessage(obj, typeClass, options);
469
+
470
+ if (!result.valid) {
471
+ const resolved = resolveTypeClass(typeClass);
472
+ const messageType = resolved
473
+ ? getMessageTypeString(resolved)
474
+ : String(typeClass);
475
+ throw new MessageValidationError(messageType, result.issues);
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Create a validator function for a specific message type
481
+ * @param {function|string|object} typeClass - Message type class or identifier
482
+ * @param {object} [defaultOptions] - Default validation options
483
+ * @returns {function} Validator function that takes (obj, options?) and returns validation result
484
+ */
485
+ function createMessageValidator(typeClass, defaultOptions = {}) {
486
+ const resolved = resolveTypeClass(typeClass);
487
+ if (!resolved) {
488
+ throw new TypeValidationError(
489
+ 'typeClass',
490
+ typeClass,
491
+ 'valid message type class'
492
+ );
493
+ }
494
+
495
+ return function validator(obj, options = {}) {
496
+ return validateMessage(obj, resolved, {
497
+ ...defaultOptions,
498
+ ...options,
499
+ });
500
+ };
501
+ }
502
+
503
+ module.exports = {
504
+ ValidationProblem,
505
+ getMessageSchema,
506
+ getFieldNames,
507
+ getFieldType,
508
+ validateMessage,
509
+ assertValidMessage,
510
+ createMessageValidator,
511
+ getMessageTypeString,
512
+ };
@@ -17,6 +17,7 @@
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { execSync } = require('child_process');
20
+ const { NativeError } = require('./errors.js');
20
21
  const bindings = require('bindings');
21
22
  const debug = require('debug')('rclnodejs');
22
23
  const { detectUbuntuCodename } = require('./utils');
@@ -100,8 +101,10 @@ function loadNativeAddon() {
100
101
  return nativeModule;
101
102
  } catch (compileError) {
102
103
  debug('Forced compilation failed:', compileError.message);
103
- throw new Error(
104
- `Failed to force build rclnodejs from source: ${compileError.message}`
104
+ throw new NativeError(
105
+ `Failed to force build rclnodejs from source: ${compileError.message}`,
106
+ 'Forced compilation',
107
+ { cause: compileError }
105
108
  );
106
109
  }
107
110
  }
@@ -163,8 +166,10 @@ function loadNativeAddon() {
163
166
  return nativeModule;
164
167
  } catch (compileError) {
165
168
  debug('Compilation failed:', compileError.message);
166
- throw new Error(
167
- `Failed to build rclnodejs from source: ${compileError.message}`
169
+ throw new NativeError(
170
+ `Failed to build rclnodejs from source: ${compileError.message}`,
171
+ 'Compilation',
172
+ { cause: compileError }
168
173
  );
169
174
  }
170
175
  }