node-opcua-address-space 2.175.0 → 2.175.2

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.0",
3
+ "version": "2.175.2",
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,49 +21,49 @@
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.174.0",
25
- "node-opcua-assert": "2.164.0",
26
- "node-opcua-basic-types": "2.174.0",
27
- "node-opcua-binary-stream": "2.173.0",
28
- "node-opcua-client-dynamic-extension-object": "2.174.0",
29
- "node-opcua-constants": "2.174.0",
24
+ "node-opcua-address-space-base": "2.175.2",
25
+ "node-opcua-assert": "2.175.1",
26
+ "node-opcua-basic-types": "2.175.1",
27
+ "node-opcua-binary-stream": "2.175.1",
28
+ "node-opcua-client-dynamic-extension-object": "2.175.2",
29
+ "node-opcua-constants": "2.175.1",
30
30
  "node-opcua-crypto": "5.3.6",
31
- "node-opcua-data-access": "2.174.0",
32
- "node-opcua-data-model": "2.174.0",
33
- "node-opcua-data-value": "2.174.0",
34
- "node-opcua-date-time": "2.174.0",
35
- "node-opcua-debug": "2.172.0",
36
- "node-opcua-enum": "2.173.0",
37
- "node-opcua-extension-object": "2.174.0",
38
- "node-opcua-factory": "2.174.0",
39
- "node-opcua-nodeid": "2.174.0",
40
- "node-opcua-nodeset-ua": "2.174.0",
41
- "node-opcua-numeric-range": "2.174.0",
42
- "node-opcua-object-registry": "2.172.0",
43
- "node-opcua-pseudo-session": "2.174.0",
44
- "node-opcua-service-browse": "2.174.0",
45
- "node-opcua-service-call": "2.174.0",
46
- "node-opcua-service-history": "2.174.0",
47
- "node-opcua-service-translate-browse-path": "2.174.0",
48
- "node-opcua-service-write": "2.174.0",
49
- "node-opcua-status-code": "2.173.0",
50
- "node-opcua-types": "2.174.0",
51
- "node-opcua-utils": "2.173.0",
52
- "node-opcua-variant": "2.174.0",
53
- "node-opcua-xml2json": "2.173.0",
31
+ "node-opcua-data-access": "2.175.2",
32
+ "node-opcua-data-model": "2.175.1",
33
+ "node-opcua-data-value": "2.175.1",
34
+ "node-opcua-date-time": "2.175.1",
35
+ "node-opcua-debug": "2.175.1",
36
+ "node-opcua-enum": "2.175.1",
37
+ "node-opcua-extension-object": "2.175.1",
38
+ "node-opcua-factory": "2.175.1",
39
+ "node-opcua-nodeid": "2.175.1",
40
+ "node-opcua-nodeset-ua": "2.175.2",
41
+ "node-opcua-numeric-range": "2.175.1",
42
+ "node-opcua-object-registry": "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
+ "node-opcua-status-code": "2.175.1",
50
+ "node-opcua-types": "2.175.2",
51
+ "node-opcua-utils": "2.175.1",
52
+ "node-opcua-variant": "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"
57
57
  },
58
58
  "devDependencies": {
59
59
  "humanize": "0.0.9",
60
- "node-opcua-benchmarker": "2.173.0",
61
- "node-opcua-leak-detector": "2.172.0",
62
- "node-opcua-nodesets": "2.174.0",
63
- "node-opcua-packet-analyzer": "2.174.0",
64
- "node-opcua-schemas": "2.174.0",
65
- "node-opcua-service-filter": "2.174.0",
66
- "node-opcua-test-fixtures": "2.157.0"
60
+ "node-opcua-benchmarker": "2.175.1",
61
+ "node-opcua-leak-detector": "2.175.1",
62
+ "node-opcua-nodesets": "2.175.1",
63
+ "node-opcua-packet-analyzer": "2.175.1",
64
+ "node-opcua-schemas": "2.175.2",
65
+ "node-opcua-service-filter": "2.175.2",
66
+ "node-opcua-test-fixtures": "2.175.1"
67
67
  },
68
68
  "author": "Etienne Rossignon",
69
69
  "license": "MIT",
@@ -80,7 +80,7 @@
80
80
  "internet of things"
81
81
  ],
82
82
  "homepage": "http://node-opcua.github.io/",
83
- "gitHead": "b15c534b0f03502d30c49934c36291368bdacb80",
83
+ "gitHead": "4e2a73e5b96bdf3bd3a88709c813289038caf19b",
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;
@@ -260,11 +303,11 @@ function _makeTypeReader(
260
303
  throw new Error(`??? ${field.dataType} ${field.name}`);
261
304
  }
262
305
 
