node-opcua-address-space 2.187.12 → 2.187.16

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.
@@ -278,7 +278,7 @@ export class UAVariableImpl extends BaseNodeImpl {
278
278
  // Table 51: no encoding can be applied to a non-Structure value
279
279
  return makeNowDataValue({ statusCode: StatusCodes.BadDataEncodingInvalid });
280
280
  }
281
- if (this._timestamped_get_func) {
281
+ if (this._timestamped_get_func && !this._getterIsAsync) {
282
282
  if (this._timestamped_get_func.length === 0) {
283
283
  const dataValueOrPromise = this._timestamped_get_func();
284
284
  if (!Object.hasOwn(dataValueOrPromise.constructor.prototype, "then")) {
@@ -350,7 +350,7 @@ export class UAVariableImpl extends BaseNodeImpl {
350
350
  const dataTypeNode = this.addressSpace.findDataType(this.dataType);
351
351
  return dataTypeNode._getEnumerationInfo();
352
352
  }
353
- asyncRefresh(oldestDate, callback) {
353
+ asyncRefresh(oldestDate, callback, context) {
354
354
  // asyncRefresh is always invoked with a callback at runtime: the public overload without
355
355
  // a callback is only reachable through the withCallback() wrapper installed below, which
356
356
  // synthesizes one before calling into this implementation.
@@ -409,7 +409,7 @@ export class UAVariableImpl extends BaseNodeImpl {
409
409
  this._internal_set_dataValue(refreshed, null);
410
410
  }
411
411
  cb(err, this.$dataValue);
412
- });
412
+ }, context);
413
413
  answeredAtOnce = false;
414
414
  }
415
415
  catch (err) {
@@ -729,6 +729,11 @@ export class UAVariableImpl extends BaseNodeImpl {
729
729
  assert(write_func);
730
730
  write_func.call(this, dataValue, (err, statusCode1) => {
731
731
  if (!err) {
732
+ // a write the setter answers Bad did not happen: the stored value stays as it was
733
+ if (statusCode1?.isNotGood()) {
734
+ callback?.(null, statusCode1);
735
+ return;
736
+ }
732
737
  dataValue && this.verifyVariantCompatibility(dataValue.value);
733
738
  if (indexRange && !indexRange.isEmpty()) {
734
739
  if (!indexRange.isValid()) {
@@ -791,7 +796,7 @@ export class UAVariableImpl extends BaseNodeImpl {
791
796
  }
792
797
  }
793
798
  callback?.(err || null, statusCode1);
794
- });
799
+ }, context, indexRange);
795
800
  return undefined;
796
801
  }
797
802
  writeAttribute(context, writeValueOptions, callback) {
@@ -1005,6 +1010,7 @@ export class UAVariableImpl extends BaseNodeImpl {
1005
1010
  this._set_func = null;
1006
1011
  this.refreshFunc = undefined;
1007
1012
  this._refreshFuncWrapsGetter = false;
1013
+ this._getterIsAsync = false;
1008
1014
  this._historyRead = UAVariableImpl.prototype._historyRead;
1009
1015
  }
1010
1016
  options = options || {};
@@ -1034,37 +1040,26 @@ export class UAVariableImpl extends BaseNodeImpl {
1034
1040
  this._readValueAsync(context, (err, dataValue) => (err ? reject(err) : resolve(dataValue)));
1035
1041
  });
1036
1042
  }
1037
- _readValueAsync(context, callback, readAt) {
1038
- context = context || SessionContext.defaultContext;
1039
- if (this.__waiting_callback) {
1040
- // a refresh is in progress: its value answers this read too
1041
- this.__more_waiting_callbacks ??= [];
1042
- this.__more_waiting_callbacks.push(callback);
1043
- return;
1044
- }
1045
- if (this.isDisposed()) {
1046
- callback?.(null, new DataValue({ statusCode: StatusCodes.BadNodeIdUnknown }));
1047
- return;
1048
- }
1049
- this.__waiting_callback = callback;
1050
- // runs once per sample of every monitored item, so nothing here allocates a closure or
1051
- // binds a function: the denial (if any) is a StatusCode, and the callback that releases
1052
- // the waiting readers is made once per variable
1053
- let denied = null;
1043
+ /**
1044
+ * why this context may not read the Value, or null when it may: the gates of readValue(),
1045
+ * applied to every read of readValueAsync, including one that joins a refresh in flight.
1046
+ * Runs once per sample of every monitored item: it allocates nothing
1047
+ */
1048
+ _readDenial(context) {
1054
1049
  if (context.isAccessRestricted(this)) {
1055
1050
  // same gate as readValue(): a variable bound with a simple get()
1056
1051
  // also gets a refreshFunc, sending every read down the asyncRefresh
1057
- // branch below - which calls the getter directly and never passes
1052
+ // branch of _readValueAsync - which calls the getter directly and never passes
1058
1053
  // through readValue(). Without this check here, a node hardened
1059
1054
  // with AccessRestrictions(EncryptionRequired) answered its value
1060
1055
  // over an unencrypted channel.
1061
- denied = StatusCodes.BadSecurityModeInsufficient;
1056
+ return StatusCodes.BadSecurityModeInsufficient;
1062
1057
  }
1063
1058
  else if (!this.isReadable(context)) {
1064
- denied = StatusCodes.BadNotReadable;
1059
+ return StatusCodes.BadNotReadable;
1065
1060
  }
1066
1061
  else if (!this.checkPermissionPrivate(context, PermissionType.Read)) {
1067
- denied = StatusCodes.BadUserAccessDenied;
1062
+ return StatusCodes.BadUserAccessDenied;
1068
1063
  }
1069
1064
  else if (
1070
1065
  // as in readValue(): the Read permission has just passed, only the access level is
@@ -1072,8 +1067,33 @@ export class UAVariableImpl extends BaseNodeImpl {
1072
1067
  this.isUserReadable === UAVariableImpl.prototype.isUserReadable
1073
1068
  ? !this.checkAccessLevelPrivate(context, AccessLevelFlag.CurrentRead)
1074
1069
  : !this.isUserReadable(context)) {
1075
- denied = StatusCodes.BadNotReadable;
1070
+ return StatusCodes.BadNotReadable;
1071
+ }
1072
+ return null;
1073
+ }
1074
+ _readValueAsync(context, callback, readAt) {
1075
+ context = context || SessionContext.defaultContext;
1076
+ if (this.__waiting_callback) {
1077
+ // a refresh is in progress: its value answers this read too, once this read's own
1078
+ // access checks have passed (they depend on its context, not on the first reader's)
1079
+ const joinerDenied = this._readDenial(context);
1080
+ if (joinerDenied) {
1081
+ callback(null, makeNowDataValue({ statusCode: joinerDenied }));
1082
+ return;
1083
+ }
1084
+ this.__more_waiting_callbacks ??= [];
1085
+ this.__more_waiting_callbacks.push(callback);
1086
+ return;
1087
+ }
1088
+ if (this.isDisposed()) {
1089
+ callback?.(null, new DataValue({ statusCode: StatusCodes.BadNodeIdUnknown }));
1090
+ return;
1076
1091
  }
1092
+ this.__waiting_callback = callback;
1093
+ // runs once per sample of every monitored item, so nothing here allocates a closure or
1094
+ // binds a function: the denial (if any) is a StatusCode, and the callback that releases
1095
+ // the waiting readers is made once per variable
1096
+ const denied = this._readDenial(context);
1077
1097
  this.__satisfy_waiting_callbacks ??= (err, dataValue) => {
1078
1098
  // now call all pending callbacks, in the order of the reads
1079
1099
  const first = this.__waiting_callback;
@@ -1099,10 +1119,10 @@ export class UAVariableImpl extends BaseNodeImpl {
1099
1119
  this.__sharedReadClock = readAt !== undefined;
1100
1120
  if (this.asyncRefresh === wrappedAsyncRefresh) {
1101
1121
  // the thenified wrapper adds nothing when a callback is given
1102
- asyncRefreshImpl.call(this, clock, satisfy_callbacks);
1122
+ asyncRefreshImpl.call(this, clock, satisfy_callbacks, context);
1103
1123
  }
1104
1124
  else {
1105
- this.asyncRefresh(clock, satisfy_callbacks);
1125
+ this.asyncRefresh(clock, satisfy_callbacks, context);
1106
1126
  }
1107
1127
  // consumed by asyncRefresh on entry; cleared here for an overridden one that never reads it
1108
1128
  this.__sharedReadClock = false;
@@ -1711,7 +1731,17 @@ import { withCallback } from "thenify-ex";
1711
1731
  // readValueAsync calls the unwrapped asyncRefresh directly: binding the thenified wrapper on
1712
1732
  // every sample cost more than the refresh itself
1713
1733
  const asyncRefreshImpl = UAVariableImpl.prototype.asyncRefresh;
1714
- UAVariableImpl.prototype.asyncRefresh = withCallback(asyncRefreshImpl);
1734
+ // the promise form when no callback is given, as withCallback() does: written out because that
1735
+ // wrapper takes the last argument for the callback, and the context comes after it
1736
+ UAVariableImpl.prototype.asyncRefresh = function (oldestDate, callback, context) {
1737
+ if (typeof callback === "function") {
1738
+ asyncRefreshImpl.call(this, oldestDate, callback, context);
1739
+ return undefined;
1740
+ }
1741
+ return new Promise((resolve, reject) => {
1742
+ asyncRefreshImpl.call(this, oldestDate, (err, dataValue) => (err ? reject(err) : resolve(dataValue)), context);
1743
+ });
1744
+ };
1715
1745
  const wrappedAsyncRefresh = UAVariableImpl.prototype.asyncRefresh;
1716
1746
  UAVariableImpl.prototype.writeValue = withCallback(UAVariableImpl.prototype.writeValue);
1717
1747
  UAVariableImpl.prototype.writeAttribute = withCallback(UAVariableImpl.prototype.writeAttribute);
@@ -1822,7 +1852,8 @@ function turn_sync_to_async(f, numberOfArgs) {
1822
1852
  };
1823
1853
  }
1824
1854
  else {
1825
- assert(f.length === numberOfArgs + 1);
1855
+ // a callback-style function; it may declare the trailing context and indexRange parameters
1856
+ assert(f.length >= numberOfArgs + 1 && f.length <= numberOfArgs + 3);
1826
1857
  return f;
1827
1858
  }
1828
1859
  }
@@ -1901,14 +1932,16 @@ function _Variable_bind_with_timestamped_get(options) {
1901
1932
  "\n value_check.constructor.name ", dataValue_verify ? dataValue_verify.constructor.name : "null");
1902
1933
  throw new Error(" Bind variable error: " + " the timestamped_get function must return a DataValue");
1903
1934
  }
1935
+ this._getterIsAsync = isThenable(dataValue_verify);
1904
1936
  _Variable_bind_with_async_refresh.call(this, { refreshFunc: async_refresh_func });
1905
1937
  }
1906
- else if (options.timestamped_get.length === 1) {
1938
+ else if (options.timestamped_get.length === 1 || options.timestamped_get.length === 2) {
1939
+ // callback style: (callback) or (callback, context)
1907
1940
  _Variable_bind_with_async_refresh.call(this, { refreshFunc: options.timestamped_get });
1908
1941
  }
1909
1942
  else {
1910
- errorLog("timestamped_get has a invalid number of argument , should be 0 or 1 ");
1911
- throw new Error("timestamped_get has a invalid number of argument , should be 0 or 1 ");
1943
+ errorLog("timestamped_get has a invalid number of argument , should be 0, 1 or 2 ");
1944
+ throw new Error("timestamped_get has a invalid number of argument , should be 0, 1 or 2 ");
1912
1945
  }
1913
1946
  }
1914
1947
  // variation 1
@@ -1957,8 +1990,9 @@ function _Variable_bind_with_simple_set(options) {
1957
1990
  //
1958
1991
  const set_func = turn_sync_to_async(options.set, 1);
1959
1992
  this._set_func = set_func;
1960
- assert(set_func.length === 2, " set function must have 2 arguments ( variant, callback)");
1961
- this._timestamped_set_func = (timestamped_value, callback) => {
1993
+ assert(set_func.length >= 2, " set function must have 2 arguments ( variant, callback)");
1994
+ // the trailing arguments (context, indexRange) are a rest parameter so that the length stays 2
1995
+ this._timestamped_set_func = (timestamped_value, callback, ...extra) => {
1962
1996
  assert(timestamped_value instanceof DataValue);
1963
1997
  set_func.call(this, timestamped_value.value, (err, statusCode) => {
1964
1998
  // c8 ignore next
@@ -1967,30 +2001,45 @@ function _Variable_bind_with_simple_set(options) {
1967
2001
  errorLog(chalk.yellow("StatusCode.Good is assumed"));
1968
2002
  return callback(err, StatusCodes.Good, timestamped_value);
1969
2003
  }
1970
- if (statusCode?.isNotGood()) {
1971
- // record the value but still record the statusCode !
1972
- timestamped_value.statusCode = statusCode;
1973
- }
1974
2004
  callback(err, statusCode ?? StatusCodes.BadInternalError, timestamped_value);
1975
- });
2005
+ }, ...extra);
1976
2006
  };
1977
2007
  }
1978
2008
  function _Variable_bind_with_timestamped_set(options) {
1979
- assert(options.timestamped_set.length === 2 || options.timestamped_set.length === 1, "timestamped_set must have 2 parameters timestamped_set: function(dataValue,callback){} or one paramater timestamped_set: function(dataValue): Promise<StatusCode>{}");
2009
+ assert(options.timestamped_set.length >= 1 && options.timestamped_set.length <= 4, "timestamped_set must have 2 parameters timestamped_set: function(dataValue,callback){} or one paramater timestamped_set: function(dataValue): Promise<StatusCode>{}");
1980
2010
  assert(!options.set, "should not specify set when timestamped_set_func exists ");
1981
2011
  // the callback form of a setter is a stateless wrapper around it (it takes the variable as
1982
2012
  // `this`): one per setter function, not one per variable. Every writable variable without a
1983
2013
  // setter of its own binds the same default, and a closure per variable was 110 bytes each
1984
2014
  let setter = callbackSetters.get(options.timestamped_set);
1985
2015
  if (!setter) {
1986
- setter = convertToCallbackFunction1(options.timestamped_set);
2016
+ setter = convertToCallbackFunction1(options.timestamped_set, 2);
1987
2017
  callbackSetters.set(options.timestamped_set, setter);
1988
2018
  }
1989
2019
  this._timestamped_set_func = setter;
1990
2020
  }
1991
2021
  /** the callback form of each setter function met so far, see _Variable_bind_with_timestamped_set */
1992
2022
  const callbackSetters = new WeakMap();
2023
+ /** an asynchronous setter that is told who writes (`timestamped_set_ctx`) */
2024
+ function _Variable_bind_with_context_setter(setter) {
2025
+ // the context and the index range come after the callback, in a rest parameter: the length stays 2
2026
+ this._timestamped_set_func = function (dataValue, callback, ...extra) {
2027
+ const [context, indexRange] = extra;
2028
+ let promise;
2029
+ try {
2030
+ promise = setter.call(this, dataValue, { context, indexRange });
2031
+ }
2032
+ catch (err) {
2033
+ callback(err);
2034
+ return;
2035
+ }
2036
+ Promise.resolve(promise).then((statusCode) => callback(null, statusCode), (err) => callback(err));
2037
+ };
2038
+ }
1993
2039
  function bind_setter(options) {
2040
+ if (options.timestamped_set && options.timestamped_set_ctx) {
2041
+ throw new Error("bind_setter : options should not specify both timestamped_set and timestamped_set_ctx ");
2042
+ }
1994
2043
  if (options.set && typeof options.set === "function") {
1995
2044
  // variation 1
1996
2045
  if (options.timestamped_set === undefined) {
@@ -2008,8 +2057,13 @@ function bind_setter(options) {
2008
2057
  timestamped_set: options.timestamped_set
2009
2058
  });
2010
2059
  }
2011
- else if (typeof options.timestamped_get === "function") {
2012
- // timestamped_get is specified but timestamped_set is not
2060
+ else if (typeof options.timestamped_set_ctx === "function") {
2061
+ // variation 5: the setter is told who writes
2062
+ assert(typeof options.timestamped_get === "function" || typeof options.timestamped_get_ctx === "function", "timestamped_set_ctx must be used with timestamped_get or timestamped_get_ctx");
2063
+ _Variable_bind_with_context_setter.call(this, options.timestamped_set_ctx);
2064
+ }
2065
+ else if (typeof options.timestamped_get === "function" || typeof options.timestamped_get_ctx === "function") {
2066
+ // a timestamped getter is specified but no setter
2013
2067
  // => Value is read-only
2014
2068
  _Variable_bind_with_timestamped_set.call(this, {
2015
2069
  set: undefined,
@@ -2023,6 +2077,28 @@ function bind_setter(options) {
2023
2077
  });
2024
2078
  }
2025
2079
  }
2080
+ /**
2081
+ * variation 5: an asynchronous getter that is told who reads (`timestamped_get_ctx`). It is run by
2082
+ * the refresh only, like any asynchronous getter, with the session context of the read.
2083
+ */
2084
+ function _Variable_bind_with_context_getter(getter) {
2085
+ _Variable_bind_with_async_refresh.call(this, {
2086
+ refreshFunc: (callback, context) => {
2087
+ let promise;
2088
+ try {
2089
+ promise = getter.call(this, { context });
2090
+ }
2091
+ catch (err) {
2092
+ callback(err);
2093
+ return;
2094
+ }
2095
+ Promise.resolve(promise).then((dataValue) => callback(null, dataValue), (err) => {
2096
+ errorLog("timestamped_get_ctx error: Variable is ", this.nodeId.toString(), this.browseName.toString());
2097
+ callback(err);
2098
+ });
2099
+ }
2100
+ });
2101
+ }
2026
2102
  function bind_getter(options) {
2027
2103
  if (typeof options.get === "function") {
2028
2104
  // variation 1
@@ -2035,6 +2111,10 @@ function bind_getter(options) {
2035
2111
  timestamped_get: options.timestamped_get
2036
2112
  });
2037
2113
  }
2114
+ else if (typeof options.timestamped_get_ctx === "function") {
2115
+ // variation 5
2116
+ _Variable_bind_with_context_getter.call(this, options.timestamped_get_ctx);
2117
+ }
2038
2118
  else if (typeof options.refreshFunc === "function") {
2039
2119
  // variation 3
2040
2120
  if (options.get !== undefined || options.timestamped_get !== undefined) {