@tier0/opcua-client 0.4.1 → 0.5.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>
@@ -201,6 +203,94 @@ std::string BuiltinTypeName(const UA_NodeId& node_id) {
201
203
  }
202
204
  }
203
205
 
206
+ constexpr UA_Int64 kJsSafeInteger = 9007199254740991LL;
207
+
208
+ Napi::Value Int64Value(Napi::Env env, UA_Int64 value) {
209
+ if (value >= -kJsSafeInteger && value <= kJsSafeInteger) {
210
+ return Napi::Number::New(env, static_cast<double>(value));
211
+ }
212
+ return Napi::String::New(env, std::to_string(value));
213
+ }
214
+
215
+ Napi::Value UInt64Value(Napi::Env env, UA_UInt64 value) {
216
+ if (value <= static_cast<UA_UInt64>(kJsSafeInteger)) {
217
+ return Napi::Number::New(env, static_cast<double>(value));
218
+ }
219
+ return Napi::String::New(env, std::to_string(value));
220
+ }
221
+
222
+ bool InvalidInteger(Napi::Env env, const std::string& name,
223
+ const std::string& detail) {
224
+ Napi::RangeError::New(env, "Invalid " + name + " value: " + detail)
225
+ .ThrowAsJavaScriptException();
226
+ return false;
227
+ }
228
+
229
+ bool ParseIntegerNumber(Napi::Env env, const Napi::Value& input,
230
+ const std::string& name, double minimum,
231
+ double maximum, double* output) {
232
+ if (!input.IsNumber() && !input.IsString()) {
233
+ return InvalidInteger(env, name, "expected an integer number");
234
+ }
235
+ if (input.IsString() && input.As<Napi::String>().Utf8Value().empty()) {
236
+ return InvalidInteger(env, name, "empty string");
237
+ }
238
+ const double value = input.ToNumber().DoubleValue();
239
+ if (!std::isfinite(value) || std::trunc(value) != value ||
240
+ value < minimum || value > maximum) {
241
+ return InvalidInteger(env, name, input.ToString().Utf8Value());
242
+ }
243
+ *output = value;
244
+ return true;
245
+ }
246
+
247
+ bool ParseInt64(Napi::Env env, const Napi::Value& input, UA_Int64* output) {
248
+ if (input.IsBigInt()) {
249
+ bool lossless = false;
250
+ *output = input.As<Napi::BigInt>().Int64Value(&lossless);
251
+ return lossless || InvalidInteger(env, "Int64", "out of range");
252
+ }
253
+ if (input.IsString()) {
254
+ const std::string text = input.As<Napi::String>().Utf8Value();
255
+ const auto result = std::from_chars(
256
+ text.data(), text.data() + text.size(), *output, 10);
257
+ if (!text.empty() && result.ec == std::errc() &&
258
+ result.ptr == text.data() + text.size()) return true;
259
+ return InvalidInteger(env, "Int64", text);
260
+ }
261
+ double value = 0;
262
+ if (!ParseIntegerNumber(env, input, "Int64",
263
+ -static_cast<double>(kJsSafeInteger),
264
+ static_cast<double>(kJsSafeInteger), &value)) {
265
+ return false;
266
+ }
267
+ *output = static_cast<UA_Int64>(value);
268
+ return true;
269
+ }
270
+
271
+ bool ParseUInt64(Napi::Env env, const Napi::Value& input, UA_UInt64* output) {
272
+ if (input.IsBigInt()) {
273
+ bool lossless = false;
274
+ *output = input.As<Napi::BigInt>().Uint64Value(&lossless);
275
+ return lossless || InvalidInteger(env, "UInt64", "out of range");
276
+ }
277
+ if (input.IsString()) {
278
+ const std::string text = input.As<Napi::String>().Utf8Value();
279
+ const auto result = std::from_chars(
280
+ text.data(), text.data() + text.size(), *output, 10);
281
+ if (!text.empty() && result.ec == std::errc() &&
282
+ result.ptr == text.data() + text.size()) return true;
283
+ return InvalidInteger(env, "UInt64", text);
284
+ }
285
+ double value = 0;
286
+ if (!ParseIntegerNumber(env, input, "UInt64", 0,
287
+ static_cast<double>(kJsSafeInteger), &value)) {
288
+ return false;
289
+ }
290
+ *output = static_cast<UA_UInt64>(value);
291
+ return true;
292
+ }
293
+
204
294
  Napi::Value VariantValue(Napi::Env env, const UA_Variant* variant) {
205
295
  if (!variant || !variant->type || !variant->data) return env.Null();
206
296
  const UA_DataType* type = variant->type;
@@ -213,8 +303,8 @@ Napi::Value VariantValue(Napi::Env env, const UA_Variant* variant) {
213
303
  if (type == &UA_TYPES[UA_TYPES_UINT16]) return Napi::Number::New(env, *static_cast<const UA_UInt16*>(data));
214
304
  if (type == &UA_TYPES[UA_TYPES_INT32]) return Napi::Number::New(env, *static_cast<const UA_Int32*>(data));
215
305
  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)));
