node-opcua-address-space 2.186.15 → 2.187.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.
Files changed (80) hide show
  1. package/api/address_space_public.ts +16 -0
  2. package/api/helpers/resolve_opaque_on_address_space.ts +13 -1
  3. package/api/index.ts +14 -3
  4. package/api/loader/store_record_applier.ts +267 -0
  5. package/api/session_context.ts +116 -27
  6. package/dist/api/address_space_public.d.ts +14 -0
  7. package/dist/api/address_space_public.d.ts.map +1 -1
  8. package/dist/api/address_space_public.js +3 -0
  9. package/dist/api/address_space_public.js.map +1 -1
  10. package/dist/api/helpers/resolve_opaque_on_address_space.d.ts +6 -0
  11. package/dist/api/helpers/resolve_opaque_on_address_space.d.ts.map +1 -1
  12. package/dist/api/helpers/resolve_opaque_on_address_space.js +12 -1
  13. package/dist/api/helpers/resolve_opaque_on_address_space.js.map +1 -1
  14. package/dist/api/index.d.ts +4 -3
  15. package/dist/api/index.d.ts.map +1 -1
  16. package/dist/api/index.js +4 -3
  17. package/dist/api/index.js.map +1 -1
  18. package/dist/api/loader/store_record_applier.d.ts +49 -0
  19. package/dist/api/loader/store_record_applier.d.ts.map +1 -0
  20. package/dist/api/loader/store_record_applier.js +235 -0
  21. package/dist/api/loader/store_record_applier.js.map +1 -0
  22. package/dist/api/session_context.d.ts +10 -0
  23. package/dist/api/session_context.d.ts.map +1 -1
  24. package/dist/api/session_context.js +100 -27
  25. package/dist/api/session_context.js.map +1 -1
  26. package/dist/impl/address_space.d.ts +2 -0
  27. package/dist/impl/address_space.d.ts.map +1 -1
  28. package/dist/impl/address_space.js +12 -2
  29. package/dist/impl/address_space.js.map +1 -1
  30. package/dist/impl/base_node_impl.d.ts +12 -0
  31. package/dist/impl/base_node_impl.d.ts.map +1 -1
  32. package/dist/impl/base_node_impl.js +35 -29
  33. package/dist/impl/base_node_impl.js.map +1 -1
  34. package/dist/impl/base_node_private.d.ts +5 -5
  35. package/dist/impl/base_node_private.d.ts.map +1 -1
  36. package/dist/impl/base_node_private.js +11 -5
  37. package/dist/impl/base_node_private.js.map +1 -1
  38. package/dist/impl/extension_object_array_node.d.ts +4 -2
  39. package/dist/impl/extension_object_array_node.d.ts.map +1 -1
  40. package/dist/impl/extension_object_array_node.js +5 -2
  41. package/dist/impl/extension_object_array_node.js.map +1 -1
  42. package/dist/impl/historical_access/address_space_historical_data_node.d.ts +26 -2
  43. package/dist/impl/historical_access/address_space_historical_data_node.d.ts.map +1 -1
  44. package/dist/impl/historical_access/address_space_historical_data_node.js +39 -0
  45. package/dist/impl/historical_access/address_space_historical_data_node.js.map +1 -1
  46. package/dist/impl/namespace_impl.d.ts.map +1 -1
  47. package/dist/impl/namespace_impl.js +3 -1
  48. package/dist/impl/namespace_impl.js.map +1 -1
  49. package/dist/impl/nodeset_tools/construct_namespace_dependency.d.ts.map +1 -1
  50. package/dist/impl/nodeset_tools/construct_namespace_dependency.js +28 -0
  51. package/dist/impl/nodeset_tools/construct_namespace_dependency.js.map +1 -1
  52. package/dist/impl/reference_impl.d.ts +2 -1
  53. package/dist/impl/reference_impl.d.ts.map +1 -1
  54. package/dist/impl/reference_impl.js +6 -5
  55. package/dist/impl/reference_impl.js.map +1 -1
  56. package/dist/impl/reference_index.d.ts +22 -0
  57. package/dist/impl/reference_index.d.ts.map +1 -0
  58. package/dist/impl/reference_index.js +104 -0
  59. package/dist/impl/reference_index.js.map +1 -0
  60. package/dist/impl/ua_variable_impl.d.ts +8 -0
  61. package/dist/impl/ua_variable_impl.d.ts.map +1 -1
  62. package/dist/impl/ua_variable_impl.js +121 -84
  63. package/dist/impl/ua_variable_impl.js.map +1 -1
  64. package/dist/tsconfig_base.tsbuildinfo +1 -1
  65. package/distNodeJS/generate_address_space.d.ts +6 -1
  66. package/distNodeJS/generate_address_space.d.ts.map +1 -1
  67. package/distNodeJS/generate_address_space.js +33 -1
  68. package/distNodeJS/generate_address_space.js.map +1 -1
  69. package/impl/address_space.ts +16 -2
  70. package/impl/base_node_impl.ts +52 -30
  71. package/impl/base_node_private.ts +16 -10
  72. package/impl/extension_object_array_node.ts +12 -3
  73. package/impl/historical_access/address_space_historical_data_node.ts +64 -1
  74. package/impl/namespace_impl.ts +3 -1
  75. package/impl/nodeset_tools/construct_namespace_dependency.ts +23 -1
  76. package/impl/reference_impl.ts +6 -5
  77. package/impl/reference_index.ts +116 -0
  78. package/impl/ua_variable_impl.ts +161 -94
  79. package/package.json +34 -33
  80. package/source_nodejs/generate_address_space.ts +46 -1
