node-opcua-address-space 2.186.17 → 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.
- package/api/address_space_public.ts +16 -0
- package/api/helpers/resolve_opaque_on_address_space.ts +13 -1
- package/api/index.ts +14 -3
- package/api/loader/store_record_applier.ts +267 -0
- package/api/session_context.ts +116 -27
- package/dist/api/address_space_public.d.ts +14 -0
- package/dist/api/address_space_public.d.ts.map +1 -1
- package/dist/api/address_space_public.js +3 -0
- package/dist/api/address_space_public.js.map +1 -1
- package/dist/api/helpers/resolve_opaque_on_address_space.d.ts +6 -0
- package/dist/api/helpers/resolve_opaque_on_address_space.d.ts.map +1 -1
- package/dist/api/helpers/resolve_opaque_on_address_space.js +12 -1
- package/dist/api/helpers/resolve_opaque_on_address_space.js.map +1 -1
- package/dist/api/index.d.ts +4 -3
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +4 -3
- package/dist/api/index.js.map +1 -1
- package/dist/api/loader/store_record_applier.d.ts +49 -0
- package/dist/api/loader/store_record_applier.d.ts.map +1 -0
- package/dist/api/loader/store_record_applier.js +235 -0
- package/dist/api/loader/store_record_applier.js.map +1 -0
- package/dist/api/session_context.d.ts +10 -0
- package/dist/api/session_context.d.ts.map +1 -1
- package/dist/api/session_context.js +100 -27
- package/dist/api/session_context.js.map +1 -1
- package/dist/impl/address_space.d.ts +2 -0
- package/dist/impl/address_space.d.ts.map +1 -1
- package/dist/impl/address_space.js +12 -2
- package/dist/impl/address_space.js.map +1 -1
- package/dist/impl/base_node_impl.d.ts +12 -0
- package/dist/impl/base_node_impl.d.ts.map +1 -1
- package/dist/impl/base_node_impl.js +35 -29
- package/dist/impl/base_node_impl.js.map +1 -1
- package/dist/impl/base_node_private.d.ts +5 -5
- package/dist/impl/base_node_private.d.ts.map +1 -1
- package/dist/impl/base_node_private.js +11 -5
- package/dist/impl/base_node_private.js.map +1 -1
- package/dist/impl/extension_object_array_node.d.ts +4 -2
- package/dist/impl/extension_object_array_node.d.ts.map +1 -1
- package/dist/impl/extension_object_array_node.js +5 -2
- package/dist/impl/extension_object_array_node.js.map +1 -1
- package/dist/impl/historical_access/address_space_historical_data_node.d.ts +26 -2
- package/dist/impl/historical_access/address_space_historical_data_node.d.ts.map +1 -1
- package/dist/impl/historical_access/address_space_historical_data_node.js +39 -0
- package/dist/impl/historical_access/address_space_historical_data_node.js.map +1 -1
- package/dist/impl/namespace_impl.d.ts.map +1 -1
- package/dist/impl/namespace_impl.js +3 -1
- package/dist/impl/namespace_impl.js.map +1 -1
- package/dist/impl/nodeset_tools/construct_namespace_dependency.d.ts.map +1 -1
- package/dist/impl/nodeset_tools/construct_namespace_dependency.js +28 -0
- package/dist/impl/nodeset_tools/construct_namespace_dependency.js.map +1 -1
- package/dist/impl/reference_impl.d.ts +2 -1
- package/dist/impl/reference_impl.d.ts.map +1 -1
- package/dist/impl/reference_impl.js +6 -5
- package/dist/impl/reference_impl.js.map +1 -1
- package/dist/impl/reference_index.d.ts +22 -0
- package/dist/impl/reference_index.d.ts.map +1 -0
- package/dist/impl/reference_index.js +104 -0
- package/dist/impl/reference_index.js.map +1 -0
- package/dist/impl/ua_variable_impl.d.ts +8 -0
- package/dist/impl/ua_variable_impl.d.ts.map +1 -1
- package/dist/impl/ua_variable_impl.js +121 -84
- package/dist/impl/ua_variable_impl.js.map +1 -1
- package/dist/tsconfig_base.tsbuildinfo +1 -1
- package/distNodeJS/generate_address_space.d.ts +6 -1
- package/distNodeJS/generate_address_space.d.ts.map +1 -1
- package/distNodeJS/generate_address_space.js +33 -1
- package/distNodeJS/generate_address_space.js.map +1 -1
- package/impl/address_space.ts +16 -2
- package/impl/base_node_impl.ts +52 -30
- package/impl/base_node_private.ts +16 -10
- package/impl/extension_object_array_node.ts +12 -3
- package/impl/historical_access/address_space_historical_data_node.ts +64 -1
- package/impl/namespace_impl.ts +3 -1
- package/impl/nodeset_tools/construct_namespace_dependency.ts +23 -1
- package/impl/reference_impl.ts +6 -5
- package/impl/reference_index.ts +116 -0
- package/impl/ua_variable_impl.ts +161 -94
- package/package.json +34 -33
- package/source_nodejs/generate_address_space.ts +46 -1
package/impl/reference_impl.ts
CHANGED
|
@@ -157,7 +157,8 @@ export class ReferenceImpl implements UAReference {
|
|
|
157
157
|
|
|
158
158
|
// cache
|
|
159
159
|
private __hash?: string;
|
|
160
|
-
|
|
160
|
+
/** the key this reference is indexed under, computed once by key(); read by ReferenceIndex */
|
|
161
|
+
public _key?: ReferenceKey;
|
|
161
162
|
|
|
162
163
|
constructor(options: AddReferenceOpts | UAReference) {
|
|
163
164
|
assert(options.referenceType instanceof NodeId);
|
|
@@ -209,10 +210,10 @@ export class ReferenceImpl implements UAReference {
|
|
|
209
210
|
* the key this reference is indexed under in the node holding it, computed once
|
|
210
211
|
*/
|
|
211
212
|
key(ordinals: ReferenceTypeOrdinals): ReferenceKey {
|
|
212
|
-
if (this.
|
|
213
|
-
this.
|
|
213
|
+
if (this._key === undefined) {
|
|
214
|
+
this._key = referenceKey(this.isForward, ordinals.referenceTypeOrdinal(this.referenceType), nodeIdKey(this.nodeId));
|
|
214
215
|
}
|
|
215
|
-
return this.
|
|
216
|
+
return this._key;
|
|
216
217
|
}
|
|
217
218
|
|
|
218
219
|
/**
|
|
@@ -230,7 +231,7 @@ export class ReferenceImpl implements UAReference {
|
|
|
230
231
|
*/
|
|
231
232
|
public dispose(): void {
|
|
232
233
|
this.__hash = undefined;
|
|
233
|
-
this
|
|
234
|
+
// _key stays: the index holding this reference finds it by that key (ReferenceIndex)
|
|
234
235
|
this.node = undefined;
|
|
235
236
|
/*
|
|
236
237
|
this._referenceType = null;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { UAReference } from "node-opcua-address-space-base";
|
|
2
|
+
import type { ReferenceImpl, ReferenceKey } from "./reference_impl.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* above this many references a node indexes them in a Map; below, a plain array scanned on
|
|
6
|
+
* lookup. A leaf node holds two or three references, and a Map with its hash table costs
|
|
7
|
+
* about 280 bytes even when nearly empty: a quarter of what a node weighs. On a model of a
|
|
8
|
+
* million nodes that is the difference between fitting in memory and not.
|
|
9
|
+
*/
|
|
10
|
+
const MAP_THRESHOLD = 16;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The references a node holds, by key (see ReferenceImpl#key). It offers what the code uses of
|
|
14
|
+
* a Map: get, has, set, delete, values, size and iteration.
|
|
15
|
+
*
|
|
16
|
+
* The key is not stored twice: a reference caches its own key when it is indexed, and a scan
|
|
17
|
+
* compares that cached key.
|
|
18
|
+
*/
|
|
19
|
+
export class ReferenceIndex {
|
|
20
|
+
#refs: UAReference[] | null = null;
|
|
21
|
+
#map: Map<ReferenceKey, UAReference> | null = null;
|
|
22
|
+
|
|
23
|
+
public get size(): number {
|
|
24
|
+
return this.#map ? this.#map.size : this.#refs ? this.#refs.length : 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#indexOf(key: ReferenceKey): number {
|
|
28
|
+
const refs = this.#refs as ReferenceImpl[];
|
|
29
|
+
for (let i = 0; i < refs.length; i++) {
|
|
30
|
+
if (refs[i]._key === key) {
|
|
31
|
+
return i;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return -1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public has(key: ReferenceKey): boolean {
|
|
38
|
+
if (this.#map) {
|
|
39
|
+
return this.#map.has(key);
|
|
40
|
+
}
|
|
41
|
+
return this.#refs !== null && this.#indexOf(key) !== -1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public get(key: ReferenceKey): UAReference | undefined {
|
|
45
|
+
if (this.#map) {
|
|
46
|
+
return this.#map.get(key);
|
|
47
|
+
}
|
|
48
|
+
if (this.#refs === null) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const index = this.#indexOf(key);
|
|
52
|
+
return index === -1 ? undefined : this.#refs[index];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** `key` must be the reference's own key (ReferenceImpl#key), which is what callers index by */
|
|
56
|
+
public set(key: ReferenceKey, reference: UAReference): this {
|
|
57
|
+
if (this.#map) {
|
|
58
|
+
this.#map.set(key, reference);
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
if (this.#refs === null) {
|
|
62
|
+
this.#refs = [reference];
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
const index = this.#indexOf(key);
|
|
66
|
+
if (index !== -1) {
|
|
67
|
+
this.#refs[index] = reference; // check-proto-pollution: ok - numeric index
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
if (this.#refs.length >= MAP_THRESHOLD) {
|
|
71
|
+
const map = new Map<ReferenceKey, UAReference>();
|
|
72
|
+
for (const ref of this.#refs) {
|
|
73
|
+
map.set((ref as ReferenceImpl)._key as ReferenceKey, ref);
|
|
74
|
+
}
|
|
75
|
+
map.set(key, reference);
|
|
76
|
+
this.#map = map;
|
|
77
|
+
this.#refs = null;
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
// concat allocates exactly the length needed; push would grow the backing store to 17
|
|
81
|
+
// slots, which is more than the Map this replaces
|
|
82
|
+
this.#refs = this.#refs.concat(reference);
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public delete(key: ReferenceKey): boolean {
|
|
87
|
+
if (this.#map) {
|
|
88
|
+
return this.#map.delete(key);
|
|
89
|
+
}
|
|
90
|
+
if (this.#refs === null) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const index = this.#indexOf(key);
|
|
94
|
+
if (index === -1) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
this.#refs = this.#refs.length === 1 ? null : this.#refs.filter((_r, i) => i !== index);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public clear(): void {
|
|
102
|
+
this.#refs = null;
|
|
103
|
+
this.#map = null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public values(): IterableIterator<UAReference> {
|
|
107
|
+
if (this.#map) {
|
|
108
|
+
return this.#map.values();
|
|
109
|
+
}
|
|
110
|
+
return (this.#refs ?? []).values();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public [Symbol.iterator](): IterableIterator<UAReference> {
|
|
114
|
+
return this.values();
|
|
115
|
+
}
|
|
116
|
+
}
|
package/impl/ua_variable_impl.ts
CHANGED
|
@@ -135,10 +135,6 @@ function adjust_samplingInterval(minimumSamplingInterval: number): number {
|
|
|
135
135
|
return minimumSamplingInterval;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
function is_Variant(v: unknown): boolean {
|
|
139
|
-
return v instanceof Variant;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
138
|
function is_StatusCode(v: unknown): boolean {
|
|
143
139
|
return !!(
|
|
144
140
|
v?.constructor &&
|
|
@@ -149,11 +145,8 @@ function is_StatusCode(v: unknown): boolean {
|
|
|
149
145
|
);
|
|
150
146
|
}
|
|
151
147
|
|
|
152
|
-
function
|
|
153
|
-
|
|
154
|
-
// /@@assert(v.isValid());
|
|
155
|
-
}
|
|
156
|
-
return is_Variant(v) || is_StatusCode(v);
|
|
148
|
+
function isThenable<T>(v: T | PromiseLike<T>): v is PromiseLike<T> {
|
|
149
|
+
return typeof (v as PromiseLike<T> | null | undefined)?.then === "function";
|
|
157
150
|
}
|
|
158
151
|
|
|
159
152
|
function default_func(this: UAVariable, dataValue1: DataValue, callback1: CallbackT<StatusCode>) {
|
|
@@ -213,46 +206,57 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
|
|
|
213
206
|
implements UAVariable<T>, UAVariableImplExtArray
|
|
214
207
|
{
|
|
215
208
|
// -------------- UAvaraibleImplExArray
|
|
216
|
-
$$variableType?: UAVariableType;
|
|
217
|
-
$$dataType?: UADataType
|
|
218
|
-
$$getElementBrowseName?: (extObject: ExtensionObject, index: number | number[]) => QualifiedName;
|
|
219
|
-
$$extensionObjectArray?: ExtensionObject[];
|
|
220
|
-
$$indexPropertyName?: string;
|
|
209
|
+
declare $$variableType?: UAVariableType;
|
|
210
|
+
declare $$dataType?: UADataType;
|
|
211
|
+
declare $$getElementBrowseName?: (extObject: ExtensionObject, index: number | number[]) => QualifiedName;
|
|
212
|
+
declare $$extensionObjectArray?: ExtensionObject[];
|
|
213
|
+
declare $$indexPropertyName?: string;
|
|
221
214
|
// --------------
|
|
222
215
|
|
|
223
216
|
public readonly nodeClass = NodeClass.Variable;
|
|
224
217
|
|
|
225
218
|
public dataType: NodeId;
|
|
226
|
-
private _basicDataType?: DataType;
|
|
219
|
+
declare private _basicDataType?: DataType;
|
|
227
220
|
|
|
228
221
|
// narrowing this to ExtensionObject breaks declaration-merging in ua_two_state_variable.ts and elsewhere
|
|
229
222
|
// (concrete *T variants bind $extensionObject to a different, more specific shape) — see the matching
|
|
230
223
|
// comment on UAVariable.$extensionObject in node-opcua-address-space-base for the documented precedent.
|
|
231
224
|
// biome-ignore lint/suspicious/noExplicitAny: see comment above
|
|
232
|
-
public $extensionObject?: any;
|
|
233
|
-
public $set_ExtensionObject?: (
|
|
225
|
+
declare public $extensionObject?: any;
|
|
226
|
+
declare public $set_ExtensionObject?: (
|
|
227
|
+
newValue: ExtensionObject,
|
|
228
|
+
sourceTimestamp: PreciseClock,
|
|
229
|
+
cache: Set<UAVariableImpl>
|
|
230
|
+
) => void;
|
|
234
231
|
|
|
235
|
-
public $historicalDataConfiguration?: UAHistoricalDataConfiguration;
|
|
236
|
-
public varHistorian?: IVariableHistorian;
|
|
232
|
+
declare public $historicalDataConfiguration?: UAHistoricalDataConfiguration;
|
|
233
|
+
declare public varHistorian?: IVariableHistorian;
|
|
237
234
|
|
|
238
235
|
/**
|
|
239
236
|
* @internal @private
|
|
240
237
|
*/
|
|
241
238
|
public $dataValue: DataValue;
|
|
242
239
|
public accessLevel: number;
|
|
243
|
-
public userAccessLevel?: number;
|
|
240
|
+
declare public userAccessLevel?: number;
|
|
244
241
|
public valueRank: number;
|
|
245
242
|
public minimumSamplingInterval: number;
|
|
246
243
|
public historizing: boolean;
|
|
247
244
|
public semantic_version: number;
|
|
248
245
|
public arrayDimensions: null | number[];
|
|
249
246
|
|
|
250
|
-
public _timestamped_get_func?: TimestampGetFunc | null;
|
|
251
|
-
public _timestamped_set_func?: VariableDataValueSetterWithCallback | null;
|
|
252
|
-
public _get_func?: GetFunc | null;
|
|
253
|
-
public _set_func?: ((value: Variant, callback: (err: Error | null, statusCode?: StatusCode) => void) => void) | null;
|
|
254
|
-
public refreshFunc?: (callback: CallbackT<DataValue>) => void;
|
|
255
|
-
|
|
247
|
+
declare public _timestamped_get_func?: TimestampGetFunc | null;
|
|
248
|
+
declare public _timestamped_set_func?: VariableDataValueSetterWithCallback | null;
|
|
249
|
+
declare public _get_func?: GetFunc | null;
|
|
250
|
+
declare public _set_func?: ((value: Variant, callback: (err: Error | null, statusCode?: StatusCode) => void) => void) | null;
|
|
251
|
+
declare public refreshFunc?: (callback: CallbackT<DataValue>) => void;
|
|
252
|
+
/**
|
|
253
|
+
* true when refreshFunc only wraps the synchronous getter of a Variable bound with a plain
|
|
254
|
+
* `get`. readValue evaluates that getter itself: refreshing such a Variable before reading
|
|
255
|
+
* it, as the Read service does for the others, evaluates the getter twice for nothing.
|
|
256
|
+
*/
|
|
257
|
+
declare public _refreshFuncWrapsGetter?: boolean;
|
|
258
|
+
declare public __waiting_callbacks?: CallbackT<DataValue>[];
|
|
259
|
+
declare private __satisfy_waiting_callbacks?: (err: Error | null, dataValue?: DataValue) => void;
|
|
256
260
|
|
|
257
261
|
get typeDefinitionObj(): UAVariableType {
|
|
258
262
|
/* c8 ignore next */
|
|
@@ -318,7 +322,6 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
|
|
|
318
322
|
}
|
|
319
323
|
private checkPermissionPrivate(context: ISessionContext, permission: PermissionType): boolean {
|
|
320
324
|
if (!context) return true;
|
|
321
|
-
assert(context instanceof SessionContext);
|
|
322
325
|
if (context.checkPermission) {
|
|
323
326
|
if (!(context.checkPermission instanceof Function)) {
|
|
324
327
|
errorLog("context checkPermission is not a function");
|
|
@@ -437,7 +440,14 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
|
|
|
437
440
|
if (!this.checkPermissionPrivate(context, PermissionType.Read)) {
|
|
438
441
|
return makeNowDataValue({ statusCode: StatusCodes.BadUserAccessDenied });
|
|
439
442
|
}
|
|
440
|
-
|
|
443
|
+
// isUserReadable() would evaluate isReadable() and the Read permission a second time, and the
|
|
444
|
+
// permission lookup walks the namespace metadata. Both just passed above, so only the access
|
|
445
|
+
// level is left, unless a node replaced isUserReadable (see makeNotReadableIfEnabledFlagIsFalse).
|
|
446
|
+
const userReadable =
|
|
447
|
+
this.isUserReadable === UAVariableImpl.prototype.isUserReadable
|
|
448
|
+
? this.checkAccessLevelPrivate(context, AccessLevelFlag.CurrentRead)
|
|
449
|
+
: this.isUserReadable(context);
|
|
450
|
+
if (!userReadable) {
|
|
441
451
|
return makeNowDataValue({ statusCode: StatusCodes.BadNotReadable });
|
|
442
452
|
}
|
|
443
453
|
if (!isValidDataEncoding(dataEncoding)) {
|
|
@@ -1273,6 +1283,7 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
|
|
|
1273
1283
|
this._get_func = null;
|
|
1274
1284
|
this._set_func = null;
|
|
1275
1285
|
this.refreshFunc = undefined;
|
|
1286
|
+
this._refreshFuncWrapsGetter = false;
|
|
1276
1287
|
this._historyRead = UAVariableImpl.prototype._historyRead;
|
|
1277
1288
|
}
|
|
1278
1289
|
|
|
@@ -1303,12 +1314,22 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
|
|
|
1303
1314
|
public readValueAsync(context: ISessionContext | null, callback: CallbackT<DataValue>): void;
|
|
1304
1315
|
public readValueAsync(context: ISessionContext | null): Promise<DataValue>;
|
|
1305
1316
|
public readValueAsync(context: ISessionContext | null, callback?: CallbackT<DataValue>): Promise<DataValue> | undefined {
|
|
1306
|
-
|
|
1317
|
+
// written out rather than wrapped with thenify-ex: every sample of a monitored item comes
|
|
1318
|
+
// through here with a callback, and the generic wrapper forwarded it with apply(arguments)
|
|
1319
|
+
if (callback) {
|
|
1320
|
+
this._readValueAsync(context, callback);
|
|
1321
|
+
return undefined;
|
|
1322
|
+
}
|
|
1323
|
+
return new Promise<DataValue>((resolve, reject) => {
|
|
1324
|
+
this._readValueAsync(context, (err, dataValue) => (err ? reject(err) : resolve(dataValue as DataValue)));
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1307
1327
|
|
|
1328
|
+
private _readValueAsync(context: ISessionContext | null, callback: CallbackT<DataValue>): void {
|
|
1308
1329
|
context = context || SessionContext.defaultContext;
|
|
1309
1330
|
|
|
1310
1331
|
this.__waiting_callbacks = this.__waiting_callbacks || [];
|
|
1311
|
-
this.__waiting_callbacks.push(callback
|
|
1332
|
+
this.__waiting_callbacks.push(callback);
|
|
1312
1333
|
|
|
1313
1334
|
const _readValueAsync_in_progress = this.__waiting_callbacks.length >= 2;
|
|
1314
1335
|
if (_readValueAsync_in_progress) {
|
|
@@ -1320,19 +1341,10 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
|
|
|
1320
1341
|
return;
|
|
1321
1342
|
}
|
|
1322
1343
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
};
|
|
1328
|
-
|
|
1329
|
-
let func: (innerCallback: (err: Error | null, dataValue: DataValue) => void) => void;
|
|
1330
|
-
|
|
1331
|
-
// stamped like the same gates of readValue(): the requested timestamps are expected
|
|
1332
|
-
// on a Bad result too (FEAT-34)
|
|
1333
|
-
const answerStatusOnly = (statusCode: StatusCode) => (innerCallback: (err: Error | null, dataValue: DataValue) => void) =>
|
|
1334
|
-
innerCallback(null, makeNowDataValue({ statusCode }));
|
|
1335
|
-
|
|
1344
|
+
// runs once per sample of every monitored item, so nothing here allocates a closure or
|
|
1345
|
+
// binds a function: the denial (if any) is a StatusCode, and the callback that releases
|
|
1346
|
+
// the waiting readers is made once per variable
|
|
1347
|
+
let denied: StatusCode | null = null;
|
|
1336
1348
|
if (context.isAccessRestricted(this)) {
|
|
1337
1349
|
// same gate as readValue(): a variable bound with a simple get()
|
|
1338
1350
|
// also gets a refreshFunc, sending every read down the asyncRefresh
|
|
@@ -1340,30 +1352,48 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
|
|
|
1340
1352
|
// through readValue(). Without this check here, a node hardened
|
|
1341
1353
|
// with AccessRestrictions(EncryptionRequired) answered its value
|
|
1342
1354
|
// over an unencrypted channel.
|
|
1343
|
-
|
|
1355
|
+
denied = StatusCodes.BadSecurityModeInsufficient;
|
|
1344
1356
|
} else if (!this.isReadable(context)) {
|
|
1345
|
-
|
|
1357
|
+
denied = StatusCodes.BadNotReadable;
|
|
1346
1358
|
} else if (!this.checkPermissionPrivate(context, PermissionType.Read)) {
|
|
1347
|
-
|
|
1348
|
-
} else if (
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1359
|
+
denied = StatusCodes.BadUserAccessDenied;
|
|
1360
|
+
} else if (
|
|
1361
|
+
// as in readValue(): the Read permission has just passed, only the access level is
|
|
1362
|
+
// left, unless a node replaced isUserReadable (see makeNotReadableIfEnabledFlagIsFalse)
|
|
1363
|
+
this.isUserReadable === UAVariableImpl.prototype.isUserReadable
|
|
1364
|
+
? !this.checkAccessLevelPrivate(context, AccessLevelFlag.CurrentRead)
|
|
1365
|
+
: !this.isUserReadable(context)
|
|
1366
|
+
) {
|
|
1367
|
+
denied = StatusCodes.BadNotReadable;
|
|
1353
1368
|
}
|
|
1354
1369
|
|
|
1355
|
-
|
|
1370
|
+
this.__satisfy_waiting_callbacks ??= (err: Error | null, dataValue?: DataValue) => {
|
|
1356
1371
|
// now call all pending callbacks
|
|
1357
1372
|
const callbacks = this.__waiting_callbacks || [];
|
|
1358
1373
|
this.__waiting_callbacks = [];
|
|
1359
|
-
const _n = callbacks.length;
|
|
1360
1374
|
for (const callback1 of callbacks) {
|
|
1361
1375
|
callback1.call(this, err, dataValue);
|
|
1362
1376
|
}
|
|
1363
1377
|
};
|
|
1378
|
+
const satisfy_callbacks = this.__satisfy_waiting_callbacks;
|
|
1364
1379
|
|
|
1365
1380
|
try {
|
|
1366
|
-
|
|
1381
|
+
if (denied) {
|
|
1382
|
+
// stamped like the same gates of readValue(): the requested timestamps are expected
|
|
1383
|
+
// on a Bad result too (FEAT-34)
|
|
1384
|
+
satisfy_callbacks(null, makeNowDataValue({ statusCode: denied }));
|
|
1385
|
+
} else if (typeof this.refreshFunc === "function") {
|
|
1386
|
+
const clock = getCurrentClock();
|
|
1387
|
+
if (this.asyncRefresh === wrappedAsyncRefresh) {
|
|
1388
|
+
// the thenified wrapper adds nothing when a callback is given
|
|
1389
|
+
asyncRefreshImpl.call(this, clock, satisfy_callbacks);
|
|
1390
|
+
} else {
|
|
1391
|
+
this.asyncRefresh(clock, satisfy_callbacks);
|
|
1392
|
+
}
|
|
1393
|
+
} else {
|
|
1394
|
+
assert(this.$dataValue instanceof DataValue);
|
|
1395
|
+
satisfy_callbacks(null, this.readValue(context));
|
|
1396
|
+
}
|
|
1367
1397
|
} catch (err) {
|
|
1368
1398
|
// c8 ignore next
|
|
1369
1399
|
if (doDebug) {
|
|
@@ -1374,7 +1404,6 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
|
|
|
1374
1404
|
}
|
|
1375
1405
|
satisfy_callbacks(err as Error);
|
|
1376
1406
|
}
|
|
1377
|
-
return undefined;
|
|
1378
1407
|
}
|
|
1379
1408
|
|
|
1380
1409
|
public getWriteMask(): number {
|
|
@@ -1936,9 +1965,19 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
|
|
|
1936
1965
|
*/
|
|
1937
1966
|
public _inner_replace_dataValue(dataValue: DataValue, indexRange?: NumericRange | null) {
|
|
1938
1967
|
assert(this.$dataValue.value instanceof Variant);
|
|
1939
|
-
const old_dataValue = this.$dataValue.clone();
|
|
1940
1968
|
|
|
1941
|
-
|
|
1969
|
+
const replacesArrayInPlace = !!this.$$extensionObjectArray && dataValue.value.arrayType !== VariantArrayType.Scalar;
|
|
1970
|
+
const replacesExtensionObjectInPlace =
|
|
1971
|
+
this._basicDataType === DataType.ExtensionObject &&
|
|
1972
|
+
this.valueRank === -1 &&
|
|
1973
|
+
!!this.$set_ExtensionObject &&
|
|
1974
|
+
dataValue.value.arrayType === VariantArrayType.Scalar;
|
|
1975
|
+
// the previous value is needed intact for the comparison below. Only the two branches that
|
|
1976
|
+
// write into $dataValue in place need a copy of it: the common case swaps in the new object
|
|
1977
|
+
// and leaves the old one untouched
|
|
1978
|
+
const old_dataValue = replacesArrayInPlace || replacesExtensionObjectInPlace ? this.$dataValue.clone() : this.$dataValue;
|
|
1979
|
+
|
|
1980
|
+
if (replacesArrayInPlace) {
|
|
1942
1981
|
// we have a bounded array or matrix
|
|
1943
1982
|
assert(Array.isArray(dataValue.value.value));
|
|
1944
1983
|
if (this.$$extensionObjectArray !== this.$dataValue.value.value) {
|
|
@@ -1952,12 +1991,7 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
|
|
|
1952
1991
|
this.$dataValue.serverPicoseconds = dataValue.serverPicoseconds;
|
|
1953
1992
|
this.$dataValue.sourceTimestamp = dataValue.sourceTimestamp;
|
|
1954
1993
|
this.$dataValue.sourcePicoseconds = dataValue.sourcePicoseconds;
|
|
1955
|
-
} else if (
|
|
1956
|
-
this._basicDataType === DataType.ExtensionObject &&
|
|
1957
|
-
this.valueRank === -1 &&
|
|
1958
|
-
this.$set_ExtensionObject &&
|
|
1959
|
-
dataValue.value.arrayType === VariantArrayType.Scalar
|
|
1960
|
-
) {
|
|
1994
|
+
} else if (replacesExtensionObjectInPlace && this.$set_ExtensionObject) {
|
|
1961
1995
|
// the entire extension object is changed.
|
|
1962
1996
|
this.$dataValue.statusCode = this.$dataValue.statusCode || StatusCodes.Good;
|
|
1963
1997
|
const preciseClock = coerceClock(this.$dataValue.sourceTimestamp, this.$dataValue.sourcePicoseconds);
|
|
@@ -1966,14 +2000,16 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
|
|
|
1966
2000
|
this.$dataValue = dataValue;
|
|
1967
2001
|
this.$dataValue.statusCode = this.$dataValue.statusCode || StatusCodes.Good;
|
|
1968
2002
|
}
|
|
1969
|
-
// repair missing timestamps
|
|
1970
|
-
|
|
2003
|
+
// repair missing timestamps (the clock is only read when one is missing: this runs on
|
|
2004
|
+
// every stored value, and most arrive with both timestamps)
|
|
2005
|
+
let now: Date | undefined;
|
|
1971
2006
|
if (!dataValue.serverTimestamp) {
|
|
2007
|
+
now = new Date();
|
|
1972
2008
|
this.$dataValue.serverTimestamp = old_dataValue.serverTimestamp || now;
|
|
1973
2009
|
this.$dataValue.serverPicoseconds = old_dataValue.serverPicoseconds || 0;
|
|
1974
2010
|
}
|
|
1975
2011
|
if (!dataValue.sourceTimestamp) {
|
|
1976
|
-
this.$dataValue.sourceTimestamp = old_dataValue.sourceTimestamp || now;
|
|
2012
|
+
this.$dataValue.sourceTimestamp = old_dataValue.sourceTimestamp || (now ?? new Date());
|
|
1977
2013
|
this.$dataValue.sourcePicoseconds = old_dataValue.sourcePicoseconds || 0;
|
|
1978
2014
|
}
|
|
1979
2015
|
|
|
@@ -1997,7 +2033,7 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
|
|
|
1997
2033
|
this.touchValue(preciseClock);
|
|
1998
2034
|
propagateTouchValueDownward(this, preciseClock, cache);
|
|
1999
2035
|
}
|
|
2000
|
-
} else {
|
|
2036
|
+
} else if ((this as UAVariable).listenerCount("value_changed") > 0) {
|
|
2001
2037
|
(this as UAVariable).emit("value_changed", this.$dataValue.clone(), indexRange);
|
|
2002
2038
|
}
|
|
2003
2039
|
}
|
|
@@ -2105,11 +2141,15 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
|
|
|
2105
2141
|
import type { IStructuredTypeSchema } from "node-opcua-factory";
|
|
2106
2142
|
import { withCallback } from "thenify-ex";
|
|
2107
2143
|
|
|
2108
|
-
|
|
2144
|
+
// readValueAsync calls the unwrapped asyncRefresh directly: binding the thenified wrapper on
|
|
2145
|
+
// every sample cost more than the refresh itself
|
|
2146
|
+
const asyncRefreshImpl: (this: UAVariableImpl, oldestDate: PreciseClock, callback: CallbackT<DataValue>) => void =
|
|
2147
|
+
UAVariableImpl.prototype.asyncRefresh;
|
|
2148
|
+
UAVariableImpl.prototype.asyncRefresh = withCallback(asyncRefreshImpl);
|
|
2149
|
+
const wrappedAsyncRefresh = UAVariableImpl.prototype.asyncRefresh;
|
|
2109
2150
|
UAVariableImpl.prototype.writeValue = withCallback(UAVariableImpl.prototype.writeValue);
|
|
2110
2151
|
UAVariableImpl.prototype.writeAttribute = withCallback(UAVariableImpl.prototype.writeAttribute);
|
|
2111
2152
|
UAVariableImpl.prototype.historyRead = withCallback(UAVariableImpl.prototype.historyRead);
|
|
2112
|
-
UAVariableImpl.prototype.readValueAsync = withCallback(UAVariableImpl.prototype.readValueAsync);
|
|
2113
2153
|
|
|
2114
2154
|
export interface UAVariableImplExtArray {
|
|
2115
2155
|
$$variableType?: UAVariableType;
|
|
@@ -2307,12 +2347,26 @@ function _Variable_bind_with_timestamped_get(
|
|
|
2307
2347
|
assert(!this._timestamped_get_func);
|
|
2308
2348
|
|
|
2309
2349
|
const async_refresh_func = (callback: (err: Error | null, dataValue?: DataValue) => void) => {
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2350
|
+
const reportError = (err: unknown) => {
|
|
2351
|
+
errorLog("asyncRefresh error: Variable is ", this.nodeId.toString(), this.browseName.toString());
|
|
2352
|
+
callback(err as Error);
|
|
2353
|
+
};
|
|
2354
|
+
// a getter that throws is reported by asyncRefresh, which calls this function
|
|
2355
|
+
const dataValueOrPromise = (this._timestamped_get_func as () => DataValue | Promise<DataValue>).call(this);
|
|
2356
|
+
if (isThenable(dataValueOrPromise)) {
|
|
2357
|
+
Promise.resolve(dataValueOrPromise)
|
|
2358
|
+
.then((dataValue) => callback(null, dataValue))
|
|
2359
|
+
.catch(reportError);
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
// the getter is synchronous (it always is for a Variable bound with a plain `get`):
|
|
2363
|
+
// answer at once. A promise here costs every Read of the Variable an allocation and
|
|
2364
|
+
// a turn of the microtask queue, for a value that is already known.
|
|
2365
|
+
try {
|
|
2366
|
+
callback(null, dataValueOrPromise);
|
|
2367
|
+
} catch (err) {
|
|
2368
|
+
reportError(err);
|
|
2369
|
+
}
|
|
2316
2370
|
};
|
|
2317
2371
|
const pThis = this as UAVariable;
|
|
2318
2372
|
if (options.timestamped_get.length === 0) {
|
|
@@ -2356,21 +2410,10 @@ function _Variable_bind_with_simple_get(this: UAVariableImpl, options: GetterOpt
|
|
|
2356
2410
|
const timestamped_get_func_from__Variable_bind_with_simple_get = () => {
|
|
2357
2411
|
const value: Variant | StatusCode = (this._get_func as GetFunc)();
|
|
2358
2412
|
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
" : the getter must return a Variant or a StatusCode" + "\nvalue_check.constructor.name ",
|
|
2364
|
-
value ? value.constructor.name : "null"
|
|
2365
|
-
);
|
|
2366
|
-
throw new Error(
|
|
2367
|
-
" bindVariable : the value getter function returns a invalid result ( expecting a Variant or a StatusCode !!!"
|
|
2368
|
-
);
|
|
2369
|
-
}
|
|
2370
|
-
if (is_StatusCode(value)) {
|
|
2371
|
-
return new DataValue({ statusCode: value as StatusCode });
|
|
2372
|
-
} else {
|
|
2373
|
-
if (!this.$dataValue?.statusCode.isGoodish() || !sameVariant(this.$dataValue.value, value as Variant)) {
|
|
2413
|
+
// a getter returns a Variant on every read but a failing one: test for it first, as
|
|
2414
|
+
// is_StatusCode ends in comparisons of constructor names
|
|
2415
|
+
if (value instanceof Variant) {
|
|
2416
|
+
if (!this.$dataValue?.statusCode.isGoodish() || !sameVariant(this.$dataValue.value, value)) {
|
|
2374
2417
|
// rebuilding artificially timestamps with current clock as they are not provided
|
|
2375
2418
|
// by the underlying getter function
|
|
2376
2419
|
const { timestamp: sourceTimestamp, picoseconds: sourcePicoseconds } = getCurrentClock();
|
|
@@ -2382,12 +2425,26 @@ function _Variable_bind_with_simple_get(this: UAVariableImpl, options: GetterOpt
|
|
|
2382
2425
|
}
|
|
2383
2426
|
return this.$dataValue;
|
|
2384
2427
|
}
|
|
2428
|
+
|
|
2429
|
+
/* c8 ignore next */
|
|
2430
|
+
if (!is_StatusCode(value)) {
|
|
2431
|
+
errorLog(
|
|
2432
|
+
chalk.red(" Bind variable error: "),
|
|
2433
|
+
" : the getter must return a Variant or a StatusCode" + "\nvalue_check.constructor.name ",
|
|
2434
|
+
value ? value.constructor.name : "null"
|
|
2435
|
+
);
|
|
2436
|
+
throw new Error(
|
|
2437
|
+
" bindVariable : the value getter function returns a invalid result ( expecting a Variant or a StatusCode !!!"
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
return new DataValue({ statusCode: value as StatusCode });
|
|
2385
2441
|
};
|
|
2386
2442
|
|
|
2387
2443
|
_Variable_bind_with_timestamped_get.call(this, {
|
|
2388
2444
|
get: undefined,
|
|
2389
2445
|
timestamped_get: timestamped_get_func_from__Variable_bind_with_simple_get
|
|
2390
2446
|
});
|
|
2447
|
+
this._refreshFuncWrapsGetter = true;
|
|
2391
2448
|
}
|
|
2392
2449
|
|
|
2393
2450
|
type SimpleSetOptions = { set?: SetFunc; timestamped_set: undefined };
|
|
@@ -2440,8 +2497,18 @@ function _Variable_bind_with_timestamped_set(
|
|
|
2440
2497
|
"timestamped_set must have 2 parameters timestamped_set: function(dataValue,callback){} or one paramater timestamped_set: function(dataValue): Promise<StatusCode>{}"
|
|
2441
2498
|
);
|
|
2442
2499
|
assert(!options.set, "should not specify set when timestamped_set_func exists ");
|
|
2443
|
-
|
|
2500
|
+
// the callback form of a setter is a stateless wrapper around it (it takes the variable as
|
|
2501
|
+
// `this`): one per setter function, not one per variable. Every writable variable without a
|
|
2502
|
+
// setter of its own binds the same default, and a closure per variable was 110 bytes each
|
|
2503
|
+
let setter = callbackSetters.get(options.timestamped_set);
|
|
2504
|
+
if (!setter) {
|
|
2505
|
+
setter = convertToCallbackFunction1<StatusCode, DataValue, UAVariable>(options.timestamped_set);
|
|
2506
|
+
callbackSetters.set(options.timestamped_set, setter);
|
|
2507
|
+
}
|
|
2508
|
+
this._timestamped_set_func = setter;
|
|
2444
2509
|
}
|
|
2510
|
+
/** the callback form of each setter function met so far, see _Variable_bind_with_timestamped_set */
|
|
2511
|
+
const callbackSetters = new WeakMap<TimestampSetFunc, VariableDataValueSetterWithCallback>();
|
|
2445
2512
|
|
|
2446
2513
|
interface SetterOptions {
|
|
2447
2514
|
set?: SetFunc;
|
|
@@ -2528,9 +2595,9 @@ export class UAVariableImplT<T, DT extends DataType> extends UAVariableImpl impl
|
|
|
2528
2595
|
context: ISessionContext | null,
|
|
2529
2596
|
callback?: CallbackT<DataValueT<T, DT>>
|
|
2530
2597
|
): Promise<DataValueT<T, DT>> | undefined {
|
|
2531
|
-
// forward the arguments we were actually given
|
|
2532
|
-
//
|
|
2533
|
-
//
|
|
2598
|
+
// forward the arguments we were actually given: the base method picks the promise or the
|
|
2599
|
+
// callback form from the callback it receives (writeValue below, still thenified, picks
|
|
2600
|
+
// it from the argument count, where an explicit undefined callback would be passed on)
|
|
2534
2601
|
if (callback === undefined) {
|
|
2535
2602
|
return super.readValueAsync(context) as Promise<DataValueT<T, DT>>;
|
|
2536
2603
|
}
|