@tier0/opcua-client 0.4.1 → 0.6.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.
package/src/addon.cc CHANGED
@@ -8,7 +8,9 @@
8
8
 
9
9
  #include <algorithm>
10
10
  #include <atomic>
11
+ #include <charconv>
11
12
  #include <chrono>
13
+ #include <cmath>
12
14
  #include <condition_variable>
13
15
  #include <cstdint>
14
16
  #include <deque>
@@ -47,6 +49,14 @@ std::string SecurityPolicyUri(const std::string& policy) {
47
49
  policy == "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256") {
48
50
  return "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256";
49
51
  }
52
+ if (policy == "Aes128_Sha256_RsaOaep" ||
53
+ policy == "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep") {
54
+ return "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep";
55
+ }
56
+ if (policy == "Aes256_Sha256_RsaPss" ||
57
+ policy == "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss") {
58
+ return "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss";
59
+ }
50
60
  return "";
51
61
  }
52
62
 
@@ -201,6 +211,94 @@ std::string BuiltinTypeName(const UA_NodeId& node_id) {
201
211
  }
202
212
  }
203
213
 
214
+ constexpr UA_Int64 kJsSafeInteger = 9007199254740991LL;
215
+
216
+ Napi::Value Int64Value(Napi::Env env, UA_Int64 value) {
217
+ if (value >= -kJsSafeInteger && value <= kJsSafeInteger) {
218
+ return Napi::Number::New(env, static_cast<double>(value));
219
+ }
220
+ return Napi::String::New(env, std::to_string(value));
221
+ }
222
+
223
+ Napi::Value UInt64Value(Napi::Env env, UA_UInt64 value) {
224
+ if (value <= static_cast<UA_UInt64>(kJsSafeInteger)) {
225
+ return Napi::Number::New(env, static_cast<double>(value));
226
+ }
227
+ return Napi::String::New(env, std::to_string(value));
228
+ }
229
+
230
+ bool InvalidInteger(Napi::Env env, const std::string& name,
231
+ const std::string& detail) {
232
+ Napi::RangeError::New(env, "Invalid " + name + " value: " + detail)
233
+ .ThrowAsJavaScriptException();
234
+ return false;
235
+ }
236
+
237
+ bool ParseIntegerNumber(Napi::Env env, const Napi::Value& input,
238
+ const std::string& name, double minimum,
239
+ double maximum, double* output) {
240
+ if (!input.IsNumber() && !input.IsString()) {
241
+ return InvalidInteger(env, name, "expected an integer number");
242
+ }
243
+ if (input.IsString() && input.As<Napi::String>().Utf8Value().empty()) {
244
+ return InvalidInteger(env, name, "empty string");
245
+ }
246
+ const double value = input.ToNumber().DoubleValue();
247
+ if (!std::isfinite(value) || std::trunc(value) != value ||
248
+ value < minimum || value > maximum) {
249
+ return InvalidInteger(env, name, input.ToString().Utf8Value());
250
+ }
251
+ *output = value;
252
+ return true;
253
+ }
254
+
255
+ bool ParseInt64(Napi::Env env, const Napi::Value& input, UA_Int64* output) {
256
+ if (input.IsBigInt()) {
257
+ bool lossless = false;
258
+ *output = input.As<Napi::BigInt>().Int64Value(&lossless);
259
+ return lossless || InvalidInteger(env, "Int64", "out of range");
260
+ }
261
+ if (input.IsString()) {
262
+ const std::string text = input.As<Napi::String>().Utf8Value();
263
+ const auto result = std::from_chars(
264
+ text.data(), text.data() + text.size(), *output, 10);
265
+ if (!text.empty() && result.ec == std::errc() &&
266
+ result.ptr == text.data() + text.size()) return true;
267
+ return InvalidInteger(env, "Int64", text);
268
+ }
269
+ double value = 0;
270
+ if (!ParseIntegerNumber(env, input, "Int64",
271
+ -static_cast<double>(kJsSafeInteger),
272
+ static_cast<double>(kJsSafeInteger), &value)) {
273
+ return false;
274
+ }
275
+ *output = static_cast<UA_Int64>(value);
276
+ return true;
277
+ }
278
+
279
+ bool ParseUInt64(Napi::Env env, const Napi::Value& input, UA_UInt64* output) {
280
+ if (input.IsBigInt()) {
281
+ bool lossless = false;
282
+ *output = input.As<Napi::BigInt>().Uint64Value(&lossless);
283
+ return lossless || InvalidInteger(env, "UInt64", "out of range");
284
+ }
285
+ if (input.IsString()) {
286
+ const std::string text = input.As<Napi::String>().Utf8Value();
287
+ const auto result = std::from_chars(
288
+ text.data(), text.data() + text.size(), *output, 10);
289
+ if (!text.empty() && result.ec == std::errc() &&
290
+ result.ptr == text.data() + text.size()) return true;
291
+ return InvalidInteger(env, "UInt64", text);
292
+ }
293
+ double value = 0;
294
+ if (!ParseIntegerNumber(env, input, "UInt64", 0,
295
+ static_cast<double>(kJsSafeInteger), &value)) {
296
+ return false;
297
+ }
298
+ *output = static_cast<UA_UInt64>(value);
299
+ return true;
300
+ }
301
+
204
302
  Napi::Value VariantValue(Napi::Env env, const UA_Variant* variant) {
205
303
  if (!variant || !variant->type || !variant->data) return env.Null();
206
304
  const UA_DataType* type = variant->type;
@@ -213,8 +311,8 @@ Napi::Value VariantValue(Napi::Env env, const UA_Variant* variant) {
213
311
  if (type == &UA_TYPES[UA_TYPES_UINT16]) return Napi::Number::New(env, *static_cast<const UA_UInt16*>(data));
214
312
  if (type == &UA_TYPES[UA_TYPES_INT32]) return Napi::Number::New(env, *static_cast<const UA_Int32*>(data));
215
313
  if (type == &UA_TYPES[UA_TYPES_UINT32]) return Napi::Number::New(env, *static_cast<const UA_UInt32*>(data));
216
- if (type == &UA_TYPES[UA_TYPES_INT64]) return Napi::Number::New(env, static_cast<double>(*static_cast<const UA_Int64*>(data)));
217
- if (type == &UA_TYPES[UA_TYPES_UINT64]) return Napi::Number::New(env, static_cast<double>(*static_cast<const UA_UInt64*>(data)));
314
+ if (type == &UA_TYPES[UA_TYPES_INT64]) return Int64Value(env, *static_cast<const UA_Int64*>(data));
315
+ if (type == &UA_TYPES[UA_TYPES_UINT64]) return UInt64Value(env, *static_cast<const UA_UInt64*>(data));
218
316
  if (type == &UA_TYPES[UA_TYPES_FLOAT]) return Napi::Number::New(env, *static_cast<const UA_Float*>(data));
219
317
  if (type == &UA_TYPES[UA_TYPES_DOUBLE]) return Napi::Number::New(env, *static_cast<const UA_Double*>(data));
220
318
  if (type == &UA_TYPES[UA_TYPES_STRING]) return Napi::String::New(env, UaString(*static_cast<const UA_String*>(data)));
@@ -260,14 +358,14 @@ bool JsScalarToVariant(Napi::Env env, const Napi::Value& input, const std::strin
260
358
  UA_Boolean value = input.ToBoolean();
261
359
  return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type));
262
360
  }