@@ -2,11 +2,19 @@
2
2
  * @module node-opcua-address-space
3
3
  */
4
4
  import type { IAddressSpace } from "node-opcua-address-space-base";
5
+ import { type StoreAddressSpace, type StoreAddressSpaceOptions, StoreServices } from "node-opcua-address-space-store";
5
6
  import { AddressSpaceImpl } from "../impl/address_space.js";
6
7
  import type { IHistorizerFactory } from "./address_space_ts.js";
7
8
  import type { Namespace } from "./namespace.js";
8
9
  import type { UARootFolder } from "./ua_root_folder.js";
9
10
 
11
+ /** the published names of the compact address space and its options */
12
+ export type CompactAddressSpace = StoreAddressSpace;
13
+ export type CompactAddressSpaceOptions = StoreAddressSpaceOptions;
14
+ /** the Read, Write, Browse and TranslateBrowsePaths services on a compact address space */
15
+ export const CompactAddressSpaceServices = StoreServices;
16
+ export type CompactAddressSpaceServices = StoreServices;
17
+
10
18
  /**
11
19
  * The address space: the set of nodes a server exposes, and the namespaces they live in.
12
20
  *
@@ -40,6 +48,14 @@ export interface AddressSpaceConstructor {
40
48
  /** build an empty address space */
41
49
  create(): AddressSpace;
42
50
 
