@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/BENCHMARK.md +27 -0
- package/README.md +28 -1
- package/lib/client.js +23 -1
- package/lib/points.js +40 -5
- package/nodes/browse.html +13 -2
- package/nodes/browse.js +29 -2
- package/nodes/connection.js +69 -0
- package/nodes/read.html +2 -0
- package/nodes/read.js +10 -0
- package/nodes/subscribe.html +163 -5
- package/nodes/subscribe.js +180 -19
- package/nodes/write.html +2 -0
- package/nodes/write.js +10 -0
- package/package.json +3 -2
- package/prebuilds/linux-x64-musl/opcua_open62541.node +0 -0
- package/scripts/verify-offline-package.js +21 -1
- package/src/addon.cc +335 -49
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
|
|
217
|
-
if (type == &UA_TYPES[UA_TYPES_UINT64]) return
|
|
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>(
|
|
264
|
-
if (name == "Byte") { UA_Byte value = static_cast<UA_Byte>(
|
|
265
|
-
if (name == "Int16") { UA_Int16 value = static_cast<UA_Int16>(
|
|
266
|
-
if (name == "UInt16") { UA_UInt16 value = static_cast<UA_UInt16>(
|
|
267
|
-
if (name == "Int32") { UA_Int32 value =
|
|
268
|
-
if (name == "UInt32") { UA_UInt32 value =
|
|
269
|
-
if (name == "Int64") {
|
|
270
|
-
if (name == "UInt64") {
|
|
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
|
|
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
|
|
1543
|
-
|
|
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
|
-
|
|
1552
|
-
|
|
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 =
|
|
1555
|
-
request.nodesToBrowseSize =
|
|
1556
|
-
request.nodesToBrowse = static_cast<UA_BrowseDescription*>(
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
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
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
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
|
}
|