node-opcua-alias-name-client 2.176.0 → 2.178.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -101,6 +101,43 @@ for one, so a later `findAliasVerbose` after a `findAlias` makes no further roun
101
101
  Construct one `ClientAliasSet` per session; call `invalidate()` if the Server's address
102
102
  space changes underneath it.
103
103
 
104
+ ## Recovering the ReferenceType of each target
105
+
106
+ `AliasNameDataType` and `AliasNameVerboseDataType` (clauses 7.2 and 7.3) carry the
107
+ referenced Nodes but **not** the ReferenceType of each Reference: a target linked with a
108
+ vendor subtype of `AliasFor` (clause 8.2) comes back indistinguishable from one linked
109
+ with `AliasFor` itself. That is a limitation of the DataTypes, not of any Server.
110
+
111
+ Most Clients never care — they want the NodeId. An **aggregator** re-publishing pulled
112
+ aliases does: recorded as plain `AliasFor`, a downstream `FindAlias` whose
113
+ `ReferenceTypeFilter` names the subtype can no longer be answered faithfully across the
114
+ aggregation hop. `readAliasReferenceTypes` recovers the actual ReferenceType per
115
+ (alias, target) by browsing the `AliasNameType` instance Nodes:
116
+
117
+ ```ts
118
+ import { readAliasReferenceTypes } from "node-opcua-alias-name-client";
119
+
120
+ const entries = await aliases.findAliasVerbose("%", { categoryNodeId });
121
+ const referenceTypes = await readAliasReferenceTypes(session, entries);
122
+ for (const entry of entries) {
123
+ for (const { targetNodeId, referenceTypeId } of referenceTypes.get(entry) ?? []) {
124
+ republish(entry.aliasName, targetNodeId, referenceTypeId);
125
+ }
126
+ }
127
+ ```
128
+
129
+ The map is keyed by the very elements passed in; an entry the Server could not resolve
130
+ (deleted since the find, for instance) is absent rather than guessed at. Callers that
131
+ already know the `AliasNameType` instance NodeIds can pass those instead of verbose
132
+ entries and skip the lookup step.
133
+
134
+ **Cost.** One `TranslateBrowsePaths` request per 1000 aliases to locate the instance
135
+ Nodes (skipped when NodeIds are passed), one `Browse` request per 1000, plus a
136
+ `BrowseNext` round trip per continuation the Server imposes — batched precisely so a
137
+ large category is a handful of round trips, never one per alias. Lower `maxNodesPerCall`
138
+ when a Server advertises tighter `OperationLimits` (OPC 10000-5 clause 6.3.11). If you
139
+ only resolve names to NodeIds, skip all of it.
140
+
104
141
  ## Nodes on another Server (Annex A)
105
142
 
106
143
  A returned `ExpandedNodeId` may carry a non-zero `ServerIndex`, which says only "this Node
package/dist/index.d.ts CHANGED
@@ -11,4 +11,5 @@
11
11
  */
12
12
  export { ALIASES_ROOT, type ClientAliasEntry, ClientAliasSet, type ClientAliasVerboseEntry, type FindAliasOptions, TAG_VARIABLES, TOPICS } from "./client_alias_set.js";
13
13
  export { AliasNameCallError, AliasNameMethodNotSupportedError } from "./errors.js";
14
+ export { type AliasReferenceTypeEntry, type ReadAliasReferenceTypesOptions, readAliasReferenceTypes } from "./read_alias_reference_types.js";
14
15
  export { LOCAL_SERVER_INDEX, ServerIndexResolver } from "./server_index_resolver.js";
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * `ServerIndex` of a returned `ExpandedNodeId` into a URI (Annex A).
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.ServerIndexResolver = exports.LOCAL_SERVER_INDEX = exports.AliasNameMethodNotSupportedError = exports.AliasNameCallError = exports.TOPICS = exports.TAG_VARIABLES = exports.ClientAliasSet = exports.ALIASES_ROOT = void 0;
14
+ exports.ServerIndexResolver = exports.LOCAL_SERVER_INDEX = exports.readAliasReferenceTypes = exports.AliasNameMethodNotSupportedError = exports.AliasNameCallError = exports.TOPICS = exports.TAG_VARIABLES = exports.ClientAliasSet = exports.ALIASES_ROOT = void 0;
15
15
  var client_alias_set_js_1 = require("./client_alias_set.js");
16
16
  Object.defineProperty(exports, "ALIASES_ROOT", { enumerable: true, get: function () { return client_alias_set_js_1.ALIASES_ROOT; } });
17
17
  Object.defineProperty(exports, "ClientAliasSet", { enumerable: true, get: function () { return client_alias_set_js_1.ClientAliasSet; } });