51
+ /**
52
+ * build an empty compact address space: the nodes in typed columns, node objects built
53
+ * on demand. Cheaper by far for a large model, and not yet an {@link AddressSpace}: it
54
+ * answers finds, browses, attribute and value reads, bindings and writes, and a server
55
+ * cannot be built on it yet.
56
+ */
57
+ createCompact(options?: CompactAddressSpaceOptions): CompactAddressSpace;
58
+
43
59
  /**
44
60
  * How historized variables get their historian. Assigning here replaces the default that
45
61
  * the historical-access code installs on first use.
@@ -1,9 +1,18 @@
1
1
  import type { IAddressSpace } from "node-opcua-address-space-base";
2
2
  import { resolveDynamicExtensionObject } from "node-opcua-client-dynamic-extension-object";
3
- import { Variant } from "node-opcua-variant";
3
+ import { DataType, Variant } from "node-opcua-variant";
4
4
  import { ensureDatatypeExtracted } from "../loader/ensure_datatype_extracted.js";
5
5
  import { PseudoSession } from "../pseudo_session.js";
6
6
 
7
+ /**
8
+ * true when the variant may hold an opaque structure to resolve: only an ExtensionObject, or a
9
+ * Variant nested in one, can. Any other value is left as it is by resolveDynamicExtensionObject,
10
+ * so a Write of a number or a string needs neither a PseudoSession nor the DataType extraction.
11
+ */
12
+ export function mayHoldOpaqueStructure(variant: Variant | null | undefined): boolean {
13
+ return !!variant && (variant.dataType === DataType.ExtensionObject || variant.dataType === DataType.Variant);
14
+ }
15
+
7
16
  export async function resolveOpaqueOnAddressSpace(
8
17
  addressSpace: IAddressSpace,
9
18
  variants: (Variant | null) | (Variant | null)[]
@@ -11,6 +20,9 @@ export async function resolveOpaqueOnAddressSpace(
11
20
  if (!variants) {
12
21
  return;
13
22
  }
23
+ if (variants instanceof Variant ? !mayHoldOpaqueStructure(variants) : !variants.some(mayHoldOpaqueStructure)) {
24
+ return;
25
+ }
14
26
  const session = new PseudoSession(addressSpace);
15
27
  const extraDataTypeManager = await ensureDatatypeExtracted(addressSpace);
16
28
  if (variants instanceof Variant) {
package/api/index.ts CHANGED
@@ -91,7 +91,12 @@ export * from "../impl/event_data.js";
91
91
  /** Building and maintaining a variable whose value is an array of extension objects. */
92
92
  export { addElement, bindExtObjArrayNode, createExtObjArrayNode, removeElement } from "../impl/extension_object_array_node.js";
93
93
  /** The default historian, installed through {@link AddressSpace.historizerFactory}. */
94
- export { VariableHistorian } from "../impl/historical_access/address_space_historical_data_node.js";
94
+ export {
95
+ createDetachedHistorian,
96
+ historyReadThrough,
97
+ type IHistoryReadTarget,
98
+ VariableHistorian
99
+ } from "../impl/historical_access/address_space_historical_data_node.js";
95
100
  export {
96
101
  installReadProcessedDetails,
97
102
  type ReadProcessedDetailsFunc
@@ -135,7 +140,7 @@ export * from "./helpers/call_helpers.js";
135
140
  export * from "./helpers/dump_tools.js";
136
141
  export * from "./helpers/ensure_secure_access.js";
137
142
  export * from "./helpers/make_optionals_map.js";
138
- export { resolveOpaqueOnAddressSpace } from "./helpers/resolve_opaque_on_address_space.js";
143
+ export { mayHoldOpaqueStructure, resolveOpaqueOnAddressSpace } from "./helpers/resolve_opaque_on_address_space.js";
139
144
  export * from "./interfaces/alarms_and_conditions/condition_info_i.js";
140
145
  export * from "./interfaces/alarms_and_conditions/condition_snapshot.js";
141
146
  export * from "./interfaces/alarms_and_conditions/instantiate_alarm_condition_options.js";
@@ -236,12 +241,18 @@ export type {
236
241
  NodesetSource,
237
242
  NodesetSourceKind
238
243
  } from "./loader/nodeset_source.js";
239
- export { sha256Hex } from "./loader/nodeset_source.js";
244
+ export { openNodesetSource, sha256Hex } from "./loader/nodeset_source.js";
240
245
  export * from "./loader/nodeset_source_helpers.js";
241
246
  export { type NodesetToImageOptions, nodesetToImage } from "./loader/nodeset_to_image.js";
242
247
  export { headerComplete, parseDependencies, sliceHeader } from "./loader/nodeset_xml_header.js";
243
248
  export { makeXmlNodesetRecordReader, type XmlNodesetRecordReader, xmlNodesetRecords } from "./loader/nodeset_xml_producer.js";
244
249
  export * from "./loader/register_node_promoter.js";
250
+ export {
251
+ type CompactRecordConsumer,
252
+ compactRecordConsumer,
253
+ StoreRecordApplier,
254
+ type StoreRecordApplierOptions
255
+ } from "./loader/store_record_applier.js";
245
256
  export * from "./namespace.js";
246
257
  export type { INamespaceAlarmAndCondition } from "./namespace_alarm_and_condition.js";
247
258
  export * from "./namespace_data_access.js";
@@ -0,0 +1,267 @@
1
+ /**
2
+ * @module node-opcua-address-space
3
+ *
4
+ * The nodeset records written straight into a compact store: no node object is created.
5
+ * Same producer as the object applier (NodesetRecordApplier), other sink.
6
+ */
7
+ import { type CompactStore, NO_NODE, type RolePermissionEntry, type StoreAddressSpace } from "node-opcua-address-space-store";
8
+ import { NodeClass } from "node-opcua-data-model";
9
+ import { NodeId } from "node-opcua-nodeid";
10
+ import { DataType, VariantArrayType, type VariantOptions } from "node-opcua-variant";
11
+ import type { NodeSetPermissionsPolicy } from "../interfaces/nodeset_loader_options.js";
12
+ import {
13
+ type NodesetHeaderRecord,
14
+ type NodesetNodeRecord,
15
+ type NodesetRecord,
16
+ type NodesetRecordConsumer,
17
+ XmlExtensionObjectFragment
18
+ } from "./nodeset_record.js";
19
+
20
+ const UA_NAMESPACE_URI = "http://opcfoundation.org/UA/";
21
+
22
+ /** who hands out namespace indexes: the address space, or a plain table in a test */
23
+ export interface NamespaceRegistry {
24
+ /** the index of a namespace, registering it when it is new */
25
+ indexOf(namespaceUri: string): number;
26
+ }
27
+
28
+ /** the same two policies as the object loader, with the same defaults */
29
+ export interface StoreRecordApplierOptions {
30
+ /** the nodeset's RolePermissions: kept (the default) or dropped */
31
+ permissions?: NodeSetPermissionsPolicy;
32
+ /** the nodeset's AccessRestrictions: kept, or dropped (the default) */
33
+ accessRestrictions?: NodeSetPermissionsPolicy;
34
+ }
35
+
36
+ export class StoreRecordApplier implements NodesetRecordConsumer {
37
+ // per document: the file's namespace table to the store's
38
+ #table: number[] = [];
39
+ #headerSeen = false;
40
+ #nodeCount = 0;
41
+ #pendingValues = 0;
42
+ #deferredValues = 0;
43
+ // a type definition or data type declared before its node exists: set at finish()
44
+ #deferredTypeDefinitions: [number, NodeId][] = [];
45
+ #deferredDataTypes: [number, NodeId][] = [];
46
+ #deferredParents: [number, NodeId][] = [];
47
+ readonly #store: CompactStore;
48
+ readonly #namespaces: NamespaceRegistry;
49
+ readonly #loadTime = Date.now();
50
+ readonly #keepPermissions: boolean;
51
+ readonly #keepAccessRestrictions: boolean;
52
+
53
+ constructor(store: CompactStore, namespaces: NamespaceRegistry, options: StoreRecordApplierOptions = {}) {
54
+ this.#store = store;
55
+ this.#namespaces = namespaces;
56
+ this.#keepPermissions = (options.permissions ?? "apply") === "apply";
57
+ this.#keepAccessRestrictions = (options.accessRestrictions ?? "ignore") === "apply";
58
+ }
59
+
60
+ /** nodes applied by this applier */
61
+ public get nodeCount(): number {
62
+ return this.#nodeCount;
63
+ }
64
+
65
+ public apply(record: NodesetRecord): void {
66
+ if (record.kind === "header") {
67
+ this.#applyHeader(record);
68
+ return;
69
+ }
70
+ if (!this.#headerSeen) {
71
+ throw new Error("StoreRecordApplier: a node record came before the header record");
72
+ }
73
+ this.#applyNode(record);
74
+ }
75
+
76
+ /** at the end of a document, or of a set of documents: resolve the references that waited */
77
+ public finish(): { unresolved: number } {
78
+ const store = this.#store;
79
+ const unresolved = store.resolvePending();
80
+ for (const [i, nodeId] of this.#deferredTypeDefinitions) {
81
+ const t = store.find(nodeId);
82
+ if (t !== NO_NODE) store.nodes.setTypeDefinition(i, t);
83
+ }
84
+ for (const [i, nodeId] of this.#deferredDataTypes) {
85
+ const t = store.find(nodeId);
86
+ if (t !== NO_NODE) store.nodes.setDataType(i, t);
87
+ }
88
+ for (const [i, nodeId] of this.#deferredParents) {
89
+ const t = store.find(nodeId);
90
+ if (t !== NO_NODE) store.nodes.setParent(i, t);
91
+ }
92
+ this.#deferredTypeDefinitions = [];
93
+ this.#deferredDataTypes = [];
94
+ this.#deferredParents = [];
95
+ store.finish();
96
+ return { unresolved: unresolved.length };
97
+ }
98
+
99
+ #applyHeader(header: NodesetHeaderRecord): void {
100
+ this.#headerSeen = true;
101
+ this.#table = [];
102
+ for (const model of header.models) {
103
+ this.#namespaces.indexOf(model.modelUri);
104
+ }
105
+ if (header.models.length === 0) {
106
+ for (const uri of header.namespaceUris) {
107
+ this.#namespaces.indexOf(uri);
108
+ }
109
+ }
110
+ // the file's table: the UA namespace, then the declared URIs in order; a repeat takes no slot
111
+ const seen = new Set<string>();
112
+ for (const uri of [UA_NAMESPACE_URI, ...header.namespaceUris]) {
113
+ if (seen.has(uri)) continue;
114
+ seen.add(uri);
115
+ this.#table.push(this.#namespaces.indexOf(uri));
116
+ }
117
+ }
118
+
119
+ #translateNamespace(fileIndex: number): number {
120
+ const index = this.#table[fileIndex];
121
+ if (index === undefined) {
122
+ throw new Error(`StoreRecordApplier: no namespace for file index ${fileIndex}`);
123
+ }
124
+ return index;
125
+ }
126
+
127
+ #translate(fileLocal: NodeId): NodeId {
128
+ const namespace = this.#translateNamespace(fileLocal.namespace);
129
+ return namespace === fileLocal.namespace ? fileLocal : new NodeId(fileLocal.identifierType, fileLocal.value, namespace);
130
+ }
131
+
132
+ #applyNode(record: NodesetNodeRecord): void {
133
+ const store = this.#store;
134
+ const nodeId = this.#translate(record.nodeId);
135
+ const i = store.addNode({
136
+ nodeId,
137
+ nodeClass: record.nodeClass,
138
+ browseName: record.browseName.name ?? "",
139
+ browseNameNamespace: this.#translateNamespace(record.browseName.namespaceIndex),
140
+ displayName: record.displayName && record.displayName !== record.browseName.name ? record.displayName : null,
141
+ description: record.description ?? null,
142
+ valueRank: record.valueRank,
143
+ // the document gives the levels as numbers; CurrentRead when it says nothing, and the
144
+ // user level follows the node's unless declared (same reading as the object applier)
145
+ accessLevel: parseAccessLevel(record.accessLevel, 1),
146
+ userAccessLevel: record.userAccessLevel ? parseAccessLevel(record.userAccessLevel, 1) : undefined,
147
+ minimumSamplingInterval: record.minimumSamplingInterval,
148
+ historizing: record.historizing,
149
+ eventNotifier: record.eventNotifier,
150
+ isAbstract: record.isAbstract,
151
+ symmetric: record.symmetric,
152
+ inverseName: record.inverseName ?? null,
153
+ containsNoLoops: record.containsNoLoops,
154
+ accessRestrictions: this.#keepAccessRestrictions ? parseAccessRestrictions(record.accessRestrictions) : undefined,
155
+ rolePermissions: this.#keepPermissions ? this.#rolePermissions(record) : undefined
156
+ });
157
+ this.#nodeCount++;
158
+ for (const reference of record.references) {
159
+ const referenceType = this.#translate(reference.referenceType);
160
+ const target = this.#translate(reference.nodeId);
161
+ store.addReference(i, referenceType, reference.isForward, target);
162
+ // the type definition and the data type are also columns, kept resolved by index
163
+ if (reference.isForward && referenceType.namespace === 0 && referenceType.value === 40 /* HasTypeDefinition */) {
164
+ const t = store.find(target);
165
+ if (t !== NO_NODE) store.nodes.setTypeDefinition(i, t);
166
+ else this.#deferredTypeDefinitions.push([i, target]);
167
+ }
168
+ }
169
+ if (record.dataType) {
170
+ const dataTypeId = this.#translate(record.dataType);
171
+ const dataType = store.find(dataTypeId);
172
+ if (dataType !== NO_NODE) store.nodes.setDataType(i, dataType);
173
+ else this.#deferredDataTypes.push([i, dataTypeId]);
174
+ }
175
+ if (record.parentNodeId) {
176
+ const parentId = this.#translate(record.parentNodeId);
177
+ const parent = store.find(parentId);
178
+ if (parent !== NO_NODE) store.nodes.setParent(i, parent);
179
+ else this.#deferredParents.push([i, parentId]);
180
+ }
181
+ if (record.value && (record.nodeClass === NodeClass.Variable || record.nodeClass === NodeClass.VariableType)) {
182
+ this.#applyValue(i, record.value);
183
+ }
184
+ }
185
+
186
+ #rolePermissions(record: NodesetNodeRecord): RolePermissionEntry[] | undefined {
187
+ if (record.rolePermissions) {
188
+ return record.rolePermissions.map((r) => ({ roleId: this.#translate(r.roleId), permissions: r.permissions }));
189
+ }
190
+ // HasNoPermissions grants nothing; an absent RolePermissions inherits the namespace default
191
+ return record.hasNoPermissions ? [] : undefined;
192
+ }
193
+
194
+ #applyValue(i: number, value: VariantOptions): void {
195
+ if (holdsXmlFragment(value.value)) {
196
+ // an extension object the loader left as its XML: the node objects decode it once every
197
+ // document is in; the store has no decoder yet, so the Variable waits for a value rather
198
+ // than serve something a client cannot decode
199
+ this.#deferredValues++;
200
+ return;
201
+ }
202
+ const now = this.#loadTime;
203
+ const dataType =
204
+ (typeof value.dataType === "string" ? DataType[value.dataType as keyof typeof DataType] : value.dataType) ??
205
+ DataType.Null;
206
+ const isScalar = value.arrayType === undefined || value.arrayType === VariantArrayType.Scalar;
207
+ const v = value.value;
208
+ if (isScalar && (typeof v === "number" || typeof v === "boolean")) {
209
+ this.#store.values.setScalar(i, dataType, v, 0, now, now);
210
+ } else {
211
+ this.#store.values.setObject(i, dataType, value, 0, now, now);
212
+ this.#pendingValues++;
213
+ }
214
+ }
215
+
216
+ /** values kept as objects (strings, arrays, structures) */
217
+ public get objectValueCount(): number {
218
+ return this.#pendingValues;
219
+ }
220
+
221
+ /** extension object values the document gave as XML, left unset (BadWaitingForInitialData) */
222
+ public get deferredValueCount(): number {
223
+ return this.#deferredValues;
224
+ }
225
+ }
226
+
227
+ /** true when a value is, or holds, an extension object still in its XML form */
228
+ function holdsXmlFragment(value: unknown): boolean {
229
+ if (value instanceof XmlExtensionObjectFragment) return true;
230
+ return Array.isArray(value) && value.some((element) => element instanceof XmlExtensionObjectFragment);
231
+ }
232
+
233
+ function parseAccessLevel(text: string | undefined, absent: number): number {
234
+ const value = parseInt(text || "", 10);
235
+ return Number.isNaN(value) ? absent : value;
236
+ }
237
+
238
+ /** the AccessRestrictions attribute of the document: a number; absent or invalid inherits */
239
+ function parseAccessRestrictions(text: string | undefined): number | undefined {
240
+ if (text === undefined || text === "") {
241
+ return undefined;
242
+ }
243
+ const value = parseInt(text, 10);
244
+ return Number.isNaN(value) ? undefined : value & 0xf;
245
+ }
246
+
247
+ /** what a compact address space is loaded through: the records go in, finish() once they are all in */
248
+ export interface CompactRecordConsumer extends NodesetRecordConsumer {
249
+ finish(): { unresolved: number };
250
+ }
251
+
252
+ /**
253
+ * the consumer a nodeset producer feeds to load a compact address space: the records written
254
+ * into its store, the namespaces registered on it, and what the space derives from its columns
255
+ * derived again at the end
256
+ */
257
+ export function compactRecordConsumer(space: StoreAddressSpace, options: StoreRecordApplierOptions = {}): CompactRecordConsumer {
258
+ const applier = new StoreRecordApplier(space.store, { indexOf: (uri) => space.namespaceIndexOf(uri) }, options);
259
+ return {
260
+ apply: (record) => applier.apply(record),
261
+ finish: () => {
262
+ const result = applier.finish();
263
+ space.finishLoad();
264
+ return result;
265
+ }
266
+ };
267
+ }
@@ -219,27 +219,39 @@ function getPermissionForRole(
219
219
  function isDefaultContext(context: SessionContext) {
220
220
  return context === SessionContext.defaultContext;
221
221
  }
222
+ const serverNamespacesNodeId = resolveNodeId(ObjectIds.Server_Namespaces);
223
+ /**
224
+ * the Namespaces Object of the Server (RootFolder/Objects/Server/Namespaces), which holds the
225
+ * NamespaceMetadata of every namespace.
226
+ *
227
+ * It is consulted for each permission and each access-restriction check, that is several times
228
+ * per node read, so it is found by its NodeId: `rootFolder.objects.server.namespaces` resolves
229
+ * three children by name every time it is evaluated.
230
+ */
231
+ function getServerNamespaces(namespace: NamespacePrivate): BaseNode | null {
232
+ return namespace.addressSpace.findNode(serverNamespacesNodeId);
233
+ }
234
+
222
235
  function getAccessRestrictionsOnNamespace(namespace: NamespacePrivate, context: SessionContext): AccessRestrictionsFlag {
223
236
  // ignore permission when default context is provided (to avoid recursion)
224
237
  if (isDefaultContext(context)) {
225
238
  return AccessRestrictionsFlag.None;
226
239
  }
227
- const namespaces = namespace.addressSpace.rootFolder?.objects?.server?.namespaces;
228
- if (!namespaces) {
229
- return AccessRestrictionsFlag.None;
230
- }
231
- const namespaceObject = namespaces.getChildByName(namespace.namespaceUri);
232
- if (!namespaceObject) {
233
- return AccessRestrictionsFlag.None;
234
- }
235
- const defaultAccessRestriction = namespaceObject.getChildByName("defaultAccessRestriction");
236
- if (defaultAccessRestriction) {
237
- const dataValue = defaultAccessRestriction.readAttribute(null, AttributeIds.Value);
238
- if (dataValue?.statusCode.isGood()) {
239
- return dataValue.value.value as AccessRestrictionsFlag;
240
- }
241
- }
242
- return AccessRestrictionsFlag.None;
240
+ const namespaces = getServerNamespaces(namespace);
241
+ const namespaceObject = namespaces?.getChildByName(namespace.namespaceUri);
242
+ const defaultAccessRestrictions = namespaceObject?.getChildByName("DefaultAccessRestrictions");
243
+ if (defaultAccessRestrictions) {
244
+ const dataValue = defaultAccessRestrictions.readAttribute(null, AttributeIds.Value);
245
+ const value = dataValue?.value?.value as AccessRestrictionsFlag | null | undefined;
246
+ // the NamespaceMetadata nodes of the standard nodesets declare the property without a
247
+ // value: a null falls through to the namespace's own setting below
248
+ if (dataValue?.statusCode.isGood() && value !== null && value !== undefined) {
249
+ return value;
250
+ }
251
+ }
252
+ // same fallback as getDefaultUserRolePermissionsOnNamespace: the policy set with
253
+ // Namespace.setDefaultAccessRestrictions() applies even without setNamespaceMetaData()
254
+ return namespace.getDefaultAccessRestrictions();
243
255
  }
244
256
 
245
257
  function getDefaultUserRolePermissionsOnNamespace(
@@ -251,7 +263,7 @@ function getDefaultUserRolePermissionsOnNamespace(
251
263
  return null;
252
264
  }
253
265
 
254
- const namespaces = namespace.addressSpace.rootFolder?.objects?.server?.namespaces;
266
+ const namespaces = getServerNamespaces(namespace);
255
267
  const uaNamespaceObject = namespaces?.getChildByName(namespace.namespaceUri);
256
268
  if (uaNamespaceObject) {
257
269
  // DefaultUserRolePermissions is the user-specific policy and wins when it carries a value.
@@ -437,6 +449,17 @@ export function makeRoles(roleIds: RoleIdLike[] | string | WellKnownRoles, addre
437
449
  return roleIds.map((role) => resolveRole(role, addressSpace));
438
450
  }
439
451
 
452
+ /** what {@link SessionContext.withPermissionCache} remembers while its action runs */
453
+ interface PermissionCache {
454
+ roles?: NodeId[];
455
+ evaluationRoles?: NodeId[];
456
+ /** keyed by the RolePermissions that apply, which most nodes share with their namespace */
457
+ permissions: Map<RolePermissionType[] | null, PermissionFlag>;
458
+ /** keyed by namespace index: a Read stays within one address space */
459
+ defaultRolePermissions: Map<number, RolePermissionType[] | null>;
460
+ defaultAccessRestrictions: Map<number, AccessRestrictionsFlag>;
461
+ }
462
+
440
463
  export class SessionContext implements ISessionContext {
441
464
  public static defaultContext = new SessionContext({});
442
465
 
@@ -447,6 +470,8 @@ export class SessionContext implements ISessionContext {
447
470
  public readonly server?: IServerBase;
448
471
  public readonly requestHeader?: RequestHeader;
449
472
 
473
+ #permissionCache: PermissionCache | null = null;
474
+
450
475
  constructor(options?: SessionContextOptions) {
451
476
  options = options || {};
452
477
  this.session = options.session;
@@ -480,6 +505,26 @@ export class SessionContext implements ISessionContext {
480
505
  return this.requestHeader?.auditEntryId || undefined;
481
506
  }
482
507
 
508
+ /**
509
+ * see {@link ISessionContext.withPermissionCache}.
510
+ *
511
+ * A Session's sessionContext is shared by every request in flight on it, so the cache only
512
+ * lives while `action` runs, and `action` must not wait on anything: a request served in
513
+ * one synchronous run cannot be interleaved with another one. Nested calls share the
514
+ * outermost cache.
515
+ */
516
+ public withPermissionCache<T>(action: () => T): T {
517
+ if (this.#permissionCache) {
518
+ return action();
519
+ }
520
+ this.#permissionCache = { permissions: new Map(), defaultRolePermissions: new Map(), defaultAccessRestrictions: new Map() };
521
+ try {
522
+ return action();
523
+ } finally {
524
+ this.#permissionCache = null;
525
+ }
526
+ }
527
+
483
528
  /**
484
529
  * The client's application-instance certificate,
485
530
  * or `null` if no secure channel is available.
@@ -558,6 +603,15 @@ export class SessionContext implements ISessionContext {
558
603
  *
559
604
  */
560
605
  public getCurrentUserRoles(): NodeId[] {
606
+ const cache = this.#permissionCache;
607
+ if (!cache) {
608
+ return this.#resolveCurrentUserRoles();
609
+ }
610
+ cache.roles ??= this.#resolveCurrentUserRoles();
611
+ return cache.roles;
612
+ }
613
+
614
+ #resolveCurrentUserRoles(): NodeId[] {
561
615
  if (!this.session) {
562
616
  // no Session: an in-process caller. getPermissions grants it everything —
563
617
  // an empty list here does NOT mean "no rights", see unresolvedPermissions.
@@ -573,7 +627,8 @@ export class SessionContext implements ISessionContext {
573
627
  return [];
574
628
  }
575
629
 
576
- const anonymous = makeRoles([WellKnownRoles.Anonymous]);
630
+ // a fresh array each time, a caller may change it; the NodeId is resolved once, at load
631
+ const anonymous = (): NodeId[] => [anonymousRoleNodeId];
577
632
 
578
633
  const username = getUserName(userIdentityToken);
579
634
 
@@ -587,16 +642,16 @@ export class SessionContext implements ISessionContext {
587
642
  }
588
643
 
589
644
  if (username === "anonymous") {
590
- return anonymous;
645
+ return anonymous();
591
646
  }
592
647
  if (!this.server?.userManager) {
593
- return anonymous;
648
+ return anonymous();
594
649
  }
595
650
 
596
651
  assert(this.server != null, "expecting a server");
597
652
 
598
653
  if (typeof this.server.userManager.getUserRoles !== "function") {
599
- return anonymous;
654
+ return anonymous();
600
655
  }
601
656
 
602
657
  const rolesNodeId = this.server.userManager.getUserRoles(username) || [];
@@ -627,8 +682,16 @@ export class SessionContext implements ISessionContext {
627
682
 
628
683
  public getApplicableRolePermissions(node: BaseNode): RolePermissionType[] | null {
629
684
  if (!node.rolePermissions) {
630
- const namespace = node.namespace as NamespacePrivate;
631
- const defaultUserRolePermissions = getDefaultUserRolePermissionsOnNamespace(namespace, this);
685
+ const cache = this.#permissionCache;
686
+ if (!cache) {
687
+ return getDefaultUserRolePermissionsOnNamespace(node.namespace as NamespacePrivate, this);
688
+ }
689
+ const namespaceIndex = node.nodeId.namespace;
690
+ let defaultUserRolePermissions = cache.defaultRolePermissions.get(namespaceIndex);
691
+ if (defaultUserRolePermissions === undefined) {
692
+ defaultUserRolePermissions = getDefaultUserRolePermissionsOnNamespace(node.namespace as NamespacePrivate, this);
693
+ cache.defaultRolePermissions.set(namespaceIndex, defaultUserRolePermissions);
694
+ }
632
695
  return defaultUserRolePermissions;
633
696
  }
634
697
  return node.rolePermissions;
@@ -684,7 +747,11 @@ export class SessionContext implements ISessionContext {
684
747
  public getPermissions(node: BaseNode): PermissionFlag {
685
748
  const applicableRolePermissions = this.getApplicableRolePermissions(node);
686
749
 
687
- const roles = this.getCurrentUserRoles();
750
+ const cache = this.#permissionCache;
751
+ if (cache) {
752
+ cache.roles ??= this.getCurrentUserRoles();
753
+ }
754
+ const roles = cache?.roles ?? this.getCurrentUserRoles();
688
755
  if (roles.length === 0) {
689
756
  // Two very different situations land here:
690
757
  // - no Session at all: an in-process caller, always trusted (see isInProcessCaller)
@@ -694,17 +761,39 @@ export class SessionContext implements ISessionContext {
694
761
  // can set unresolvedPermissionPolicy: "deny" to fail closed instead.
695
762
  return this.unresolvedPermissions;
696
763
  }
764
+ if (!cache) {
765
+ return this.#evaluatePermissions(applicableRolePermissions, this.getRolesForPermissionEvaluation(roles));
766
+ }
767
+ let permissions = cache.permissions.get(applicableRolePermissions);
768
+ if (permissions === undefined) {
769
+ cache.evaluationRoles ??= this.getRolesForPermissionEvaluation(roles);
770
+ permissions = this.#evaluatePermissions(applicableRolePermissions, cache.evaluationRoles);
771
+ cache.permissions.set(applicableRolePermissions, permissions);
772
+ }
773
+ return permissions;
774
+ }
775
+
776
+ #evaluatePermissions(applicableRolePermissions: RolePermissionType[] | null, roles: NodeId[]): PermissionFlag {
697
777
  const unresolved = this.unresolvedPermissions;
698
778
  let orFlags: PermissionFlag = 0;
699
- for (const role of this.getRolesForPermissionEvaluation(roles)) {
779
+ for (const role of roles) {
700
780
  orFlags = orFlags | getPermissionForRole(applicableRolePermissions, role, unresolved);
701
781
  }
702
782
  return orFlags;
703
783
  }
784
+
704
785
  public getAccessRestrictions(node: BaseNode): AccessRestrictionsFlag {
705
786
  if (node.accessRestrictions === undefined) {
706
- const namespace = node.namespace as NamespacePrivate;
707
- const accessRestrictions = getAccessRestrictionsOnNamespace(namespace, this);
787
+ const cache = this.#permissionCache;
788
+ if (!cache) {
789
+ return getAccessRestrictionsOnNamespace(node.namespace as NamespacePrivate, this);
790
+ }
791
+ const namespaceIndex = node.nodeId.namespace;
792
+ let accessRestrictions = cache.defaultAccessRestrictions.get(namespaceIndex);
793
+ if (accessRestrictions === undefined) {
794
+ accessRestrictions = getAccessRestrictionsOnNamespace(node.namespace as NamespacePrivate, this);
795
+ cache.defaultAccessRestrictions.set(namespaceIndex, accessRestrictions);
796
+ }
708
797
  return accessRestrictions;
709
798
  }
710
799
  return node.accessRestrictions;
@@ -2,9 +2,16 @@
2
2
  * @module node-opcua-address-space
3
3
  */
4
4
  import type { IAddressSpace } from "node-opcua-address-space-base";
5
+ import { type StoreAddressSpace, type StoreAddressSpaceOptions, StoreServices } from "node-opcua-address-space-store";
5
6
  import type { IHistorizerFactory } from "./address_space_ts.js";
6
7
  import type { Namespace } from "./namespace.js";
7
8
  import type { UARootFolder } from "./ua_root_folder.js";
9
+ /** the published names of the compact address space and its options */
10
+ export type CompactAddressSpace = StoreAddressSpace;
11
+ export type CompactAddressSpaceOptions = StoreAddressSpaceOptions;
12
+ /** the Read, Write, Browse and TranslateBrowsePaths services on a compact address space */
13
+ export declare const CompactAddressSpaceServices: typeof StoreServices;
14
+ export type CompactAddressSpaceServices = StoreServices;
8
15
  /**
9
16
  * The address space: the set of nodes a server exposes, and the namespaces they live in.
10
17
  *
@@ -35,6 +42,13 @@ export interface AddressSpace extends IAddressSpace {
35
42
  export interface AddressSpaceConstructor {
36
43
  /** build an empty address space */
37
44
  create(): AddressSpace;
45
+ /**
46
+ * build an empty compact address space: the nodes in typed columns, node objects built
47
+ * on demand. Cheaper by far for a large model, and not yet an {@link AddressSpace}: it
48
+ * answers finds, browses, attribute and value reads, bindings and writes, and a server
49
+ * cannot be built on it yet.
50
+ */
51
+ createCompact(options?: CompactAddressSpaceOptions): CompactAddressSpace;
38
52
  /**
39
53
  * How historized variables get their historian. Assigning here replaces the default that
40
54
  * the historical-access code installs on first use.
@@ -1 +1 @@
1
- {"version":3,"file":"address_space_public.d.ts","sourceRoot":"","sources":["../../api/address_space_public.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAEnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;;GAKG;AACH,MAAM,WAAW,YAAa,SAAQ,aAAa;IAC/C,eAAe,IAAI,SAAS,CAAC;IAC7B,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACnD,UAAU,EAAE,YAAY,CAAC;IAEzB;;;;;;;OAOG;IACH,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAC3D;AAED;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACpC,mCAAmC;IACnC,MAAM,IAAI,YAAY,CAAC;IAEvB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,kBAAkB,CAAC;CAC1C;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,EAAE,uBAA0C,CAAC"}
1
+ {"version":3,"file":"address_space_public.d.ts","sourceRoot":"","sources":["../../api/address_space_public.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,wBAAwB,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAEtH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,uEAAuE;AACvE,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AACpD,MAAM,MAAM,0BAA0B,GAAG,wBAAwB,CAAC;AAClE,2FAA2F;AAC3F,eAAO,MAAM,2BAA2B,sBAAgB,CAAC;AACzD,MAAM,MAAM,2BAA2B,GAAG,aAAa,CAAC;AAExD;;;;;GAKG;AACH,MAAM,WAAW,YAAa,SAAQ,aAAa;IAC/C,eAAe,IAAI,SAAS,CAAC;IAC7B,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACnD,UAAU,EAAE,YAAY,CAAC;IAEzB;;;;;;;OAOG;IACH,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAC3D;AAED;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACpC,mCAAmC;IACnC,MAAM,IAAI,YAAY,CAAC;IAEvB;;;;;OAKG;IACH,aAAa,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,mBAAmB,CAAC;IAEzE;;;OAGG;IACH,iBAAiB,CAAC,EAAE,kBAAkB,CAAC;CAC1C;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,EAAE,uBAA0C,CAAC"}
@@ -1,4 +1,7 @@
1
+ import { StoreServices } from "node-opcua-address-space-store";
1
2
  import { AddressSpaceImpl } from "../impl/address_space.js";
3
+ /** the Read, Write, Browse and TranslateBrowsePaths services on a compact address space */
4
+ export const CompactAddressSpaceServices = StoreServices;
2
5
  /**
3
6
  * An interface and a variable may share a name, so `AddressSpace` is both the type you
4
7
  * annotate with and the value you call `create()` on, while the implementation class stays
@@ -1 +1 @@
1
- {"version":3,"file":"address_space_public.js","sourceRoot":"","sources":["../../api/address_space_public.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AA6C5D;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,YAAY,GAA4B,gBAAgB,CAAC"}
1
+ {"version":3,"file":"address_space_public.js","sourceRoot":"","sources":["../../api/address_space_public.ts"],"names":[],"mappings":"AAIA,OAAO,EAAyD,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACtH,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAQ5D,2FAA2F;AAC3F,MAAM,CAAC,MAAM,2BAA2B,GAAG,aAAa,CAAC;AAmDzD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,YAAY,GAA4B,gBAAgB,CAAC"}