node-opcua-address-space 2.187.12 → 2.187.17

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.
@@ -942,7 +942,7 @@ export class AddressSpaceImpl implements AddressSpacePrivate {
942
942
  *
943
943
  *
944
944
  */
945
- public browsePath(browsePath: BrowsePath): BrowsePathResult {
945
+ public browsePath(browsePath: BrowsePath, context?: ISessionContext): BrowsePathResult {
946
946
  assert(browsePath instanceof BrowsePath);
947
947
 
948
948
  const startingNode = this.findNode(browsePath.startingNode);
@@ -989,10 +989,21 @@ export class AddressSpaceImpl implements AddressSpacePrivate {
989
989
 
990
990
  const res: BrowsePathTargetOptions[] = [];
991
991
 
992
+ // a node the context cannot browse is invisible: neither followed nor returned
993
+ const isBrowseDenied = (nodeId: NodeId): boolean => {
994
+ if (!context) {
995
+ return false;
996
+ }
997
+ const node = this.findNode(nodeId);
998
+ return !!node && context.isBrowseAccessRestricted(node);
999
+ };
1000
+
992
1001
  const explore_element = (curNodeObject: BaseNode, elements: RelativePathElement[], index: number) => {
993
1002
  const element = elements[index];
994
1003
  const is_last = index + 1 === elements.length;
995
- const nodeIds = curNodeObject.browseNodeByTargetName(element, is_last);
1004
+ const nodeIds = curNodeObject
1005
+ .browseNodeByTargetName(element, is_last)
1006
+ .filter((nodeId: NodeId) => !isBrowseDenied(nodeId));
996
1007
  const targets = nodeIds.map((nodeId: NodeId) => {
997
1008
  return {
998
1009
  remainingPathIndex: elements.length - index,
@@ -239,6 +239,11 @@ export function _getCompleteRequiredModelsFromValuesAndReferences(
239
239
  } else if (node.nodeClass === NodeClass.VariableType) {
240
240
  exploreValue((node as UAVariableTypeImpl).value);
241
241
  }
242
+ // the exporter translates the RoleId of every RolePermission too: a role of a model the
243
+ // namespace uses nothing else from is a dependency all the same
244
+ for (const rolePermission of (<BaseNodeImpl>node).rolePermissions || []) {
245
+ consider(rolePermission.roleId.namespace);
246
+ }
242
247
  const references = (<BaseNodeImpl>node).allReferences();
243
248
  for (const reference of references) {
244
249
  // check referenceId
@@ -27,7 +27,9 @@ import {
27
27
  makeDefaultCloneExtraInfo,
28
28
  type SetFunc,
29
29
  type VariableDataValueGetterSync,
30
+ type VariableDataValueGetterWithContext,
30
31
  type VariableDataValueSetterWithCallback,
32
+ type VariableDataValueSetterWithContext,
31
33
  type VariableSetter
32
34
  } from "node-opcua-address-space-base";
33
35
  import { assert } from "node-opcua-assert";
@@ -248,13 +250,18 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
248
250
  declare public _timestamped_set_func?: VariableDataValueSetterWithCallback | null;
249
251
  declare public _get_func?: GetFunc | null;
250
252
  declare public _set_func?: ((value: Variant, callback: (err: Error | null, statusCode?: StatusCode) => void) => void) | null;
251
- declare public refreshFunc?: (callback: CallbackT<DataValue>) => void;
253
+ declare public refreshFunc?: (callback: CallbackT<DataValue>, context?: ISessionContext) => void;
252
254
  /**
253
255
  * true when refreshFunc only wraps the synchronous getter of a Variable bound with a plain
254
256
  * `get`. readValue evaluates that getter itself: refreshing such a Variable before reading
255
257
  * it, as the Read service does for the others, evaluates the getter twice for nothing.
256
258
  */
257
259
  declare public _refreshFuncWrapsGetter?: boolean;
260
+ /**
261
+ * true when the timestamped getter answered a Promise when it was bound: it is run by asyncRefresh
262
+ * only. readValue cannot use a Promise: it serves the value the last refresh left.
263
+ */
264
+ declare public _getterIsAsync?: boolean;
258
265
  // the read that started the refresh in progress, and the reads that came while it was (an array only then):
259
266
  // a sampled variable is read once per sample, and one at a time
260
267
  declare private __waiting_callback?: CallbackT<DataValue>;
@@ -463,7 +470,7 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
463
470
  return makeNowDataValue({ statusCode: StatusCodes.BadDataEncodingInvalid });
464
471
  }
465
472
 
466
- if (this._timestamped_get_func) {
473
+ if (this._timestamped_get_func && !this._getterIsAsync) {
467
474
  if (this._timestamped_get_func.length === 0) {
468
475
  const dataValueOrPromise = (this._timestamped_get_func as VariableDataValueGetterSync)();
469
476
  if (!Object.hasOwn(dataValueOrPromise.constructor.prototype, "then")) {
@@ -547,9 +554,13 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
547
554
  return dataTypeNode._getEnumerationInfo();
548
555
  }
549
556
 
550
- public asyncRefresh(oldestDate: PreciseClock, callback: CallbackT<DataValue>): void;
557
+ public asyncRefresh(oldestDate: PreciseClock, callback: CallbackT<DataValue>, context?: ISessionContext): void;
551
558
  public asyncRefresh(oldestDate: PreciseClock): Promise<DataValue>;
552
- public asyncRefresh(oldestDate: PreciseClock, callback?: CallbackT<DataValue>): Promise<DataValue> | undefined {
559
+ public asyncRefresh(
560
+ oldestDate: PreciseClock,
561
+ callback?: CallbackT<DataValue>,
562
+ context?: ISessionContext
563
+ ): Promise<DataValue> | undefined {
553
564
  // asyncRefresh is always invoked with a callback at runtime: the public overload without
554
565
  // a callback is only reachable through the withCallback() wrapper installed below, which
555
566
  // synthesizes one before calling into this implementation.
@@ -588,33 +599,37 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
588
599
  const requestedAtPicoseconds = oldestDate.picoseconds;
589
600
  let answeredAtOnce = true;
590
601
  try {
591
- this.refreshFunc.call(this, (err: Error | null, dataValue?: DataValueLike) => {
592
- // c8 ignore next
593
- if (err || !dataValue) {
594
- errorLog(
595
- "-------------- refresh call failed",
596
- this.browseName.toString(),
597
- this.nodeId.toString(),
598
- err?.message
599
- );
600
- dataValue = { statusCode: StatusCodes.BadNoDataAvailable };
601
- }
602
- if (dataValue && dataValue !== this.$dataValue) {
603
- const refreshed = coerceDataValue(dataValue);
604
- if (!refreshed.serverTimestamp) {
605
- if (answeredAtOnce && sharedClock) {
606
- refreshed.serverTimestamp = requestedAt;
607
- refreshed.serverPicoseconds = requestedAtPicoseconds;
608
- } else {
609
- const now = getCurrentClock();
610
- refreshed.serverTimestamp = now.timestamp;
611
- refreshed.serverPicoseconds = now.picoseconds;
602
+ this.refreshFunc.call(
603
+ this,
604
+ (err: Error | null, dataValue?: DataValueLike) => {
605
+ // c8 ignore next
606
+ if (err || !dataValue) {
607
+ errorLog(
608
+ "-------------- refresh call failed",
609
+ this.browseName.toString(),
610
+ this.nodeId.toString(),
611
+ err?.message
612
+ );
613
+ dataValue = { statusCode: StatusCodes.BadNoDataAvailable };
614
+ }
615
+ if (dataValue && dataValue !== this.$dataValue) {
616
+ const refreshed = coerceDataValue(dataValue);
617
+ if (!refreshed.serverTimestamp) {
618
+ if (answeredAtOnce && sharedClock) {
619
+ refreshed.serverTimestamp = requestedAt;
620
+ refreshed.serverPicoseconds = requestedAtPicoseconds;
621
+ } else {
622
+ const now = getCurrentClock();
623
+ refreshed.serverTimestamp = now.timestamp;
624
+ refreshed.serverPicoseconds = now.picoseconds;
625
+ }
612
626
  }
627
+ this._internal_set_dataValue(refreshed, null);
613
628
  }
614
- this._internal_set_dataValue(refreshed, null);
615
- }
616
- cb(err, this.$dataValue);
617
- });
629
+ cb(err, this.$dataValue);
630
+ },
631
+ context
632
+ );
618
633
  answeredAtOnce = false;
619
634
  } catch (err) {
620
635
  errorLog("-------------- refresh call failed 2", this.browseName.toString(), this.nodeId.toString());
@@ -996,77 +1011,88 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
996
1011
  }
997
1012
  assert(write_func);
998
1013
 
999
- write_func.call(this, dataValue, (err?: Error | null, statusCode1?: StatusCode) => {
1000
- if (!err) {
1001
- dataValue && this.verifyVariantCompatibility(dataValue.value);
1002
-
1003
- if (indexRange && !indexRange.isEmpty()) {
1004
- if (!indexRange.isValid()) {
1005
- callback?.(null, StatusCodes.BadIndexRangeInvalid);
1014
+ write_func.call(
1015
+ this,
1016
+ dataValue,
1017
+ (err?: Error | null, statusCode1?: StatusCode) => {
1018
+ if (!err) {
1019
+ // a write the setter answers Bad did not happen: the stored value stays as it was
1020
+ if (statusCode1?.isNotGood()) {
1021
+ callback?.(null, statusCode1);
1006
1022
  return;
1007
1023
  }
1024
+ dataValue && this.verifyVariantCompatibility(dataValue.value);
1008
1025
 
1009
- const newArrayOrMatrix = dataValue.value.value;
1010
-
1011
- if (dataValue.value.arrayType === VariantArrayType.Array) {
1012
- if (this.$dataValue.value.arrayType !== VariantArrayType.Array) {
1013
- callback(null, StatusCodes.BadTypeMismatch);
1026
+ if (indexRange && !indexRange.isEmpty()) {
1027
+ if (!indexRange.isValid()) {
1028
+ callback?.(null, StatusCodes.BadIndexRangeInvalid);
1014
1029
  return;
1015
1030
  }
1016
- // check that destination data is also an array
1017
- assert(check_valid_array(this.$dataValue.value.dataType, this.$dataValue.value.value));
1018
- const destArr = this.$dataValue.value.value;
1019
- const result = indexRange.set_values(destArr, newArrayOrMatrix);
1020
1031
 
1021
- if (result.statusCode.isNot(StatusCodes.Good)) {
1022
- callback?.(null, result.statusCode);
1023
- return;
1024
- }
1025
- dataValue.value.value = result.array;
1026
-
1027
- // scrap original array so we detect range
1028
- this.$dataValue.value.value = null;
1029
- } else if (dataValue.value.arrayType === VariantArrayType.Matrix) {
1030
- const dimensions = this.$dataValue.value.dimensions;
1031
- if (this.$dataValue.value.arrayType !== VariantArrayType.Matrix || !dimensions) {
1032
- // not a matrix !
1032
+ const newArrayOrMatrix = dataValue.value.value;
1033
+
1034
+ if (dataValue.value.arrayType === VariantArrayType.Array) {
1035
+ if (this.$dataValue.value.arrayType !== VariantArrayType.Array) {
1036
+ callback(null, StatusCodes.BadTypeMismatch);
1037
+ return;
1038
+ }
1039
+ // check that destination data is also an array
1040
+ assert(check_valid_array(this.$dataValue.value.dataType, this.$dataValue.value.value));
1041
+ const destArr = this.$dataValue.value.value;
1042
+ const result = indexRange.set_values(destArr, newArrayOrMatrix);
1043
+
1044
+ if (result.statusCode.isNot(StatusCodes.Good)) {
1045
+ callback?.(null, result.statusCode);
1046
+ return;
1047
+ }
1048
+ dataValue.value.value = result.array;
1049
+
1050
+ // scrap original array so we detect range
1051
+ this.$dataValue.value.value = null;
1052
+ } else if (dataValue.value.arrayType === VariantArrayType.Matrix) {
1053
+ const dimensions = this.$dataValue.value.dimensions;
1054
+ if (this.$dataValue.value.arrayType !== VariantArrayType.Matrix || !dimensions) {
1055
+ // not a matrix !
1056
+ callback?.(null, StatusCodes.BadTypeMismatch);
1057
+ return;
1058
+ }
1059
+ const matrix = this.$dataValue.value.value;
1060
+ const result = indexRange.set_values_matrix(
1061
+ {
1062
+ matrix,
1063
+ dimensions
1064
+ },
1065
+ newArrayOrMatrix
1066
+ );
1067
+ if (result.statusCode.isNot(StatusCodes.Good)) {
1068
+ callback?.(null, result.statusCode);
1069
+ return;
1070
+ }
1071
+ dataValue.value.dimensions = this.$dataValue.value.dimensions;
1072
+ dataValue.value.value = result.matrix;
1073
+
1074
+ // scrap original array so we detect range
1075
+ this.$dataValue.value.value = null;
1076
+ } else {
1033
1077
  callback?.(null, StatusCodes.BadTypeMismatch);
1034
1078
  return;
1035
1079
  }
1036
- const matrix = this.$dataValue.value.value;
1037
- const result = indexRange.set_values_matrix(
1038
- {
1039
- matrix,
1040
- dimensions
1041
- },
1042
- newArrayOrMatrix
1043
- );
1044
- if (result.statusCode.isNot(StatusCodes.Good)) {
1045
- callback?.(null, result.statusCode);
1046
- return;
1080
+ }
1081
+ try {
1082
+ this._internal_set_dataValue(dataValue, indexRange);
1083
+ } catch (err) {
1084
+ if (types.isNativeError(err)) {
1085
+ warningLog(err.message);
1047
1086
  }
1048
- dataValue.value.dimensions = this.$dataValue.value.dimensions;
1049
- dataValue.value.value = result.matrix;
1050
-
1051
- // scrap original array so we detect range
1052
- this.$dataValue.value.value = null;
1053
- } else {
1054
- callback?.(null, StatusCodes.BadTypeMismatch);
1087
+ callback?.(null, StatusCodes.BadInternalError);
1055
1088
  return;
1056
1089
  }
1057
1090
  }
1058
- try {
1059
- this._internal_set_dataValue(dataValue, indexRange);
1060
- } catch (err) {
1061
- if (types.isNativeError(err)) {
1062
- warningLog(err.message);
1063
- }
1064
- callback?.(null, StatusCodes.BadInternalError);
1065
- return;
1066
- }
1067
- }
1068
- callback?.(err || null, statusCode1);
1069
- });
1091
+ callback?.(err || null, statusCode1);
1092
+ },
1093
+ context,
1094
+ indexRange
1095
+ );
1070
1096
  return undefined;
1071
1097
  }
1072
1098
 
@@ -1299,6 +1325,7 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
1299
1325
  this._set_func = null;
1300
1326
  this.refreshFunc = undefined;
1301
1327
  this._refreshFuncWrapsGetter = false;
1328
+ this._getterIsAsync = false;
1302
1329
  this._historyRead = UAVariableImpl.prototype._historyRead;
1303
1330
  }
1304
1331
 
@@ -1344,11 +1371,47 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
1344
1371
  });
1345
1372
  }
1346
1373
 
1374
+ /**
1375
+ * why this context may not read the Value, or null when it may: the gates of readValue(),
1376
+ * applied to every read of readValueAsync, including one that joins a refresh in flight.
1377
+ * Runs once per sample of every monitored item: it allocates nothing
1378
+ */
1379
+ private _readDenial(context: ISessionContext): StatusCode | null {
1380
+ if (context.isAccessRestricted(this)) {
1381
+ // same gate as readValue(): a variable bound with a simple get()
1382
+ // also gets a refreshFunc, sending every read down the asyncRefresh
1383
+ // branch of _readValueAsync - which calls the getter directly and never passes
1384
+ // through readValue(). Without this check here, a node hardened
1385
+ // with AccessRestrictions(EncryptionRequired) answered its value
1386
+ // over an unencrypted channel.
1387
+ return StatusCodes.BadSecurityModeInsufficient;
1388
+ } else if (!this.isReadable(context)) {
1389
+ return StatusCodes.BadNotReadable;
1390
+ } else if (!this.checkPermissionPrivate(context, PermissionType.Read)) {
1391
+ return StatusCodes.BadUserAccessDenied;
1392
+ } else if (
1393
+ // as in readValue(): the Read permission has just passed, only the access level is
1394
+ // left, unless a node replaced isUserReadable (see makeNotReadableIfEnabledFlagIsFalse)
1395
+ this.isUserReadable === UAVariableImpl.prototype.isUserReadable
1396
+ ? !this.checkAccessLevelPrivate(context, AccessLevelFlag.CurrentRead)
1397
+ : !this.isUserReadable(context)
1398
+ ) {
1399
+ return StatusCodes.BadNotReadable;
1400
+ }
1401
+ return null;
1402
+ }
1403
+
1347
1404
  private _readValueAsync(context: ISessionContext | null, callback: CallbackT<DataValue>, readAt?: PreciseClock): void {
1348
1405
  context = context || SessionContext.defaultContext;
1349
1406
 
1350
1407
  if (this.__waiting_callback) {
1351
- // a refresh is in progress: its value answers this read too
1408
+ // a refresh is in progress: its value answers this read too, once this read's own
1409
+ // access checks have passed (they depend on its context, not on the first reader's)
1410
+ const joinerDenied = this._readDenial(context);
1411
+ if (joinerDenied) {
1412
+ callback(null, makeNowDataValue({ statusCode: joinerDenied }));
1413
+ return;
1414
+ }
1352
1415
  this.__more_waiting_callbacks ??= [];
1353
1416
  this.__more_waiting_callbacks.push(callback);
1354
1417
  return;
@@ -1363,28 +1426,7 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
1363
1426
  // runs once per sample of every monitored item, so nothing here allocates a closure or
1364
1427
  // binds a function: the denial (if any) is a StatusCode, and the callback that releases
1365
1428
  // the waiting readers is made once per variable
1366
- let denied: StatusCode | null = null;
1367
- if (context.isAccessRestricted(this)) {
1368
- // same gate as readValue(): a variable bound with a simple get()
1369
- // also gets a refreshFunc, sending every read down the asyncRefresh
1370
- // branch below - which calls the getter directly and never passes
1371
- // through readValue(). Without this check here, a node hardened
1372
- // with AccessRestrictions(EncryptionRequired) answered its value
1373
- // over an unencrypted channel.
1374
- denied = StatusCodes.BadSecurityModeInsufficient;
1375
- } else if (!this.isReadable(context)) {
1376
- denied = StatusCodes.BadNotReadable;
1377
- } else if (!this.checkPermissionPrivate(context, PermissionType.Read)) {
1378
- denied = StatusCodes.BadUserAccessDenied;
1379
- } else if (
1380
- // as in readValue(): the Read permission has just passed, only the access level is
1381
- // left, unless a node replaced isUserReadable (see makeNotReadableIfEnabledFlagIsFalse)
1382
- this.isUserReadable === UAVariableImpl.prototype.isUserReadable
1383
- ? !this.checkAccessLevelPrivate(context, AccessLevelFlag.CurrentRead)
1384
- : !this.isUserReadable(context)
1385
- ) {
1386
- denied = StatusCodes.BadNotReadable;
1387
- }
1429
+ const denied = this._readDenial(context);
1388
1430
 
1389
1431
  this.__satisfy_waiting_callbacks ??= (err: Error | null, dataValue?: DataValue) => {
1390
1432
  // now call all pending callbacks, in the order of the reads
@@ -1411,9 +1453,9 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
1411
1453
  this.__sharedReadClock = readAt !== undefined;
1412
1454
  if (this.asyncRefresh === wrappedAsyncRefresh) {
1413
1455
  // the thenified wrapper adds nothing when a callback is given
1414
- asyncRefreshImpl.call(this, clock, satisfy_callbacks);
1456
+ asyncRefreshImpl.call(this, clock, satisfy_callbacks, context);
1415
1457
  } else {
1416
- this.asyncRefresh(clock, satisfy_callbacks);
1458
+ this.asyncRefresh(clock, satisfy_callbacks, context);
1417
1459
  }
1418
1460
  // consumed by asyncRefresh on entry; cleared here for an overridden one that never reads it
1419
1461
  this.__sharedReadClock = false;
@@ -2190,9 +2232,28 @@ import { withCallback } from "thenify-ex";
2190
2232
 
2191
2233
  // readValueAsync calls the unwrapped asyncRefresh directly: binding the thenified wrapper on
2192
2234
  // every sample cost more than the refresh itself
2193
- const asyncRefreshImpl: (this: UAVariableImpl, oldestDate: PreciseClock, callback: CallbackT<DataValue>) => void =
2194
- UAVariableImpl.prototype.asyncRefresh;
2195
- UAVariableImpl.prototype.asyncRefresh = withCallback(asyncRefreshImpl);
2235
+ const asyncRefreshImpl: (
2236
+ this: UAVariableImpl,
2237
+ oldestDate: PreciseClock,
2238
+ callback: CallbackT<DataValue>,
2239
+ context?: ISessionContext
2240
+ ) => void = UAVariableImpl.prototype.asyncRefresh;
2241
+ // the promise form when no callback is given, as withCallback() does: written out because that
2242
+ // wrapper takes the last argument for the callback, and the context comes after it
2243
+ UAVariableImpl.prototype.asyncRefresh = function (
2244
+ this: UAVariableImpl,
2245
+ oldestDate: PreciseClock,
2246
+ callback?: CallbackT<DataValue>,
2247
+ context?: ISessionContext
2248
+ ): Promise<DataValue> | undefined {
2249
+ if (typeof callback === "function") {
2250
+ asyncRefreshImpl.call(this, oldestDate, callback, context);
2251
+ return undefined;
2252
+ }
2253
+ return new Promise<DataValue>((resolve, reject) => {
2254
+ asyncRefreshImpl.call(this, oldestDate, (err, dataValue) => (err ? reject(err) : resolve(dataValue as DataValue)), context);
2255
+ });
2256
+ } as UAVariableImpl["asyncRefresh"];
2196
2257
  const wrappedAsyncRefresh = UAVariableImpl.prototype.asyncRefresh;
2197
2258
  UAVariableImpl.prototype.writeValue = withCallback(UAVariableImpl.prototype.writeValue);
2198
2259
  UAVariableImpl.prototype.writeAttribute = withCallback(UAVariableImpl.prototype.writeAttribute);
@@ -2337,7 +2398,8 @@ function turn_sync_to_async<T, D, R>(f: f1<T, D, R> | f2<T, D, R> | f3<T, D, R>,
2337
2398
  callback(null, r);
2338
2399
  };
2339
2400
  } else {
2340
- assert(f.length === numberOfArgs + 1);
2401
+ // a callback-style function; it may declare the trailing context and indexRange parameters
2402
+ assert(f.length >= numberOfArgs + 1 && f.length <= numberOfArgs + 3);
2341
2403
  return f;
2342
2404
  }
2343
2405
  }
@@ -2439,12 +2501,14 @@ function _Variable_bind_with_timestamped_get(
2439
2501
 
2440
2502
  throw new Error(" Bind variable error: " + " the timestamped_get function must return a DataValue");
2441
2503
  }
2504
+ this._getterIsAsync = isThenable(dataValue_verify);
2442
2505
  _Variable_bind_with_async_refresh.call(this, { refreshFunc: async_refresh_func });
2443
- } else if (options.timestamped_get.length === 1) {
2444
- _Variable_bind_with_async_refresh.call(this, { refreshFunc: options.timestamped_get });
2506
+ } else if (options.timestamped_get.length === 1 || options.timestamped_get.length === 2) {
2507
+ // callback style: (callback) or (callback, context)
2508
+ _Variable_bind_with_async_refresh.call(this, { refreshFunc: options.timestamped_get as RefreshFunc });
2445
2509
  } else {
2446
- errorLog("timestamped_get has a invalid number of argument , should be 0 or 1 ");
2447
- throw new Error("timestamped_get has a invalid number of argument , should be 0 or 1 ");
2510
+ errorLog("timestamped_get has a invalid number of argument , should be 0, 1 or 2 ");
2511
+ throw new Error("timestamped_get has a invalid number of argument , should be 0, 1 or 2 ");
2448
2512
  }
2449
2513
  }
2450
2514
 
@@ -2512,28 +2576,38 @@ function _Variable_bind_with_simple_set(this: UAVariableImpl, options: SimpleSet
2512
2576
  const set_func = turn_sync_to_async<UAVariable, Variant, StatusCode>(options.set as VariableSetter, 1);
2513
2577
  this._set_func = set_func;
2514
2578
 
2515
- assert(set_func.length === 2, " set function must have 2 arguments ( variant, callback)");
2579
+ assert(set_func.length >= 2, " set function must have 2 arguments ( variant, callback)");
2516
2580
 
2581
+ // the trailing arguments (context, indexRange) are a rest parameter so that the length stays 2
2517
2582
  this._timestamped_set_func = (
2518
2583
  timestamped_value: DataValue,
2519
- callback: (err: Error | null, statusCode: StatusCode, dataValue: DataValue) => void
2584
+ callback: (err: Error | null, statusCode: StatusCode, dataValue: DataValue) => void,
2585
+ ...extra: [ISessionContext?, (NumericRange | null)?]
2520
2586
  ) => {
2521
2587
  assert(timestamped_value instanceof DataValue);
2522
- set_func.call(this, timestamped_value.value, (err: Error | null, statusCode?: StatusCode) => {
2523
- // c8 ignore next
2524
- if (!err && !statusCode) {
2525
- errorLog(
2526
- chalk.red("UAVariable Binding Error _set_func must return a StatusCode, check the bindVariable parameters")
2527
- );
2528
- errorLog(chalk.yellow("StatusCode.Good is assumed"));
2529
- return callback(err, StatusCodes.Good, timestamped_value);
2530
- }
2531
- if (statusCode?.isNotGood()) {
2532
- // record the value but still record the statusCode !
2533
- timestamped_value.statusCode = statusCode;
2534
- }
2535
- callback(err, statusCode ?? StatusCodes.BadInternalError, timestamped_value);
2536
- });
2588
+ (
2589
+ set_func as (
2590
+ this: UAVariable,
2591
+ value: Variant,
2592
+ callback: (err: Error | null, statusCode?: StatusCode) => void,
2593
+ ...extra: unknown[]
2594
+ ) => void
2595
+ ).call(
2596
+ this,
2597
+ timestamped_value.value,
2598
+ (err: Error | null, statusCode?: StatusCode) => {
2599
+ // c8 ignore next
2600
+ if (!err && !statusCode) {
2601
+ errorLog(
2602
+ chalk.red("UAVariable Binding Error _set_func must return a StatusCode, check the bindVariable parameters")
2603
+ );
2604
+ errorLog(chalk.yellow("StatusCode.Good is assumed"));
2605
+ return callback(err, StatusCodes.Good, timestamped_value);
2606
+ }
2607
+ callback(err, statusCode ?? StatusCodes.BadInternalError, timestamped_value);
2608
+ },
2609
+ ...extra
2610
+ );
2537
2611
  };
2538
2612
  }
2539
2613
 
@@ -2545,7 +2619,7 @@ function _Variable_bind_with_timestamped_set(
2545
2619
  }
2546
2620
  ) {
2547
2621
  assert(
2548
- options.timestamped_set.length === 2 || options.timestamped_set.length === 1,
2622
+ options.timestamped_set.length >= 1 && options.timestamped_set.length <= 4,
2549
2623
  "timestamped_set must have 2 parameters timestamped_set: function(dataValue,callback){} or one paramater timestamped_set: function(dataValue): Promise<StatusCode>{}"
2550
2624
  );
2551
2625
  assert(!options.set, "should not specify set when timestamped_set_func exists ");
@@ -2554,7 +2628,7 @@ function _Variable_bind_with_timestamped_set(
2554
2628
  // setter of its own binds the same default, and a closure per variable was 110 bytes each
2555
2629
  let setter = callbackSetters.get(options.timestamped_set);
2556
2630
  if (!setter) {
2557
- setter = convertToCallbackFunction1<StatusCode, DataValue, UAVariable>(options.timestamped_set);
2631
+ setter = convertToCallbackFunction1<StatusCode, DataValue, UAVariable>(options.timestamped_set, 2);
2558
2632
  callbackSetters.set(options.timestamped_set, setter);
2559
2633
  }
2560
2634
  this._timestamped_set_func = setter;
@@ -2562,12 +2636,41 @@ function _Variable_bind_with_timestamped_set(
2562
2636
  /** the callback form of each setter function met so far, see _Variable_bind_with_timestamped_set */
2563
2637
  const callbackSetters = new WeakMap<TimestampSetFunc, VariableDataValueSetterWithCallback>();
2564
2638
 
2639
+ /** an asynchronous setter that is told who writes (`timestamped_set_ctx`) */
2640
+ function _Variable_bind_with_context_setter(this: UAVariableImpl, setter: VariableDataValueSetterWithContext) {
2641
+ // the context and the index range come after the callback, in a rest parameter: the length stays 2
2642
+ this._timestamped_set_func = function (
2643
+ this: UAVariable,
2644
+ dataValue: DataValue,
2645
+ callback: (err: Error | null, statusCode?: StatusCode) => void,
2646
+ ...extra: [ISessionContext?, (NumericRange | null)?]
2647
+ ) {
2648
+ const [context, indexRange] = extra;
2649
+ let promise: Promise<StatusCode>;
2650
+ try {
2651
+ promise = setter.call(this, dataValue, { context, indexRange });
2652
+ } catch (err) {
2653
+ callback(err as Error);
2654
+ return;
2655
+ }
2656
+ Promise.resolve(promise).then(
2657
+ (statusCode) => callback(null, statusCode),
2658
+ (err) => callback(err as Error)
2659
+ );
2660
+ };
2661
+ }
2662
+
2565
2663
  interface SetterOptions {
2566
2664
  set?: SetFunc;
2567
2665
  timestamped_set?: TimestampSetFunc;
2666
+ timestamped_set_ctx?: VariableDataValueSetterWithContext;
2568
2667
  timestamped_get?: TimestampGetFunc;
2668
+ timestamped_get_ctx?: VariableDataValueGetterWithContext;
2569
2669
  }
2570
2670
  function bind_setter(this: UAVariableImpl, options: SetterOptions) {
2671
+ if (options.timestamped_set && options.timestamped_set_ctx) {
2672
+ throw new Error("bind_setter : options should not specify both timestamped_set and timestamped_set_ctx ");
2673
+ }
2571
2674
  if (options.set && typeof options.set === "function") {
2572
2675
  // variation 1
2573
2676
  if (options.timestamped_set === undefined) {
@@ -2582,8 +2685,15 @@ function bind_setter(this: UAVariableImpl, options: SetterOptions) {
2582
2685
  set: undefined,
2583
2686
  timestamped_set: options.timestamped_set
2584
2687
  });
2585
- } else if (typeof options.timestamped_get === "function") {
2586
- // timestamped_get is specified but timestamped_set is not
2688
+ } else if (typeof options.timestamped_set_ctx === "function") {
2689
+ // variation 5: the setter is told who writes
2690
+ assert(
2691
+ typeof options.timestamped_get === "function" || typeof options.timestamped_get_ctx === "function",
2692
+ "timestamped_set_ctx must be used with timestamped_get or timestamped_get_ctx"
2693
+ );
2694
+ _Variable_bind_with_context_setter.call(this, options.timestamped_set_ctx);
2695
+ } else if (typeof options.timestamped_get === "function" || typeof options.timestamped_get_ctx === "function") {
2696
+ // a timestamped getter is specified but no setter
2587
2697
  // => Value is read-only
2588
2698
  _Variable_bind_with_timestamped_set.call(this, {
2589
2699
  set: undefined,
@@ -2597,10 +2707,36 @@ function bind_setter(this: UAVariableImpl, options: SetterOptions) {
2597
2707
  }
2598
2708
  }
2599
2709
 
2600
- type RefreshFunc = (callback: CallbackT<DataValue>) => void;
2710
+ type RefreshFunc = (callback: CallbackT<DataValue>, context?: ISessionContext) => void;
2711
+ /**
2712
+ * variation 5: an asynchronous getter that is told who reads (`timestamped_get_ctx`). It is run by
2713
+ * the refresh only, like any asynchronous getter, with the session context of the read.
2714
+ */
2715
+ function _Variable_bind_with_context_getter(this: UAVariableImpl, getter: VariableDataValueGetterWithContext) {
2716
+ _Variable_bind_with_async_refresh.call(this, {
2717
+ refreshFunc: (callback: CallbackT<DataValue>, context?: ISessionContext) => {
2718
+ let promise: Promise<DataValue>;
2719
+ try {
2720
+ promise = getter.call(this, { context });
2721
+ } catch (err) {
2722
+ callback(err as Error);
2723
+ return;
2724
+ }
2725
+ Promise.resolve(promise).then(
2726
+ (dataValue) => callback(null, dataValue),
2727
+ (err) => {
2728
+ errorLog("timestamped_get_ctx error: Variable is ", this.nodeId.toString(), this.browseName.toString());
2729
+ callback(err as Error);
2730
+ }
2731
+ );
2732
+ }
2733
+ });
2734
+ }
2735
+
2601
2736
  interface GetterOptions {
2602
2737
  get?: GetFunc;
2603
2738
  timestamped_get?: TimestampGetFunc;
2739
+ timestamped_get_ctx?: VariableDataValueGetterWithContext;
2604
2740
  refreshFunc?: RefreshFunc;
2605
2741
  dataType?: DataType | string;
2606
2742
  value?: unknown;
@@ -2615,6 +2751,9 @@ function bind_getter(this: UAVariableImpl, options: GetterOptions) {
2615
2751
  get: undefined,
2616
2752
  timestamped_get: options.timestamped_get
2617
2753
  });
2754
+ } else if (typeof options.timestamped_get_ctx === "function") {
2755
+ // variation 5
2756
+ _Variable_bind_with_context_getter.call(this, options.timestamped_get_ctx);
2618
2757
  } else if (typeof options.refreshFunc === "function") {
2619
2758
  // variation 3
2620
2759
  if (options.get !== undefined || options.timestamped_get !== undefined) {