node-opcua-server
Version:
pure nodejs OPCUA SDK - module server
1,053 lines (1,052 loc) • 65.3 kB
JavaScript
"use strict";
/**
* @module node-opcua-server
*/
// tslint:disable:no-console
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Subscription = exports.SubscriptionState = void 0;
const events_1 = require("events");
const chalk_1 = __importDefault(require("chalk"));
const node_opcua_address_space_1 = require("node-opcua-address-space");
const node_opcua_assert_1 = require("node-opcua-assert");
const node_opcua_common_1 = require("node-opcua-common");
const node_opcua_data_model_1 = require("node-opcua-data-model");
const node_opcua_debug_1 = require("node-opcua-debug");
const node_opcua_nodeid_1 = require("node-opcua-nodeid");
const node_opcua_object_registry_1 = require("node-opcua-object-registry");
const node_opcua_secure_channel_1 = require("node-opcua-secure-channel");
const node_opcua_service_filter_1 = require("node-opcua-service-filter");
const node_opcua_service_subscription_1 = require("node-opcua-service-subscription");
const node_opcua_service_subscription_2 = require("node-opcua-service-subscription");
const node_opcua_status_code_1 = require("node-opcua-status-code");
const node_opcua_types_1 = require("node-opcua-types");
const queue_1 = require("./queue");
const monitored_item_1 = require("./monitored_item");
const validate_filter_1 = require("./validate_filter");
const i_server_side_publish_engine_1 = require("./i_server_side_publish_engine");
const debugLog = (0, node_opcua_debug_1.make_debugLog)(__filename);
const doDebug = (0, node_opcua_debug_1.checkDebugFlag)(__filename);
const warningLog = (0, node_opcua_debug_1.make_warningLog)(__filename);
const maxNotificationMessagesInQueue = 100;
var SubscriptionState;
(function (SubscriptionState) {
SubscriptionState[SubscriptionState["CLOSED"] = 1] = "CLOSED";
SubscriptionState[SubscriptionState["CREATING"] = 2] = "CREATING";
SubscriptionState[SubscriptionState["NORMAL"] = 3] = "NORMAL";
// The keep-alive counter is not used in this state.
SubscriptionState[SubscriptionState["LATE"] = 4] = "LATE";
// ready to be sent, but there are no Publish requests queued. When in this state, the next Publish
// request is processed when it is received. The keep-alive counter is not used in this state.
SubscriptionState[SubscriptionState["KEEPALIVE"] = 5] = "KEEPALIVE";
// alive counter to count down to 0 from its maximum.
SubscriptionState[SubscriptionState["TERMINATED"] = 6] = "TERMINATED";
})(SubscriptionState || (exports.SubscriptionState = SubscriptionState = {}));
function _adjust_publishing_interval(publishingInterval) {
publishingInterval =
publishingInterval === undefined || Number.isNaN(publishingInterval)
? Subscription.defaultPublishingInterval
: publishingInterval;
publishingInterval = Math.max(publishingInterval, Subscription.minimumPublishingInterval);
publishingInterval = Math.min(publishingInterval, Subscription.maximumPublishingInterval);
return publishingInterval;
}
const minimumMaxKeepAliveCount = 2;
const maximumMaxKeepAliveCount = 12000;
function _adjust_maxKeepAliveCount(maxKeepAliveCount /*,publishingInterval*/) {
maxKeepAliveCount = maxKeepAliveCount || minimumMaxKeepAliveCount;
maxKeepAliveCount = Math.max(maxKeepAliveCount, minimumMaxKeepAliveCount);
maxKeepAliveCount = Math.min(maxKeepAliveCount, maximumMaxKeepAliveCount);
return maxKeepAliveCount;
}
const MaxUint32 = 0xffffffff;
function _adjust_lifeTimeCount(lifeTimeCount, maxKeepAliveCount, publishingInterval) {
lifeTimeCount = lifeTimeCount || 1;
const minTicks = Math.ceil(Subscription.minimumLifetimeDuration / publishingInterval);
const maxTicks = Math.floor(Subscription.maximumLifetimeDuration / publishingInterval);
lifeTimeCount = Math.max(minTicks, lifeTimeCount);
lifeTimeCount = Math.min(maxTicks, lifeTimeCount);
// let's make sure that lifeTimeCount is at least three time maxKeepAliveCount
// Note : the specs say ( part 3 - CreateSubscriptionParameter )
// "The lifetime count shall be a minimum of three times the keep keep-alive count."
lifeTimeCount = Math.max(lifeTimeCount, Math.min(maxKeepAliveCount * 3, MaxUint32));
return lifeTimeCount;
}
function _adjust_publishingEnable(publishingEnabled) {
return publishingEnabled === null || publishingEnabled === undefined ? true : !!publishingEnabled;
}
function _adjust_maxNotificationsPerPublish(maxNotificationsPerPublish) {
(0, node_opcua_assert_1.assert)(Subscription.maxNotificationPerPublishHighLimit > 0, "Subscription.maxNotificationPerPublishHighLimit must be positive");
maxNotificationsPerPublish = maxNotificationsPerPublish || 0;
(0, node_opcua_assert_1.assert)(typeof maxNotificationsPerPublish === "number");
// must be strictly positive
maxNotificationsPerPublish = maxNotificationsPerPublish >= 0 ? maxNotificationsPerPublish : 0;
if (maxNotificationsPerPublish === 0) {
// if zero then => use our HighLimit
maxNotificationsPerPublish = Subscription.maxNotificationPerPublishHighLimit;
}
else {
// if not zero then should be capped by maxNotificationPerPublishHighLimit
maxNotificationsPerPublish = Math.min(Subscription.maxNotificationPerPublishHighLimit, maxNotificationsPerPublish);
}
(0, node_opcua_assert_1.assert)(maxNotificationsPerPublish !== 0 && maxNotificationsPerPublish <= Subscription.maxNotificationPerPublishHighLimit);
return maxNotificationsPerPublish;
}
function w(s, length) {
return ("000" + s).padStart(length);
}
function t(d) {
return w(d.getHours(), 2) + ":" + w(d.getMinutes(), 2) + ":" + w(d.getSeconds(), 2) + ":" + w(d.getMilliseconds(), 3);
}
function _getSequenceNumbers(arr) {
return arr.map((notificationMessage) => notificationMessage.sequenceNumber);
}
function analyzeEventFilterResult(node, eventFilter) {
/* istanbul ignore next */
if (!(eventFilter instanceof node_opcua_service_filter_1.EventFilter)) {
throw new Error("Internal Error");
}
const selectClauseResults = (0, node_opcua_service_filter_1.checkSelectClauses)(node, eventFilter.selectClauses || []);
const whereClauseResult = new node_opcua_types_1.ContentFilterResult();
return new node_opcua_types_1.EventFilterResult({
selectClauseDiagnosticInfos: [],
selectClauseResults,
whereClauseResult
});
}
function analyzeDataChangeFilterResult(node, dataChangeFilter) {
(0, node_opcua_assert_1.assert)(dataChangeFilter instanceof node_opcua_service_subscription_2.DataChangeFilter);
// the opcua specification doesn't provide dataChangeFilterResult
return null;
}
function analyzeAggregateFilterResult(node, aggregateFilter) {
(0, node_opcua_assert_1.assert)(aggregateFilter instanceof node_opcua_service_subscription_1.AggregateFilter);
return new node_opcua_types_1.AggregateFilterResult({});
}
function _process_filter(node, filter) {
if (!filter) {
return null;
}
if (filter instanceof node_opcua_service_filter_1.EventFilter) {
return analyzeEventFilterResult(node, filter);
}
else if (filter instanceof node_opcua_service_subscription_2.DataChangeFilter) {
return analyzeDataChangeFilterResult(node, filter);
}
else if (filter instanceof node_opcua_service_subscription_1.AggregateFilter) {
return analyzeAggregateFilterResult(node, filter);
}
// istanbul ignore next
throw new Error("invalid filter");
}
/**
* @private
*/
function createSubscriptionDiagnostics(subscription) {
(0, node_opcua_assert_1.assert)(subscription instanceof Subscription);
const subscriptionDiagnostics = new node_opcua_common_1.SubscriptionDiagnosticsDataType({});
const subscription_subscriptionDiagnostics = subscriptionDiagnostics;
subscription_subscriptionDiagnostics.$subscription = subscription;
// "sessionId"
subscription_subscriptionDiagnostics.__defineGetter__("sessionId", function () {
if (!this.$subscription) {
return node_opcua_nodeid_1.NodeId.nullNodeId;
}
return this.$subscription.getSessionId();
});
subscription_subscriptionDiagnostics.__defineGetter__("subscriptionId", function () {
if (!this.$subscription) {
return 0;
}
return this.$subscription.id;
});
subscription_subscriptionDiagnostics.__defineGetter__("priority", function () {
if (!this.$subscription) {
return 0;
}
return this.$subscription.priority;
});
subscription_subscriptionDiagnostics.__defineGetter__("publishingInterval", function () {
if (!this.$subscription) {
return 0;
}
return this.$subscription.publishingInterval;
});
subscription_subscriptionDiagnostics.__defineGetter__("maxLifetimeCount", function () {
return this.$subscription.lifeTimeCount;
});
subscription_subscriptionDiagnostics.__defineGetter__("maxKeepAliveCount", function () {
if (!this.$subscription) {
return 0;
}
return this.$subscription.maxKeepAliveCount;
});
subscription_subscriptionDiagnostics.__defineGetter__("maxNotificationsPerPublish", function () {
if (!this.$subscription) {
return 0;
}
return this.$subscription.maxNotificationsPerPublish;
});
subscription_subscriptionDiagnostics.__defineGetter__("publishingEnabled", function () {
if (!this.$subscription) {
return false;
}
return this.$subscription.publishingEnabled;
});
subscription_subscriptionDiagnostics.__defineGetter__("monitoredItemCount", function () {
if (!this.$subscription) {
return 0;
}
return this.$subscription.monitoredItemCount;
});
subscription_subscriptionDiagnostics.__defineGetter__("nextSequenceNumber", function () {
if (!this.$subscription) {
return 0;
}
return this.$subscription.futureSequenceNumber;
});
subscription_subscriptionDiagnostics.__defineGetter__("disabledMonitoredItemCount", function () {
if (!this.$subscription) {
return 0;
}
return this.$subscription.disabledMonitoredItemCount;
});
/* those member of self.subscriptionDiagnostics are handled directly
modifyCount
enableCount,
disableCount,
republishRequestCount,
notificationsCount,
publishRequestCount,
dataChangeNotificationsCount,
eventNotificationsCount,
*/
/*
those members are not updated yet in the code :
"republishMessageRequestCount",
"republishMessageCount",
"transferRequestCount",
"transferredToAltClientCount",
"transferredToSameClientCount",
"latePublishRequestCount",
"unacknowledgedMessageCount",
"discardedMessageCount",
"monitoringQueueOverflowCount",
"eventQueueOverflowCount"
*/
subscription_subscriptionDiagnostics.__defineGetter__("currentKeepAliveCount", function () {
if (!this.$subscription) {
return 0;
}
return this.$subscription.currentKeepAliveCount;
});
subscription_subscriptionDiagnostics.__defineGetter__("currentLifetimeCount", function () {
if (!this.$subscription) {
return 0;
}
return this.$subscription.currentLifetimeCount;
});
// add object in Variable SubscriptionDiagnosticArray (i=2290) ( Array of SubscriptionDiagnostics)
// add properties in Variable to reflect
return subscriptionDiagnostics;
}
let g_monitoredItemId = Math.ceil(Math.random() * 100000);
function getNextMonitoredItemId() {
return g_monitoredItemId++;
}
// function myFilter<T>(t1: any, chunk: any[]): T[] {
// return chunk.filter(filter_instanceof.bind(null, t1));
// }
// function makeNotificationData(notifications_chunk: QueueItem): NotificationData {
// const dataChangedNotificationData = myFilter<MonitoredItemNotification>(MonitoredItemNotification, notifications_chunk);
// const eventNotificationListData = myFilter<EventFieldList>(EventFieldList, notifications_chunk);
// assert(notifications_chunk.length === dataChangedNotificationData.length + eventNotificationListData.length);
// const notifications: (DataChangeNotification | EventNotificationList)[] = [];
// // add dataChangeNotification
// if (dataChangedNotificationData.length) {
// const dataChangeNotification = new DataChangeNotification({
// diagnosticInfos: [],
// monitoredItems: dataChangedNotificationData
// });
// notifications.push(dataChangeNotification);
// }
// // add dataChangeNotification
// if (eventNotificationListData.length) {
// const eventNotificationList = new EventNotificationList({
// events: eventNotificationListData
// });
// notifications.push(eventNotificationList);
// }
// return notifications.length === 0 ? null : notifications;
// }
const INVALID_ID = -1;
/**
* The Subscription class used in the OPCUA server side.
*/
class Subscription extends events_1.EventEmitter {
/**
* @deprecated use serverCapacity.maxMonitoredItems and serverCapacity.maxMonitoredItemsPerSubscription instead
*/
static get maxMonitoredItemCount() {
return Subscription.defaultMaxMonitoredItemCount;
}
set state(value) {
if (this._state !== value) {
this._state = value;
this.emit("stateChanged", value);
}
}
get state() {
return this._state;
}
get sessionId() {
return this.$session ? this.$session.nodeId : node_opcua_nodeid_1.NodeId.nullNodeId;
}
get currentLifetimeCount() {
return this._life_time_counter;
}
get currentKeepAliveCount() {
return this._keep_alive_counter;
}
constructor(options) {
super();
this._state = -1;
this._keep_alive_counter = 0;
this._hasUncollectedMonitoredItemNotifications = false;
options = options || {};
Subscription.registry.register(this);
(0, node_opcua_assert_1.assert)(this.sessionId instanceof node_opcua_nodeid_1.NodeId, "expecting a sessionId NodeId");
this.publishEngine = options.publishEngine;
this.id = options.id || INVALID_ID;
this.priority = options.priority || 0;
this.publishingInterval = _adjust_publishing_interval(options.publishingInterval);
this.maxKeepAliveCount = _adjust_maxKeepAliveCount(options.maxKeepAliveCount); // , this.publishingInterval);
this.resetKeepAliveCounter();
this.lifeTimeCount = _adjust_lifeTimeCount(options.lifeTimeCount || 0, this.maxKeepAliveCount, this.publishingInterval);
this.maxNotificationsPerPublish = _adjust_maxNotificationsPerPublish(options.maxNotificationsPerPublish);
this._life_time_counter = 0;
this.resetLifeTimeCounter();
// notification message that are ready to be sent to the client
this._pending_notifications = new queue_1.Queue();
this._sent_notification_messages = [];
this._sequence_number_generator = new node_opcua_secure_channel_1.SequenceNumberGenerator();
// initial state of the subscription
this.state = SubscriptionState.CREATING;
this.publishIntervalCount = 0;
this.monitoredItems = {}; // monitored item map
this.monitoredItemIdCounter = 0;
this.publishingEnabled = _adjust_publishingEnable(options.publishingEnabled);
this.subscriptionDiagnostics = createSubscriptionDiagnostics(this);
// A boolean value that is set to TRUE to mean that either a NotificationMessage or a keep-alive
// Message has been sent on the Subscription. It is a flag that is used to ensure that either a
// NotificationMessage or a keep-alive Message is sent out the first time the publishing
// timer expires.
this.messageSent = false;
this.timerId = null;
this._start_timer({ firstTime: true });
debugLog(chalk_1.default.green(`creating subscription ${this.id}`));
this.serverCapabilities = options.serverCapabilities;
this.serverCapabilities.maxMonitoredItems =
this.serverCapabilities.maxMonitoredItems || Subscription.defaultMaxMonitoredItemCount;
this.serverCapabilities.maxMonitoredItemsPerSubscription =
this.serverCapabilities.maxMonitoredItemsPerSubscription || Subscription.defaultMaxMonitoredItemCount;
this.globalCounter = options.globalCounter;
}
getSessionId() {
return this.sessionId;
}
toString() {
let str = "Subscription:\n";
str += " subscriptionId " + this.id + "\n";
str += " sessionId " + this.getSessionId()?.toString() + "\n";
str += " publishingEnabled " + this.publishingEnabled + "\n";
str += " maxKeepAliveCount " + this.maxKeepAliveCount + "\n";
str += " publishingInterval " + this.publishingInterval + "\n";
str += " lifeTimeCount " + this.lifeTimeCount + "\n";
str += " maxKeepAliveCount " + this.maxKeepAliveCount + "\n";
return str;
}
/**
* modify subscription parameters
* @param param
*/
modify(param) {
// update diagnostic counter
this.subscriptionDiagnostics.modifyCount += 1;
const publishingInterval_old = this.publishingInterval;
param.requestedPublishingInterval = param.requestedPublishingInterval || 0;
param.requestedMaxKeepAliveCount = param.requestedMaxKeepAliveCount || this.maxKeepAliveCount;
param.requestedLifetimeCount = param.requestedLifetimeCount || this.lifeTimeCount;
this.publishingInterval = _adjust_publishing_interval(param.requestedPublishingInterval);
this.maxKeepAliveCount = _adjust_maxKeepAliveCount(param.requestedMaxKeepAliveCount);
this.lifeTimeCount = _adjust_lifeTimeCount(param.requestedLifetimeCount, this.maxKeepAliveCount, this.publishingInterval);
this.maxNotificationsPerPublish = _adjust_maxNotificationsPerPublish(param.maxNotificationsPerPublish || 0);
this.priority = param.priority || 0;
this.resetLifeTimeAndKeepAliveCounters();
if (publishingInterval_old !== this.publishingInterval) {
// todo
}
this._stop_timer();
this._start_timer({ firstTime: false });
}
/**
* set publishing mode
* @param publishingEnabled
*/
setPublishingMode(publishingEnabled) {
this.publishingEnabled = !!publishingEnabled;
// update diagnostics
if (this.publishingEnabled) {
this.subscriptionDiagnostics.enableCount += 1;
}
else {
this.subscriptionDiagnostics.disableCount += 1;
}
this.resetLifeTimeCounter();
if (!publishingEnabled && this.state !== SubscriptionState.CLOSED) {
this.state = SubscriptionState.NORMAL;
}
return node_opcua_status_code_1.StatusCodes.Good;
}
/**
* @private
*/
get keepAliveCounterHasExpired() {
return this._keep_alive_counter >= this.maxKeepAliveCount || this.state === SubscriptionState.LATE;
}
/**
* Reset the Lifetime Counter Variable to the value specified for the lifetime of a Subscription in
* the CreateSubscription Service( 5.13.2).
* @private
*/
resetLifeTimeCounter() {
this._life_time_counter = 0;
}
/**
* @private
*/
increaseLifeTimeCounter() {
this._life_time_counter += 1;
if (this._life_time_counter >= this.lifeTimeCount) {
this.emit("lifeTimeExpired");
}
this.emit("lifeTimeCounterChanged", this._life_time_counter);
}
/**
* True if the subscription life time has expired.
*
*/
get lifeTimeHasExpired() {
(0, node_opcua_assert_1.assert)(this.lifeTimeCount > 0);
return this._life_time_counter >= this.lifeTimeCount;
}
/**
* number of milliseconds before this subscription times out (lifeTimeHasExpired === true);
*/
get timeToExpiration() {
return (this.lifeTimeCount - this._life_time_counter) * this.publishingInterval;
}
get timeToKeepAlive() {
return (this.maxKeepAliveCount - this._keep_alive_counter) * this.publishingInterval;
}
/**
* Terminates the subscription.
* Calling this method will also remove any monitored items.
*
*/
terminate() {
(0, node_opcua_assert_1.assert)(arguments.length === 0);
debugLog("Subscription#terminate status", SubscriptionState[this.state]);
if (this.state === SubscriptionState.CLOSED) {
// todo verify if asserting is required here
return;
}
// stop timer
this._stop_timer();
debugLog("terminating Subscription ", this.id, " with ", this.monitoredItemCount, " monitored items");
// dispose all monitoredItem
const keys = Object.keys(this.monitoredItems);
for (const key of keys) {
const status = this.removeMonitoredItem(parseInt(key, 10));
(0, node_opcua_assert_1.assert)(status === node_opcua_status_code_1.StatusCodes.Good);
}
(0, node_opcua_assert_1.assert)(this.monitoredItemCount === 0);
if (this.$session && this.$session._unexposeSubscriptionDiagnostics) {
this.$session._unexposeSubscriptionDiagnostics(this);
}
this.state = SubscriptionState.CLOSED;
/**
* notify the subscription owner that the subscription has been terminated.
* @event "terminated"
*/
this.emit("terminated");
if (this.publishEngine) {
this.publishEngine.on_close_subscription(this);
}
}
setTriggering(triggeringItemId, linksToAdd, linksToRemove) {
/** Bad_NothingToDo, Bad_TooManyOperations,Bad_SubscriptionIdInvalid, Bad_MonitoredItemIdInvalid */
linksToAdd = linksToAdd || [];
linksToRemove = linksToRemove || [];
if (linksToAdd.length === 0 && linksToRemove.length === 0) {
return { statusCode: node_opcua_status_code_1.StatusCodes.BadNothingToDo, addResults: [], removeResults: [] };
}
const triggeringItem = this.getMonitoredItem(triggeringItemId);
const monitoredItemsToAdd = linksToAdd.map((id) => this.getMonitoredItem(id));
const monitoredItemsToRemove = linksToRemove.map((id) => this.getMonitoredItem(id));
if (!triggeringItem) {
const removeResults1 = monitoredItemsToRemove.map((m) => m ? node_opcua_status_code_1.StatusCodes.Good : node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid);
const addResults1 = monitoredItemsToAdd.map((m) => m ? node_opcua_status_code_1.StatusCodes.Good : node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid);
return {
statusCode: node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid,
addResults: addResults1,
removeResults: removeResults1
};
}
//
// note: it seems that CTT imposed that we do remove before add
const removeResults = monitoredItemsToRemove.map((m) => !m ? node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid : triggeringItem.removeLinkItem(m.monitoredItemId));
const addResults = monitoredItemsToAdd.map((m) => !m ? node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid : triggeringItem.addLinkItem(m.monitoredItemId));
const statusCode = node_opcua_status_code_1.StatusCodes.Good;
// do binding
return {
statusCode,
addResults,
removeResults
};
}
dispose() {
// istanbul ignore next
if (doDebug) {
debugLog("Subscription#dispose", this.id, this.monitoredItemCount);
}
(0, node_opcua_assert_1.assert)(this.monitoredItemCount === 0, "MonitoredItems haven't been deleted first !!!");
(0, node_opcua_assert_1.assert)(this.timerId === null, "Subscription timer haven't been terminated");
if (this.subscriptionDiagnostics) {
this.subscriptionDiagnostics.$subscription = null;
}
this.publishEngine = undefined;
this._pending_notifications.clear();
this._sent_notification_messages = [];
this.$session = undefined;
this.removeAllListeners();
Subscription.registry.unregister(this);
}
get aborted() {
const session = this.$session;
if (!session) {
return true;
}
return session.aborted;
}
/**
* number of pending notifications
*/
get pendingNotificationsCount() {
return this._pending_notifications ? this._pending_notifications.size : 0;
}
/**
* is 'true' if there are pending notifications for this subscription. (i.e moreNotifications)
*/
get hasPendingNotifications() {
return this.pendingNotificationsCount > 0;
}
/**
* number of sent notifications
*/
get sentNotificationMessageCount() {
return this._sent_notification_messages.length;
}
/**
* @internal
*/
_flushSentNotifications() {
const tmp = this._sent_notification_messages;
this._sent_notification_messages = [];
return tmp;
}
/**
* number of monitored items handled by this subscription
*/
get monitoredItemCount() {
return Object.keys(this.monitoredItems).length;
}
/**
* number of disabled monitored items.
*/
get disabledMonitoredItemCount() {
return Object.values(this.monitoredItems).reduce((sum, monitoredItem) => {
return sum + (monitoredItem.monitoringMode === node_opcua_service_subscription_2.MonitoringMode.Disabled ? 1 : 0);
}, 0);
}
/**
* The number of unacknowledged messages saved in the republish queue.
*/
get unacknowledgedMessageCount() {
return this.subscriptionDiagnostics.unacknowledgedMessageCount;
}
/**
* adjust monitored item sampling interval
* - an samplingInterval ===0 means that we use a event-base model ( no sampling)
* - otherwise the sampling is adjusted
* @private
*/
adjustSamplingInterval(samplingInterval, node) {
if (samplingInterval < 0) {
// - The value -1 indicates that the default sampling interval defined by the publishing
// interval of the Subscription is requested.
// - Any negative number is interpreted as -1.
samplingInterval = this.publishingInterval;
}
else if (samplingInterval === 0) {
// istanbul ignore next
if (!node)
throw new Error("Internal Error");
// OPCUA 1.0.3 Part 4 - 5.12.1.2
// The value 0 indicates that the Server should use the fastest practical rate.
// The fastest supported sampling interval may be equal to 0, which indicates
// that the data item is exception-based rather than being sampled at some period.
// An exception-based model means that the underlying system does not require
// sampling and reports data changes.
const dataValueSamplingInterval = node.readAttribute(node_opcua_address_space_1.SessionContext.defaultContext, node_opcua_data_model_1.AttributeIds.MinimumSamplingInterval);
// TODO if attributeId === AttributeIds.Value : sampling interval required here
if (dataValueSamplingInterval.statusCode.isGood()) {
// node provides a Minimum sampling interval ...
samplingInterval = dataValueSamplingInterval.value.value;
(0, node_opcua_assert_1.assert)(samplingInterval >= 0 && samplingInterval <= monitored_item_1.MonitoredItem.maximumSamplingInterval);
// note : at this stage, a samplingInterval===0 means that the data item is really exception-based
}
}
else if (samplingInterval < monitored_item_1.MonitoredItem.minimumSamplingInterval) {
samplingInterval = monitored_item_1.MonitoredItem.minimumSamplingInterval;
}
else if (samplingInterval > monitored_item_1.MonitoredItem.maximumSamplingInterval) {
// If the requested samplingInterval is higher than the
// maximum sampling interval supported by the Server, the maximum sampling
// interval is returned.
samplingInterval = monitored_item_1.MonitoredItem.maximumSamplingInterval;
}
const node_minimumSamplingInterval = node && node.minimumSamplingInterval ? node.minimumSamplingInterval : 0;
samplingInterval = Math.max(samplingInterval, node_minimumSamplingInterval);
return samplingInterval;
}
/**
* create a monitored item
* @param addressSpace - address space
* @param timestampsToReturn - the timestamp to return
* @param monitoredItemCreateRequest - the parameters describing the monitored Item to create
*/
preCreateMonitoredItem(addressSpace, timestampsToReturn, monitoredItemCreateRequest) {
(0, node_opcua_assert_1.assert)(monitoredItemCreateRequest instanceof node_opcua_service_subscription_2.MonitoredItemCreateRequest);
function handle_error(statusCode) {
return {
createResult: new node_opcua_service_subscription_2.MonitoredItemCreateResult({ statusCode }),
monitoredItemCreateRequest
};
}
const itemToMonitor = monitoredItemCreateRequest.itemToMonitor;
const node = addressSpace.findNode(itemToMonitor.nodeId);
if (!node || (node.nodeClass !== node_opcua_data_model_1.NodeClass.Variable && node.nodeClass !== node_opcua_data_model_1.NodeClass.Object && node.nodeClass !== node_opcua_data_model_1.NodeClass.Method)) {
return handle_error(node_opcua_status_code_1.StatusCodes.BadNodeIdUnknown);
}
if (itemToMonitor.attributeId === node_opcua_data_model_1.AttributeIds.Value && !(node.nodeClass === node_opcua_data_model_1.NodeClass.Variable)) {
// AttributeIds.Value is only valid for monitoring value of UAVariables.
return handle_error(node_opcua_status_code_1.StatusCodes.BadAttributeIdInvalid);
}
if (itemToMonitor.attributeId === node_opcua_data_model_1.AttributeIds.INVALID) {
return handle_error(node_opcua_status_code_1.StatusCodes.BadAttributeIdInvalid);
}
if (!itemToMonitor.indexRange.isValid()) {
return handle_error(node_opcua_status_code_1.StatusCodes.BadIndexRangeInvalid);
}
// check dataEncoding applies only on Values
if (itemToMonitor.dataEncoding.name && itemToMonitor.attributeId !== node_opcua_data_model_1.AttributeIds.Value) {
return handle_error(node_opcua_status_code_1.StatusCodes.BadDataEncodingInvalid);
}
// check dataEncoding
if (!(0, node_opcua_data_model_1.isValidDataEncoding)(itemToMonitor.dataEncoding)) {
return handle_error(node_opcua_status_code_1.StatusCodes.BadDataEncodingUnsupported);
}
// check that item can be read by current user session
// filter
const requestedParameters = monitoredItemCreateRequest.requestedParameters;
const filter = requestedParameters.filter;
const statusCodeFilter = (0, validate_filter_1.validateFilter)(filter, itemToMonitor, node);
if (statusCodeFilter !== node_opcua_status_code_1.StatusCodes.Good) {
return handle_error(statusCodeFilter);
}
// do we have enough room for new monitored items ?
if (this.monitoredItemCount >= this.serverCapabilities.maxMonitoredItemsPerSubscription) {
return handle_error(node_opcua_status_code_1.StatusCodes.BadTooManyMonitoredItems);
}
if (this.globalCounter.totalMonitoredItemCount >= this.serverCapabilities.maxMonitoredItems) {
return handle_error(node_opcua_status_code_1.StatusCodes.BadTooManyMonitoredItems);
}
const createResult = this._createMonitoredItemStep2(timestampsToReturn, monitoredItemCreateRequest, node);
(0, node_opcua_assert_1.assert)(createResult.statusCode.isGood());
const monitoredItem = this.getMonitoredItem(createResult.monitoredItemId);
// istanbul ignore next
if (!monitoredItem) {
throw new Error("internal error");
}
// TODO: fix old way to set node. !!!!
monitoredItem.setNode(node);
this.emit("monitoredItem", monitoredItem, itemToMonitor);
return { monitoredItem, monitoredItemCreateRequest, createResult };
}
async applyOnMonitoredItem(functor) {
for (const m of Object.values(this.monitoredItems)) {
await functor(m);
}
}
postCreateMonitoredItem(monitoredItem, monitoredItemCreateRequest, createResult) {
this._createMonitoredItemStep3(monitoredItem, monitoredItemCreateRequest);
}
async createMonitoredItem(addressSpace, timestampsToReturn, monitoredItemCreateRequest) {
const { monitoredItem, createResult } = this.preCreateMonitoredItem(addressSpace, timestampsToReturn, monitoredItemCreateRequest);
this.postCreateMonitoredItem(monitoredItem, monitoredItemCreateRequest, createResult);
return createResult;
}
/**
* get a monitoredItem by Id.
* @param monitoredItemId : the id of the monitored item to get.
* @return the monitored item matching monitoredItemId
*/
getMonitoredItem(monitoredItemId) {
return this.monitoredItems[monitoredItemId] || null;
}
/**
* remove a monitored Item from the subscription.
* @param monitoredItemId : the id of the monitored item to get.
*/
removeMonitoredItem(monitoredItemId) {
debugLog("Removing monitoredIem ", monitoredItemId);
if (!Object.prototype.hasOwnProperty.call(this.monitoredItems, monitoredItemId.toString())) {
return node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid;
}
const monitoredItem = this.monitoredItems[monitoredItemId];
monitoredItem.terminate();
/**
*
* notify that a monitored item has been removed from the subscription
* @param monitoredItem {MonitoredItem}
*/
this.emit("removeMonitoredItem", monitoredItem);
monitoredItem.dispose();
delete this.monitoredItems[monitoredItemId];
this.globalCounter.totalMonitoredItemCount -= 1;
this._removePendingNotificationsFor(monitoredItemId);
// flush pending notifications
// assert(this._pending_notifications.size === 0);
return node_opcua_status_code_1.StatusCodes.Good;
}
/**
* rue if monitored Item have uncollected Notifications
*/
get hasUncollectedMonitoredItemNotifications() {
if (this._hasUncollectedMonitoredItemNotifications) {
return true;
}
const keys = Object.keys(this.monitoredItems);
const n = keys.length;
for (let i = 0; i < n; i++) {
const key = parseInt(keys[i], 10);
const monitoredItem = this.monitoredItems[key];
if (monitoredItem.hasMonitoredItemNotifications) {
this._hasUncollectedMonitoredItemNotifications = true;
return true;
}
}
return false;
}
get subscriptionId() {
return this.id;
}
getMessageForSequenceNumber(sequenceNumber) {
const notification_message = this._sent_notification_messages.find((e) => e.sequenceNumber === sequenceNumber);
return notification_message || null;
}
/**
* returns true if the notification has expired
* @param notification
*/
notificationHasExpired(notification) {
(0, node_opcua_assert_1.assert)(Object.prototype.hasOwnProperty.call(notification, "start_tick"));
(0, node_opcua_assert_1.assert)(isFinite(notification.start_tick + this.maxKeepAliveCount));
return notification.start_tick + this.maxKeepAliveCount < this.publishIntervalCount;
}
/**
* returns in an array the sequence numbers of the notifications that have been sent
* and that haven't been acknowledged yet.
*/
getAvailableSequenceNumbers() {
const availableSequenceNumbers = _getSequenceNumbers(this._sent_notification_messages);
return availableSequenceNumbers;
}
/**
* acknowledges a notification identified by its sequence number
*/
acknowledgeNotification(sequenceNumber) {
debugLog("acknowledgeNotification ", sequenceNumber);
let foundIndex = -1;
this._sent_notification_messages.forEach((e, index) => {
if (e.sequenceNumber === sequenceNumber) {
foundIndex = index;
}
});
if (foundIndex === -1) {
// istanbul ignore next
if (doDebug) {
debugLog(chalk_1.default.red("acknowledging sequence FAILED !!! "), chalk_1.default.cyan(sequenceNumber.toString()));
}
return node_opcua_status_code_1.StatusCodes.BadSequenceNumberUnknown;
}
else {
// istanbul ignore next
if (doDebug) {
debugLog(chalk_1.default.yellow("acknowledging sequence "), chalk_1.default.cyan(sequenceNumber.toString()));
}
this._sent_notification_messages.splice(foundIndex, 1);
this.subscriptionDiagnostics.unacknowledgedMessageCount--;
return node_opcua_status_code_1.StatusCodes.Good;
}
}
/**
* getMonitoredItems is used to get information about monitored items of a subscription.Its intended
* use is defined in Part 4. This method is the implementation of the Standard OPCUA GetMonitoredItems Method.
* from spec:
* This method can be used to get the list of monitored items in a subscription if CreateMonitoredItems
* failed due to a network interruption and the client does not know if the creation succeeded in the server.
*
*/
getMonitoredItems() {
const monitoredItems = Object.keys(this.monitoredItems);
const monitoredItemCount = monitoredItems.length;
const result = {
clientHandles: new Uint32Array(monitoredItemCount),
serverHandles: new Uint32Array(monitoredItemCount),
statusCode: node_opcua_status_code_1.StatusCodes.Good
};
for (let index = 0; index < monitoredItemCount; index++) {
const monitoredItemId = monitoredItems[index];
const serverHandle = parseInt(monitoredItemId, 10);
const monitoredItem = this.getMonitoredItem(serverHandle);
result.clientHandles[index] = monitoredItem.clientHandle;
// TODO: serverHandle is defined anywhere in the OPCUA Specification 1.02
// I am not sure what shall be reported for serverHandle...
// using monitoredItem.monitoredItemId instead...
// May be a clarification in the OPCUA Spec is required.
result.serverHandles[index] = serverHandle;
}
return result;
}
/**
* @private
*/
async resendInitialValues() {
this._keep_alive_counter = 0;
try {
const promises = [];
for (const monitoredItem of Object.values(this.monitoredItems)) {
promises.push((async () => {
try {
monitoredItem.resendInitialValue();
}
catch (err) {
warningLog("resendInitialValues:", monitoredItem.node?.nodeId.toString(), "error:", err.message);
}
})());
}
await Promise.all(promises);
}
catch (err) {
warningLog("resendInitialValues: error:", err.message);
}
// make sure data will be sent immediately
this._keep_alive_counter = this.maxKeepAliveCount - 1;
this.state = SubscriptionState.NORMAL;
this._harvestMonitoredItems();
}
/**
* @private
*/
notifyTransfer() {
// OPCUA UA Spec 1.0.3 : part 3 - page 82 - 5.13.7 TransferSubscriptions:
// If the Server transfers the Subscription to the new Session, the Server shall issue
// a StatusChangeNotification notificationMessage with the status code
// Good_SubscriptionTransferred to the old Session.
debugLog(chalk_1.default.red(" Subscription => Notifying Transfer "));
const notificationData = new node_opcua_service_subscription_2.StatusChangeNotification({
status: node_opcua_status_code_1.StatusCodes.GoodSubscriptionTransferred
});
if (this.publishEngine.pendingPublishRequestCount) {
// the GoodSubscriptionTransferred can be processed immediately
this._addNotificationMessage(notificationData);
debugLog(chalk_1.default.red("pendingPublishRequestCount"), this.publishEngine?.pendingPublishRequestCount);
this._publish_pending_notifications();
}
else {
debugLog(chalk_1.default.red("Cannot send GoodSubscriptionTransferred => lets create a TransferredSubscription "));
const ts = new i_server_side_publish_engine_1.TransferredSubscription({
generator: this._sequence_number_generator,
id: this.id,
publishEngine: this.publishEngine
});
ts._pending_notification = notificationData;
this.publishEngine._closed_subscriptions.push(ts);
}
}
/**
*
* the server invokes the resetLifeTimeAndKeepAliveCounters method of the subscription
* when the server has send a Publish Response, so that the subscription
* can reset its life time counter.
*
* @private
*/
resetLifeTimeAndKeepAliveCounters() {
this.resetLifeTimeCounter();
this.resetKeepAliveCounter();
}
_updateCounters(notificationMessage) {
for (const notificationData of notificationMessage.notificationData || []) {
// update diagnostics
if (notificationData instanceof node_opcua_service_subscription_2.DataChangeNotification) {
const nbNotifs = notificationData.monitoredItems.length;
this.subscriptionDiagnostics.dataChangeNotificationsCount += nbNotifs;
this.subscriptionDiagnostics.notificationsCount += nbNotifs;
}
else if (notificationData instanceof node_opcua_service_subscription_2.EventNotificationList) {
const nbNotifs = notificationData.events.length;
this.subscriptionDiagnostics.eventNotificationsCount += nbNotifs;
this.subscriptionDiagnostics.notificationsCount += nbNotifs;
}
else {
(0, node_opcua_assert_1.assert)(notificationData instanceof node_opcua_service_subscription_2.StatusChangeNotification);
// TODO
// note: :there is no way to count StatusChangeNotifications in opcua yet.
}
}
}
/**
* _publish_pending_notifications send a "notification" event:
*
* @private
*
* precondition
* - pendingPublishRequestCount > 0
*/
_publish_pending_notifications() {
const publishEngine = this.publishEngine;
const subscriptionId = this.id;
// preconditions
(0, node_opcua_assert_1.assert)(publishEngine.pendingPublishRequestCount > 0);
(0, node_opcua_assert_1.assert)(this.hasPendingNotifications);
const notificationMessage = this._popNotificationToSend();
if (notificationMessage.notificationData.length === 0) {
return; // nothing to do
}
const moreNotifications = this.hasPendingNotifications;
this.emit("notification", notificationMessage);
// Update counters ....
this._updateCounters(notificationMessage);
(0, node_opcua_assert_1.assert)(Object.prototype.hasOwnProperty.call(notificationMessage, "sequenceNumber"));
(0, node_opcua_assert_1.assert)(Object.prototype.hasOwnProperty.call(notificationMessage, "notificationData"));
// update diagnostics
this.subscriptionDiagnostics.publishRequestCount += 1;
const response = new node_opcua_service_subscription_2.PublishResponse({
moreNotifications,
notificationMessage: {
notificationData: notificationMessage.notificationData,
sequenceNumber: this._get_next_sequence_number()
},
subscriptionId
});
this._sent_notification_messages.push(response.notificationMessage);
// get available sequence number;
const availableSequenceNumbers = this.getAvailableSequenceNumbers();
(0, node_opcua_assert_1.assert)(!response.notificationMessage ||
availableSequenceNumbers[availableSequenceNumbers.length - 1] === response.notificationMessage.sequenceNumber);
response.availableSequenceNumbers = availableSequenceNumbers;
publishEngine._send_response(this, response);
this.messageSent = true;
this.subscriptionDiagnostics.unacknowledgedMessageCount++;
this.resetLifeTimeAndKeepAliveCounters();
// istanbul ignore next
if (doDebug) {
debugLog("Subscription sending a notificationMessage subscriptionId=", subscriptionId, "sequenceNumber = ", notificationMessage.sequenceNumber.toString(), notificationMessage.notificationData?.map((x) => x?.constructor.name).join(" "));
// debugLog(notificationMessage.toString());
}
if (this.state !== SubscriptionState.CLOSED) {
(0, node_opcua_assert_1.assert)(notificationMessage.notificationData.length > 0, "We are not expecting a keep-alive message here");
this.state = SubscriptionState.NORMAL;
debugLog("subscription " + this.id + chalk_1.default.bgYellow(" set to NORMAL"));
}
}
process_subscription() {
(0, node_opcua_assert_1.assert)(this.publishEngine.pendingPublishRequestCount > 0);
if (!this.publishingEnabled) {
// no publish to do, except keep alive
debugLog(" -> no publish to do, except keep alive");
this._process_keepAlive();
return;
}
if (!this.hasPendingNotifications && this.hasUncollectedMonitoredItemNotifications) {
// collect notification from monitored items
this._harvestMonitoredItems();
}
// let process them first
if (this.hasPendingNotifications) {
this._publish_pending_notifications();
if (this.state === SubscriptionState.NORMAL ||
(this.state === SubscriptionState.LATE && this.hasPendingNotifications)) {
// istanbul ignore next
if (doDebug) {
debugLog(" -> pendingPublishRequestCount > 0 " + "&& normal state => re-trigger tick event immediately ");
}
// let process an new publish request
setImmediate(this._tick.bind(this));
}
}
else {
this._process_keepAlive();
}
}
_process_keepAlive() {
this.increaseKeepAliveCounter();
if (this.keepAliveCounterHasExpired) {
debugLog(` -> _process_keepAlive => keepAliveCounterHasExpired`);
if (this._sendKeepAliveResponse()) {
this.resetLifeTimeAndKeepAliveCounters();
}
else {
debugLog(" -> subscription.state === LATE , " +
"because keepAlive Response cannot be send due to lack of PublishRequest");
if (this.messageSent || this.keepAliveCounterHasExpired) {
this.state = SubscriptionState.LATE;
}
}
}
}
_stop_timer() {
if (this.timerId) {
debugLog(chalk_1.default.bgWhite.blue("Subscription#_stop_timer subscriptionId="), this.id);
clearInterval(this.timerId);
this.timerId = null;
}
}
_start_timer({ firstTime }) {
debugLog(chalk_1.default.bgWhite.blue("Subscription#_start_timer subscriptionId="), this.id, " publishingInterval = ", this.publishingInterval);
(0, node_opcua_assert_1.assert)(this.timerId === null);
// from the spec:
// When a Subscription is created, the first Message is sent at the end of the first publishing cycle to
// inform the Client that the Subscription is operational. A NotificationMessage is sent if there are
// Notifications ready to be reported. If there are none, a keep-alive Message is sent instead that
// contains a sequence number of 1, indicating that the first NotificationMessage has not yet been sent.
// This is the only time a keep-alive Message is sent without waiting for the maximum keep-alive count
// to be reached, as specified in (f) above.
// make sure that a keep-alive Message will be send at the end of the first publishing cycle
// if there are no Notifications ready.
this._keep_alive_counter = this.