306
+ if (type == &UA_TYPES[UA_TYPES_INT64]) return Int64Value(env, *static_cast<const UA_Int64*>(data));
307
+ if (type == &UA_TYPES[UA_TYPES_UINT64]) return UInt64Value(env, *static_cast<const UA_UInt64*>(data));
218
308
  if (type == &UA_TYPES[UA_TYPES_FLOAT]) return Napi::Number::New(env, *static_cast<const UA_Float*>(data));
219
309
  if (type == &UA_TYPES[UA_TYPES_DOUBLE]) return Napi::Number::New(env, *static_cast<const UA_Double*>(data));
220
310
  if (type == &UA_TYPES[UA_TYPES_STRING]) return Napi::String::New(env, UaString(*static_cast<const UA_String*>(data)));
@@ -260,14 +350,14 @@ bool JsScalarToVariant(Napi::Env env, const Napi::Value& input, const std::strin
260
350
  UA_Boolean value = input.ToBoolean();
261
351
  return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type));
262
352
  }
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)); }
353
+ 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)); }
354
+ 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)); }
355
+ 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)); }
356
+ 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)); }
357
+ 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)); }
358
+ 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)); }
359
+ if (name == "Int64") { UA_Int64 value; if (!ParseInt64(env, input, &value)) return false; return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
360
+ if (name == "UInt64") { UA_UInt64 value; if (!ParseUInt64(env, input, &value)) return false; return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
271
361
  if (name == "Float") { UA_Float value = input.ToNumber().FloatValue(); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
272
362
  if (name == "Double") { UA_Double value = input.ToNumber().DoubleValue(); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
273
363
  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 +1571,8 @@ struct BrowseOptions {
1481
1571
  bool variables_only = true;
1482
1572
  uint32_t max_depth = 10;
1483
1573
  uint32_t max_nodes = 10000;
1574
+ uint32_t browse_batch_size = 100;
1575
+ uint32_t references_per_node = 1000;
1484
1576
  uint32_t attribute_batch_size = 200;
1485
1577
  std::set<uint16_t> namespaces;
1486
1578
  };
@@ -1512,16 +1604,51 @@ class BrowseWorker : public ClientCommandWorker {
1512
1604
  item.Set("userAccessLevel", point.user_access_level); item.Set("minimumSamplingInterval", point.minimum_sampling_interval);
1513
1605
  output.Set(index, item);
1514
1606
  }
1607
+ output.Set("truncated", truncated_);
1608
+ if (!truncation_reason_.empty()) output.Set("truncationReason", truncation_reason_);
1609
+ output.Set("serviceCalls", Napi::Number::New(
1610
+ Env(), static_cast<double>(service_calls_)));
1611
+ output.Set("browseBatchSize", browse_batch_size_);
1612
+ output.Set("releasedContinuationPoints", Napi::Number::New(
1613
+ Env(), static_cast<double>(released_continuation_points_)));
1614
+ Napi::Array failures = Napi::Array::New(Env(), browse_failures_.size());
1615
+ for (size_t index = 0; index < browse_failures_.size(); ++index) {
1616
+ Napi::Object failure = Napi::Object::New(Env());
1617
+ failure.Set("nodeId", browse_failures_[index].first);
1618
+ failure.Set("statusCode", browse_failures_[index].second);
1619
+ failures.Set(index, failure);
1620
+ }
1621
+ output.Set("browseFailures", failures);
1515
1622
  deferred_.Resolve(output);
1516
1623
  }
1517
1624
  private:
1518
1625
  struct QueueItem { std::string node_id; std::string path; uint32_t depth; };
1626
+ struct PendingContinuation {
1627
+ QueueItem parent;
1628
+ std::vector<UA_Byte> token;
1629
+ };
1519
1630
 
1520
1631
  bool NamespaceAllowed(uint16_t value) const { return options_.namespaces.empty() || options_.namespaces.count(value) > 0; }
1521
1632
 
1633
+ void MarkTruncated(const std::string& reason) {
1634
+ truncated_ = true;
1635
+ if (truncation_reason_.empty() || truncation_reason_ == "maxDepth") {
1636
+ truncation_reason_ = reason;
1637
+ }
1638
+ }
1639
+
1640
+ void MarkDepthLimited() {
1641
+ depth_limited_ = true;
1642
+ if (truncation_reason_.empty()) truncation_reason_ = "maxDepth";
1643
+ }
1644
+
1522
1645
  void ProcessBrowseResult(const QueueItem& parent, const UA_BrowseResult& result,
1523
1646
  std::deque<QueueItem>* queue, std::set<std::string>* visited) {
1524
- for (size_t index = 0; index < result.referencesSize && points_.size() < options_.max_nodes; ++index) {
1647
+ for (size_t index = 0; index < result.referencesSize; ++index) {
1648
+ if (points_.size() >= options_.max_nodes) {
1649
+ MarkTruncated("maxNodes");
1650
+ return;
1651
+ }
1525
1652
  const UA_ReferenceDescription& reference = result.references[index];
1526
1653
  if (reference.nodeId.serverIndex != 0 || reference.nodeId.namespaceUri.length != 0) continue;
1527
1654
  const UA_NodeId& node_id = reference.nodeId.nodeId;
@@ -1539,58 +1666,206 @@ class BrowseWorker : public ClientCommandWorker {
1539
1666
  point.node_class = reference.nodeClass; point.type_definition = NodeIdString(reference.typeDefinition.nodeId);
1540
1667
  points_.push_back(std::move(point));
1541
1668
  }
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});
1669
+ if (options_.recursive) {
1670
+ if (parent.depth < options_.max_depth) {
1671
+ if (visited->insert(node_id_text).second) {
1672
+ queue->push_back({node_id_text, path, parent.depth + 1});
1673
+ }
1674
+ } else if (reference.nodeClass == UA_NODECLASS_OBJECT) {
1675
+ MarkDepthLimited();
1676
+ }
1677
+ }
1678
+ }
1679
+ }
1680
+
1681
+ static PendingContinuation Continuation(
1682
+ const QueueItem& parent, const UA_ByteString& point) {
1683
+ PendingContinuation output{parent, {}};
1684
+ if (point.length && point.data) {
1685
+ output.token.assign(point.data, point.data + point.length);
1686
+ }
1687
+ return output;
1688
+ }
1689
+
1690
+ bool FillContinuationPoints(
1691
+ const std::vector<PendingContinuation>& pending, size_t start, size_t count,
1692
+ UA_BrowseNextRequest* request) {
1693
+ request->continuationPointsSize = count;
1694
+ request->continuationPoints = static_cast<UA_ByteString*>(
1695
+ UA_Array_new(count, &UA_TYPES[UA_TYPES_BYTESTRING]));
1696
+ if (!request->continuationPoints) return false;
1697
+ for (size_t index = 0; index < count; ++index) {
1698
+ const auto& token = pending[start + index].token;
1699
+ UA_ByteString source;
1700
+ source.length = token.size();
1701
+ source.data = const_cast<UA_Byte*>(token.data());
1702
+ if (UA_ByteString_copy(&source, &request->continuationPoints[index]) !=
1703
+ UA_STATUSCODE_GOOD) {
1704
+ return false;
1705
+ }
1706
+ }
1707
+ return true;
1708
+ }
1709
+
1710
+ void ReleaseContinuations(const std::vector<PendingContinuation>& pending) {
1711
+ for (size_t start = 0; start < pending.size(); start += browse_batch_size_) {
1712
+ const size_t count = std::min<size_t>(
1713
+ browse_batch_size_, pending.size() - start);
1714
+ UA_BrowseNextRequest request;
1715
+ UA_BrowseNextRequest_init(&request);
1716
+ request.releaseContinuationPoints = true;
1717
+ if (!FillContinuationPoints(pending, start, count, &request)) {
1718
+ UA_BrowseNextRequest_clear(&request);
1719
+ continue;
1720
+ }
1721
+ UA_BrowseNextResponse response =
1722
+ UA_Client_Service_browseNext(context_->client, request);
1723
+ ++service_calls_;
1724
+ UA_BrowseNextRequest_clear(&request);
1725
+ UA_BrowseNextResponse_clear(&response);
1726
+ released_continuation_points_ += count;
1727
+ }
1728
+ }
1729
+
1730
+ bool BrowseContinuations(
1731
+ std::vector<PendingContinuation>* pending, std::deque<QueueItem>* queue,
1732
+ std::set<std::string>* visited) {
1733
+ while (!pending->empty()) {
1734
+ std::vector<PendingContinuation> next_pending;
1735
+ for (size_t start = 0; start < pending->size(); start += browse_batch_size_) {
1736
+ const size_t count = std::min<size_t>(
1737
+ browse_batch_size_, pending->size() - start);
1738
+ UA_BrowseNextRequest request;
1739
+ UA_BrowseNextRequest_init(&request);
1740
+ if (!FillContinuationPoints(*pending, start, count, &request)) {
1741
+ UA_BrowseNextRequest_clear(&request);
1742
+ ReleaseContinuations(*pending);
1743
+ SetError("Failed to allocate BrowseNext continuation points");
1744
+ return false;
1745
+ }
1746
+ UA_BrowseNextResponse response =
1747
+ UA_Client_Service_browseNext(context_->client, request);
1748
+ ++service_calls_;
1749
+ UA_BrowseNextRequest_clear(&request);
1750
+ if (!UA_StatusCode_isGood(response.responseHeader.serviceResult)) {
1751
+ const std::string status = StatusName(
1752
+ response.responseHeader.serviceResult);
1753
+ UA_BrowseNextResponse_clear(&response);
1754
+ ReleaseContinuations(*pending);
1755
+ SetError("BrowseNext failed: " + status);
1756
+ return false;
1757
+ }
1758
+ for (size_t index = 0; index < count; ++index) {
1759
+ const auto& parent = (*pending)[start + index].parent;
1760
+ if (index < response.resultsSize) {
1761
+ const UA_BrowseResult& result = response.results[index];
1762
+ if (UA_StatusCode_isGood(result.statusCode) && !truncated_) {
1763
+ ProcessBrowseResult(parent, result, queue, visited);
1764
+ } else if (!UA_StatusCode_isGood(result.statusCode)) {
1765
+ browse_failures_.push_back(
1766
+ {parent.node_id, StatusName(result.statusCode)});
1767
+ }
1768
+ if (result.continuationPoint.length) {
1769
+ next_pending.push_back(
1770
+ Continuation(parent, result.continuationPoint));
1771
+ }
1772
+ } else {
1773
+ browse_failures_.push_back(
1774
+ {parent.node_id, "BadUnexpectedError"});
1775
+ }
1776
+ }
1777
+ UA_BrowseNextResponse_clear(&response);
1778
+ }
1779
+ if (truncated_) {
1780
+ ReleaseContinuations(next_pending);
1781
+ return true;
1544
1782
  }
1783
+ *pending = std::move(next_pending);
1545
1784
  }
1785
+ return true;
1546
1786
  }
1547
1787
 
1548
1788
  bool BrowseTree() {
1549
1789
  std::deque<QueueItem> queue{{options_.root_node_id, "", 0}};
1550
1790
  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();
1791
+ browse_batch_size_ = std::max<uint32_t>(1, options_.browse_batch_size);
1792
+ const uint32_t server_limit =
1793
+ context_->operation_limits.max_nodes_per_browse.load();
1794
+ if (server_limit > 0) {
1795
+ browse_batch_size_ = std::min(browse_batch_size_, server_limit);
1796
+ }
1797
+ while (!queue.empty() && !truncated_) {
1798
+ std::vector<QueueItem> batch;
1799
+ batch.reserve(browse_batch_size_);
1800
+ while (!queue.empty() && batch.size() < browse_batch_size_) {
1801
+ batch.push_back(std::move(queue.front()));
1802
+ queue.pop_front();
1803
+ }
1553
1804
  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;
1805
+ request.requestedMaxReferencesPerNode = options_.references_per_node;
1806
+ request.nodesToBrowseSize = batch.size();
1807
+ request.nodesToBrowse = static_cast<UA_BrowseDescription*>(
1808
+ UA_Array_new(batch.size(), &UA_TYPES[UA_TYPES_BROWSEDESCRIPTION]));
1809
+ if (!request.nodesToBrowse) {
1810
+ SetError("Failed to allocate Browse request");
1811
+ return false;
1812
+ }
1813
+ for (size_t index = 0; index < batch.size(); ++index) {
1814
+ if (!CopyCachedNodeId(context_.get(), batch[index].node_id,
1815
+ &request.nodesToBrowse[index].nodeId)) {
1816
+ const std::string invalid = batch[index].node_id;
1817
+ UA_BrowseRequest_clear(&request);
1818
+ SetError("Invalid browse NodeId: " + invalid);
1819
+ return false;
1820
+ }
1821
+ auto& description = request.nodesToBrowse[index];
1822
+ description.browseDirection = UA_BROWSEDIRECTION_FORWARD;
1823
+ description.referenceTypeId =
1824
+ UA_NODEID_NUMERIC(0, UA_NS0ID_HIERARCHICALREFERENCES);
1825
+ description.includeSubtypes = true;
1826
+ description.nodeClassMask = UA_NODECLASS_OBJECT | UA_NODECLASS_VARIABLE;
1827
+ description.resultMask = UA_BROWSERESULTMASK_ALL;
1559
1828
  }
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
1829
  UA_BrowseResponse response = UA_Client_Service_browse(context_->client, request);
1830
+ ++service_calls_;
1567
1831
  UA_BrowseRequest_clear(&request);
1568
1832
  if (!UA_StatusCode_isGood(response.responseHeader.serviceResult)) {
1569
1833
  UA_BrowseResponse_clear(&response); SetError("Browse failed: " + StatusName(response.responseHeader.serviceResult)); return false;
1570
1834
  }
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;
1835
+ std::vector<PendingContinuation> pending;
1836
+ for (size_t index = 0; index < batch.size(); ++index) {
1837
+ if (index < response.resultsSize) {
1838
+ const UA_BrowseResult& result = response.results[index];
1839
+ if (UA_StatusCode_isGood(result.statusCode) && !truncated_) {
1840
+ ProcessBrowseResult(batch[index], result, &queue, &visited);
1841
+ } else if (!UA_StatusCode_isGood(result.statusCode)) {
1842
+ browse_failures_.push_back(
1843
+ {batch[index].node_id, StatusName(result.statusCode)});
1844
+ }
1845
+ if (result.continuationPoint.length) {
1846
+ pending.push_back(
1847
+ Continuation(batch[index], result.continuationPoint));
1585
1848
  }
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);
1849
+ } else {
1850
+ browse_failures_.push_back(
1851
+ {batch[index].node_id, "BadUnexpectedError"});
1589
1852
  }
1590
- UA_ByteString_clear(&continuation);
1591
1853
  }
1592
1854
  UA_BrowseResponse_clear(&response);
1855
+ if (points_.size() >= options_.max_nodes &&
1856
+ (!pending.empty() || !queue.empty())) {
1857
+ MarkTruncated("maxNodes");
1858
+ }
1859
+ if (truncated_) {
1860
+ ReleaseContinuations(pending);
1861
+ break;
1862
+ }
1863
+ if (!BrowseContinuations(&pending, &queue, &visited)) return false;
1864
+ if (points_.size() >= options_.max_nodes && !queue.empty()) {
1865
+ MarkTruncated("maxNodes");
1866
+ }
1593
1867
  }
1868
+ if (depth_limited_) truncated_ = true;
1594
1869
  return true;
1595
1870
  }
1596
1871
 
@@ -1647,6 +1922,13 @@ class BrowseWorker : public ClientCommandWorker {
1647
1922
 
1648
1923
  BrowseOptions options_;
1649
1924
  std::vector<BrowsePoint> points_;
1925
+ uint32_t browse_batch_size_ = 1;
1926
+ uint64_t service_calls_ = 0;
1927
+ uint64_t released_continuation_points_ = 0;
1928
+ bool truncated_ = false;
1929
+ bool depth_limited_ = false;
1930
+ std::string truncation_reason_;
1931
+ std::vector<std::pair<std::string, std::string>> browse_failures_;
1650
1932
  };
1651
1933
 
1652
1934
  class CreateSubscriptionWorker : public ClientCommandWorker {
@@ -1673,6 +1955,9 @@ class CreateSubscriptionWorker : public ClientCommandWorker {
1673
1955
  if (!context_->client) { SetError("Client disconnected"); return; }
1674
1956
  UA_CreateSubscriptionResponse response = UA_Client_Subscriptions_create(context_->client, request, nullptr, nullptr, nullptr);
1675
1957
  status_ = response.responseHeader.serviceResult; subscription_id_ = response.subscriptionId;
1958
+ revised_publishing_interval_ = response.revisedPublishingInterval;
1959
+ revised_lifetime_ = response.revisedLifetimeCount;
1960
+ revised_keepalive_ = response.revisedMaxKeepAliveCount;
1676
1961
  UA_CreateSubscriptionResponse_clear(&response);
1677
1962
  if (!UA_StatusCode_isGood(status_)) { SetError("Create subscription failed: " + StatusName(status_)); return; }
1678
1963
  auto subscription = std::make_unique<SubscriptionContext>();
@@ -1685,17 +1970,18 @@ class CreateSubscriptionWorker : public ClientCommandWorker {
1685
1970
  subscription->notifications->overflow_policy = overflow_policy_;
1686
1971
  context_->subscriptions[subscription_id_] = std::move(subscription); transferred_ = true;
1687
1972
  }
1688
- void OnOK() override { Napi::Object result = Napi::Object::New(Env()); result.Set("subscriptionId", subscription_id_); result.Set("statusCode", StatusName(status_)); deferred_.Resolve(result); }
1973
+ 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
1974
  void OnError(const Napi::Error& error) override { if (!transferred_) callback_.Release(); PromiseWorker::OnError(error); }
1690
1975
  private:
1691
1976
  Napi::ThreadSafeFunction callback_; bool transferred_ = false;
1692
1977
  double publishing_interval_; uint32_t lifetime_; uint32_t keepalive_; uint32_t max_notifications_;
1693
1978
  uint32_t queue_size_; uint32_t callback_batch_size_; std::string queue_mode_; std::string overflow_policy_;
1694
1979
  uint32_t subscription_id_ = 0; UA_StatusCode status_ = UA_STATUSCODE_GOOD;
1980
+ double revised_publishing_interval_ = 0; uint32_t revised_lifetime_ = 0; uint32_t revised_keepalive_ = 0;
1695
1981
  };
1696
1982
 
1697
1983
  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; };
1984
+ 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
1985
 
1700
1986
  class AddMonitoredWorker : public ClientCommandWorker {
1701
1987
  public:
@@ -1750,14 +2036,14 @@ class AddMonitoredWorker : public ClientCommandWorker {
1750
2036
  for (size_t index = 0; index < inputs_.size(); ++index) {
1751
2037
  MonitorResult result; result.node_id = inputs_[index].node_id; result.client_handle = contexts[index]->client_handle;
1752
2038
  if (index < response.resultsSize) {
1753
- result.status = response.results[index].statusCode; result.monitored_item_id = response.results[index].monitoredItemId;
2039
+ 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
2040
  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
2041
  }
1756
2042
  results_.push_back(result);
1757
2043
  }
1758
2044
  UA_CreateMonitoredItemsResponse_clear(&response);
1759
2045
  }
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); }
2046
+ 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
2047
  private:
1762
2048
  uint32_t subscription_id_; std::vector<MonitorInput> inputs_; std::vector<MonitorResult> results_;
1763
2049
  UA_TimestampsToReturn timestamps_to_return_;
@@ -1956,7 +2242,7 @@ Napi::Value WriteManyAsync(const Napi::CallbackInfo& info) {
1956
2242
 
1957
2243
  Napi::Value BrowseAsync(const Napi::CallbackInfo& info) {
1958
2244
  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);
2245
+ 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
2246
  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
2247
  auto deferred = Napi::Promise::Deferred::New(env); (new BrowseWorker(deferred, context, std::move(options)))->Queue(); return deferred.Promise();
1962
2248
  }