@tier0/opcua-client 0.1.0 → 0.4.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 (49) hide show
  1. package/BENCHMARK.md +53 -2
  2. package/README.md +180 -14
  3. package/THIRD_PARTY_NOTICES.md +20 -0
  4. package/binding.gyp +1 -1
  5. package/lib/client.js +48 -7
  6. package/lib/connection-registry.js +68 -0
  7. package/lib/native.js +25 -10
  8. package/lib/points.js +187 -21
  9. package/lib/prebuild.js +42 -0
  10. package/lib/security-file-store.js +119 -0
  11. package/lib/security.js +96 -0
  12. package/node_modules/node-addon-api/LICENSE.md +9 -0
  13. package/node_modules/node-addon-api/README.md +95 -0
  14. package/node_modules/node-addon-api/common.gypi +21 -0
  15. package/node_modules/node-addon-api/except.gypi +25 -0
  16. package/node_modules/node-addon-api/index.js +14 -0
  17. package/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  18. package/node_modules/node-addon-api/napi-inl.h +7244 -0
  19. package/node_modules/node-addon-api/napi.h +3420 -0
  20. package/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  21. package/node_modules/node-addon-api/node_api.gyp +9 -0
  22. package/node_modules/node-addon-api/noexcept.gypi +26 -0
  23. package/node_modules/node-addon-api/nothing.c +0 -0
  24. package/node_modules/node-addon-api/package-support.json +21 -0
  25. package/node_modules/node-addon-api/package.json +481 -0
  26. package/node_modules/node-addon-api/tools/README.md +73 -0
  27. package/node_modules/node-addon-api/tools/check-napi.js +99 -0
  28. package/node_modules/node-addon-api/tools/clang-format.js +71 -0
  29. package/node_modules/node-addon-api/tools/conversion.js +301 -0
  30. package/nodes/browse.html +27 -2
  31. package/nodes/connection.html +293 -6
  32. package/nodes/connection.js +160 -23
  33. package/nodes/read.html +42 -3
  34. package/nodes/read.js +51 -12
  35. package/nodes/subscribe.html +57 -5
  36. package/nodes/subscribe.js +386 -99
  37. package/nodes/write.html +29 -2
  38. package/nodes/write.js +2 -1
  39. package/package.json +15 -4
  40. package/prebuilds/linux-x64-musl/opcua_open62541.node +0 -0
  41. package/scripts/build-offline-prebuild.sh +15 -0
  42. package/scripts/build-open62541.sh +14 -4
  43. package/scripts/build-test-server.sh +23 -0
  44. package/scripts/install.js +28 -0
  45. package/scripts/verify-offline-package.js +20 -0
  46. package/scripts/verify-runtime.js +20 -0
  47. package/src/addon.cc +707 -119
  48. package/vendor/open62541-LICENSE +373 -0
  49. package/vendor/open62541-v1.4.16.tar.gz +0 -0
package/src/addon.cc CHANGED
@@ -9,8 +9,13 @@
9
9
  #include <algorithm>
10
10
  #include <atomic>
11
11
  #include <chrono>
12
+ #include <condition_variable>
12
13
  #include <cstdint>
13
14
  #include <deque>
15
+ #include <exception>
16
+ #include <fstream>
17
+ #include <functional>
18
+ #include <limits>
14
19
  #include <map>
15
20
  #include <memory>
16
21
  #include <mutex>
@@ -33,6 +38,78 @@ std::string UaString(const UA_String& value) {
33
38
  return value.data ? std::string(reinterpret_cast<const char*>(value.data), value.length) : "";
34
39
  }
35
40
 