@@ -20,6 +20,8 @@ Object.defineProperty(exports, "TOPICS", { enumerable: true, get: function () {
20
20
  var errors_js_1 = require("./errors.js");
21
21
  Object.defineProperty(exports, "AliasNameCallError", { enumerable: true, get: function () { return errors_js_1.AliasNameCallError; } });
22
22
  Object.defineProperty(exports, "AliasNameMethodNotSupportedError", { enumerable: true, get: function () { return errors_js_1.AliasNameMethodNotSupportedError; } });
23
+ var read_alias_reference_types_js_1 = require("./read_alias_reference_types.js");
24
+ Object.defineProperty(exports, "readAliasReferenceTypes", { enumerable: true, get: function () { return read_alias_reference_types_js_1.readAliasReferenceTypes; } });
23
25
  var server_index_resolver_js_1 = require("./server_index_resolver.js");
24
26
  Object.defineProperty(exports, "LOCAL_SERVER_INDEX", { enumerable: true, get: function () { return server_index_resolver_js_1.LOCAL_SERVER_INDEX; } });
25
27
  Object.defineProperty(exports, "ServerIndexResolver", { enumerable: true, get: function () { return server_index_resolver_js_1.ServerIndexResolver; } });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../source/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAEH,6DAQ+B;AAP3B,mHAAA,YAAY,OAAA;AAEZ,qHAAA,cAAc,OAAA;AAGd,oHAAA,aAAa,OAAA;AACb,6GAAA,MAAM,OAAA;AAEV,yCAAmF;AAA1E,+GAAA,kBAAkB,OAAA;AAAE,6HAAA,gCAAgC,OAAA;AAC7D,uEAAqF;AAA5E,8HAAA,kBAAkB,OAAA;AAAE,+HAAA,mBAAmB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../source/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAEH,6DAQ+B;AAP3B,mHAAA,YAAY,OAAA;AAEZ,qHAAA,cAAc,OAAA;AAGd,oHAAA,aAAa,OAAA;AACb,6GAAA,MAAM,OAAA;AAEV,yCAAmF;AAA1E,+GAAA,kBAAkB,OAAA;AAAE,6HAAA,gCAAgC,OAAA;AAC7D,iFAIyC;AADrC,wIAAA,uBAAuB,OAAA;AAE3B,uEAAqF;AAA5E,8HAAA,kBAAkB,OAAA;AAAE,+HAAA,mBAAmB,OAAA"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @module node-opcua-alias-name-client
3
+ *
4
+ * Recover the ReferenceType linking each AliasName to each of its targets.
5
+ *
6
+ * `AliasNameDataType` and `AliasNameVerboseDataType` (OPC 10000-17 clauses 7.2
7
+ * and 7.3) carry the referenced Nodes, but **not** the ReferenceType of each
8
+ * Reference: a target linked with a vendor subtype of `AliasFor` (clause 8.2)
9
+ * comes back from `FindAlias` / `FindAliasVerbose` indistinguishable from one
10
+ * linked with `AliasFor` itself. That is a limitation of the DataTypes, not of
11
+ * any particular Server.
12
+ *
13
+ * Most Clients never notice — they want the NodeId and the subtype carries no
14
+ * extra meaning for them. An **aggregating** Server does: re-published as plain
15
+ * `AliasFor`, a downstream `FindAlias` whose `ReferenceTypeFilter` names the
16
+ * subtype (clause 6.3.2 Table 3) cannot be answered faithfully across the
17
+ * aggregation hop. {@link readAliasReferenceTypes} closes that gap by going
18
+ * back to the address space: the `AliasNameType` instances are Nodes, and
19
+ * Browse reports the ReferenceType of every Reference they hold.
20
+ */
21
+ import { type ExpandedNodeId, type NodeId } from "node-opcua-nodeid";
22
+ import type { IBasicSessionAsync2 } from "node-opcua-pseudo-session";
23
+ import type { ClientAliasVerboseEntry } from "./client_alias_set.js";
24
+ /** One (alias, target) link, with the ReferenceType the find Methods cannot report. */
25
+ export interface AliasReferenceTypeEntry {
26
+ /** The Node the alias names — matches one element of `referencedNodes`. */
27
+ targetNodeId: ExpandedNodeId;
28
+ /** `AliasFor` (i=23469) or the subtype the publisher actually used. */
29
+ referenceTypeId: NodeId;
30
+ }
31
+ export interface ReadAliasReferenceTypesOptions {
32
+ /**
33
+ * Upper bound on the operations packed into one `TranslateBrowsePaths` or
34
+ * `Browse` request. The default of 1000 fits most Servers; lower it when a
35
+ * Server advertises tighter `OperationLimits` (OPC 10000-5 clause 6.3.11) —
36
+ * `Bad_TooManyOperations` is the symptom of exceeding them.
37
+ */
38
+ maxNodesPerCall?: number;
39
+ }
40
+ /**
41
+ * Read the ReferenceType of every `AliasFor` Reference (and subtype) each alias
42
+ * holds, which is the one thing `FindAliasVerbose` cannot report.
43
+ *
44
+ * ```ts
45
+ * const entries = await aliases.findAliasVerbose("%", { categoryNodeId });
46
+ * const referenceTypes = await readAliasReferenceTypes(session, entries);
47
+ * for (const entry of entries) {
48
+ * for (const { targetNodeId, referenceTypeId } of referenceTypes.get(entry) ?? []) {
49
+ * republish(entry.aliasName, targetNodeId, referenceTypeId);
50
+ * }
51
+ * }
52
+ * ```
53
+ *
54
+ * **When to use it.** Only when the ReferenceType itself matters — typically an
55
+ * aggregator that must re-publish pulled aliases with reference-type fidelity,
56
+ * so a downstream `ReferenceTypeFilter` naming an `AliasFor` subtype keeps
57
+ * working across the hop. A Client that only resolves names to NodeIds gets
58
+ * nothing from it and should not pay for it.
59
+ *
60
+ * **What it costs.** On top of the find call already made: one
61
+ * `TranslateBrowsePaths` request per {@link ReadAliasReferenceTypesOptions.maxNodesPerCall}
62
+ * aliases to locate the `AliasNameType` instance Nodes (skipped when NodeIds
63
+ * are passed directly), then one `Browse` request per batch, plus one
64
+ * `BrowseNext` round trip per continuation the Server imposes. The requests are
65
+ * batched precisely so a large category does **not** become one round trip per
66
+ * alias: 1000 aliases resolve in two or three round trips, not 1000.
67
+ *
68
+ * The result is keyed by the **very elements passed in** — look entries up with
69
+ * the objects from the input array, not with reconstructed equals. An input the
70
+ * Server could not resolve (the alias was deleted since the find, a NodeId that
71
+ * no longer browses) is absent from the map rather than present with a guess.
72
+ *
73
+ * @param session any `IBasicSessionAsync2` — a remote `ClientSession` or an
74
+ * in-process `PseudoSession`, as everywhere in this package
75
+ * @param aliases either the entries of a `findAliasVerbose` call, or the
76
+ * NodeIds of `AliasNameType` instance Nodes when the caller already knows
77
+ * them (from browsing a category, or from its own bookkeeping)
78
+ */
79
+ export declare function readAliasReferenceTypes(session: IBasicSessionAsync2, aliases: NodeId[], options?: ReadAliasReferenceTypesOptions): Promise<Map<NodeId, AliasReferenceTypeEntry[]>>;
80
+ export declare function readAliasReferenceTypes(session: IBasicSessionAsync2, aliases: ClientAliasVerboseEntry[], options?: ReadAliasReferenceTypesOptions): Promise<Map<ClientAliasVerboseEntry, AliasReferenceTypeEntry[]>>;
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ /**
3
+ * @module node-opcua-alias-name-client
4
+ *
5
+ * Recover the ReferenceType linking each AliasName to each of its targets.
6
+ *
7
+ * `AliasNameDataType` and `AliasNameVerboseDataType` (OPC 10000-17 clauses 7.2
8
+ * and 7.3) carry the referenced Nodes, but **not** the ReferenceType of each
9
+ * Reference: a target linked with a vendor subtype of `AliasFor` (clause 8.2)
10
+ * comes back from `FindAlias` / `FindAliasVerbose` indistinguishable from one
11
+ * linked with `AliasFor` itself. That is a limitation of the DataTypes, not of
12
+ * any particular Server.
13
+ *
14
+ * Most Clients never notice — they want the NodeId and the subtype carries no
15
+ * extra meaning for them. An **aggregating** Server does: re-published as plain
16
+ * `AliasFor`, a downstream `FindAlias` whose `ReferenceTypeFilter` names the
17
+ * subtype (clause 6.3.2 Table 3) cannot be answered faithfully across the
18
+ * aggregation hop. {@link readAliasReferenceTypes} closes that gap by going
19
+ * back to the address space: the `AliasNameType` instances are Nodes, and
20
+ * Browse reports the ReferenceType of every Reference they hold.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.readAliasReferenceTypes = readAliasReferenceTypes;
24
+ const node_opcua_constants_1 = require("node-opcua-constants");
25
+ const node_opcua_data_model_1 = require("node-opcua-data-model");
26
+ const node_opcua_nodeid_1 = require("node-opcua-nodeid");
27
+ const node_opcua_service_translate_browse_path_1 = require("node-opcua-service-translate-browse-path");
28
+ /** `AliasFor` (i=23469), the base ReferenceType of every alias link (clause 8.2). */
29
+ const ALIAS_FOR = (0, node_opcua_nodeid_1.resolveNodeId)(node_opcua_constants_1.ReferenceTypeIds.AliasFor);
30
+ const HIERARCHICAL_REFERENCES = (0, node_opcua_nodeid_1.resolveNodeId)(node_opcua_constants_1.ReferenceTypeIds.HierarchicalReferences);
31
+ const REFERENCE_TYPE_RESULT_MASK = (0, node_opcua_data_model_1.makeResultMask)("ReferenceType");
32
+ const DEFAULT_MAX_NODES_PER_CALL = 1000;
33
+ async function readAliasReferenceTypes(session, aliases, options) {
34
+ const maxNodesPerCall = Math.max(1, options?.maxNodesPerCall ?? DEFAULT_MAX_NODES_PER_CALL);
35
+ const result = new Map();
36
+ if (aliases.length === 0) {
37
+ return result;
38
+ }
39
+ const aliasNodeIds = aliases[0] instanceof node_opcua_nodeid_1.NodeId
40
+ ? aliases
41
+ : await resolveAliasNodeIds(session, aliases, maxNodesPerCall);
42
+ const targets = await browseAliasTargets(session, aliasNodeIds, maxNodesPerCall);
43
+ aliases.forEach((key, index) => {
44
+ const entries = targets[index];
45
+ if (entries) {
46
+ result.set(key, entries);
47
+ }
48
+ });
49
+ return result;
50
+ }
51
+ /**
52
+ * Locate the `AliasNameType` instance Node behind each verbose entry.
53
+ *
54
+ * `FindAliasVerbose` names the category that held the alias
55
+ * (`aliasNameCategoryId`, clause 7.3) but not the alias Node itself, so the
56
+ * Node is found by translating one browse path below the category. The
57
+ * `RelativePath` is built element by element rather than parsed from a string,
58
+ * so an alias name containing `/`, `&` or the other OPC 10000-4 Annex A
59
+ * reserved characters needs no escaping. The QualifiedName match is exact —
60
+ * the entry's `namespaceIndex` is the one the Server reported, so this is the
61
+ * one place the namespace of an AliasName is **not** ignored.
62
+ *
63
+ * Unresolvable entries yield `null`, keeping positions aligned with the input.
64
+ */
65
+ async function resolveAliasNodeIds(session, entries, maxNodesPerCall) {
66
+ const aliasNodeIds = new Array(entries.length).fill(null);
67
+ for (let offset = 0; offset < entries.length; offset += maxNodesPerCall) {
68
+ const chunk = entries.slice(offset, offset + maxNodesPerCall);
69
+ const results = await session.translateBrowsePath(chunk.map((entry) => new node_opcua_service_translate_browse_path_1.BrowsePath({
70
+ startingNode: entry.aliasNameCategoryId,
71
+ relativePath: {
72
+ elements: [
73
+ {
74
+ referenceTypeId: HIERARCHICAL_REFERENCES,
75
+ isInverse: false,
76
+ includeSubtypes: true,
77
+ targetName: { namespaceIndex: entry.namespaceIndex, name: entry.aliasName }
78
+ }
79
+ ]
80
+ }
81
+ })));
82
+ results.forEach((browsePathResult, index) => {
83
+ if (browsePathResult.statusCode.isGood() && browsePathResult.targets?.length) {
84
+ // the targetId of a local Node is an ExpandedNodeId with
85
+ // serverIndex 0, usable as a NodeId as-is
86
+ aliasNodeIds[offset + index] = browsePathResult.targets[0].targetId;
87
+ }
88
+ });
89
+ }
90
+ return aliasNodeIds;
91
+ }
92
+ /**
93
+ * Browse the forward `AliasFor` References (subtypes included) of many alias
94
+ * Nodes in as few requests as possible, following every continuation point.
95
+ *
96
+ * Positions align with the input; `null` marks a Node that could not be
97
+ * resolved upstream or whose Browse did not complete — for the fidelity use
98
+ * case, an absent answer beats a truncated one presented as complete.
99
+ */
100
+ async function browseAliasTargets(session, aliasNodeIds, maxNodesPerCall) {
101
+ const targets = new Array(aliasNodeIds.length).fill(null);
102
+ const toBrowse = [];
103
+ aliasNodeIds.forEach((nodeId, index) => {
104
+ if (nodeId) {
105
+ toBrowse.push({ index, nodeId });
106
+ }
107
+ });
108
+ const toEntry = (reference) => ({
109
+ targetNodeId: reference.nodeId,
110
+ referenceTypeId: reference.referenceTypeId
111
+ });
112
+ for (let offset = 0; offset < toBrowse.length; offset += maxNodesPerCall) {
113
+ const chunk = toBrowse.slice(offset, offset + maxNodesPerCall);
114
+ const browseResults = await session.browse(chunk.map(({ nodeId }) => ({
115
+ nodeId,
116
+ browseDirection: node_opcua_data_model_1.BrowseDirection.Forward,
117
+ referenceTypeId: ALIAS_FOR,
118
+ includeSubtypes: true,
119
+ nodeClassMask: 0,
120
+ resultMask: REFERENCE_TYPE_RESULT_MASK
121
+ })));
122
+ // continuations, batched too: one BrowseNext round trip serves every
123
+ // Node of the chunk that was truncated, however many there are
124
+ let pending = [];
125
+ browseResults.forEach((browseResult, chunkIndex) => {
126
+ const { index } = chunk[chunkIndex];
127
+ if (!browseResult.statusCode.isGood()) {
128
+ return;
129
+ }
130
+ targets[index] = (browseResult.references ?? []).map(toEntry);
131
+ if (browseResult.continuationPoint?.length) {
132
+ pending.push({ index, continuationPoint: browseResult.continuationPoint });
133
+ }
134
+ });
135
+ while (pending.length > 0) {
136
+ const nextResults = await session.browseNext(pending.map(({ continuationPoint }) => continuationPoint), false);
137
+ const stillPending = [];
138
+ nextResults.forEach((browseResult, pendingIndex) => {
139
+ const { index } = pending[pendingIndex];
140
+ if (!browseResult.statusCode.isGood()) {
141
+ // incomplete: withdraw the partial answer rather than let it
142
+ // pass for the whole truth
143
+ targets[index] = null;
144
+ return;
145
+ }
146
+ targets[index]?.push(...(browseResult.references ?? []).map(toEntry));
147
+ if (browseResult.continuationPoint?.length) {
148
+ stillPending.push({ index, continuationPoint: browseResult.continuationPoint });
149
+ }
150
+ });
151
+ pending = stillPending;
152
+ }
153
+ }
154
+ return targets;
155
+ }
156
+ //# sourceMappingURL=read_alias_reference_types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read_alias_reference_types.js","sourceRoot":"","sources":["../source/read_alias_reference_types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-opcua-alias-name-client",
3
- "version": "2.176.0",
3
+ "version": "2.178.0",
4
4
  "description": "pure nodejs OPCUA SDK - client-side alias names (OPC 10000-17)",
5
5
  "scripts": {
6
6
  "build": "tsc -b",
@@ -18,17 +18,17 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "node-opcua-constants": "2.176.0",
21
- "node-opcua-data-model": "2.176.0",
22
- "node-opcua-nodeid": "2.176.0",
23
- "node-opcua-pseudo-session": "2.176.0",
24
- "node-opcua-service-translate-browse-path": "2.176.0",
21
+ "node-opcua-data-model": "2.177.0",
22
+ "node-opcua-nodeid": "2.177.0",
23
+ "node-opcua-pseudo-session": "2.178.0",
24
+ "node-opcua-service-translate-browse-path": "2.177.0",
25
25
  "node-opcua-status-code": "2.176.0",
26
- "node-opcua-types": "2.176.0",
27
- "node-opcua-variant": "2.176.0"
26
+ "node-opcua-types": "2.177.0",
27
+ "node-opcua-variant": "2.177.0"
28
28
  },
29
29
  "devDependencies": {
30
- "node-opcua-address-space": "2.176.0",
31
- "node-opcua-alias-name-server": "2.176.0",
30
+ "node-opcua-address-space": "2.178.0",
31
+ "node-opcua-alias-name-server": "2.178.0",
32
32
  "node-opcua-leak-detector": "2.175.6",
33
33
  "node-opcua-nodesets": "2.175.3"
34
34
  },
@@ -50,5 +50,6 @@
50
50
  "files": [
51
51
  "dist",
52
52
  "source"
53
- ]
53
+ ],
54
+ "gitHead": "d0e850e5f7df80e14ceb6d8e092e793472965301"
54
55
  }
package/source/index.ts CHANGED
@@ -20,4 +20,9 @@ export {
20
20
  TOPICS
21
21
  } from "./client_alias_set.js";
22
22
  export { AliasNameCallError, AliasNameMethodNotSupportedError } from "./errors.js";
23
+ export {
24
+ type AliasReferenceTypeEntry,
25
+ type ReadAliasReferenceTypesOptions,
26
+ readAliasReferenceTypes
27
+ } from "./read_alias_reference_types.js";
23
28
  export { LOCAL_SERVER_INDEX, ServerIndexResolver } from "./server_index_resolver.js";
@@ -0,0 +1,257 @@
1
+ /**
2
+ * @module node-opcua-alias-name-client
3
+ *
4
+ * Recover the ReferenceType linking each AliasName to each of its targets.
5
+ *
6
+ * `AliasNameDataType` and `AliasNameVerboseDataType` (OPC 10000-17 clauses 7.2
7
+ * and 7.3) carry the referenced Nodes, but **not** the ReferenceType of each
8
+ * Reference: a target linked with a vendor subtype of `AliasFor` (clause 8.2)
9
+ * comes back from `FindAlias` / `FindAliasVerbose` indistinguishable from one
10
+ * linked with `AliasFor` itself. That is a limitation of the DataTypes, not of
11
+ * any particular Server.
12
+ *
13
+ * Most Clients never notice — they want the NodeId and the subtype carries no
14
+ * extra meaning for them. An **aggregating** Server does: re-published as plain
15
+ * `AliasFor`, a downstream `FindAlias` whose `ReferenceTypeFilter` names the
16
+ * subtype (clause 6.3.2 Table 3) cannot be answered faithfully across the
17
+ * aggregation hop. {@link readAliasReferenceTypes} closes that gap by going
18
+ * back to the address space: the `AliasNameType` instances are Nodes, and
19
+ * Browse reports the ReferenceType of every Reference they hold.
20
+ */
21
+
22
+ import { ReferenceTypeIds } from "node-opcua-constants";
23
+ import { BrowseDirection, makeResultMask } from "node-opcua-data-model";
24
+ import { type ExpandedNodeId, type NodeId, NodeId as NodeIdClass, resolveNodeId } from "node-opcua-nodeid";
25
+ import type { IBasicSessionAsync2 } from "node-opcua-pseudo-session";
26
+ import { BrowsePath } from "node-opcua-service-translate-browse-path";
27
+ import type { ReferenceDescription } from "node-opcua-types";
28
+ import type { ClientAliasVerboseEntry } from "./client_alias_set.js";
29
+
30
+ /** `AliasFor` (i=23469), the base ReferenceType of every alias link (clause 8.2). */
31
+ const ALIAS_FOR: NodeId = resolveNodeId(ReferenceTypeIds.AliasFor);
32
+ const HIERARCHICAL_REFERENCES: NodeId = resolveNodeId(ReferenceTypeIds.HierarchicalReferences);
33
+ const REFERENCE_TYPE_RESULT_MASK = makeResultMask("ReferenceType");
34
+ const DEFAULT_MAX_NODES_PER_CALL = 1000;
35
+
36
+ /** One (alias, target) link, with the ReferenceType the find Methods cannot report. */
37
+ export interface AliasReferenceTypeEntry {
38
+ /** The Node the alias names — matches one element of `referencedNodes`. */
39
+ targetNodeId: ExpandedNodeId;
40
+ /** `AliasFor` (i=23469) or the subtype the publisher actually used. */
41
+ referenceTypeId: NodeId;
42
+ }
43
+
44
+ export interface ReadAliasReferenceTypesOptions {
45
+ /**
46
+ * Upper bound on the operations packed into one `TranslateBrowsePaths` or
47
+ * `Browse` request. The default of 1000 fits most Servers; lower it when a
48
+ * Server advertises tighter `OperationLimits` (OPC 10000-5 clause 6.3.11) —
49
+ * `Bad_TooManyOperations` is the symptom of exceeding them.
50
+ */
51
+ maxNodesPerCall?: number;
52
+ }
53
+
54
+ /**
55
+ * Read the ReferenceType of every `AliasFor` Reference (and subtype) each alias
56
+ * holds, which is the one thing `FindAliasVerbose` cannot report.
57
+ *
58
+ * ```ts
59
+ * const entries = await aliases.findAliasVerbose("%", { categoryNodeId });
60
+ * const referenceTypes = await readAliasReferenceTypes(session, entries);
61
+ * for (const entry of entries) {
62
+ * for (const { targetNodeId, referenceTypeId } of referenceTypes.get(entry) ?? []) {
63
+ * republish(entry.aliasName, targetNodeId, referenceTypeId);
64
+ * }
65
+ * }
66
+ * ```
67
+ *
68
+ * **When to use it.** Only when the ReferenceType itself matters — typically an
69
+ * aggregator that must re-publish pulled aliases with reference-type fidelity,
70
+ * so a downstream `ReferenceTypeFilter` naming an `AliasFor` subtype keeps
71
+ * working across the hop. A Client that only resolves names to NodeIds gets
72
+ * nothing from it and should not pay for it.
73
+ *
74
+ * **What it costs.** On top of the find call already made: one
75
+ * `TranslateBrowsePaths` request per {@link ReadAliasReferenceTypesOptions.maxNodesPerCall}
76
+ * aliases to locate the `AliasNameType` instance Nodes (skipped when NodeIds
77
+ * are passed directly), then one `Browse` request per batch, plus one
78
+ * `BrowseNext` round trip per continuation the Server imposes. The requests are
79
+ * batched precisely so a large category does **not** become one round trip per
80
+ * alias: 1000 aliases resolve in two or three round trips, not 1000.
81
+ *
82
+ * The result is keyed by the **very elements passed in** — look entries up with
83
+ * the objects from the input array, not with reconstructed equals. An input the
84
+ * Server could not resolve (the alias was deleted since the find, a NodeId that
85
+ * no longer browses) is absent from the map rather than present with a guess.
86
+ *
87
+ * @param session any `IBasicSessionAsync2` — a remote `ClientSession` or an
88
+ * in-process `PseudoSession`, as everywhere in this package
89
+ * @param aliases either the entries of a `findAliasVerbose` call, or the
90
+ * NodeIds of `AliasNameType` instance Nodes when the caller already knows
91
+ * them (from browsing a category, or from its own bookkeeping)
92
+ */
93
+ export async function readAliasReferenceTypes(
94
+ session: IBasicSessionAsync2,
95
+ aliases: NodeId[],
96
+ options?: ReadAliasReferenceTypesOptions
97
+ ): Promise<Map<NodeId, AliasReferenceTypeEntry[]>>;
98
+ export async function readAliasReferenceTypes(
99
+ session: IBasicSessionAsync2,
100
+ aliases: ClientAliasVerboseEntry[],
101
+ options?: ReadAliasReferenceTypesOptions
102
+ ): Promise<Map<ClientAliasVerboseEntry, AliasReferenceTypeEntry[]>>;
103
+ export async function readAliasReferenceTypes(
104
+ session: IBasicSessionAsync2,
105
+ aliases: NodeId[] | ClientAliasVerboseEntry[],
106
+ options?: ReadAliasReferenceTypesOptions
107
+ ): Promise<Map<NodeId | ClientAliasVerboseEntry, AliasReferenceTypeEntry[]>> {
108
+ const maxNodesPerCall = Math.max(1, options?.maxNodesPerCall ?? DEFAULT_MAX_NODES_PER_CALL);
109
+ const result = new Map<NodeId | ClientAliasVerboseEntry, AliasReferenceTypeEntry[]>();
110
+ if (aliases.length === 0) {
111
+ return result;
112
+ }
113
+
114
+ const aliasNodeIds: (NodeId | null)[] =
115
+ aliases[0] instanceof NodeIdClass
116
+ ? (aliases as NodeId[])
117
+ : await resolveAliasNodeIds(session, aliases as ClientAliasVerboseEntry[], maxNodesPerCall);
118
+
119
+ const targets = await browseAliasTargets(session, aliasNodeIds, maxNodesPerCall);
120
+ aliases.forEach((key: NodeId | ClientAliasVerboseEntry, index: number) => {
121
+ const entries = targets[index];
122
+ if (entries) {
123
+ result.set(key, entries);
124
+ }
125
+ });
126
+ return result;
127
+ }
128
+
129
+ /**
130
+ * Locate the `AliasNameType` instance Node behind each verbose entry.
131
+ *
132
+ * `FindAliasVerbose` names the category that held the alias
133
+ * (`aliasNameCategoryId`, clause 7.3) but not the alias Node itself, so the
134
+ * Node is found by translating one browse path below the category. The
135
+ * `RelativePath` is built element by element rather than parsed from a string,
136
+ * so an alias name containing `/`, `&` or the other OPC 10000-4 Annex A
137
+ * reserved characters needs no escaping. The QualifiedName match is exact —
138
+ * the entry's `namespaceIndex` is the one the Server reported, so this is the
139
+ * one place the namespace of an AliasName is **not** ignored.
140
+ *
141
+ * Unresolvable entries yield `null`, keeping positions aligned with the input.
142
+ */
143
+ async function resolveAliasNodeIds(
144
+ session: IBasicSessionAsync2,
145
+ entries: ClientAliasVerboseEntry[],
146
+ maxNodesPerCall: number
147
+ ): Promise<(NodeId | null)[]> {
148
+ const aliasNodeIds: (NodeId | null)[] = new Array(entries.length).fill(null);
149
+ for (let offset = 0; offset < entries.length; offset += maxNodesPerCall) {
150
+ const chunk = entries.slice(offset, offset + maxNodesPerCall);
151
+ const results = await session.translateBrowsePath(
152
+ chunk.map(
153
+ (entry) =>
154
+ new BrowsePath({
155
+ startingNode: entry.aliasNameCategoryId,
156
+ relativePath: {
157
+ elements: [
158
+ {
159
+ referenceTypeId: HIERARCHICAL_REFERENCES,
160
+ isInverse: false,
161
+ includeSubtypes: true,
162
+ targetName: { namespaceIndex: entry.namespaceIndex, name: entry.aliasName }
163
+ }
164
+ ]
165
+ }
166
+ })
167
+ )
168
+ );
169
+ results.forEach((browsePathResult, index) => {
170
+ if (browsePathResult.statusCode.isGood() && browsePathResult.targets?.length) {
171
+ // the targetId of a local Node is an ExpandedNodeId with
172
+ // serverIndex 0, usable as a NodeId as-is
173
+ aliasNodeIds[offset + index] = browsePathResult.targets[0].targetId;
174
+ }
175
+ });
176
+ }
177
+ return aliasNodeIds;
178
+ }
179
+
180
+ /**
181
+ * Browse the forward `AliasFor` References (subtypes included) of many alias
182
+ * Nodes in as few requests as possible, following every continuation point.
183
+ *
184
+ * Positions align with the input; `null` marks a Node that could not be
185
+ * resolved upstream or whose Browse did not complete — for the fidelity use
186
+ * case, an absent answer beats a truncated one presented as complete.
187
+ */
188
+ async function browseAliasTargets(
189
+ session: IBasicSessionAsync2,
190
+ aliasNodeIds: (NodeId | null)[],
191
+ maxNodesPerCall: number
192
+ ): Promise<(AliasReferenceTypeEntry[] | null)[]> {
193
+ const targets: (AliasReferenceTypeEntry[] | null)[] = new Array(aliasNodeIds.length).fill(null);
194
+
195
+ const toBrowse: { index: number; nodeId: NodeId }[] = [];
196
+ aliasNodeIds.forEach((nodeId, index) => {
197
+ if (nodeId) {
198
+ toBrowse.push({ index, nodeId });
199
+ }
200
+ });
201
+
202
+ const toEntry = (reference: ReferenceDescription): AliasReferenceTypeEntry => ({
203
+ targetNodeId: reference.nodeId,
204
+ referenceTypeId: reference.referenceTypeId
205
+ });
206
+
207
+ for (let offset = 0; offset < toBrowse.length; offset += maxNodesPerCall) {
208
+ const chunk = toBrowse.slice(offset, offset + maxNodesPerCall);
209
+ const browseResults = await session.browse(
210
+ chunk.map(({ nodeId }) => ({
211
+ nodeId,
212
+ browseDirection: BrowseDirection.Forward,
213
+ referenceTypeId: ALIAS_FOR,
214
+ includeSubtypes: true,
215
+ nodeClassMask: 0,
216
+ resultMask: REFERENCE_TYPE_RESULT_MASK
217
+ }))
218
+ );
219
+
220
+ // continuations, batched too: one BrowseNext round trip serves every
221
+ // Node of the chunk that was truncated, however many there are
222
+ let pending: { index: number; continuationPoint: Buffer }[] = [];
223
+ browseResults.forEach((browseResult, chunkIndex) => {
224
+ const { index } = chunk[chunkIndex];
225
+ if (!browseResult.statusCode.isGood()) {
226
+ return;
227
+ }
228
+ targets[index] = (browseResult.references ?? []).map(toEntry);
229
+ if (browseResult.continuationPoint?.length) {
230
+ pending.push({ index, continuationPoint: browseResult.continuationPoint });
231
+ }
232
+ });
233
+
234
+ while (pending.length > 0) {
235
+ const nextResults = await session.browseNext(
236
+ pending.map(({ continuationPoint }) => continuationPoint),
237
+ false
238
+ );
239
+ const stillPending: typeof pending = [];
240
+ nextResults.forEach((browseResult, pendingIndex) => {
241
+ const { index } = pending[pendingIndex];
242
+ if (!browseResult.statusCode.isGood()) {
243
+ // incomplete: withdraw the partial answer rather than let it
244
+ // pass for the whole truth
245
+ targets[index] = null;
246
+ return;
247
+ }
248
+ targets[index]?.push(...(browseResult.references ?? []).map(toEntry));
249
+ if (browseResult.continuationPoint?.length) {
250
+ stillPending.push({ index, continuationPoint: browseResult.continuationPoint });
251
+ }
252
+ });
253
+ pending = stillPending;
254
+ }
255
+ }
256
+ return targets;
257
+ }