node-opcua-address-space 2.175.1 → 2.175.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-opcua-address-space",
3
- "version": "2.175.1",
3
+ "version": "2.175.3",
4
4
  "description": "pure nodejs OPCUA SDK - module address-space",
5
5
  "main": "./dist/src/index_current.js",
6
6
  "types": "./dist/source/index.d.ts",
@@ -21,14 +21,14 @@
21
21
  "@types/semver": "7.7.1",
22
22
  "chalk": "4.1.2",
23
23
  "dequeue": "^1.0.5",
24
- "node-opcua-address-space-base": "2.175.1",
24
+ "node-opcua-address-space-base": "2.175.2",
25
25
  "node-opcua-assert": "2.175.1",
26
26
  "node-opcua-basic-types": "2.175.1",
27
27
  "node-opcua-binary-stream": "2.175.1",
28
- "node-opcua-client-dynamic-extension-object": "2.175.1",
28
+ "node-opcua-client-dynamic-extension-object": "2.175.2",
29
29
  "node-opcua-constants": "2.175.1",
30
30
  "node-opcua-crypto": "5.3.6",
31
- "node-opcua-data-access": "2.175.1",
31
+ "node-opcua-data-access": "2.175.2",
32
32
  "node-opcua-data-model": "2.175.1",
33
33
  "node-opcua-data-value": "2.175.1",
34
34
  "node-opcua-date-time": "2.175.1",
@@ -37,20 +37,20 @@
37
37
  "node-opcua-extension-object": "2.175.1",
38
38
  "node-opcua-factory": "2.175.1",
39
39
  "node-opcua-nodeid": "2.175.1",
40
- "node-opcua-nodeset-ua": "2.175.1",
40
+ "node-opcua-nodeset-ua": "2.175.3",
41
41
  "node-opcua-numeric-range": "2.175.1",
42
42
  "node-opcua-object-registry": "2.175.1",
43
- "node-opcua-pseudo-session": "2.175.1",
44
- "node-opcua-service-browse": "2.175.1",
45
- "node-opcua-service-call": "2.175.1",
46
- "node-opcua-service-history": "2.175.1",
47
- "node-opcua-service-translate-browse-path": "2.175.1",
48
- "node-opcua-service-write": "2.175.1",
43
+ "node-opcua-pseudo-session": "2.175.2",
44
+ "node-opcua-service-browse": "2.175.2",
45
+ "node-opcua-service-call": "2.175.2",
46
+ "node-opcua-service-history": "2.175.2",
47
+ "node-opcua-service-translate-browse-path": "2.175.2",
48
+ "node-opcua-service-write": "2.175.2",
49
49
  "node-opcua-status-code": "2.175.1",
50
- "node-opcua-types": "2.175.1",
50
+ "node-opcua-types": "2.175.2",
51
51
  "node-opcua-utils": "2.175.1",
52
52
  "node-opcua-variant": "2.175.1",
53
- "node-opcua-xml2json": "2.175.1",
53
+ "node-opcua-xml2json": "2.175.2",
54
54
  "semver": "^7.8.5",
55
55
  "thenify-ex": "4.4.0",
56
56
  "xml-writer": "^1.7.0"
@@ -59,10 +59,10 @@
59
59
  "humanize": "0.0.9",
60
60
  "node-opcua-benchmarker": "2.175.1",
61
61
  "node-opcua-leak-detector": "2.175.1",
62
- "node-opcua-nodesets": "2.175.1",
62
+ "node-opcua-nodesets": "2.175.3",
63
63
  "node-opcua-packet-analyzer": "2.175.1",
64
- "node-opcua-schemas": "2.175.1",
65
- "node-opcua-service-filter": "2.175.1",
64
+ "node-opcua-schemas": "2.175.2",
65
+ "node-opcua-service-filter": "2.175.2",
66
66
  "node-opcua-test-fixtures": "2.175.1"
67
67
  },
68
68
  "author": "Etienne Rossignon",