263
- reader.parser![field.name!] = {
306
+ reader.parser![field.name || ""] = {
264
307
  parser: fieldParser.parser,
265
308
  // endElement: fieldReader.endElement,
266
309
  finish(this: any) {
267
- const elName = lowerFirstLetter(field.name!);
310
+ const elName = lowerFirstLetter(field.name || "");
268
311
  if (fieldParser.finish) {
269
312
  fieldParser.finish.call(this);
270
313
  } else {
@@ -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 {
@@ -169,13 +169,14 @@ export class NodeIdManager {
169
169
  const [parentNodeId, suffix] = parentInfo;
170
170
  fullParentName = buildUpName2(parentNodeId, suffix);
171
171
  }
172
- const fullName = compose(fullParentName, prepareName(options.browseName!));
173
- if (this._cacheSymbolicName[fullName]) {
174
- return makeNodeId(this._cacheSymbolicName[fullName][0], this.namespaceIndex);
172
+ const fullName = compose(fullParentName, prepareName(options.browseName));
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
- this._cacheSymbolicName[fullName] = [nodeId.value as number, options.nodeClass!];
178
+ if (nodeId.identifierType === NodeIdType.NUMERIC && !cached) {
179
+ this._cacheSymbolicName[fullName] = [nodeId.value as number, options.nodeClass || NodeClass.Unspecified];
179
180
  this._cacheSymbolicNameRev.add(nodeId.value as number);
180
181
  }
181
182
  return nodeId;
@@ -192,15 +193,55 @@ export class NodeIdManager {
192
193
  const [parentNodeId, suffix] = parentInfo;
193
194
  fullParentName = buildUpName2(parentNodeId, suffix);
194
195
  }
195
- const fullName = compose(fullParentName, prepareName(options.browseName!));
196
+ const fullName = compose(fullParentName, prepareName(options.browseName));
196
197
  if (!this._cacheSymbolicName[fullName]) {
197
- this._cacheSymbolicName[fullName] = [nodeId.value as number, options.nodeClass!];
198
+ this._cacheSymbolicName[fullName] = [nodeId.value as number, options.nodeClass || NodeClass.Unspecified];
198
199
  this._cacheSymbolicNameRev.add(nodeId.value as number);
199
200
  }
200
201
  }
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) =>
@@ -16,7 +16,7 @@ function dumpEnumeratedType(xw: XmlWriter, e: EnumDefinition, name: string): voi
16
16
  xw.writeAttribute("LengthInBits", "32");
17
17
  for (const f of e.fields || []) {
18
18
  xw.startElement("opc:EnumeratedValue");
19
- xw.writeAttribute("Name", f.name!);
19
+ xw.writeAttribute("Name", f.name|| "");
20
20
  assert(f.value[0] === 0, "unsupported 64 bit value !");
21
21
  xw.writeAttribute("Value", f.value[1].toString());
22
22
  xw.endElement();
@@ -119,15 +119,15 @@ function dumpDataTypeStructure(
119
119
  }
120
120
 
121
121
  xw.startElement("opc:Field");
122
- xw.writeAttribute("Name", f.name!);
122
+ xw.writeAttribute("Name", f.name|| "");
123
123
 
124
124
  const typeName = buildXmlName(addressSpace as AddressSpacePrivate, map, f.dataType);
125
125
  xw.writeAttribute("TypeName", typeName);
126
126
  if (isArray) {
127
- xw.writeAttribute("LengthField", `NoOf${f.name!}`);
127
+ xw.writeAttribute("LengthField", `NoOf${f.name|| ""}`);
128
128
  }
129
129
  if (f.isOptional) {
130
- xw.writeAttribute("SwitchField", `${f.name}Specified`);
130
+ xw.writeAttribute("SwitchField", `${f.name|| ""}Specified`);
131
131
  }
132
132
  xw.endElement();
133
133
  }
@@ -137,7 +137,7 @@ function dumpDataTypeStructure(
137
137
  function dumpDataTypeToBSD(xw: XmlWriter, dataType: UADataType, map: Map<number, string>) {
138
138
  const addressSpace = dataType.addressSpace;
139
139
 
140
- const name: string = dataType.browseName.name!;
140
+ const name: string = dataType.browseName.name || "";
141
141
 
142
142
  const definition = dataType.getDefinition();
143
143
  if (definition instanceof StructureDefinition) {
@@ -898,7 +898,7 @@ function _dumpStructureDefinition(
898
898
  for (let index = nbFieldsInBase; index < fields.length; index++) {
899
899
  const defItem = fields[index];
900
900
  xw.startElement("Field");
901
- xw.writeAttribute("Name", defItem.name!);
901
+ xw.writeAttribute("Name", defItem.name as string);
902
902
 
903
903
  if (defItem.arrayDimensions) {
904
904
  xw.writeAttribute("ArrayDimensions", defItem.arrayDimensions.map((x) => x.toString()).join(","));