41
+ std::string SecurityPolicyUri(const std::string& policy) {
42
+ if (policy == "None" ||
43
+ policy == "http://opcfoundation.org/UA/SecurityPolicy#None") {
44
+ return "http://opcfoundation.org/UA/SecurityPolicy#None";
45
+ }
46
+ if (policy == "Basic256Sha256" ||
47
+ policy == "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256") {
48
+ return "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256";
49
+ }
50
+ return "";
51
+ }
52
+
53
+ UA_MessageSecurityMode SecurityMode(const std::string& mode) {
54
+ if (mode == "None") return UA_MESSAGESECURITYMODE_NONE;
55
+ if (mode == "Sign") return UA_MESSAGESECURITYMODE_SIGN;
56
+ if (mode == "SignAndEncrypt") return UA_MESSAGESECURITYMODE_SIGNANDENCRYPT;
57
+ return UA_MESSAGESECURITYMODE_INVALID;
58
+ }
59
+
60
+ std::string SecurityModeName(UA_MessageSecurityMode mode) {
61
+ if (mode == UA_MESSAGESECURITYMODE_NONE) return "None";
62
+ if (mode == UA_MESSAGESECURITYMODE_SIGN) return "Sign";
63
+ if (mode == UA_MESSAGESECURITYMODE_SIGNANDENCRYPT) return "SignAndEncrypt";
64
+ return "Invalid";
65
+ }
66
+
67
+ bool LoadFile(const std::string& path, UA_ByteString* output, std::string* error) {
68
+ UA_ByteString_init(output);
69
+ std::ifstream stream(path, std::ios::binary | std::ios::ate);
70
+ if (!stream) {
71
+ *error = "Cannot open OPC UA security file: " + path;
72
+ return false;
73
+ }
74
+ const std::streamoff end = stream.tellg();
75
+ if (end <= 0) {
76
+ *error = "OPC UA security file is empty: " + path;
77
+ return false;
78
+ }
79
+ const size_t size = static_cast<size_t>(end);
80
+ if (!UA_StatusCode_isGood(UA_ByteString_allocBuffer(output, size))) {
81
+ *error = "Cannot allocate OPC UA security file buffer for: " + path;
82
+ return false;
83
+ }
84
+ stream.seekg(0, std::ios::beg);
85
+ stream.read(reinterpret_cast<char*>(output->data), static_cast<std::streamsize>(size));
86
+ if (!stream) {
87
+ UA_ByteString_clear(output);
88
+ *error = "Cannot read OPC UA security file: " + path;
89
+ return false;
90
+ }
91
+ return true;
92
+ }
93
+
94
+ void ClearByteStrings(std::vector<UA_ByteString>* values) {
95
+ for (auto& value : *values) UA_ByteString_clear(&value);
96
+ values->clear();
97
+ }
98
+
99
+ bool LoadFiles(const std::vector<std::string>& paths, std::vector<UA_ByteString>* output,
100
+ std::string* error) {
101
+ output->reserve(paths.size());
102
+ for (const auto& path : paths) {
103
+ UA_ByteString value;
104
+ if (!LoadFile(path, &value, error)) {
105
+ ClearByteStrings(output);
106
+ return false;
107
+ }
108
+ output->push_back(value);
109
+ }
110
+ return true;
111
+ }
112
+
36
113
  std::string NodeIdString(const UA_NodeId& node_id) {
37
114
  UA_String text = UA_STRING_NULL;
38
115
  if (!UA_StatusCode_isGood(UA_NodeId_print(&node_id, &text))) return "";
@@ -202,6 +279,83 @@ bool JsScalarToVariant(Napi::Env env, const Napi::Value& input, const std::strin
202
279
  return false;
203
280
  }
204
281
 
282
+ bool FlattenJsArray(Napi::Env env, const Napi::Value& input, size_t depth,
283
+ std::vector<uint32_t>* dimensions,
284
+ std::vector<Napi::Value>* values, size_t* leaf_depth) {
285
+ if (!input.IsArray()) {
286
+ if (*leaf_depth == std::numeric_limits<size_t>::max()) *leaf_depth = depth;
287
+ if (*leaf_depth != depth) {
288
+ Napi::TypeError::New(env, "OPC UA array value must be rectangular").ThrowAsJavaScriptException();
289
+ return false;
290
+ }
291
+ values->push_back(input);
292
+ return true;
293
+ }
294
+ Napi::Array array = input.As<Napi::Array>();
295
+ if (array.Length() == 0) {
296
+ Napi::TypeError::New(env, "OPC UA array value must not be empty").ThrowAsJavaScriptException();
297
+ return false;
298
+ }
299
+ if (dimensions->size() == depth) dimensions->push_back(array.Length());
300
+ else if ((*dimensions)[depth] != array.Length()) {
301
+ Napi::TypeError::New(env, "OPC UA array value must be rectangular").ThrowAsJavaScriptException();
302
+ return false;
303
+ }
304
+ for (uint32_t index = 0; index < array.Length(); ++index) {
305
+ if (!FlattenJsArray(env, array.Get(index), depth + 1, dimensions, values, leaf_depth)) {
306
+ return false;
307
+ }
308
+ }
309
+ return true;
310
+ }
311
+
312
+ bool JsValueToVariant(Napi::Env env, const Napi::Value& input, const std::string& name,
313
+ UA_Variant* output) {
314
+ if (!input.IsArray()) return JsScalarToVariant(env, input, name, output);
315
+ const UA_DataType* type = DataTypeByName(name);
316
+ if (!type) {
317
+ Napi::TypeError::New(env, "Unsupported OPC UA data type: " + name).ThrowAsJavaScriptException();
318
+ return false;
319
+ }
320
+ std::vector<uint32_t> dimensions;
321
+ std::vector<Napi::Value> values;
322
+ size_t leaf_depth = std::numeric_limits<size_t>::max();
323
+ if (!FlattenJsArray(env, input, 0, &dimensions, &values, &leaf_depth)) return false;
324
+ void* data = UA_Array_new(values.size(), type);
325
+ if (!data) {
326
+ Napi::Error::New(env, "Failed to allocate OPC UA array value").ThrowAsJavaScriptException();
327
+ return false;
328
+ }
329
+ for (size_t index = 0; index < values.size(); ++index) {
330
+ UA_Variant scalar;
331
+ if (!JsScalarToVariant(env, values[index], name, &scalar)) {
332
+ UA_Variant_clear(&scalar);
333
+ UA_Array_delete(data, values.size(), type);
334
+ return false;
335
+ }
336
+ void* target = static_cast<char*>(data) + index * type->memSize;
337
+ const UA_StatusCode status = UA_copy(scalar.data, target, type);
338
+ UA_Variant_clear(&scalar);
339
+ if (!UA_StatusCode_isGood(status)) {
340
+ UA_Array_delete(data, values.size(), type);
341
+ Napi::Error::New(env, "Failed to copy OPC UA array value").ThrowAsJavaScriptException();
342
+ return false;
343
+ }
344
+ }
345
+ UA_Variant_init(output);
346
+ UA_Variant_setArray(output, data, values.size(), type);
347
+ output->arrayDimensions = static_cast<UA_UInt32*>(
348
+ UA_Array_new(dimensions.size(), &UA_TYPES[UA_TYPES_UINT32]));
349
+ if (!output->arrayDimensions) {
350
+ UA_Variant_clear(output);
351
+ Napi::Error::New(env, "Failed to allocate OPC UA array dimensions").ThrowAsJavaScriptException();
352
+ return false;
353
+ }
354
+ output->arrayDimensionsSize = dimensions.size();
355
+ std::copy(dimensions.begin(), dimensions.end(), output->arrayDimensions);
356
+ return true;
357
+ }
358
+
205
359
  std::string GetString(const Napi::Object& object, const char* name, const std::string& fallback = "") {
206
360
  if (!object.Has(name)) return fallback;
207
361
  Napi::Value value = object.Get(name);
@@ -227,6 +381,7 @@ bool GetBool(const Napi::Object& object, const char* name, bool fallback) {
227
381
 
228
382
  struct RawChange {
229
383
  uint32_t monitored_item_id = 0;
384
+ uint32_t client_handle = 0;
230
385
  std::string node_id;
231
386
  UA_DataValue value;
232
387
  std::chrono::steady_clock::time_point enqueued_at;
@@ -261,19 +416,23 @@ struct MonitoredContext {
261
416
  std::shared_ptr<NotificationQueue> notifications;
262
417
  std::string node_id;
263
418
  uint32_t monitored_item_id = 0;
419
+ uint32_t client_handle = 0;
264
420
  };
265
421
 
266
422
  struct SubscriptionContext {
267
423
  uint32_t subscription_id = 0;
268
424
  std::shared_ptr<NotificationQueue> notifications;
269
425
  std::map<uint32_t, std::unique_ptr<MonitoredContext>> monitored;
426
+ std::unordered_map<uint32_t, MonitoredContext*> monitored_by_client_handle;
427
+ uint32_t next_client_handle = 1;
270
428
  };
271
429
 
272
430
  struct OperationLimits {
273
- uint32_t max_nodes_per_read = 0;
274
- uint32_t max_nodes_per_write = 0;
275
- uint32_t max_nodes_per_browse = 0;
276
- uint32_t max_monitored_items_per_call = 0;
431
+ std::atomic<uint32_t> max_nodes_per_read{0};
432
+ std::atomic<uint32_t> max_nodes_per_write{0};
433
+ std::atomic<uint32_t> max_nodes_per_browse{0};
434
+ std::atomic<uint32_t> max_monitored_items_per_call{0};
435
+ std::atomic<uint32_t> max_monitored_items_per_subscription{0};
277
436
  };
278
437
 
279
438
  struct CachedNodeId {
@@ -284,11 +443,34 @@ struct CachedNodeId {
284
443
  CachedNodeId& operator=(const CachedNodeId&) = delete;
285
444
  };
286
445
 
446
+ struct ClientCommand {
447
+ std::function<void()> execute;
448
+ std::mutex completion_mutex;
449
+ std::condition_variable completion_cv;
450
+ bool completed = false;
451
+ std::string rejection;
452
+ };
453
+
287
454
  struct ClientContext {
288
455
  UA_Client* client = nullptr;
456
+ std::string endpoint;
457
+ std::string security_mode;
458
+ std::string security_policy_uri;
459
+ std::string identity_type;
460
+ std::string private_key_password;
289
461
  std::atomic<bool> running{false};
290
462
  std::thread iterate_thread;
291
- std::mutex mutex;
463
+ std::mutex command_mutex;
464
+ std::condition_variable command_cv;
465
+ std::deque<std::shared_ptr<ClientCommand>> commands;
466
+ size_t command_queue_capacity = 256;
467
+ uint32_t command_queue_timeout_ms = 10000;
468
+ size_t command_queue_max_depth = 0;
469
+ std::atomic<uint64_t> commands_enqueued{0};
470
+ std::atomic<uint64_t> commands_completed{0};
471
+ std::atomic<uint64_t> commands_rejected{0};
472
+ std::atomic<uint64_t> commands_timed_out{0};
473
+ std::atomic<uint32_t> commands_active{0};
292
474
  std::map<uint32_t, std::unique_ptr<SubscriptionContext>> subscriptions;
293
475
  OperationLimits operation_limits;
294
476
  std::mutex node_id_cache_mutex;
@@ -315,6 +497,18 @@ struct ClientContext {
315
497
  std::atomic<uint64_t> session_generation{0};
316
498
  };
317
499
 
500
+ #ifdef UA_ENABLE_ENCRYPTION
501
+ UA_StatusCode PrivateKeyPasswordCallback(UA_ClientConfig* config, UA_ByteString* password) {
502
+ auto* context = static_cast<ClientContext*>(config ? config->clientContext : nullptr);
503
+ if (!context || context->private_key_password.empty()) {
504
+ return UA_STATUSCODE_BADSECURITYCHECKSFAILED;
505
+ }
506
+ *password = UA_BYTESTRING_ALLOC(context->private_key_password.c_str());
507
+ return password->length == context->private_key_password.size()
508
+ ? UA_STATUSCODE_GOOD : UA_STATUSCODE_BADOUTOFMEMORY;
509
+ }
510
+ #endif
511
+
318
512
  struct ClientStateEvent {
319
513
  std::string state;
320
514
  std::string reason;
@@ -474,17 +668,14 @@ bool CopyCachedNodeId(ClientContext* context, const std::string& text, UA_NodeId
474
668
  }
475
669
 
476
670
  void ReadOperationLimits(ClientContext* context) {
477
- constexpr size_t count = 4;
671
+ constexpr size_t count = 5;
478
672
  const UA_UInt32 node_ids[count] = {
479
673
  UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERREAD,
480
674
  UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERWRITE,
481
675
  UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERBROWSE,
482
- UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXMONITOREDITEMSPERCALL};
483
- uint32_t* targets[count] = {
484
- &context->operation_limits.max_nodes_per_read,
485
- &context->operation_limits.max_nodes_per_write,
486
- &context->operation_limits.max_nodes_per_browse,
487
- &context->operation_limits.max_monitored_items_per_call};
676
+ UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXMONITOREDITEMSPERCALL,
677
+ 24104}; // Server.ServerCapabilities.MaxMonitoredItemsPerSubscription
678
+ uint32_t values[count] = {};
488
679
  UA_ReadRequest request;
489
680
  UA_ReadRequest_init(&request);
490
681
  request.timestampsToReturn = UA_TIMESTAMPSTORETURN_NEITHER;
@@ -503,11 +694,16 @@ void ReadOperationLimits(ClientContext* context) {
503
694
  const UA_DataValue& value = response.results[index];
504
695
  if (value.hasValue && UA_Variant_isScalar(&value.value) &&
505
696
  value.value.type == &UA_TYPES[UA_TYPES_UINT32] && value.value.data) {
506
- *targets[index] = *static_cast<UA_UInt32*>(value.value.data);
697
+ values[index] = *static_cast<UA_UInt32*>(value.value.data);
507
698
  }
508
699
  }
509
700
  }
510
701
  UA_ReadResponse_clear(&response);
702
+ context->operation_limits.max_nodes_per_read.store(values[0]);
703
+ context->operation_limits.max_nodes_per_write.store(values[1]);
704
+ context->operation_limits.max_nodes_per_browse.store(values[2]);
705
+ context->operation_limits.max_monitored_items_per_call.store(values[3]);
706
+ context->operation_limits.max_monitored_items_per_subscription.store(values[4]);
511
707
  }
512
708
 
513
709
  std::mutex clients_mutex;
@@ -520,11 +716,106 @@ std::shared_ptr<ClientContext> FindClient(uint64_t client_id) {
520
716
  return found == clients.end() ? nullptr : found->second;
521
717
  }
522
718
 
719
+ void CloseNotificationQueue(const std::shared_ptr<NotificationQueue>& queue);
720
+
721
+ void CompleteClientCommand(const std::shared_ptr<ClientCommand>& command,
722
+ std::string rejection = {}) {
723
+ {
724
+ std::lock_guard<std::mutex> lock(command->completion_mutex);
725
+ command->rejection = std::move(rejection);
726
+ command->completed = true;
727
+ }
728
+ command->completion_cv.notify_one();
729
+ }
730
+
731
+ std::string RunClientCommand(const std::shared_ptr<ClientContext>& context,
732
+ std::function<void()> execute) {
733
+ auto command = std::make_shared<ClientCommand>();
734
+ command->execute = std::move(execute);
735
+ {
736
+ std::lock_guard<std::mutex> lock(context->command_mutex);
737
+ if (!context->running.load()) {
738
+ context->commands_rejected.fetch_add(1);
739
+ return "OPC UA client is closing";
740
+ }
741
+ if (context->commands.size() >= context->command_queue_capacity) {
742
+ context->commands_rejected.fetch_add(1);
743
+ return "OPC UA client command queue is full";
744
+ }
745
+ context->commands.push_back(command);
746
+ context->commands_enqueued.fetch_add(1);
747
+ context->command_queue_max_depth =
748
+ std::max(context->command_queue_max_depth, context->commands.size());
749
+ }
750
+ context->command_cv.notify_one();
751
+
752
+ std::unique_lock<std::mutex> lock(command->completion_mutex);
753
+ if (!command->completion_cv.wait_for(
754
+ lock, std::chrono::milliseconds(context->command_queue_timeout_ms),
755
+ [&] { return command->completed; })) {
756
+ lock.unlock();
757
+ bool removed = false;
758
+ {
759
+ std::lock_guard<std::mutex> queue_lock(context->command_mutex);
760
+ auto found = std::find(context->commands.begin(), context->commands.end(), command);
761
+ if (found != context->commands.end()) {
762
+ context->commands.erase(found);
763
+ removed = true;
764
+ }
765
+ }
766
+ if (removed) {
767
+ context->commands_rejected.fetch_add(1);
768
+ context->commands_timed_out.fetch_add(1);
769
+ CompleteClientCommand(command, "OPC UA client command timed out while queued");
770
+ }
771
+ lock.lock();
772
+ command->completion_cv.wait(lock, [&] { return command->completed; });
773
+ }
774
+ return command->rejection;
775
+ }
776
+
777
+ void RejectPendingClientCommands(ClientContext* context, const std::string& reason) {
778
+ std::deque<std::shared_ptr<ClientCommand>> pending;
779
+ {
780
+ std::lock_guard<std::mutex> lock(context->command_mutex);
781
+ pending.swap(context->commands);
782
+ }
783
+ context->commands_rejected.fetch_add(pending.size());
784
+ for (auto& command : pending) CompleteClientCommand(command, reason);
785
+ }
786
+
787
+ void DrainClientCommands(ClientContext* context, size_t maximum) {
788
+ for (size_t index = 0; index < maximum; ++index) {
789
+ std::shared_ptr<ClientCommand> command;
790
+ {
791
+ std::lock_guard<std::mutex> lock(context->command_mutex);
792
+ if (context->commands.empty()) return;
793
+ command = std::move(context->commands.front());
794
+ context->commands.pop_front();
795
+ }
796
+ context->commands_active.store(1);
797
+ try {
798
+ command->execute();
799
+ context->commands_active.store(0);
800
+ context->commands_completed.fetch_add(1);
801
+ CompleteClientCommand(command);
802
+ } catch (const std::exception& error) {
803
+ context->commands_active.store(0);
804
+ context->commands_rejected.fetch_add(1);
805
+ CompleteClientCommand(command, std::string("OPC UA client command failed: ") + error.what());
806
+ } catch (...) {
807
+ context->commands_active.store(0);
808
+ context->commands_rejected.fetch_add(1);
809
+ CompleteClientCommand(command, "OPC UA client command failed unexpectedly");
810
+ }
811
+ if (!context->running.load()) return;
812
+ }
813
+ }
814
+
523
815
  void Iterate(ClientContext* context) {
524
816
  while (context->running.load()) {
525
- {
526
- std::lock_guard<std::mutex> lock(context->mutex);
527
- if (!context->client) return;
817
+ DrainClientCommands(context, 32);
818
+ if (context->client) {
528
819
  UA_Client_run_iterate(context->client, 20);
529
820
  if (context->force_secure_channel_reconnect.exchange(false)) {
530
821
  UA_Client_disconnectSecureChannelAsync(context->client);
@@ -534,7 +825,18 @@ void Iterate(ClientContext* context) {
534
825
  ReadOperationLimits(context);
535
826
  }
536
827
  }
537
- std::this_thread::sleep_for(std::chrono::milliseconds(5));
828
+ std::unique_lock<std::mutex> lock(context->command_mutex);
829
+ if (context->commands.empty() && context->running.load()) {
830
+ context->command_cv.wait_for(lock, std::chrono::milliseconds(5));
831
+ }
832
+ }
833
+ RejectPendingClientCommands(context, "OPC UA client is closing");
834
+ for (auto& entry : context->subscriptions) CloseNotificationQueue(entry.second->notifications);
835
+ context->subscriptions.clear();
836
+ if (context->client) {
837
+ UA_Client_disconnect(context->client);
838
+ UA_Client_delete(context->client);
839
+ context->client = nullptr;
538
840
  }
539
841
  }
540
842
 
@@ -553,16 +855,11 @@ void CloseClient(uint64_t client_id) {
553
855
  context = found->second;
554
856
  clients.erase(found);
555
857
  }
556
- context->running.store(false);
858
+ const std::string shutdown_rejection = RunClientCommand(
859
+ context, [context] { context->running.store(false); });
860
+ if (!shutdown_rejection.empty()) context->running.store(false);
861
+ context->command_cv.notify_all();
557
862
  if (context->iterate_thread.joinable()) context->iterate_thread.join();
558
- std::lock_guard<std::mutex> lock(context->mutex);
559
- for (auto& entry : context->subscriptions) CloseNotificationQueue(entry.second->notifications);
560
- context->subscriptions.clear();
561
- if (context->client) {
562
- UA_Client_disconnect(context->client);
563
- UA_Client_delete(context->client);
564
- context->client = nullptr;
565
- }
566
863
  if (context->has_state_callback.load() &&
567
864
  !context->state_callback_released.exchange(true)) {
568
865
  context->state_callback.Release();
@@ -624,6 +921,7 @@ void DrainNotifications(Napi::Env env, Napi::Function callback, DrainRequest* re
624
921
  Napi::Object result = Napi::Object::New(env);
625
922
  result.Set("nodeId", data->node_id);
626
923
  result.Set("monitoredItemId", data->monitored_item_id);
924
+ result.Set("clientHandle", data->client_handle);
627
925
  result.Set("statusCode", StatusName(data->value.status));
628
926
  if (data->value.hasValue) {
629
927
  result.Set("value", VariantValue(env, &data->value.value));
@@ -714,6 +1012,7 @@ void DataChange(UA_Client*, UA_UInt32, void*, UA_UInt32 monitored_item_id,
714
1012
  if (!monitored || !monitored->notifications || !value) return;
715
1013
  auto change = std::make_unique<RawChange>();
716
1014
  change->monitored_item_id = monitored_item_id;
1015
+ change->client_handle = monitored->client_handle;
717
1016
  change->node_id = monitored->node_id;
718
1017
  UA_DataValue_copy(value, &change->value);
719
1018
  EnqueueNotification(monitored->notifications, std::move(change));
@@ -728,16 +1027,150 @@ class PromiseWorker : public Napi::AsyncWorker {
728
1027
  Napi::Promise::Deferred deferred_;
729
1028
  };
730
1029
 
1030
+ std::string UserTokenTypeName(UA_UserTokenType type) {
1031
+ if (type == UA_USERTOKENTYPE_ANONYMOUS) return "Anonymous";
1032
+ if (type == UA_USERTOKENTYPE_USERNAME) return "Username";
1033
+ if (type == UA_USERTOKENTYPE_CERTIFICATE) return "Certificate";
1034
+ if (type == UA_USERTOKENTYPE_ISSUEDTOKEN) return "IssuedToken";
1035
+ return "Unknown";
1036
+ }
1037
+
1038
+ struct DiscoveredUserToken {
1039
+ std::string policy_id;
1040
+ std::string token_type;
1041
+ std::string security_policy_uri;
1042
+ std::string issued_token_type;
1043
+ std::string issuer_endpoint_url;
1044
+ };
1045
+
1046
+ struct DiscoveredEndpoint {
1047
+ std::string endpoint_url;
1048
+ std::string security_mode;
1049
+ std::string security_policy_uri;
1050
+ std::string server_application_uri;
1051
+ std::string product_uri;
1052
+ uint8_t security_level = 0;
1053
+ std::vector<DiscoveredUserToken> user_identity_tokens;
1054
+ };
1055
+
1056
+ class DiscoverEndpointsWorker : public PromiseWorker {
1057
+ public:
1058
+ DiscoverEndpointsWorker(Napi::Promise::Deferred deferred, std::string endpoint,
1059
+ uint32_t timeout)
1060
+ : PromiseWorker(deferred), endpoint_(std::move(endpoint)), timeout_(timeout) {}
1061
+
1062
+ void Execute() override {
1063
+ if (endpoint_.empty()) { SetError("OPC UA endpoint is required"); return; }
1064
+ UA_Client* client = UA_Client_new();
1065
+ if (!client) { SetError("Failed to create open62541 discovery client"); return; }
1066
+ UA_ClientConfig* config = UA_Client_getConfig(client);
1067
+ config->timeout = timeout_;
1068
+ size_t endpoint_count = 0;
1069
+ UA_EndpointDescription* endpoints = nullptr;
1070
+ const UA_StatusCode status = UA_Client_getEndpoints(
1071
+ client, endpoint_.c_str(), &endpoint_count, &endpoints);
1072
+ if (UA_StatusCode_isGood(status)) {
1073
+ endpoints_.reserve(endpoint_count);
1074
+ for (size_t index = 0; index < endpoint_count; ++index) {
1075
+ const UA_EndpointDescription& source = endpoints[index];
1076
+ DiscoveredEndpoint target;
1077
+ target.endpoint_url = UaString(source.endpointUrl);
1078
+ target.security_mode = SecurityModeName(source.securityMode);
1079
+ target.security_policy_uri = UaString(source.securityPolicyUri);
1080
+ target.server_application_uri = UaString(source.server.applicationUri);
1081
+ target.product_uri = UaString(source.server.productUri);
1082
+ target.security_level = source.securityLevel;
1083
+ target.user_identity_tokens.reserve(source.userIdentityTokensSize);
1084
+ for (size_t token_index = 0; token_index < source.userIdentityTokensSize;
1085
+ ++token_index) {
1086
+ const UA_UserTokenPolicy& token = source.userIdentityTokens[token_index];
1087
+ target.user_identity_tokens.push_back({
1088
+ UaString(token.policyId),
1089
+ UserTokenTypeName(token.tokenType),
1090
+ UaString(token.securityPolicyUri),
1091
+ UaString(token.issuedTokenType),
1092
+ UaString(token.issuerEndpointUrl),
1093
+ });
1094
+ }
1095
+ endpoints_.push_back(std::move(target));
1096
+ }
1097
+ }
1098
+ if (endpoints) {
1099
+ UA_Array_delete(endpoints, endpoint_count, &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]);
1100
+ }
1101
+ UA_Client_delete(client);
1102
+ if (!UA_StatusCode_isGood(status)) {
1103
+ SetError("GetEndpoints failed: " + StatusName(status));
1104
+ }
1105
+ }
1106
+
1107
+ void OnOK() override {
1108
+ Napi::Array output = Napi::Array::New(Env(), endpoints_.size());
1109
+ for (size_t index = 0; index < endpoints_.size(); ++index) {
1110
+ const auto& endpoint = endpoints_[index];
1111
+ Napi::Object value = Napi::Object::New(Env());
1112
+ value.Set("endpointUrl", endpoint.endpoint_url);
1113
+ value.Set("securityMode", endpoint.security_mode);
1114
+ value.Set("securityPolicyUri", endpoint.security_policy_uri);
1115
+ value.Set("securityLevel", endpoint.security_level);
1116
+ value.Set("serverApplicationUri", endpoint.server_application_uri);
1117
+ value.Set("productUri", endpoint.product_uri);
1118
+ Napi::Array tokens = Napi::Array::New(Env(), endpoint.user_identity_tokens.size());
1119
+ for (size_t token_index = 0; token_index < endpoint.user_identity_tokens.size();
1120
+ ++token_index) {
1121
+ const auto& token = endpoint.user_identity_tokens[token_index];
1122
+ Napi::Object token_value = Napi::Object::New(Env());
1123
+ token_value.Set("policyId", token.policy_id);
1124
+ token_value.Set("tokenType", token.token_type);
1125
+ token_value.Set("securityPolicyUri", token.security_policy_uri);
1126
+ token_value.Set("issuedTokenType", token.issued_token_type);
1127
+ token_value.Set("issuerEndpointUrl", token.issuer_endpoint_url);
1128
+ tokens.Set(token_index, token_value);
1129
+ }
1130
+ value.Set("userIdentityTokens", tokens);
1131
+ output.Set(index, value);
1132
+ }
1133
+ deferred_.Resolve(output);
1134
+ }
1135
+
1136
+ private:
1137
+ std::string endpoint_;
1138
+ uint32_t timeout_;
1139
+ std::vector<DiscoveredEndpoint> endpoints_;
1140
+ };
1141
+
1142
+ class ClientCommandWorker : public PromiseWorker {
1143
+ public:
1144
+ ClientCommandWorker(Napi::Promise::Deferred deferred,
1145
+ std::shared_ptr<ClientContext> context)
1146
+ : PromiseWorker(deferred), context_(std::move(context)) {}
1147
+ void Execute() override final {
1148
+ const std::string rejection = RunClientCommand(
1149
+ context_, [this] { ExecuteClientCommand(); });
1150
+ if (!rejection.empty()) SetError(rejection);
1151
+ }
1152
+ protected:
1153
+ virtual void ExecuteClientCommand() = 0;
1154
+ std::shared_ptr<ClientContext> context_;
1155
+ };
1156
+
731
1157
  struct ConnectionOptions {
732
1158
  std::string endpoint;
733
1159
  std::string security_mode;
734
1160
  std::string security_policy;
735
1161
  std::string user;
736
1162
  std::string password;
1163
+ std::string certificate;
1164
+ std::string private_key;
1165
+ std::string private_key_password;
1166
+ std::string application_uri;
1167
+ std::vector<std::string> trusted_certificates;
1168
+ std::vector<std::string> revocation_list;
737
1169
  uint32_t timeout = 10000;
738
1170
  uint32_t session_timeout = 60000;
739
1171
  uint32_t connectivity_check_interval = 5000;
740
1172
  uint32_t node_id_cache_size = 100000;
1173
+ uint32_t command_queue_size = 256;
741
1174
  };
742
1175
 
743
1176
  class ConnectWorker : public PromiseWorker {
@@ -749,16 +1182,123 @@ class ConnectWorker : public PromiseWorker {
749
1182
  has_state_callback_(has_state_callback) {}
750
1183
  void Execute() override {
751
1184
  if (options_.endpoint.empty()) { SetError("OPC UA endpoint is required"); return; }
752
- if (!options_.security_mode.empty() && options_.security_mode != "None") {
753
- SetError("Sign and SignAndEncrypt require an encryption-enabled open62541 build"); return;
1185
+ const UA_MessageSecurityMode security_mode = SecurityMode(options_.security_mode);
1186
+ const std::string security_policy_uri = SecurityPolicyUri(options_.security_policy);
1187
+ const bool policy_none = security_policy_uri ==
1188
+ "http://opcfoundation.org/UA/SecurityPolicy#None";
1189
+ if (security_mode == UA_MESSAGESECURITYMODE_INVALID) {
1190
+ SetError("Unsupported OPC UA security mode: " + options_.security_mode); return;
1191
+ }
1192
+ if (security_policy_uri.empty()) {
1193
+ SetError("Unsupported OPC UA security policy: " + options_.security_policy); return;
1194
+ }
1195
+ if (security_mode == UA_MESSAGESECURITYMODE_NONE && !policy_none) {
1196
+ SetError("SecurityMode None requires SecurityPolicy None"); return;
1197
+ }
1198
+ if (security_mode != UA_MESSAGESECURITYMODE_NONE) {
1199
+ if (policy_none) {
1200
+ SetError(options_.security_mode + " requires a non-None security policy"); return;
1201
+ }
1202
+ if (options_.certificate.empty() || options_.private_key.empty()) {
1203
+ SetError("Secure OPC UA connections require a client certificate and private key"); return;
1204
+ }
1205
+ if (options_.trusted_certificates.empty()) {
1206
+ SetError("Secure OPC UA connections require at least one trusted server or CA certificate"); return;
1207
+ }
754
1208
  }
755
1209
  context_ = std::make_shared<ClientContext>();
1210
+ context_->endpoint = options_.endpoint;
1211
+ context_->security_mode = options_.security_mode;
1212
+ context_->security_policy_uri = security_policy_uri;
1213
+ context_->identity_type = options_.user.empty() ? "Anonymous" : "Username";
1214
+ context_->private_key_password = options_.private_key_password;
756
1215
  context_->state_callback = state_callback_;
757
1216
  context_->has_state_callback.store(has_state_callback_);
1217
+ context_->node_id_cache_capacity = options_.node_id_cache_size;
1218
+ context_->command_queue_capacity = std::max<uint32_t>(1, options_.command_queue_size);
1219
+ context_->command_queue_timeout_ms = std::max<uint32_t>(1000, options_.timeout);
1220
+ context_->running.store(true);
1221
+ context_->iterate_thread = std::thread(Iterate, context_.get());
1222
+ const std::string rejection = RunClientCommand(
1223
+ context_, [this] { ConnectClient(); });
1224
+ if (!rejection.empty()) SetError(rejection);
1225
+ if (!connected_) {
1226
+ context_->running.store(false);
1227
+ context_->command_cv.notify_all();
1228
+ if (context_->iterate_thread.joinable()) context_->iterate_thread.join();
1229
+ return;
1230
+ }
1231
+ client_id_ = next_client_id.fetch_add(1);
1232
+ {
1233
+ std::lock_guard<std::mutex> lock(clients_mutex);
1234
+ clients[client_id_] = context_;
1235
+ }
1236
+ transferred_ = true;
1237
+ }
1238
+ void OnOK() override { deferred_.Resolve(Napi::Number::New(Env(), static_cast<double>(client_id_))); }
1239
+ void OnError(const Napi::Error& error) override {
1240
+ if (has_state_callback_ && !transferred_) state_callback_.Release();
1241
+ PromiseWorker::OnError(error);
1242
+ }
1243
+ private:
1244
+ void ConnectClient() {
758
1245
  context_->client = UA_Client_new();
759
1246
  if (!context_->client) { SetError("Failed to create open62541 client"); return; }
760
1247
  UA_ClientConfig* config = UA_Client_getConfig(context_->client);
761
- UA_ClientConfig_setDefault(config);
1248
+ const UA_MessageSecurityMode security_mode = SecurityMode(options_.security_mode);
1249
+ const std::string security_policy_uri = SecurityPolicyUri(options_.security_policy);
1250
+ UA_StatusCode config_status = UA_STATUSCODE_GOOD;
1251
+ if (security_mode == UA_MESSAGESECURITYMODE_NONE) {
1252
+ config_status = UA_ClientConfig_setDefault(config);
1253
+ } else {
1254
+ #ifdef UA_ENABLE_ENCRYPTION
1255
+ UA_ByteString certificate;
1256
+ UA_ByteString private_key;
1257
+ std::vector<UA_ByteString> trust_list;
1258
+ std::vector<UA_ByteString> revocation_list;
1259
+ std::string file_error;
1260
+ if (!LoadFile(options_.certificate, &certificate, &file_error)) {
1261
+ SetError(file_error); return;
1262
+ }
1263
+ if (!LoadFile(options_.private_key, &private_key, &file_error)) {
1264
+ UA_ByteString_clear(&certificate);
1265
+ SetError(file_error); return;
1266
+ }
1267
+ if (!LoadFiles(options_.trusted_certificates, &trust_list, &file_error) ||
1268
+ !LoadFiles(options_.revocation_list, &revocation_list, &file_error)) {
1269
+ UA_ByteString_clear(&certificate);
1270
+ UA_ByteString_clear(&private_key);
1271
+ ClearByteStrings(&trust_list);
1272
+ ClearByteStrings(&revocation_list);
1273
+ SetError(file_error); return;
1274
+ }
1275
+ config->clientContext = context_.get();
1276
+ config->privateKeyPasswordCallback = PrivateKeyPasswordCallback;
1277
+ config_status = UA_ClientConfig_setDefaultEncryption(
1278
+ config, certificate, private_key,
1279
+ trust_list.data(), trust_list.size(),
1280
+ revocation_list.data(), revocation_list.size());
1281
+ std::fill(context_->private_key_password.begin(),
1282
+ context_->private_key_password.end(), '\0');
1283
+ context_->private_key_password.clear();
1284
+ UA_ByteString_clear(&certificate);
1285
+ UA_ByteString_clear(&private_key);
1286
+ ClearByteStrings(&trust_list);
1287
+ ClearByteStrings(&revocation_list);
1288
+ #else
1289
+ SetError("The native open62541 addon was built without encryption support"); return;
1290
+ #endif
1291
+ }
1292
+ if (!UA_StatusCode_isGood(config_status)) {
1293
+ SetError("Configure OPC UA security failed: " + StatusName(config_status)); return;
1294
+ }
1295
+ config->securityMode = security_mode;
1296
+ UA_String_clear(&config->securityPolicyUri);
1297
+ config->securityPolicyUri = UA_STRING_ALLOC(security_policy_uri.c_str());
1298
+ if (!options_.application_uri.empty()) {
1299
+ UA_String_clear(&config->clientDescription.applicationUri);
1300
+ config->clientDescription.applicationUri = UA_STRING_ALLOC(options_.application_uri.c_str());
1301
+ }
762
1302
  config->timeout = options_.timeout;
763
1303
  config->requestedSessionTimeout = options_.session_timeout;
764
1304
  config->connectivityCheckInterval = options_.connectivity_check_interval;
@@ -773,28 +1313,18 @@ class ConnectWorker : public PromiseWorker {
773
1313
  : UA_Client_connectUsername(context_->client, options_.endpoint.c_str(), options_.user.c_str(), options_.password.c_str());
774
1314
  if (!UA_StatusCode_isGood(status)) {
775
1315
  SetError("Connect failed: " + StatusName(status));
776
- UA_Client_delete(context_->client); context_->client = nullptr; return;
1316
+ return;
777
1317
  }
778
- context_->node_id_cache_capacity = options_.node_id_cache_size;
779
1318
  ReadOperationLimits(context_.get());
780
1319
  context_->refresh_operation_limits.store(false);
781
- client_id_ = next_client_id.fetch_add(1);
782
- context_->running.store(true);
783
- context_->iterate_thread = std::thread(Iterate, context_.get());
784
- std::lock_guard<std::mutex> lock(clients_mutex);
785
- clients[client_id_] = context_;
1320
+ connected_ = true;
786
1321
  }
787
- void OnOK() override { deferred_.Resolve(Napi::Number::New(Env(), static_cast<double>(client_id_))); }
788
- void OnError(const Napi::Error& error) override {
789
- if (has_state_callback_ && !transferred_) state_callback_.Release();
790
- PromiseWorker::OnError(error);
791
- }
792
- private:
793
1322
  ConnectionOptions options_;
794
1323
  std::shared_ptr<ClientContext> context_;
795
1324
  Napi::ThreadSafeFunction state_callback_;
796
1325
  bool has_state_callback_ = false;
797
1326
  bool transferred_ = false;
1327
+ bool connected_ = false;
798
1328
  uint64_t client_id_ = 0;
799
1329
  };
800
1330
 
@@ -808,42 +1338,51 @@ class DisconnectWorker : public PromiseWorker {
808
1338
  uint64_t client_id_;
809
1339
  };
810
1340
 
811
- class ReadManyWorker : public PromiseWorker {
1341
+ struct ReadInput {
1342
+ std::string node_id;
1343
+ std::string index_range;
1344
+ };
1345
+
1346
+ class ReadManyWorker : public ClientCommandWorker {
812
1347
  public:
813
1348
  ReadManyWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
814
- std::vector<std::string> node_ids)
815
- : PromiseWorker(deferred), context_(std::move(context)), node_ids_(std::move(node_ids)) {
1349
+ std::vector<ReadInput> inputs)
1350
+ : ClientCommandWorker(deferred, std::move(context)), inputs_(std::move(inputs)) {
816
1351
  UA_ReadResponse_init(&response_);
817
1352
  }
818
1353
  ~ReadManyWorker() override { UA_ReadResponse_clear(&response_); }
819
- void Execute() override {
1354
+ void ExecuteClientCommand() override {
820
1355
  UA_ReadRequest request;
821
1356
  UA_ReadRequest_init(&request);
822
1357
  request.timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH;
823
- request.nodesToReadSize = node_ids_.size();
824
- request.nodesToRead = static_cast<UA_ReadValueId*>(UA_Array_new(node_ids_.size(), &UA_TYPES[UA_TYPES_READVALUEID]));
1358
+ request.nodesToReadSize = inputs_.size();
1359
+ request.nodesToRead = static_cast<UA_ReadValueId*>(UA_Array_new(inputs_.size(), &UA_TYPES[UA_TYPES_READVALUEID]));
825
1360
  if (!request.nodesToRead) { SetError("Failed to allocate batch read request"); return; }
826
- for (size_t index = 0; index < node_ids_.size(); ++index) {
827
- if (!CopyCachedNodeId(context_.get(), node_ids_[index], &request.nodesToRead[index].nodeId)) {
828
- UA_ReadRequest_clear(&request); SetError("Invalid NodeId: " + node_ids_[index]); return;
1361
+ for (size_t index = 0; index < inputs_.size(); ++index) {
1362
+ if (!CopyCachedNodeId(context_.get(), inputs_[index].node_id, &request.nodesToRead[index].nodeId)) {
1363
+ UA_ReadRequest_clear(&request); SetError("Invalid NodeId: " + inputs_[index].node_id); return;
829
1364
  }
830
1365
  request.nodesToRead[index].attributeId = UA_ATTRIBUTEID_VALUE;
1366
+ if (!inputs_[index].index_range.empty()) {
1367
+ request.nodesToRead[index].indexRange = UA_STRING_ALLOC(inputs_[index].index_range.c_str());
1368
+ if (!request.nodesToRead[index].indexRange.data) {
1369
+ UA_ReadRequest_clear(&request); SetError("Failed to allocate IndexRange"); return;
1370
+ }
1371
+ }
831
1372
  }
832
- {
833
- std::lock_guard<std::mutex> lock(context_->mutex);
834
- if (!context_->client) { UA_ReadRequest_clear(&request); SetError("Client disconnected"); return; }
835
- response_ = UA_Client_Service_read(context_->client, request);
836
- }
1373
+ if (!context_->client) { UA_ReadRequest_clear(&request); SetError("Client disconnected"); return; }
1374
+ response_ = UA_Client_Service_read(context_->client, request);
837
1375
  UA_ReadRequest_clear(&request);
838
1376
  if (!UA_StatusCode_isGood(response_.responseHeader.serviceResult)) {
839
1377
  SetError("Batch read failed: " + StatusName(response_.responseHeader.serviceResult));
840
1378
  }
841
1379
  }
842
1380
  void OnOK() override {
843
- Napi::Array output = Napi::Array::New(Env(), node_ids_.size());
844
- for (size_t index = 0; index < node_ids_.size(); ++index) {
1381
+ Napi::Array output = Napi::Array::New(Env(), inputs_.size());
1382
+ for (size_t index = 0; index < inputs_.size(); ++index) {
845
1383
  Napi::Object item = Napi::Object::New(Env());
846
- item.Set("nodeId", node_ids_[index]);
1384
+ item.Set("nodeId", inputs_[index].node_id);
1385
+ if (!inputs_[index].index_range.empty()) item.Set("indexRange", inputs_[index].index_range);
847
1386
  if (index < response_.resultsSize) {
848
1387
  const UA_DataValue& value = response_.results[index];
849
1388
  item.Set("statusCode", StatusName(value.status));
@@ -856,28 +1395,28 @@ class ReadManyWorker : public PromiseWorker {
856
1395
  deferred_.Resolve(output);
857
1396
  }
858
1397
  private:
859
- std::shared_ptr<ClientContext> context_;
860
- std::vector<std::string> node_ids_;
1398
+ std::vector<ReadInput> inputs_;
861
1399
  UA_ReadResponse response_;
862
1400
  };
863
1401
 
864
1402
  struct WriteInput {
865
1403
  std::string node_id;
866
1404
  std::string data_type;
1405
+ std::string index_range;
867
1406
  UA_Variant value;
868
1407
  WriteInput() { UA_Variant_init(&value); }
869
1408
  ~WriteInput() { UA_Variant_clear(&value); }
870
1409
  };
871
1410
 
872
- class WriteManyWorker : public PromiseWorker {
1411
+ class WriteManyWorker : public ClientCommandWorker {
873
1412
  public:
874
1413
  WriteManyWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
875
1414
  std::vector<std::unique_ptr<WriteInput>> inputs)
876
- : PromiseWorker(deferred), context_(std::move(context)), inputs_(std::move(inputs)) {
1415
+ : ClientCommandWorker(deferred, std::move(context)), inputs_(std::move(inputs)) {
877
1416
  UA_WriteResponse_init(&response_);
878
1417
  }
879
1418
  ~WriteManyWorker() override { UA_WriteResponse_clear(&response_); }
880
- void Execute() override {
1419
+ void ExecuteClientCommand() override {
881
1420
  UA_WriteRequest request;
882
1421
  UA_WriteRequest_init(&request);
883
1422
  request.nodesToWriteSize = inputs_.size();
@@ -889,14 +1428,17 @@ class WriteManyWorker : public PromiseWorker {
889
1428
  UA_WriteRequest_clear(&request); SetError("Invalid NodeId: " + inputs_[index]->node_id); return;
890
1429
  }
891
1430
  target.attributeId = UA_ATTRIBUTEID_VALUE;
1431
+ if (!inputs_[index]->index_range.empty()) {
1432
+ target.indexRange = UA_STRING_ALLOC(inputs_[index]->index_range.c_str());
1433
+ if (!target.indexRange.data) {
1434
+ UA_WriteRequest_clear(&request); SetError("Failed to allocate IndexRange"); return;
1435
+ }
1436
+ }
892
1437
  target.value.hasValue = true;
893
1438
  UA_Variant_copy(&inputs_[index]->value, &target.value.value);
894
1439
  }
895
- {
896
- std::lock_guard<std::mutex> lock(context_->mutex);
897
- if (!context_->client) { UA_WriteRequest_clear(&request); SetError("Client disconnected"); return; }
898
- response_ = UA_Client_Service_write(context_->client, request);
899
- }
1440
+ if (!context_->client) { UA_WriteRequest_clear(&request); SetError("Client disconnected"); return; }
1441
+ response_ = UA_Client_Service_write(context_->client, request);
900
1442
  UA_WriteRequest_clear(&request);
901
1443
  if (!UA_StatusCode_isGood(response_.responseHeader.serviceResult)) {
902
1444
  SetError("Batch write failed: " + StatusName(response_.responseHeader.serviceResult));
@@ -907,13 +1449,13 @@ class WriteManyWorker : public PromiseWorker {
907
1449
  for (size_t index = 0; index < inputs_.size(); ++index) {
908
1450
  Napi::Object item = Napi::Object::New(Env());
909
1451
  item.Set("nodeId", inputs_[index]->node_id);
1452
+ if (!inputs_[index]->index_range.empty()) item.Set("indexRange", inputs_[index]->index_range);
910
1453
  item.Set("statusCode", index < response_.resultsSize ? StatusName(response_.results[index]) : "BadUnexpectedError");
911
1454
  output.Set(index, item);
912
1455
  }
913
1456
  deferred_.Resolve(output);
914
1457
  }
915
1458
  private:
916
- std::shared_ptr<ClientContext> context_;
917
1459
  std::vector<std::unique_ptr<WriteInput>> inputs_;
918
1460
  UA_WriteResponse response_;
919
1461
  };
@@ -943,17 +1485,16 @@ struct BrowseOptions {
943
1485
  std::set<uint16_t> namespaces;
944
1486
  };
945
1487
 
946
- class BrowseWorker : public PromiseWorker {
1488
+ class BrowseWorker : public ClientCommandWorker {
947
1489
  public:
948
1490
  BrowseWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
949
1491
  BrowseOptions options)
950
- : PromiseWorker(deferred), context_(std::move(context)), options_(std::move(options)) {}
951
- void Execute() override {
1492
+ : ClientCommandWorker(deferred, std::move(context)), options_(std::move(options)) {}
1493
+ void ExecuteClientCommand() override {
952
1494
  UA_NodeId root;
953
1495
  UA_NodeId_init(&root);
954
1496
  if (!CopyCachedNodeId(context_.get(), options_.root_node_id, &root)) { SetError("Invalid root NodeId: " + options_.root_node_id); return; }
955
1497
  UA_NodeId_clear(&root);
956
- std::lock_guard<std::mutex> lock(context_->mutex);
957
1498
  if (!context_->client) { SetError("Client disconnected"); return; }
958
1499
  if (!BrowseTree()) return;
959
1500
  ReadAttributes();
@@ -1056,7 +1597,7 @@ class BrowseWorker : public PromiseWorker {
1056
1597
  void ReadAttributes() {
1057
1598
  const size_t attrs = 5;
1058
1599
  size_t batch = std::max<size_t>(1, options_.attribute_batch_size) * attrs;
1059
- uint32_t read_limit = context_->operation_limits.max_nodes_per_read;
1600
+ uint32_t read_limit = context_->operation_limits.max_nodes_per_read.load();
1060
1601
  if (read_limit > 0) batch = std::min<size_t>(batch, read_limit);
1061
1602
  const size_t total_attributes = points_.size() * attrs;
1062
1603
  const UA_UInt32 ids[attrs] = {
@@ -1104,19 +1645,18 @@ class BrowseWorker : public PromiseWorker {
1104
1645
  }
1105
1646
  }
1106
1647
 
1107
- std::shared_ptr<ClientContext> context_;
1108
1648
  BrowseOptions options_;
1109
1649
  std::vector<BrowsePoint> points_;
1110
1650
  };
1111
1651
 
1112
- class CreateSubscriptionWorker : public PromiseWorker {
1652
+ class CreateSubscriptionWorker : public ClientCommandWorker {
1113
1653
  public:
1114
1654
  CreateSubscriptionWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
1115
1655
  Napi::Function callback, uint32_t queue_size, double publishing_interval,
1116
1656
  uint32_t lifetime, uint32_t keepalive, uint32_t max_notifications,
1117
1657
  uint32_t callback_batch_size, std::string queue_mode,
1118
1658
  std::string overflow_policy)
1119
- : PromiseWorker(deferred), context_(std::move(context)), publishing_interval_(publishing_interval),
1659
+ : ClientCommandWorker(deferred, std::move(context)), publishing_interval_(publishing_interval),
1120
1660
  lifetime_(lifetime), keepalive_(keepalive), max_notifications_(max_notifications),
1121
1661
  queue_size_(std::max<uint32_t>(1, queue_size)),
1122
1662
  callback_batch_size_(std::max<uint32_t>(1, callback_batch_size)),
@@ -1124,13 +1664,12 @@ class CreateSubscriptionWorker : public PromiseWorker {
1124
1664
  overflow_policy_(overflow_policy == "drop-newest" ? "drop-newest" : "drop-oldest") {
1125
1665
  callback_ = Napi::ThreadSafeFunction::New(Env(), callback, "OpcUaDataChangeBatches", 1, 1);
1126
1666
  }
1127
- void Execute() override {
1667
+ void ExecuteClientCommand() override {
1128
1668
  UA_CreateSubscriptionRequest request = UA_CreateSubscriptionRequest_default();
1129
1669
  request.requestedPublishingInterval = publishing_interval_;
1130
1670
  request.requestedLifetimeCount = lifetime_;
1131
1671
  request.requestedMaxKeepAliveCount = keepalive_;
1132
1672
  request.maxNotificationsPerPublish = max_notifications_;
1133
- std::lock_guard<std::mutex> lock(context_->mutex);
1134
1673
  if (!context_->client) { SetError("Client disconnected"); return; }
1135
1674
  UA_CreateSubscriptionResponse response = UA_Client_Subscriptions_create(context_->client, request, nullptr, nullptr, nullptr);
1136
1675
  status_ = response.responseHeader.serviceResult; subscription_id_ = response.subscriptionId;
@@ -1149,27 +1688,28 @@ class CreateSubscriptionWorker : public PromiseWorker {
1149
1688
  void OnOK() override { Napi::Object result = Napi::Object::New(Env()); result.Set("subscriptionId", subscription_id_); result.Set("statusCode", StatusName(status_)); deferred_.Resolve(result); }
1150
1689
  void OnError(const Napi::Error& error) override { if (!transferred_) callback_.Release(); PromiseWorker::OnError(error); }
1151
1690
  private:
1152
- std::shared_ptr<ClientContext> context_; Napi::ThreadSafeFunction callback_; bool transferred_ = false;
1691
+ Napi::ThreadSafeFunction callback_; bool transferred_ = false;
1153
1692
  double publishing_interval_; uint32_t lifetime_; uint32_t keepalive_; uint32_t max_notifications_;
1154
1693
  uint32_t queue_size_; uint32_t callback_batch_size_; std::string queue_mode_; std::string overflow_policy_;
1155
1694
  uint32_t subscription_id_ = 0; UA_StatusCode status_ = UA_STATUSCODE_GOOD;
1156
1695
  };
1157
1696
 
1158
1697
  struct MonitorInput { std::string node_id; double sampling_interval; uint32_t queue_size; bool discard_oldest; std::string deadband_type; double deadband_value; };
1159
- struct MonitorResult { std::string node_id; uint32_t monitored_item_id = 0; UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; };
1698
+ struct MonitorResult { std::string node_id; uint32_t monitored_item_id = 0; uint32_t client_handle = 0; UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; };
1160
1699
 
1161
- class AddMonitoredWorker : public PromiseWorker {
1700
+ class AddMonitoredWorker : public ClientCommandWorker {
1162
1701
  public:
1163
1702
  AddMonitoredWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
1164
- uint32_t subscription_id, std::vector<MonitorInput> inputs)
1165
- : PromiseWorker(deferred), context_(std::move(context)), subscription_id_(subscription_id), inputs_(std::move(inputs)) {}
1166
- void Execute() override {
1167
- std::lock_guard<std::mutex> lock(context_->mutex);
1703
+ uint32_t subscription_id, std::vector<MonitorInput> inputs,
1704
+ UA_TimestampsToReturn timestamps_to_return)
1705
+ : ClientCommandWorker(deferred, std::move(context)), subscription_id_(subscription_id),
1706
+ inputs_(std::move(inputs)), timestamps_to_return_(timestamps_to_return) {}
1707
+ void ExecuteClientCommand() override {
1168
1708
  auto subscription_it = context_->subscriptions.find(subscription_id_);
1169
1709
  if (!context_->client || subscription_it == context_->subscriptions.end()) { SetError("Subscription not found"); return; }
1170
1710
  auto* subscription = subscription_it->second.get();
1171
1711
  UA_CreateMonitoredItemsRequest request; UA_CreateMonitoredItemsRequest_init(&request);
1172
- request.subscriptionId = subscription_id_; request.timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH;
1712
+ request.subscriptionId = subscription_id_; request.timestampsToReturn = timestamps_to_return_;
1173
1713
  request.itemsToCreateSize = inputs_.size();
1174
1714
  request.itemsToCreate = static_cast<UA_MonitoredItemCreateRequest*>(UA_Array_new(inputs_.size(), &UA_TYPES[UA_TYPES_MONITOREDITEMCREATEREQUEST]));
1175
1715
  std::vector<std::unique_ptr<MonitoredContext>> contexts(inputs_.size());
@@ -1180,7 +1720,13 @@ class AddMonitoredWorker : public PromiseWorker {
1180
1720
  auto& item = request.itemsToCreate[index];
1181
1721
  if (!CopyCachedNodeId(context_.get(), inputs_[index].node_id, &item.itemToMonitor.nodeId)) { UA_CreateMonitoredItemsRequest_clear(&request); SetError("Invalid NodeId: " + inputs_[index].node_id); return; }
1182
1722
  item.itemToMonitor.attributeId = UA_ATTRIBUTEID_VALUE; item.monitoringMode = UA_MONITORINGMODE_REPORTING;
1183
- item.requestedParameters.clientHandle = static_cast<uint32_t>(index + 1);
1723
+ uint32_t client_handle = 0;
1724
+ do {
1725
+ client_handle = subscription->next_client_handle++;
1726
+ } while (client_handle == 0 ||
1727
+ subscription->monitored_by_client_handle.find(client_handle) !=
1728
+ subscription->monitored_by_client_handle.end());
1729
+ item.requestedParameters.clientHandle = client_handle;
1184
1730
  item.requestedParameters.samplingInterval = inputs_[index].sampling_interval;
1185
1731
  item.requestedParameters.queueSize = inputs_[index].queue_size;
1186
1732
  item.requestedParameters.discardOldest = inputs_[index].discard_oldest;
@@ -1193,7 +1739,7 @@ class AddMonitoredWorker : public PromiseWorker {
1193
1739
  item.requestedParameters.filter.content.decoded.type = &UA_TYPES[UA_TYPES_DATACHANGEFILTER];
1194
1740
  item.requestedParameters.filter.content.decoded.data = filters[index].get();
1195
1741
  }
1196
- contexts[index] = std::make_unique<MonitoredContext>(); contexts[index]->notifications = subscription->notifications; contexts[index]->node_id = inputs_[index].node_id; context_ptrs[index] = contexts[index].get();
1742
+ contexts[index] = std::make_unique<MonitoredContext>(); contexts[index]->notifications = subscription->notifications; contexts[index]->node_id = inputs_[index].node_id; contexts[index]->client_handle = client_handle; context_ptrs[index] = contexts[index].get();
1197
1743
  }
1198
1744
  UA_CreateMonitoredItemsResponse response = UA_Client_MonitoredItems_createDataChanges(context_->client, request, context_ptrs.data(), callbacks.data(), nullptr);
1199
1745
  for (size_t index = 0; index < filters.size(); ++index) {
@@ -1202,46 +1748,46 @@ class AddMonitoredWorker : public PromiseWorker {
1202
1748
  UA_CreateMonitoredItemsRequest_clear(&request);
1203
1749
  if (!UA_StatusCode_isGood(response.responseHeader.serviceResult)) { UA_CreateMonitoredItemsResponse_clear(&response); SetError("Create monitored items failed: " + StatusName(response.responseHeader.serviceResult)); return; }
1204
1750
  for (size_t index = 0; index < inputs_.size(); ++index) {
1205
- MonitorResult result; result.node_id = inputs_[index].node_id;
1751
+ MonitorResult result; result.node_id = inputs_[index].node_id; result.client_handle = contexts[index]->client_handle;
1206
1752
  if (index < response.resultsSize) {
1207
1753
  result.status = response.results[index].statusCode; result.monitored_item_id = response.results[index].monitoredItemId;
1208
- if (UA_StatusCode_isGood(result.status)) { contexts[index]->monitored_item_id = result.monitored_item_id; subscription->monitored[result.monitored_item_id] = std::move(contexts[index]); }
1754
+ if (UA_StatusCode_isGood(result.status)) { contexts[index]->monitored_item_id = result.monitored_item_id; auto* monitored_context = contexts[index].get(); subscription->monitored_by_client_handle[monitored_context->client_handle] = monitored_context; subscription->monitored[result.monitored_item_id] = std::move(contexts[index]); }
1209
1755
  }
1210
1756
  results_.push_back(result);
1211
1757
  }
1212
1758
  UA_CreateMonitoredItemsResponse_clear(&response);
1213
1759
  }
1214
- void OnOK() override { Napi::Array output = Napi::Array::New(Env(), results_.size()); for (size_t index = 0; index < results_.size(); ++index) { Napi::Object item = Napi::Object::New(Env()); item.Set("nodeId", results_[index].node_id); item.Set("monitoredItemId", results_[index].monitored_item_id); item.Set("statusCode", StatusName(results_[index].status)); output.Set(index, item); } deferred_.Resolve(output); }
1760
+ void OnOK() override { Napi::Array output = Napi::Array::New(Env(), results_.size()); for (size_t index = 0; index < results_.size(); ++index) { Napi::Object item = Napi::Object::New(Env()); item.Set("nodeId", results_[index].node_id); item.Set("monitoredItemId", results_[index].monitored_item_id); item.Set("clientHandle", results_[index].client_handle); item.Set("statusCode", StatusName(results_[index].status)); output.Set(index, item); } deferred_.Resolve(output); }
1215
1761
  private:
1216
- std::shared_ptr<ClientContext> context_; uint32_t subscription_id_; std::vector<MonitorInput> inputs_; std::vector<MonitorResult> results_;
1762
+ uint32_t subscription_id_; std::vector<MonitorInput> inputs_; std::vector<MonitorResult> results_;
1763
+ UA_TimestampsToReturn timestamps_to_return_;
1217
1764
  };
1218
1765
 
1219
- class RemoveMonitoredWorker : public PromiseWorker {
1766
+ class RemoveMonitoredWorker : public ClientCommandWorker {
1220
1767
  public:
1221
1768
  RemoveMonitoredWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
1222
1769
  uint32_t subscription_id, std::vector<uint32_t> ids)
1223
- : PromiseWorker(deferred), context_(std::move(context)), subscription_id_(subscription_id), ids_(std::move(ids)) { UA_DeleteMonitoredItemsResponse_init(&response_); }
1770
+ : ClientCommandWorker(deferred, std::move(context)), subscription_id_(subscription_id), ids_(std::move(ids)) { UA_DeleteMonitoredItemsResponse_init(&response_); }
1224
1771
  ~RemoveMonitoredWorker() override { UA_DeleteMonitoredItemsResponse_clear(&response_); }
1225
- void Execute() override {
1226
- std::lock_guard<std::mutex> lock(context_->mutex); auto sub = context_->subscriptions.find(subscription_id_);
1772
+ void ExecuteClientCommand() override {
1773
+ auto sub = context_->subscriptions.find(subscription_id_);
1227
1774
  if (!context_->client || sub == context_->subscriptions.end()) { SetError("Subscription not found"); return; }
1228
1775
  UA_DeleteMonitoredItemsRequest request; UA_DeleteMonitoredItemsRequest_init(&request); request.subscriptionId = subscription_id_; request.monitoredItemIdsSize = ids_.size(); request.monitoredItemIds = static_cast<UA_UInt32*>(UA_Array_new(ids_.size(), &UA_TYPES[UA_TYPES_UINT32]));
1229
1776
  std::copy(ids_.begin(), ids_.end(), request.monitoredItemIds); response_ = UA_Client_MonitoredItems_delete(context_->client, request); UA_DeleteMonitoredItemsRequest_clear(&request);
1230
1777
  if (!UA_StatusCode_isGood(response_.responseHeader.serviceResult)) { SetError("Delete monitored items failed: " + StatusName(response_.responseHeader.serviceResult)); return; }
1231
- for (size_t index = 0; index < ids_.size() && index < response_.resultsSize; ++index) if (UA_StatusCode_isGood(response_.results[index])) sub->second->monitored.erase(ids_[index]);
1778
+ for (size_t index = 0; index < ids_.size() && index < response_.resultsSize; ++index) if (UA_StatusCode_isGood(response_.results[index])) { auto monitored = sub->second->monitored.find(ids_[index]); if (monitored != sub->second->monitored.end()) { sub->second->monitored_by_client_handle.erase(monitored->second->client_handle); sub->second->monitored.erase(monitored); } }
1232
1779
  }
1233
1780
  void OnOK() override { Napi::Array output = Napi::Array::New(Env(), ids_.size()); for (size_t index = 0; index < ids_.size(); ++index) { Napi::Object item = Napi::Object::New(Env()); item.Set("monitoredItemId", ids_[index]); item.Set("statusCode", index < response_.resultsSize ? StatusName(response_.results[index]) : "BadUnexpectedError"); output.Set(index, item); } deferred_.Resolve(output); }
1234
1781
  private:
1235
- std::shared_ptr<ClientContext> context_; uint32_t subscription_id_; std::vector<uint32_t> ids_; UA_DeleteMonitoredItemsResponse response_;
1782
+ uint32_t subscription_id_; std::vector<uint32_t> ids_; UA_DeleteMonitoredItemsResponse response_;
1236
1783
  };
1237
1784
 
1238
- class ValidateSubscriptionWorker : public PromiseWorker {
1785
+ class ValidateSubscriptionWorker : public ClientCommandWorker {
1239
1786
  public:
1240
1787
  ValidateSubscriptionWorker(Napi::Promise::Deferred deferred,
1241
1788
  std::shared_ptr<ClientContext> context, uint32_t id)
1242
- : PromiseWorker(deferred), context_(std::move(context)), id_(id) {}
1243
- void Execute() override {
1244
- std::lock_guard<std::mutex> lock(context_->mutex);
1789
+ : ClientCommandWorker(deferred, std::move(context)), id_(id) {}
1790
+ void ExecuteClientCommand() override {
1245
1791
  if (!context_->client || !context_->connected.load()) {
1246
1792
  SetError("Client reconnecting");
1247
1793
  return;
@@ -1281,19 +1827,18 @@ class ValidateSubscriptionWorker : public PromiseWorker {
1281
1827
  deferred_.Resolve(output);
1282
1828
  }
1283
1829
  private:
1284
- std::shared_ptr<ClientContext> context_;
1285
1830
  uint32_t id_;
1286
1831
  UA_StatusCode status_ = UA_STATUSCODE_GOOD;
1287
1832
  };
1288
1833
 
1289
- class DeleteSubscriptionWorker : public PromiseWorker {
1834
+ class DeleteSubscriptionWorker : public ClientCommandWorker {
1290
1835
  public:
1291
1836
  DeleteSubscriptionWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context, uint32_t id)
1292
- : PromiseWorker(deferred), context_(std::move(context)), id_(id) {}
1293
- void Execute() override { std::lock_guard<std::mutex> lock(context_->mutex); auto found = context_->subscriptions.find(id_); if (found == context_->subscriptions.end()) return; status_ = context_->client ? UA_Client_Subscriptions_deleteSingle(context_->client, id_) : UA_STATUSCODE_BADCONNECTIONCLOSED; CloseNotificationQueue(found->second->notifications); context_->subscriptions.erase(found); if (!UA_StatusCode_isGood(status_)) SetError("Delete subscription failed: " + StatusName(status_)); }
1837
+ : ClientCommandWorker(deferred, std::move(context)), id_(id) {}
1838
+ void ExecuteClientCommand() override { auto found = context_->subscriptions.find(id_); if (found == context_->subscriptions.end()) return; status_ = context_->client ? UA_Client_Subscriptions_deleteSingle(context_->client, id_) : UA_STATUSCODE_BADCONNECTIONCLOSED; CloseNotificationQueue(found->second->notifications); context_->subscriptions.erase(found); if (!UA_StatusCode_isGood(status_)) SetError("Delete subscription failed: " + StatusName(status_)); }
1294
1839
  void OnOK() override { Napi::Object result = Napi::Object::New(Env()); result.Set("subscriptionId", id_); result.Set("statusCode", StatusName(status_)); deferred_.Resolve(result); }
1295
1840
  private:
1296
- std::shared_ptr<ClientContext> context_; uint32_t id_; UA_StatusCode status_ = UA_STATUSCODE_GOOD;
1841
+ uint32_t id_; UA_StatusCode status_ = UA_STATUSCODE_GOOD;
1297
1842
  };
1298
1843
 
1299
1844
  std::shared_ptr<ClientContext> RequireClient(Napi::Env env, const Napi::Value& value) {
@@ -1309,10 +1854,11 @@ Napi::Value GetClientDiagnostics(const Napi::CallbackInfo& info) {
1309
1854
  if (!context) return env.Undefined();
1310
1855
  Napi::Object result = Napi::Object::New(env);
1311
1856
  Napi::Object limits = Napi::Object::New(env);
1312
- limits.Set("maxNodesPerRead", context->operation_limits.max_nodes_per_read);
1313
- limits.Set("maxNodesPerWrite", context->operation_limits.max_nodes_per_write);
1314
- limits.Set("maxNodesPerBrowse", context->operation_limits.max_nodes_per_browse);
1315
- limits.Set("maxMonitoredItemsPerCall", context->operation_limits.max_monitored_items_per_call);
1857
+ limits.Set("maxNodesPerRead", context->operation_limits.max_nodes_per_read.load());
1858
+ limits.Set("maxNodesPerWrite", context->operation_limits.max_nodes_per_write.load());
1859
+ limits.Set("maxNodesPerBrowse", context->operation_limits.max_nodes_per_browse.load());
1860
+ limits.Set("maxMonitoredItemsPerCall", context->operation_limits.max_monitored_items_per_call.load());
1861
+ limits.Set("maxMonitoredItemsPerSubscription", context->operation_limits.max_monitored_items_per_subscription.load());
1316
1862
  result.Set("operationLimits", limits);
1317
1863
 
1318
1864
  Napi::Object connection = Napi::Object::New(env);
@@ -1326,8 +1872,30 @@ Napi::Value GetClientDiagnostics(const Napi::CallbackInfo& info) {
1326
1872
  connection.Set("reconnectCount", Napi::Number::New(env, static_cast<double>(context->reconnect_count.load())));
1327
1873
  connection.Set("disconnectCount", Napi::Number::New(env, static_cast<double>(context->disconnect_count.load())));
1328
1874
  connection.Set("sessionGeneration", Napi::Number::New(env, static_cast<double>(context->session_generation.load())));
1875
+ connection.Set("endpoint", context->endpoint);
1876
+ connection.Set("securityMode", context->security_mode);
1877
+ connection.Set("securityPolicyUri", context->security_policy_uri);
1878
+ connection.Set("identityType", context->identity_type);
1329
1879
  result.Set("connection", connection);
1330
1880
 
1881
+ size_t command_queue_depth = 0;
1882
+ size_t command_queue_max_depth = 0;
1883
+ {
1884
+ std::lock_guard<std::mutex> lock(context->command_mutex);
1885
+ command_queue_depth = context->commands.size();
1886
+ command_queue_max_depth = context->command_queue_max_depth;
1887
+ }
1888
+ Napi::Object command_queue = Napi::Object::New(env);
1889
+ command_queue.Set("depth", Napi::Number::New(env, static_cast<double>(command_queue_depth)));
1890
+ command_queue.Set("capacity", Napi::Number::New(env, static_cast<double>(context->command_queue_capacity)));
1891
+ command_queue.Set("maxDepth", Napi::Number::New(env, static_cast<double>(command_queue_max_depth)));
1892
+ command_queue.Set("active", context->commands_active.load());
1893
+ command_queue.Set("enqueued", Napi::Number::New(env, static_cast<double>(context->commands_enqueued.load())));
1894
+ command_queue.Set("completed", Napi::Number::New(env, static_cast<double>(context->commands_completed.load())));
1895
+ command_queue.Set("rejected", Napi::Number::New(env, static_cast<double>(context->commands_rejected.load())));
1896
+ command_queue.Set("timedOut", Napi::Number::New(env, static_cast<double>(context->commands_timed_out.load())));
1897
+ result.Set("commandQueue", command_queue);
1898
+
1331
1899
  size_t cache_size = 0;
1332
1900
  {
1333
1901
  std::lock_guard<std::mutex> lock(context->node_id_cache_mutex);
@@ -1347,7 +1915,9 @@ Napi::Value GetClientDiagnostics(const Napi::CallbackInfo& info) {
1347
1915
  Napi::Value ConnectAsync(const Napi::CallbackInfo& info) {
1348
1916
  Napi::Env env = info.Env(); if (info.Length() < 1 || !info[0].IsObject()) { Napi::TypeError::New(env, "Connection options are required").ThrowAsJavaScriptException(); return env.Undefined(); }
1349
1917
  Napi::Object object = info[0].As<Napi::Object>(); ConnectionOptions options;
1350
- options.endpoint = GetString(object, "endpoint"); options.security_mode = GetString(object, "securityMode", "None"); options.security_policy = GetString(object, "securityPolicy", "None"); options.user = GetString(object, "user"); options.password = GetString(object, "password"); options.timeout = GetUInt(object, "requestTimeout", 10000); options.session_timeout = GetUInt(object, "requestedSessionTimeout", 60000); options.connectivity_check_interval = GetUInt(object, "connectivityCheckInterval", 5000); options.node_id_cache_size = GetUInt(object, "nodeIdCacheSize", 100000);
1918
+ options.endpoint = GetString(object, "endpoint"); options.security_mode = GetString(object, "securityMode", "None"); options.security_policy = GetString(object, "securityPolicy", "None"); options.user = GetString(object, "user"); options.password = GetString(object, "password"); options.certificate = GetString(object, "certificate"); options.private_key = GetString(object, "privateKey"); options.private_key_password = GetString(object, "privateKeyPassword"); options.application_uri = GetString(object, "applicationUri"); options.timeout = GetUInt(object, "requestTimeout", 10000); options.session_timeout = GetUInt(object, "requestedSessionTimeout", 60000); options.connectivity_check_interval = GetUInt(object, "connectivityCheckInterval", 5000); options.node_id_cache_size = GetUInt(object, "nodeIdCacheSize", 100000); options.command_queue_size = GetUInt(object, "commandQueueSize", 256);
1919
+ if (object.Has("trustedCertificates") && object.Get("trustedCertificates").IsArray()) { Napi::Array values = object.Get("trustedCertificates").As<Napi::Array>(); for (uint32_t index = 0; index < values.Length(); ++index) if (values.Get(index).IsString()) options.trusted_certificates.push_back(values.Get(index).As<Napi::String>().Utf8Value()); }
1920
+ if (object.Has("revocationList") && object.Get("revocationList").IsArray()) { Napi::Array values = object.Get("revocationList").As<Napi::Array>(); for (uint32_t index = 0; index < values.Length(); ++index) if (values.Get(index).IsString()) options.revocation_list.push_back(values.Get(index).As<Napi::String>().Utf8Value()); }
1351
1921
  bool has_state_callback = info.Length() > 1 && info[1].IsFunction();
1352
1922
  Napi::ThreadSafeFunction state_callback;
1353
1923
  if (has_state_callback) {
@@ -1357,17 +1927,30 @@ Napi::Value ConnectAsync(const Napi::CallbackInfo& info) {
1357
1927
  auto deferred = Napi::Promise::Deferred::New(env); (new ConnectWorker(deferred, std::move(options), std::move(state_callback), has_state_callback))->Queue(); return deferred.Promise();
1358
1928
  }
1359
1929
 
1930
+ Napi::Value DiscoverEndpointsAsync(const Napi::CallbackInfo& info) {
1931
+ Napi::Env env = info.Env();
1932
+ if (info.Length() < 1 || !info[0].IsObject()) {
1933
+ Napi::TypeError::New(env, "Discovery options are required").ThrowAsJavaScriptException();
1934
+ return env.Undefined();
1935
+ }
1936
+ Napi::Object options = info[0].As<Napi::Object>();
1937
+ auto deferred = Napi::Promise::Deferred::New(env);
1938
+ (new DiscoverEndpointsWorker(
1939
+ deferred, GetString(options, "endpoint"), GetUInt(options, "requestTimeout", 10000)))->Queue();
1940
+ return deferred.Promise();
1941
+ }
1942
+
1360
1943
  Napi::Value DisconnectAsync(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); uint64_t id = info[0].As<Napi::Number>().Int64Value(); auto deferred = Napi::Promise::Deferred::New(env); (new DisconnectWorker(deferred, id))->Queue(); return deferred.Promise(); }
1361
1944
 
1362
1945
  Napi::Value ReadManyAsync(const Napi::CallbackInfo& info) {
1363
- Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context || !info[1].IsArray()) return env.Undefined(); Napi::Array array = info[1].As<Napi::Array>(); std::vector<std::string> ids; ids.reserve(array.Length());
1364
- for (uint32_t index = 0; index < array.Length(); ++index) { Napi::Value value = array.Get(index); ids.push_back(value.IsString() ? value.As<Napi::String>().Utf8Value() : GetString(value.As<Napi::Object>(), "nodeId")); }
1365
- auto deferred = Napi::Promise::Deferred::New(env); (new ReadManyWorker(deferred, context, std::move(ids)))->Queue(); return deferred.Promise();
1946
+ Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context || !info[1].IsArray()) return env.Undefined(); Napi::Array array = info[1].As<Napi::Array>(); std::vector<ReadInput> inputs; inputs.reserve(array.Length());
1947
+ for (uint32_t index = 0; index < array.Length(); ++index) { Napi::Value value = array.Get(index); ReadInput input; if (value.IsString()) input.node_id = value.As<Napi::String>().Utf8Value(); else { Napi::Object object = value.As<Napi::Object>(); input.node_id = GetString(object, "nodeId"); input.index_range = GetString(object, "indexRange"); } inputs.push_back(std::move(input)); }
1948
+ auto deferred = Napi::Promise::Deferred::New(env); (new ReadManyWorker(deferred, context, std::move(inputs)))->Queue(); return deferred.Promise();
1366
1949
  }
1367
1950
 
1368
1951
  Napi::Value WriteManyAsync(const Napi::CallbackInfo& info) {
1369
1952
  Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context || !info[1].IsArray()) return env.Undefined(); Napi::Array array = info[1].As<Napi::Array>(); std::vector<std::unique_ptr<WriteInput>> inputs;
1370
- for (uint32_t index = 0; index < array.Length(); ++index) { Napi::Object object = array.Get(index).As<Napi::Object>(); auto input = std::make_unique<WriteInput>(); input->node_id = GetString(object, "nodeId"); input->data_type = GetString(object, "dataType", "String"); if (!object.Has("value")) { Napi::TypeError::New(env, "Write point is missing value").ThrowAsJavaScriptException(); return env.Undefined(); } if (!JsScalarToVariant(env, object.Get("value"), input->data_type, &input->value)) return env.Undefined(); inputs.push_back(std::move(input)); }
1953
+ for (uint32_t index = 0; index < array.Length(); ++index) { Napi::Object object = array.Get(index).As<Napi::Object>(); auto input = std::make_unique<WriteInput>(); input->node_id = GetString(object, "nodeId"); input->data_type = GetString(object, "dataType", "String"); input->index_range = GetString(object, "indexRange"); if (!object.Has("value")) { Napi::TypeError::New(env, "Write point is missing value").ThrowAsJavaScriptException(); return env.Undefined(); } if (!JsValueToVariant(env, object.Get("value"), input->data_type, &input->value)) return env.Undefined(); inputs.push_back(std::move(input)); }
1371
1954
  auto deferred = Napi::Promise::Deferred::New(env); (new WriteManyWorker(deferred, context, std::move(inputs)))->Queue(); return deferred.Promise();
1372
1955
  }
1373
1956
 
@@ -1386,7 +1969,11 @@ Napi::Value CreateSubscriptionAsync(const Napi::CallbackInfo& info) {
1386
1969
  Napi::Value AddMonitoredItemsAsync(const Napi::CallbackInfo& info) {
1387
1970
  Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context || !info[2].IsArray()) return env.Undefined(); uint32_t subscription_id = info[1].ToNumber().Uint32Value(); Napi::Array array = info[2].As<Napi::Array>(); Napi::Object defaults = info.Length() > 3 && info[3].IsObject() ? info[3].As<Napi::Object>() : Napi::Object::New(env); std::vector<MonitorInput> inputs;
1388
1971
  for (uint32_t index = 0; index < array.Length(); ++index) { Napi::Object object = array.Get(index).As<Napi::Object>(); MonitorInput input; input.node_id = GetString(object, "nodeId"); input.sampling_interval = GetDouble(object, "samplingInterval", GetDouble(defaults, "samplingInterval", 1000)); input.queue_size = GetUInt(object, "queueSize", GetUInt(defaults, "queueSize", 10)); input.discard_oldest = GetBool(object, "discardOldest", GetBool(defaults, "discardOldest", true)); input.deadband_type = GetString(object, "deadbandType", GetString(defaults, "deadbandType", "none")); input.deadband_value = GetDouble(object, "deadbandValue", GetDouble(defaults, "deadbandValue", 0)); inputs.push_back(std::move(input)); }
1389
- auto deferred = Napi::Promise::Deferred::New(env); (new AddMonitoredWorker(deferred, context, subscription_id, std::move(inputs)))->Queue(); return deferred.Promise();
1972
+ std::string timestamp_mode = GetString(defaults, "timestampMode", "source");
1973
+ UA_TimestampsToReturn timestamps_to_return = timestamp_mode == "both" ? UA_TIMESTAMPSTORETURN_BOTH :
1974
+ (timestamp_mode == "server" ? UA_TIMESTAMPSTORETURN_SERVER :
1975
+ (timestamp_mode == "neither" ? UA_TIMESTAMPSTORETURN_NEITHER : UA_TIMESTAMPSTORETURN_SOURCE));
1976
+ auto deferred = Napi::Promise::Deferred::New(env); (new AddMonitoredWorker(deferred, context, subscription_id, std::move(inputs), timestamps_to_return))->Queue(); return deferred.Promise();
1390
1977
  }
1391
1978
 
1392
1979
  Napi::Value RemoveMonitoredItemsAsync(const Napi::CallbackInfo& info) {
@@ -1399,6 +1986,7 @@ Napi::Value ValidateSubscriptionAsync(const Napi::CallbackInfo& info) { Napi::En
1399
1986
 
1400
1987
  Napi::Object Init(Napi::Env env, Napi::Object exports) {
1401
1988
  exports.Set("connectAsync", Napi::Function::New(env, ConnectAsync)); exports.Set("disconnectAsync", Napi::Function::New(env, DisconnectAsync));
1989
+ exports.Set("discoverEndpointsAsync", Napi::Function::New(env, DiscoverEndpointsAsync));
1402
1990
  exports.Set("getClientDiagnostics", Napi::Function::New(env, GetClientDiagnostics));
1403
1991
  exports.Set("browseAsync", Napi::Function::New(env, BrowseAsync)); exports.Set("readManyAsync", Napi::Function::New(env, ReadManyAsync)); exports.Set("writeManyAsync", Napi::Function::New(env, WriteManyAsync));
1404
1992
  exports.Set("createSubscriptionAsync", Napi::Function::New(env, CreateSubscriptionAsync)); exports.Set("addMonitoredItemsAsync", Napi::Function::New(env, AddMonitoredItemsAsync)); exports.Set("removeMonitoredItemsAsync", Napi::Function::New(env, RemoveMonitoredItemsAsync)); exports.Set("deleteSubscriptionAsync", Napi::Function::New(env, DeleteSubscriptionAsync)); exports.Set("validateSubscriptionAsync", Napi::Function::New(env, ValidateSubscriptionAsync));