@@ -80,7 +80,7 @@
80
80
  "internet of things"
81
81
  ],
82
82
  "homepage": "http://node-opcua.github.io/",
83
- "gitHead": "7c0416a09c7fd83a034b4de3dc8197dff84b8ad8",
83
+ "gitHead": "31f407d4ad2712b7bf2882d4dc70085f598b0fcd",
84
84
  "files": [
85
85
  "dist",
86
86
  "distHelpers",
@@ -12,7 +12,7 @@ import {
12
12
  type UInt64
13
13
  } from "node-opcua-basic-types";
14
14
  import { make_debugLog, make_warningLog } from "node-opcua-debug";
15
- import { coerceNodeId, type INodeId, type NodeId, NodeIdType } from "node-opcua-nodeid";
15
+ import { coerceNodeId, ExpandedNodeId, type INodeId, type NodeId, NodeIdType } from "node-opcua-nodeid";
16
16
  import { EnumDefinition, StructureDefinition } from "node-opcua-types";
17
17
  import { lowerFirstLetter } from "node-opcua-utils";
18
18
  import { DataType, Variant, type VariantOptions } from "node-opcua-variant";
@@ -31,6 +31,30 @@ import { makeVariantReader } from "./parsers/variant_parser";
31
31
  const warningLog = make_warningLog(__filename);
32
32
  const debugLog = make_debugLog(__filename);
33
33
 
34
+ // textual form of an ExpandedNodeId, as found in <ExpandedNodeId><Identifier>...</Identifier></ExpandedNodeId>:
35
+ // an optional server index, an optional namespace uri, then a plain nodeId. see OPC UA part 6.
36
+ const regexServerIndex = /^svr=([0-9]+);(.*)$/;
37
+ const regexNamespaceUri = /^nsu=(.*?);(.*)$/;
38
+
39
+ function parseExpandedNodeId(text: string): ExpandedNodeId {
40
+ let remaining = text.trim();
41
+ let serverIndex = 0;
42
+ let namespaceUri: string | null = null;
43
+
44
+ const serverIndexMatch = remaining.match(regexServerIndex);
45
+ if (serverIndexMatch) {
46
+ serverIndex = parseInt(serverIndexMatch[1], 10);
47
+ remaining = serverIndexMatch[2];
48
+ }
49
+ const namespaceUriMatch = remaining.match(regexNamespaceUri);
50
+ if (namespaceUriMatch) {
51
+ namespaceUri = namespaceUriMatch[1];
52
+ remaining = namespaceUriMatch[2];
53
+ }
54
+ const nodeId = coerceNodeId(remaining);
55
+ return new ExpandedNodeId(nodeId.identifierType, nodeId.value, nodeId.namespace, namespaceUri, serverIndex);
56
+ }
57
+
34
58
  function clamp(value: number, minValue: number, maxValue: number) {
35
59
  if (value < minValue) {
36
60
  warningLog(`invalid value range : ${value} < ${minValue} but should be [${minValue} , ${maxValue}]`);
@@ -169,6 +193,13 @@ const partials = {
169
193
  // to do check Local or GMT
170
194
  this.value = coerceNodeId(this.text);
171
195
  }
196
+ },
197
+
198
+ // <ExpandedNodeId><Identifier>svr=1;nsu=http://acme.com/UA/;i=42</Identifier></ExpandedNodeId>
199
+ ExpandedNodeId: <Parser<ExpandedNodeId>>{
200
+ finish(this: Parser<ExpandedNodeId>) {
201
+ this.value = parseExpandedNodeId(this.text);
202
+ }
172
203
  }
173
204
  };
174
205
 
@@ -231,10 +262,16 @@ function _makeTypeReader(
231
262
  let reader: ReaderStateParserLike = readerMap.get(dataTypeName)!;
232
263
 
233
264
  if (reader) {
234
- return { name, reader: reader.parser! };
265
+ return { name, reader };
235
266
  }
236
267
 
237
268
  reader = {
269
+ init(this: any) {
270
+ // the same reader instance is used for every element of that data type: the value must be
271
+ // reset here, or the element being read would inherit the fields of the previously read one
272
+ // ( or of the enclosing one, when the structure refers to itself ).
273
+ this.value = undefined;
274
+ },
238
275
  finish(this: any) {
239
276
  /** empty */
240
277
  },
@@ -244,6 +281,12 @@ function _makeTypeReader(
244
281
  };
245
282
 
246
283
  if (definition instanceof StructureDefinition) {
284
+ // the reader must be registered *before* its fields are explored: a structure may refer to
285
+ // itself, directly or through one of its fields, and the recursion below would never end.
286
+ // `reader` and `reader.parser` keep their identity while being filled, so a nested reference
287
+ // that picks the reader up from the map ends up pointing at the completed reader.
288
+ readerMap.set(dataTypeName, reader);
289
+
247
290
  for (const field of definition.fields || []) {
248
291
  const typeReader = _makeTypeReader(field.dataType, definitionMap, readerMap, translateNodeId);
249
292
  const fieldParser = typeReader.reader;
@@ -301,7 +344,6 @@ function _makeTypeReader(
301
344
  throw new Error("Unsupported ValueRank !");
302
345
  }
303
346
  }
304
- readerMap.set(dataTypeName, reader);
305
347
  return { name, reader };
306
348
  } else if (definition instanceof EnumDefinition) {
307
349
  const turnToInt = (value: any) => {
@@ -2,7 +2,8 @@ import type {
2
2
  AddVariableOptionsWithoutValue,
3
3
  AddYArrayItemOptions,
4
4
  BindVariableOptions,
5
- UADataType
5
+ UADataType,
6
+ UAVariableType
6
7
  } from "node-opcua-address-space-base";
7
8
  import type { NodeIdLike } from "node-opcua-nodeid";
8
9
  import type { UAAnalogItem, UADataItem } from "node-opcua-nodeset-ua";
@@ -45,6 +46,13 @@ export interface AddAnalogDataItemOptions extends AddDataItemOptions {
45
46
  minimumSamplingInterval?: number;
46
47
  dataType?: string | NodeIdLike | UADataType;
47
48
 
49
+ /**
50
+ * an optional VariableType, which must be a subtype of AnalogItemType, to use instead of the
51
+ * plain AnalogItemType. Its own mandatory members (if any) will be instantiated as well.
52
+ * @default AnalogItemType
53
+ */
54
+ typeDefinition?: string | NodeIdLike | UAVariableType;
55
+
48
56
  /**
49
57
  * the acceptValueOutOfRange property indicates whether the write operation will accept or reject a value which
50
58
  * is out of range of the instrumentRange.
@@ -45,7 +45,7 @@ import {
45
45
  type QualifiedNameLike
46
46
  } from "node-opcua-data-model";
47
47
  import { dumpIf, make_errorLog, make_warningLog } from "node-opcua-debug";
48
- import { coerceNodeId, NodeId, type NodeIdLike, NodeIdType, resolveNodeId } from "node-opcua-nodeid";
48
+ import { coerceNodeId, NodeId, type NodeIdLike, NodeIdType, resolveNodeId, sameNodeId } from "node-opcua-nodeid";
49
49
  import type { UAAnalogItem, UADataItem, UAInitialState, UAState } from "node-opcua-nodeset-ua";
50
50
  import { StatusCodes } from "node-opcua-status-code";
51
51
  import {
@@ -903,9 +903,52 @@ export class NamespaceImpl implements NamespacePrivate {
903
903
  throw new Error("expecting AnalogItemType to be defined , check nodeset xml file");
904
904
  }
905
905
 
906
- const clone_options = { ...options, dataType, typeDefinition: analogItemType.nodeId } as AddVariableOptions;
907
-
908
- const variable = this.addVariable(clone_options) as unknown as UAVariableImpl;
906
+ // options.typeDefinition may designate a subtype of AnalogItemType (e.g. a companion
907
+ // specification's own AnalogItemType-derived VariableType). In that case we must honor it
908
+ // so that the subtype's identity - and its own mandatory members - are preserved, instead of
909
+ // silently downgrading the variable to a plain AnalogItemType.
910
+ let requestedTypeDefinition = analogItemType;
911
+ if (options.typeDefinition) {
912
+ const t =
913
+ options.typeDefinition instanceof BaseNodeImpl
914
+ ? (options.typeDefinition as unknown as UAVariableType)
915
+ : addressSpace.findVariableType(options.typeDefinition as NodeIdLike);
916
+ if (!t) {
917
+ throw new Error(`cannot find requested typeDefinition ${options.typeDefinition.toString()}`);
918
+ }
919
+ if (!t.isSubtypeOf(analogItemType)) {
920
+ throw new Error(`typeDefinition ${t.browseName.toString()} shall be a subtype of AnalogItemType`);
921
+ }
922
+ requestedTypeDefinition = t;
923
+ }
924
+ const isStrictSubtype = !sameNodeId(requestedTypeDefinition.nodeId, analogItemType.nodeId);
925
+
926
+ let variable: UAVariableImpl;
927
+ if (isStrictSubtype) {
928
+ // instantiate the requested subtype so that its own mandatory members
929
+ // (in addition to the AnalogItemType ones) get materialized.
930
+ const optionals: string[] = [];
931
+ if (Object.hasOwn(options, "instrumentRange")) {
932
+ optionals.push("InstrumentRange");
933
+ }
934
+ if (Object.hasOwn(options, "engineeringUnits")) {
935
+ optionals.push("EngineeringUnits");
936
+ }
937
+ variable = requestedTypeDefinition.instantiate({
938
+ browseName: options.browseName,
939
+ componentOf: options.componentOf,
940
+ dataType: addressSpace._coerce_DataType(dataType as NodeIdLike),
941
+ modellingRule: options.modellingRule,
942
+ nodeId: options.nodeId,
943
+ notifierOf: options.notifierOf,
944
+ eventSourceOf: options.eventSourceOf,
945
+ organizedBy: options.organizedBy,
946
+ optionals
947
+ }) as unknown as UAVariableImpl;
948
+ } else {
949
+ const clone_options = { ...options, dataType, typeDefinition: analogItemType.nodeId } as AddVariableOptions;
950
+ variable = this.addVariable(clone_options) as unknown as UAVariableImpl;
951
+ }
909
952
 
910
953
  add_dataItem_stuff(variable, options);
911
954
 
@@ -920,38 +963,59 @@ export class NamespaceImpl implements NamespacePrivate {
920
963
  // prepared to handle this.
921
964
  // Example: EURange ::= {-200.0,1400.0}
922
965
 
923
- const euRange = this.addVariable({
924
- browseName: { name: "EURange", namespaceIndex: 0 },
925
- dataType: "Range",
926
- minimumSamplingInterval: 0,
927
- modellingRule: options.modellingRule,
928
- propertyOf: variable,
929
- typeDefinition: "PropertyType",
930
- value: new Variant({
931
- dataType: DataType.ExtensionObject,
932
- value: new Range(options.engineeringUnitsRange)
933
- })
934
- }) as unknown as UAVariableImpl;
935
-
936
- assert(euRange.readValue().value.value instanceof Range);
937
-
938
- const handler = variable.handle_semantic_changed.bind(variable);
939
- euRange.on("value_changed", handler);
940
-
941
- if (Object.hasOwn(options, "instrumentRange")) {
942
- const instrumentRange = this.addVariable({
943
- accessLevel: "CurrentRead | CurrentWrite",
944
- browseName: { name: "InstrumentRange", namespaceIndex: 0 },
966
+ // when `variable` was instantiated from a strict AnalogItemType subtype (isStrictSubtype),
967
+ // EURange/InstrumentRange/EngineeringUnits may already exist as cloned mandatory/optional
968
+ // members of that subtype - reuse them instead of creating duplicate property nodes.
969
+ const euRangeExisting = variable.getPropertyByName("EURange") as UAVariableImpl | null;
970
+ const euRange =
971
+ euRangeExisting ||
972
+ (this.addVariable({
973
+ browseName: { name: "EURange", namespaceIndex: 0 },
945
974
  dataType: "Range",
946
975
  minimumSamplingInterval: 0,
947
- modellingRule: options.modellingRule ? "Mandatory" : undefined,
976
+ modellingRule: options.modellingRule,
948
977
  propertyOf: variable,
949
978
  typeDefinition: "PropertyType",
950
979
  value: new Variant({
951
980
  dataType: DataType.ExtensionObject,
952
- value: new Range(options.instrumentRange)
981
+ value: new Range(options.engineeringUnitsRange)
953
982
  })
954
- });
983
+ }) as unknown as UAVariableImpl);
984
+ if (euRangeExisting) {
985
+ euRange.setValueFromSource(
986
+ new Variant({ dataType: DataType.ExtensionObject, value: new Range(options.engineeringUnitsRange) }),
987
+ StatusCodes.Good
988
+ );
989
+ }
990
+
991
+ assert(euRange.readValue().value.value instanceof Range);
992
+
993
+ const handler = variable.handle_semantic_changed.bind(variable);
994
+ euRange.on("value_changed", handler);
995
+
996
+ if (Object.hasOwn(options, "instrumentRange")) {
997
+ const instrumentRangeExisting = variable.getPropertyByName("InstrumentRange") as UAVariableImpl | null;
998
+ const instrumentRange =
999
+ instrumentRangeExisting ||
1000
+ this.addVariable({
1001
+ accessLevel: "CurrentRead | CurrentWrite",
1002
+ browseName: { name: "InstrumentRange", namespaceIndex: 0 },
1003
+ dataType: "Range",
1004
+ minimumSamplingInterval: 0,
1005
+ modellingRule: options.modellingRule ? "Mandatory" : undefined,
1006
+ propertyOf: variable,
1007
+ typeDefinition: "PropertyType",
1008
+ value: new Variant({
1009
+ dataType: DataType.ExtensionObject,
1010
+ value: new Range(options.instrumentRange)
1011
+ })
1012
+ });
1013
+ if (instrumentRangeExisting) {
1014
+ instrumentRange.setValueFromSource(
1015
+ new Variant({ dataType: DataType.ExtensionObject, value: new Range(options.instrumentRange) }),
1016
+ StatusCodes.Good
1017
+ );
1018
+ }
955
1019
 
956
1020
  instrumentRange.on("value_changed", handler);
957
1021
  }
@@ -964,19 +1028,28 @@ export class NamespaceImpl implements NamespacePrivate {
964
1028
  // EngineeringUnits specifies the units for the DataItem‟s value (e.g., degree, hertz, seconds). The
965
1029
  // EUInformation type is specified in 5.6.3.
966
1030
 
967
- const eu = this.addVariable({
968
- accessLevel: "CurrentRead",
969
- browseName: { name: "EngineeringUnits", namespaceIndex: 0 },
970
- dataType: "EUInformation",
971
- minimumSamplingInterval: 0,
972
- modellingRule: options.modellingRule ? "Mandatory" : undefined,
973
- propertyOf: variable,
974
- typeDefinition: "PropertyType",
975
- value: new Variant({
976
- dataType: DataType.ExtensionObject,
977
- value: engineeringUnits
978
- })
979
- });
1031
+ const euExisting = variable.getPropertyByName("EngineeringUnits") as UAVariableImpl | null;
1032
+ const eu =
1033
+ euExisting ||
1034
+ this.addVariable({
1035
+ accessLevel: "CurrentRead",
1036
+ browseName: { name: "EngineeringUnits", namespaceIndex: 0 },
1037
+ dataType: "EUInformation",
1038
+ minimumSamplingInterval: 0,
1039
+ modellingRule: options.modellingRule ? "Mandatory" : undefined,
1040
+ propertyOf: variable,
1041
+ typeDefinition: "PropertyType",
1042
+ value: new Variant({
1043
+ dataType: DataType.ExtensionObject,
1044
+ value: engineeringUnits
1045
+ })
1046
+ });
1047
+ if (euExisting) {
1048
+ eu.setValueFromSource(
1049
+ new Variant({ dataType: DataType.ExtensionObject, value: engineeringUnits }),
1050
+ StatusCodes.Good
1051
+ );
1052
+ }
980
1053
 
981
1054
  eu.on("value_changed", handler);
982
1055
  }
@@ -72,7 +72,7 @@ function _findParentNodeId(addressSpace: AddressSpacePartial, options: Construct
72
72
 
73
73
  function prepareName(browseName?: QualifiedName | QualifiedNameOptions): string {
74
74
  const m = browseName?.name?.toString().replace(/[ ]/g, "").replace(/(<|>)/g, "");
75
- return m ||"";
75
+ return m || "";
76
76
  }
77
77
 
78
78
  export interface AddressSpacePartial {
@@ -170,11 +170,12 @@ export class NodeIdManager {
170
170
  fullParentName = buildUpName2(parentNodeId, suffix);
171
171
  }
172
172
  const fullName = compose(fullParentName, prepareName(options.browseName));
173
- if (this._cacheSymbolicName[fullName]) {
174
- return makeNodeId(this._cacheSymbolicName[fullName][0], this.namespaceIndex);
173
+ const cached = this._cacheSymbolicName[fullName];
174
+ if (cached && this._isCacheHitReusable(cached[0], options, parentInfo)) {
175
+ return makeNodeId(cached[0], this.namespaceIndex);
175
176
  }
176
177
  const nodeId = this._constructNodeId(options);
177
- if (nodeId.identifierType === NodeIdType.NUMERIC) {
178
+ if (nodeId.identifierType === NodeIdType.NUMERIC && !cached) {
178
179
  this._cacheSymbolicName[fullName] = [nodeId.value as number, options.nodeClass || NodeClass.Unspecified];
179
180
  this._cacheSymbolicNameRev.add(nodeId.value as number);
180
181
  }
@@ -201,6 +202,46 @@ export class NodeIdManager {
201
202
  return nodeId;
202
203
  }
203
204
 
205
+ /**
206
+ * A symbol-cache hit is only safe to reuse when it actually refers to
207
+ * the node we are about to construct. Because the cache key is built
208
+ * from a (possibly truncated, e.g. during a clone where parentNodeId
209
+ * isn't linked yet) parent-name chain, two unrelated nodes can end up
210
+ * sharing the same key. When that happens, reusing the cached nodeId
211
+ * would hand a brand-new node the identity of a completely different,
212
+ * already-registered one.
213
+ *
214
+ * We only trust the cache hit when either:
215
+ * - nothing occupies that nodeId yet (first-time reservation), or
216
+ * - the occupant is genuinely the same node (same browseName AND same
217
+ * parent) - in which case we deliberately return the same id so
218
+ * that the caller's duplicate-registration error still fires.
219
+ */
220
+ private _isCacheHitReusable(value: number, options: ConstructNodeIdOptions, parentInfo: [NodeId, Suffix] | null): boolean {
221
+ const candidate = makeNodeId(value, this.namespaceIndex);
222
+ const occupant = this.addressSpace.findNode(candidate);
223
+ if (!occupant) {
224
+ return true;
225
+ }
226
+ // options.browseName is expected to already carry its real, resolved namespaceIndex by the
227
+ // time it reaches here: internalCreateNode() normalizes plain-string browseNames to
228
+ // `namespaceIndex: this.index` before construction, so `0` here means "explicitly namespace 0"
229
+ // (e.g. the standard "0:EURange" / "0:EngineeringUnits" properties), not "unspecified".
230
+ // Comparing literally keeps genuinely different namespaces (own-namespace vs explicit ns 0)
231
+ // from being treated as the same node.
232
+ const sameBrowseName =
233
+ !!occupant.browseName &&
234
+ occupant.browseName.name === (options.browseName?.name ?? null) &&
235
+ (occupant.browseName.namespaceIndex ?? 0) === (options.browseName?.namespaceIndex ?? 0);
236
+
237
+ const expectedParentNodeId = parentInfo ? parentInfo[0] : null;
238
+ const sameParent = expectedParentNodeId
239
+ ? !!occupant.parentNodeId && sameNodeId(occupant.parentNodeId, expectedParentNodeId)
240
+ : !occupant.parentNodeId;
241
+
242
+ return sameBrowseName && sameParent;
243
+ }
244
+
204
245
  private _constructNodeId(options: ConstructNodeIdOptions): NodeId {
205
246
 
206
247
  const resolveNodeIdEx = (nodeId: BaseNode | NodeIdLike) =>
@@ -71,13 +71,15 @@ export function _recomputeRequiredModelsFromTypes(namespace: INamespace, cache?:
71
71
 
72
72
  const namespaceIndex = dataType.namespace;
73
73
  consider(namespaceIndex);
74
+ // mark the dataType as visited *before* exploring its fields: a structure may refer to
75
+ // itself (directly or through a cycle), and marking it afterwards would recurse forever
76
+ _visitedDataType.add(dataType.toString());
74
77
  if (dataTypeNode.isStructure()) {
75
78
  const definition = dataTypeNode.getStructureDefinition();
76
79
  for (const field of definition.fields || []) {
77
80
  exploreDataTypeField(field);
78
81
  }
79
82
  }
80
- _visitedDataType.add(dataType.toString());
81
83
  }
82
84
 
83
85
  function exploreExtensionObject(e: ExtensionObject) {
@@ -758,26 +758,23 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T>
758
758
  dataValue.value = variant1;
759
759
 
760
760
  if (dataValue.value.dataType === DataType.ExtensionObject) {
761
+ // early, detailed diagnostics on a malformed extension object (the actual throw
762
+ // happens below in _internal_set_dataValue).
761
763
  const valueIsCorrect = this.checkExtensionObjectIsCorrect(dataValue.value.value);
762
764
  if (!valueIsCorrect) {
763
765
  errorLog("setValueFromSource Invalid value !");
764
766
  errorLog(this.toString());
765
767
  errorLog(dataValue.toString());
766
- this.checkExtensionObjectIsCorrect(dataValue.value.value);
767
768
  }
768
- // ----------------------------------
769
- if (this.$extensionObject || this.$$extensionObjectArray) {
770
- // we have an extension object already bound to this node
771
- // the client is asking us to replace the object entirely by a new one
772
- // const ext = dataValue.value.value;
773
- this._internal_set_dataValue(dataValue);
774
- return;
775
- } else {
776
- this.$dataValue = dataValue;
777
- }
778
- } else {
779
- this._internal_set_dataValue(dataValue);
780
769
  }
770
+ // Route every value - scalar or ExtensionObject - through _internal_set_dataValue.
771
+ // This guarantees the change is consistently propagated to observers (historian,
772
+ // monitored items) and, when the node is a bound extension object (or a bound
773
+ // sub-structure child), cascaded through the proxy field setters by the overridden
774
+ // _inner_replace_dataValue. Previously ExtensionObject values were special-cased with a
775
+ // silent "this.$dataValue = dataValue" assignment that skipped notification for plain
776
+ // (unbound) extension object variables and threw on bound sub-structure children.
777
+ this._internal_set_dataValue(dataValue);
781
778
  } catch (err) {
782
779
  errorLog("UAVariable#setValueFromString Error : ", this.browseName.toString(), this.nodeId.toString());
783
780
  errorLog((err as Error).message);