263
- if (name == "SByte") { UA_SByte value = static_cast<UA_SByte>(input.ToNumber().Int32Value()); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
264
- if (name == "Byte") { UA_Byte value = static_cast<UA_Byte>(input.ToNumber().Uint32Value()); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
265
- if (name == "Int16") { UA_Int16 value = static_cast<UA_Int16>(input.ToNumber().Int32Value()); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
266
- if (name == "UInt16") { UA_UInt16 value = static_cast<UA_UInt16>(input.ToNumber().Uint32Value()); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
267
- if (name == "Int32") { UA_Int32 value = input.ToNumber().Int32Value(); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
268
- if (name == "UInt32") { UA_UInt32 value = input.ToNumber().Uint32Value(); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
269
- if (name == "Int64") { bool lossless = false; UA_Int64 value = input.IsBigInt() ? input.As<Napi::BigInt>().Int64Value(&lossless) : static_cast<UA_Int64>(input.ToNumber().Int64Value()); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
270
- if (name == "UInt64") { bool lossless = false; UA_UInt64 value = input.IsBigInt() ? input.As<Napi::BigInt>().Uint64Value(&lossless) : static_cast<UA_UInt64>(input.ToNumber().Int64Value()); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
361
+ if (name == "SByte") { double parsed; if (!ParseIntegerNumber(env, input, name, -128, 127, &parsed)) return false; UA_SByte value = static_cast<UA_SByte>(parsed); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
362
+ if (name == "Byte") { double parsed; if (!ParseIntegerNumber(env, input, name, 0, 255, &parsed)) return false; UA_Byte value = static_cast<UA_Byte>(parsed); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
363
+ if (name == "Int16") { double parsed; if (!ParseIntegerNumber(env, input, name, -32768, 32767, &parsed)) return false; UA_Int16 value = static_cast<UA_Int16>(parsed); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
364
+ if (name == "UInt16") { double parsed; if (!ParseIntegerNumber(env, input, name, 0, 65535, &parsed)) return false; UA_UInt16 value = static_cast<UA_UInt16>(parsed); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
365
+ if (name == "Int32") { double parsed; if (!ParseIntegerNumber(env, input, name, -2147483648.0, 2147483647.0, &parsed)) return false; UA_Int32 value = static_cast<UA_Int32>(parsed); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
366
+ if (name == "UInt32") { double parsed; if (!ParseIntegerNumber(env, input, name, 0, 4294967295.0, &parsed)) return false; UA_UInt32 value = static_cast<UA_UInt32>(parsed); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
367
+ if (name == "Int64") { UA_Int64 value; if (!ParseInt64(env, input, &value)) return false; return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
368
+ if (name == "UInt64") { UA_UInt64 value; if (!ParseUInt64(env, input, &value)) return false; return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
271
369
  if (name == "Float") { UA_Float value = input.ToNumber().FloatValue(); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
272
370
  if (name == "Double") { UA_Double value = input.ToNumber().DoubleValue(); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
273
371
  if (name == "String") { std::string text = input.ToString().Utf8Value(); UA_String value = UA_STRING(const_cast<char*>(text.c_str())); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
@@ -1481,6 +1579,8 @@ struct BrowseOptions {
1481
1579
  bool variables_only = true;
1482
1580
  uint32_t max_depth = 10;
1483
1581
  uint32_t max_nodes = 10000;
1582
+ uint32_t browse_batch_size = 100;
1583
+ uint32_t references_per_node = 1000;
1484
1584
  uint32_t attribute_batch_size = 200;
1485
1585
  std::set<uint16_t> namespaces;
1486
1586
  };
@@ -1512,16 +1612,51 @@ class BrowseWorker : public ClientCommandWorker {
1512
1612
  item.Set("userAccessLevel", point.user_access_level); item.Set("minimumSamplingInterval", point.minimum_sampling_interval);
1513
1613
  output.Set(index, item);
1514
1614
  }
1615
+ output.Set("truncated", truncated_);
1616
+ if (!truncation_reason_.empty()) output.Set("truncationReason", truncation_reason_);
1617
+ output.Set("serviceCalls", Napi::Number::New(
1618
+ Env(), static_cast<double>(service_calls_)));
1619
+ output.Set("browseBatchSize", browse_batch_size_);
1620
+ output.Set("releasedContinuationPoints", Napi::Number::New(
1621
+ Env(), static_cast<double>(released_continuation_points_)));
1622
+ Napi::Array failures = Napi::Array::New(Env(), browse_failures_.size());
1623
+ for (size_t index = 0; index < browse_failures_.size(); ++index) {
1624
+ Napi::Object failure = Napi::Object::New(Env());
1625
+ failure.Set("nodeId", browse_failures_[index].first);
1626
+ failure.Set("statusCode", browse_failures_[index].second);
1627
+ failures.Set(index, failure);
1628
+ }
1629
+ output.Set("browseFailures", failures);
1515
1630
  deferred_.Resolve(output);
1516
1631
  }
1517
1632
  private:
1518
1633
  struct QueueItem { std::string node_id; std::string path; uint32_t depth; };
1634
+ struct PendingContinuation {
1635
+ QueueItem parent;
1636
+ std::vector<UA_Byte> token;
1637
+ };
1519
1638
 
1520
1639
  bool NamespaceAllowed(uint16_t value) const { return options_.namespaces.empty() || options_.namespaces.count(value) > 0; }
1521
1640
 
1641
+ void MarkTruncated(const std::string& reason) {
1642
+ truncated_ = true;
1643
+ if (truncation_reason_.empty() || truncation_reason_ == "maxDepth") {
1644
+ truncation_reason_ = reason;
1645
+ }
1646
+ }
1647
+
1648
+ void MarkDepthLimited() {
1649
+ depth_limited_ = true;
1650
+ if (truncation_reason_.empty()) truncation_reason_ = "maxDepth";
1651
+ }
1652
+
1522
1653
  void ProcessBrowseResult(const QueueItem& parent, const UA_BrowseResult& result,
1523
1654
  std::deque<QueueItem>* queue, std::set<std::string>* visited) {
1524
- for (size_t index = 0; index < result.referencesSize && points_.size() < options_.max_nodes; ++index) {
1655
+ for (size_t index = 0; index < result.referencesSize; ++index) {
1656
+ if (points_.size() >= options_.max_nodes) {
1657
+ MarkTruncated("maxNodes");
1658
+ return;
1659
+ }
1525
1660
  const UA_ReferenceDescription& reference = result.references[index];
1526
1661
  if (reference.nodeId.serverIndex != 0 || reference.nodeId.namespaceUri.length != 0) continue;
1527
1662
  const UA_NodeId& node_id = reference.nodeId.nodeId;
@@ -1539,58 +1674,206 @@ class BrowseWorker : public ClientCommandWorker {
1539
1674
  point.node_class = reference.nodeClass; point.type_definition = NodeIdString(reference.typeDefinition.nodeId);
1540
1675
  points_.push_back(std::move(point));
1541
1676
  }
1542
- if (options_.recursive && parent.depth < options_.max_depth && visited->insert(node_id_text).second) {
1543
- queue->push_back({node_id_text, path, parent.depth + 1});
1677
+ if (options_.recursive) {
1678
+ if (parent.depth < options_.max_depth) {
1679
+ if (visited->insert(node_id_text).second) {
1680
+ queue->push_back({node_id_text, path, parent.depth + 1});
1681
+ }
1682
+ } else if (reference.nodeClass == UA_NODECLASS_OBJECT) {
1683
+ MarkDepthLimited();
1684
+ }
1685
+ }
1686
+ }
1687
+ }
1688
+
1689
+ static PendingContinuation Continuation(
1690
+ const QueueItem& parent, const UA_ByteString& point) {
1691
+ PendingContinuation output{parent, {}};
1692
+ if (point.length && point.data) {
1693
+ output.token.assign(point.data, point.data + point.length);
1694
+ }
1695
+ return output;
1696
+ }
1697
+
1698
+ bool FillContinuationPoints(
1699
+ const std::vector<PendingContinuation>& pending, size_t start, size_t count,
1700
+ UA_BrowseNextRequest* request) {
1701
+ request->continuationPointsSize = count;
1702
+ request->continuationPoints = static_cast<UA_ByteString*>(
1703
+ UA_Array_new(count, &UA_TYPES[UA_TYPES_BYTESTRING]));
1704
+ if (!request->continuationPoints) return false;
1705
+ for (size_t index = 0; index < count; ++index) {
1706
+ const auto& token = pending[start + index].token;
1707
+ UA_ByteString source;
1708
+ source.length = token.size();
1709
+ source.data = const_cast<UA_Byte*>(token.data());
1710
+ if (UA_ByteString_copy(&source, &request->continuationPoints[index]) !=
1711
+ UA_STATUSCODE_GOOD) {
1712
+ return false;
1544
1713
  }
1545
1714
  }
1715
+ return true;
1716
+ }
1717
+
1718
+ void ReleaseContinuations(const std::vector<PendingContinuation>& pending) {
1719
+ for (size_t start = 0; start < pending.size(); start += browse_batch_size_) {
1720
+ const size_t count = std::min<size_t>(
1721
+ browse_batch_size_, pending.size() - start);
1722
+ UA_BrowseNextRequest request;
1723
+ UA_BrowseNextRequest_init(&request);
1724
+ request.releaseContinuationPoints = true;
1725
+ if (!FillContinuationPoints(pending, start, count, &request)) {
1726
+ UA_BrowseNextRequest_clear(&request);
1727
+ continue;
1728
+ }
1729
+ UA_BrowseNextResponse response =
1730
+ UA_Client_Service_browseNext(context_->client, request);
1731
+ ++service_calls_;
1732
+ UA_BrowseNextRequest_clear(&request);
1733
+ UA_BrowseNextResponse_clear(&response);
1734
+ released_continuation_points_ += count;
1735
+ }
1736
+ }
1737
+
1738
+ bool BrowseContinuations(
1739
+ std::vector<PendingContinuation>* pending, std::deque<QueueItem>* queue,
1740
+ std::set<std::string>* visited) {
1741
+ while (!pending->empty()) {
1742
+ std::vector<PendingContinuation> next_pending;
1743
+ for (size_t start = 0; start < pending->size(); start += browse_batch_size_) {
1744
+ const size_t count = std::min<size_t>(
1745
+ browse_batch_size_, pending->size() - start);
1746
+ UA_BrowseNextRequest request;
1747
+ UA_BrowseNextRequest_init(&request);
1748
+ if (!FillContinuationPoints(*pending, start, count, &request)) {
1749
+ UA_BrowseNextRequest_clear(&request);
1750
+ ReleaseContinuations(*pending);
1751
+ SetError("Failed to allocate BrowseNext continuation points");
1752
+ return false;
1753
+ }
1754
+ UA_BrowseNextResponse response =
1755
+ UA_Client_Service_browseNext(context_->client, request);
1756
+ ++service_calls_;
1757
+ UA_BrowseNextRequest_clear(&request);
1758
+ if (!UA_StatusCode_isGood(response.responseHeader.serviceResult)) {
1759
+ const std::string status = StatusName(
1760
+ response.responseHeader.serviceResult);
1761
+ UA_BrowseNextResponse_clear(&response);
1762
+ ReleaseContinuations(*pending);
1763
+ SetError("BrowseNext failed: " + status);
1764
+ return false;
1765
+ }
1766
+ for (size_t index = 0; index < count; ++index) {
1767
+ const auto& parent = (*pending)[start + index].parent;
1768
+ if (index < response.resultsSize) {
1769
+ const UA_BrowseResult& result = response.results[index];
1770
+ if (UA_StatusCode_isGood(result.statusCode) && !truncated_) {
1771
+ ProcessBrowseResult(parent, result, queue, visited);
1772
+ } else if (!UA_StatusCode_isGood(result.statusCode)) {
1773
+ browse_failures_.push_back(
1774
+ {parent.node_id, StatusName(result.statusCode)});
1775
+ }
1776
+ if (result.continuationPoint.length) {
1777
+ next_pending.push_back(
1778
+ Continuation(parent, result.continuationPoint));
1779
+ }
1780
+ } else {
1781
+ browse_failures_.push_back(
1782
+ {parent.node_id, "BadUnexpectedError"});
1783
+ }
1784
+ }
1785
+ UA_BrowseNextResponse_clear(&response);
1786
+ }
1787
+ if (truncated_) {
1788
+ ReleaseContinuations(next_pending);
1789
+ return true;
1790
+ }
1791
+ *pending = std::move(next_pending);
1792
+ }
1793
+ return true;
1546
1794
  }
1547
1795
 
1548
1796
  bool BrowseTree() {
1549
1797
  std::deque<QueueItem> queue{{options_.root_node_id, "", 0}};
1550
1798
  std::set<std::string> visited{options_.root_node_id};
1551
- while (!queue.empty() && points_.size() < options_.max_nodes) {
1552
- QueueItem current = std::move(queue.front()); queue.pop_front();
1799
+ browse_batch_size_ = std::max<uint32_t>(1, options_.browse_batch_size);
1800
+ const uint32_t server_limit =
1801
+ context_->operation_limits.max_nodes_per_browse.load();
1802
+ if (server_limit > 0) {
1803
+ browse_batch_size_ = std::min(browse_batch_size_, server_limit);
1804
+ }
1805
+ while (!queue.empty() && !truncated_) {
1806
+ std::vector<QueueItem> batch;
1807
+ batch.reserve(browse_batch_size_);
1808
+ while (!queue.empty() && batch.size() < browse_batch_size_) {
1809
+ batch.push_back(std::move(queue.front()));
1810
+ queue.pop_front();
1811
+ }
1553
1812
  UA_BrowseRequest request; UA_BrowseRequest_init(&request);
1554
- request.requestedMaxReferencesPerNode = 1000;
1555
- request.nodesToBrowseSize = 1;
1556
- request.nodesToBrowse = static_cast<UA_BrowseDescription*>(UA_Array_new(1, &UA_TYPES[UA_TYPES_BROWSEDESCRIPTION]));
1557
- if (!request.nodesToBrowse || !CopyCachedNodeId(context_.get(), current.node_id, &request.nodesToBrowse[0].nodeId)) {
1558
- UA_BrowseRequest_clear(&request); SetError("Invalid browse NodeId: " + current.node_id); return false;
1813
+ request.requestedMaxReferencesPerNode = options_.references_per_node;
1814
+ request.nodesToBrowseSize = batch.size();
1815
+ request.nodesToBrowse = static_cast<UA_BrowseDescription*>(
1816
+ UA_Array_new(batch.size(), &UA_TYPES[UA_TYPES_BROWSEDESCRIPTION]));
1817
+ if (!request.nodesToBrowse) {
1818
+ SetError("Failed to allocate Browse request");
1819
+ return false;
1820
+ }
1821
+ for (size_t index = 0; index < batch.size(); ++index) {
1822
+ if (!CopyCachedNodeId(context_.get(), batch[index].node_id,
1823
+ &request.nodesToBrowse[index].nodeId)) {
1824
+ const std::string invalid = batch[index].node_id;
1825
+ UA_BrowseRequest_clear(&request);
1826
+ SetError("Invalid browse NodeId: " + invalid);
1827
+ return false;
1828
+ }
1829
+ auto& description = request.nodesToBrowse[index];
1830
+ description.browseDirection = UA_BROWSEDIRECTION_FORWARD;
1831
+ description.referenceTypeId =
1832
+ UA_NODEID_NUMERIC(0, UA_NS0ID_HIERARCHICALREFERENCES);
1833
+ description.includeSubtypes = true;
1834
+ description.nodeClassMask = UA_NODECLASS_OBJECT | UA_NODECLASS_VARIABLE;
1835
+ description.resultMask = UA_BROWSERESULTMASK_ALL;
1559
1836
  }
1560
- auto& description = request.nodesToBrowse[0];
1561
- description.browseDirection = UA_BROWSEDIRECTION_FORWARD;
1562
- description.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HIERARCHICALREFERENCES);
1563
- description.includeSubtypes = true;
1564
- description.nodeClassMask = UA_NODECLASS_OBJECT | UA_NODECLASS_VARIABLE;
1565
- description.resultMask = UA_BROWSERESULTMASK_ALL;
1566
1837
  UA_BrowseResponse response = UA_Client_Service_browse(context_->client, request);
1838
+ ++service_calls_;
1567
1839
  UA_BrowseRequest_clear(&request);
1568
1840
  if (!UA_StatusCode_isGood(response.responseHeader.serviceResult)) {
1569
1841
  UA_BrowseResponse_clear(&response); SetError("Browse failed: " + StatusName(response.responseHeader.serviceResult)); return false;
1570
1842
  }
1571
- if (response.resultsSize) {
1572
- ProcessBrowseResult(current, response.results[0], &queue, &visited);
1573
- UA_ByteString continuation = UA_BYTESTRING_NULL;
1574
- UA_ByteString_copy(&response.results[0].continuationPoint, &continuation);
1575
- while (continuation.length && points_.size() < options_.max_nodes) {
1576
- UA_BrowseNextRequest next; UA_BrowseNextRequest_init(&next);
1577
- next.continuationPointsSize = 1;
1578
- next.continuationPoints = static_cast<UA_ByteString*>(UA_Array_new(1, &UA_TYPES[UA_TYPES_BYTESTRING]));
1579
- UA_ByteString_copy(&continuation, &next.continuationPoints[0]);
1580
- UA_ByteString_clear(&continuation);
1581
- UA_BrowseNextResponse next_response = UA_Client_Service_browseNext(context_->client, next);
1582
- UA_BrowseNextRequest_clear(&next);
1583
- if (!UA_StatusCode_isGood(next_response.responseHeader.serviceResult) || !next_response.resultsSize) {
1584
- UA_BrowseNextResponse_clear(&next_response); break;
1843
+ std::vector<PendingContinuation> pending;
1844
+ for (size_t index = 0; index < batch.size(); ++index) {
1845
+ if (index < response.resultsSize) {
1846
+ const UA_BrowseResult& result = response.results[index];
1847
+ if (UA_StatusCode_isGood(result.statusCode) && !truncated_) {
1848
+ ProcessBrowseResult(batch[index], result, &queue, &visited);
1849
+ } else if (!UA_StatusCode_isGood(result.statusCode)) {
1850
+ browse_failures_.push_back(
1851
+ {batch[index].node_id, StatusName(result.statusCode)});
1852
+ }
1853
+ if (result.continuationPoint.length) {
1854
+ pending.push_back(
1855
+ Continuation(batch[index], result.continuationPoint));
1585
1856
  }
1586
- ProcessBrowseResult(current, next_response.results[0], &queue, &visited);
1587
- UA_ByteString_copy(&next_response.results[0].continuationPoint, &continuation);
1588
- UA_BrowseNextResponse_clear(&next_response);
1857
+ } else {
1858
+ browse_failures_.push_back(
1859
+ {batch[index].node_id, "BadUnexpectedError"});
1589
1860
  }
1590
- UA_ByteString_clear(&continuation);
1591
1861
  }
1592
1862
  UA_BrowseResponse_clear(&response);
1863
+ if (points_.size() >= options_.max_nodes &&
1864
+ (!pending.empty() || !queue.empty())) {
1865
+ MarkTruncated("maxNodes");
1866
+ }
1867
+ if (truncated_) {
1868
+ ReleaseContinuations(pending);
1869
+ break;
1870
+ }
1871
+ if (!BrowseContinuations(&pending, &queue, &visited)) return false;
1872
+ if (points_.size() >= options_.max_nodes && !queue.empty()) {
1873
+ MarkTruncated("maxNodes");
1874
+ }
1593
1875
  }
1876
+ if (depth_limited_) truncated_ = true;
1594
1877
  return true;
1595
1878
  }
1596
1879
 
@@ -1647,6 +1930,13 @@ class BrowseWorker : public ClientCommandWorker {
1647
1930
 
1648
1931
  BrowseOptions options_;
1649
1932
  std::vector<BrowsePoint> points_;
1933
+ uint32_t browse_batch_size_ = 1;
1934
+ uint64_t service_calls_ = 0;
1935
+ uint64_t released_continuation_points_ = 0;
1936
+ bool truncated_ = false;
1937
+ bool depth_limited_ = false;
1938
+ std::string truncation_reason_;
1939
+ std::vector<std::pair<std::string, std::string>> browse_failures_;
1650
1940
  };
1651
1941
 
1652
1942
  class CreateSubscriptionWorker : public ClientCommandWorker {
@@ -1673,6 +1963,9 @@ class CreateSubscriptionWorker : public ClientCommandWorker {
1673
1963
  if (!context_->client) { SetError("Client disconnected"); return; }
1674
1964
  UA_CreateSubscriptionResponse response = UA_Client_Subscriptions_create(context_->client, request, nullptr, nullptr, nullptr);
1675
1965
  status_ = response.responseHeader.serviceResult; subscription_id_ = response.subscriptionId;
1966
+ revised_publishing_interval_ = response.revisedPublishingInterval;
1967
+ revised_lifetime_ = response.revisedLifetimeCount;
1968
+ revised_keepalive_ = response.revisedMaxKeepAliveCount;
1676
1969
  UA_CreateSubscriptionResponse_clear(&response);
1677
1970
  if (!UA_StatusCode_isGood(status_)) { SetError("Create subscription failed: " + StatusName(status_)); return; }
1678
1971
  auto subscription = std::make_unique<SubscriptionContext>();
@@ -1685,17 +1978,18 @@ class CreateSubscriptionWorker : public ClientCommandWorker {
1685
1978
  subscription->notifications->overflow_policy = overflow_policy_;
1686
1979
  context_->subscriptions[subscription_id_] = std::move(subscription); transferred_ = true;
1687
1980
  }
1688
- void OnOK() override { Napi::Object result = Napi::Object::New(Env()); result.Set("subscriptionId", subscription_id_); result.Set("statusCode", StatusName(status_)); deferred_.Resolve(result); }
1981
+ void OnOK() override { Napi::Object result = Napi::Object::New(Env()); result.Set("subscriptionId", subscription_id_); result.Set("statusCode", StatusName(status_)); result.Set("revisedPublishingInterval", revised_publishing_interval_); result.Set("revisedLifetimeCount", revised_lifetime_); result.Set("revisedMaxKeepAliveCount", revised_keepalive_); deferred_.Resolve(result); }
1689
1982
  void OnError(const Napi::Error& error) override { if (!transferred_) callback_.Release(); PromiseWorker::OnError(error); }
1690
1983
  private:
1691
1984
  Napi::ThreadSafeFunction callback_; bool transferred_ = false;
1692
1985
  double publishing_interval_; uint32_t lifetime_; uint32_t keepalive_; uint32_t max_notifications_;
1693
1986
  uint32_t queue_size_; uint32_t callback_batch_size_; std::string queue_mode_; std::string overflow_policy_;
1694
1987
  uint32_t subscription_id_ = 0; UA_StatusCode status_ = UA_STATUSCODE_GOOD;
1988
+ double revised_publishing_interval_ = 0; uint32_t revised_lifetime_ = 0; uint32_t revised_keepalive_ = 0;
1695
1989
  };
1696
1990
 
1697
1991
  struct MonitorInput { std::string node_id; double sampling_interval; uint32_t queue_size; bool discard_oldest; std::string deadband_type; double deadband_value; };
1698
- struct MonitorResult { std::string node_id; uint32_t monitored_item_id = 0; uint32_t client_handle = 0; UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; };
1992
+ struct MonitorResult { std::string node_id; uint32_t monitored_item_id = 0; uint32_t client_handle = 0; double revised_sampling_interval = 0; uint32_t revised_queue_size = 0; UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; };
1699
1993
 
1700
1994
  class AddMonitoredWorker : public ClientCommandWorker {
1701
1995
  public:
@@ -1750,14 +2044,14 @@ class AddMonitoredWorker : public ClientCommandWorker {
1750
2044
  for (size_t index = 0; index < inputs_.size(); ++index) {
1751
2045
  MonitorResult result; result.node_id = inputs_[index].node_id; result.client_handle = contexts[index]->client_handle;
1752
2046
  if (index < response.resultsSize) {
1753
- result.status = response.results[index].statusCode; result.monitored_item_id = response.results[index].monitoredItemId;
2047
+ result.status = response.results[index].statusCode; result.monitored_item_id = response.results[index].monitoredItemId; result.revised_sampling_interval = response.results[index].revisedSamplingInterval; result.revised_queue_size = response.results[index].revisedQueueSize;
1754
2048
  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]); }
1755
2049
  }
1756
2050
  results_.push_back(result);
1757
2051
  }
1758
2052
  UA_CreateMonitoredItemsResponse_clear(&response);
1759
2053
  }
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); }
2054
+ 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("revisedSamplingInterval", results_[index].revised_sampling_interval); item.Set("revisedQueueSize", results_[index].revised_queue_size); item.Set("statusCode", StatusName(results_[index].status)); output.Set(index, item); } deferred_.Resolve(output); }
1761
2055
  private:
1762
2056
  uint32_t subscription_id_; std::vector<MonitorInput> inputs_; std::vector<MonitorResult> results_;
1763
2057
  UA_TimestampsToReturn timestamps_to_return_;
@@ -1956,7 +2250,7 @@ Napi::Value WriteManyAsync(const Napi::CallbackInfo& info) {
1956
2250
 
1957
2251
  Napi::Value BrowseAsync(const Napi::CallbackInfo& info) {
1958
2252
  Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context || !info[1].IsObject()) return env.Undefined(); Napi::Object object = info[1].As<Napi::Object>(); BrowseOptions options;
1959
- options.root_node_id = GetString(object, "rootNodeId", "ns=0;i=85"); options.recursive = GetBool(object, "recursive", true); options.variables_only = GetBool(object, "variablesOnly", true); options.max_depth = GetUInt(object, "maxDepth", 10); options.max_nodes = GetUInt(object, "maxNodes", 10000); options.attribute_batch_size = GetUInt(object, "attributeBatchSize", 200);
2253
+ options.root_node_id = GetString(object, "rootNodeId", "ns=0;i=85"); options.recursive = GetBool(object, "recursive", true); options.variables_only = GetBool(object, "variablesOnly", true); options.max_depth = GetUInt(object, "maxDepth", 10); options.max_nodes = GetUInt(object, "maxNodes", 10000); options.browse_batch_size = GetUInt(object, "browseBatchSize", 100); options.references_per_node = GetUInt(object, "referencesPerNode", 1000); options.attribute_batch_size = GetUInt(object, "attributeBatchSize", 200);
1960
2254
  if (object.Has("namespaceIndexes") && object.Get("namespaceIndexes").IsArray()) { Napi::Array values = object.Get("namespaceIndexes").As<Napi::Array>(); for (uint32_t index = 0; index < values.Length(); ++index) options.namespaces.insert(static_cast<uint16_t>(values.Get(index).ToNumber().Uint32Value())); }
1961
2255
  auto deferred = Napi::Promise::Deferred::New(env); (new BrowseWorker(deferred, context, std::move(options)))->Queue(); return deferred.Promise();
1962
2256
  }