UNPKG

node-opcua-client

Version:

pure nodejs OPCUA SDK - module client

962 lines (850 loc) 41.5 kB
/** * @module node-opcua-client-private */ // tslint:disable:unified-signatures import chalk from "chalk"; import { EventEmitter } from "events"; import { assert } from "node-opcua-assert"; import { promoteOpaqueStructureInNotificationData } from "node-opcua-client-dynamic-extension-object"; import { AttributeIds } from "node-opcua-data-model"; import { checkDebugFlag, make_debugLog, make_warningLog } from "node-opcua-debug"; import { resolveNodeId } from "node-opcua-nodeid"; import type { ReadValueIdOptions, TimestampsToReturn } from "node-opcua-service-read"; import { CreateSubscriptionRequest, type CreateSubscriptionResponse, type DataChangeNotification, type DeleteMonitoredItemsResponse, type DeleteSubscriptionsResponse, type EventNotificationList, type ModifySubscriptionRequestOptions, type ModifySubscriptionResponse, MonitoringMode, type MonitoringParametersOptions, type NotificationData, type NotificationMessage, SetTriggeringRequest, type SetTriggeringResponse, type StatusChangeNotification } from "node-opcua-service-subscription"; import { type Callback, type ErrorCallback, type StatusCode, StatusCodes } from "node-opcua-status-code"; import { isNullOrUndefined } from "node-opcua-utils"; import type { ClientMonitoredItem } from "../client_monitored_item"; import type { ClientMonitoredItemBase } from "../client_monitored_item_base"; import type { ClientMonitoredItemGroup } from "../client_monitored_item_group"; import { ClientMonitoredItemToolbox } from "../client_monitored_item_toolbox"; import type { ClientSession, MonitoredItemData, SubscriptionId } from "../client_session"; import { type ClientHandle, type ClientMonitoredItemBaseMap, ClientSubscription, type ClientSubscriptionOptions, type ModifySubscriptionOptions, type ModifySubscriptionResult } from "../client_subscription"; import { ClientMonitoredItemGroupImpl } from "./client_monitored_item_group_impl"; import { ClientMonitoredItemImpl } from "./client_monitored_item_impl"; import type { ClientSidePublishEngine } from "./client_publish_engine"; import type { ClientSessionImpl } from "./client_session_impl"; import { detectLongOperation } from "./performance"; const debugLog = make_debugLog("CLIENT_SUBSCRIPTION"); const doDebug = checkDebugFlag("CLIENT_SUBSCRIPTION"); const warningLog = make_warningLog("CLIENT_SUBSCRIPTION"); export const PENDING_SUBSCRIPTION_ID = 0xc0cac01a; export const TERMINATED_SUBSCRIPTION_ID = 0xc0cac01b; export const TERMINATING_SUBSCRIPTION_ID = 0xc0cac01c; const minimumMaxKeepAliveCount = 3; function displayKeepAliveWarning(sessionTimeout: number, maxKeepAliveCount: number, publishingInterval: number): boolean { const keepAliveInterval = maxKeepAliveCount * publishingInterval; // c8 ignore next if (sessionTimeout < keepAliveInterval) { warningLog( chalk.yellowBright( `[NODE-OPCUA-W09] The subscription parameters are not compatible with the session timeout ! session timeout = ${sessionTimeout} milliseconds maxKeepAliveCount = ${maxKeepAliveCount} publishingInterval = ${publishingInterval} milliseconds" It is important that the session timeout ( ${chalk.red(sessionTimeout)} ms) is largely greater than : (maxKeepAliveCount*publishingInterval = ${chalk.red(keepAliveInterval)} ms), otherwise you may experience unexpected disconnection from the server if your monitored items are not changing frequently.` ) ); if (sessionTimeout < 3000 && publishingInterval <= 1000) { warningLog(`[NODE-OPCUA-W10] You'll need to increase your sessionTimeout significantly.`); } if ( sessionTimeout >= 3000 && sessionTimeout < publishingInterval * minimumMaxKeepAliveCount && maxKeepAliveCount <= minimumMaxKeepAliveCount + 2 ) { warningLog(`[NODE-OPCUA-W11] your publishingInterval interval is probably too large, consider reducing it.`); } const idealMaxKeepAliveCount = Math.max(4, Math.floor((sessionTimeout * 0.8) / publishingInterval - 0.5)); const idealPublishingInternal = Math.min(publishingInterval, sessionTimeout / (idealMaxKeepAliveCount + 3)); const idealKeepAliveInterval = idealMaxKeepAliveCount * publishingInterval; warningLog( `[NODE-OPCUA-W12] An ideal value for maxKeepAliveCount could be ${idealMaxKeepAliveCount}. An ideal value for publishingInterval could be ${idealPublishingInternal} ms. This will make your subscription emit a keep alive signal every ${idealKeepAliveInterval} ms if no monitored items are generating notifications. for instance: const client = OPCUAClient.create({ requestedSessionTimeout: 30* 60* 1000, // 30 minutes }); ` ); if (!ClientSubscription.ignoreNextWarning) { throw new Error("[NODE-OPCUA-W09] The subscription parameters are not compatible with the session timeout "); } return true; } return false; } export class ClientSubscriptionImpl extends EventEmitter implements ClientSubscription { /** * the associated session * @property session * @type {ClientSession} */ public get session(): ClientSessionImpl { assert(this.hasSession, "expecting a valid session"); return this.publishEngine.session! as ClientSessionImpl; } public get hasSession(): boolean { return !!this.publishEngine?.session; } public get isActive(): boolean { return ( this.hasSession && !( this.subscriptionId === PENDING_SUBSCRIPTION_ID || this.subscriptionId === TERMINATED_SUBSCRIPTION_ID || this.subscriptionId === TERMINATING_SUBSCRIPTION_ID ) ); } public subscriptionId: SubscriptionId; public publishingInterval: number; public lifetimeCount: number; public maxKeepAliveCount: number; public maxNotificationsPerPublish: number; public publishingEnabled: boolean; public priority: number; #monitoredItems: ClientMonitoredItemBaseMap; public get monitoredItems(): ClientMonitoredItemBaseMap { return this.#monitoredItems; } public set monitoredItems(value: ClientMonitoredItemBaseMap) { this.#monitoredItems = value; } #monitoredItemGroups: ClientMonitoredItemGroup[] = []; public timeoutHint = 0; public publishEngine: ClientSidePublishEngine; public lastSequenceNumber: number; #nextClientHandle = 0; public hasTimedOut: boolean; constructor(session: ClientSession, options: ClientSubscriptionOptions) { super(); const sessionImpl = session as ClientSessionImpl; this.publishEngine = sessionImpl.getPublishEngine(); this.lastSequenceNumber = -1; options = options || {}; options.requestedPublishingInterval = options.requestedPublishingInterval || 100; options.requestedLifetimeCount = options.requestedLifetimeCount || 60; options.requestedMaxKeepAliveCount = options.requestedMaxKeepAliveCount || 10; options.requestedMaxKeepAliveCount = Math.max(options.requestedMaxKeepAliveCount, minimumMaxKeepAliveCount); // perform some verification const warningEmitted = displayKeepAliveWarning( session.timeout, options.requestedMaxKeepAliveCount, options.requestedPublishingInterval ); // c8 ignore next if (warningEmitted) { warningLog( JSON.stringify( { ...options }, null, " " ) ); } options.maxNotificationsPerPublish = isNullOrUndefined(options.maxNotificationsPerPublish) ? 0 : options.maxNotificationsPerPublish; options.publishingEnabled = !!options.publishingEnabled; options.priority = options.priority || 1; this.publishingInterval = options.requestedPublishingInterval; this.lifetimeCount = options.requestedLifetimeCount; this.maxKeepAliveCount = options.requestedMaxKeepAliveCount; this.maxNotificationsPerPublish = options.maxNotificationsPerPublish || 0; this.publishingEnabled = options.publishingEnabled === undefined ? true : options.publishingEnabled; this.priority = options.priority; this.subscriptionId = PENDING_SUBSCRIPTION_ID; this.#nextClientHandle = 0; this.#monitoredItems = {}; /** * set to True when the server has notified us that this subscription has timed out * ( maxLifeCounter x published interval without being able to process a PublishRequest * @property hasTimedOut * @type {boolean} */ this.hasTimedOut = false; setImmediate(() => { __create_subscription(this, (err?: Error) => { if (!err) { setImmediate(() => { /** * notify the observers that the subscription has now started * @event started */ this.emit("started", this.subscriptionId); }); } else { setImmediate(() => { /** * notify the observers that the subscription has now failed * @event failed */ this.emit("error", err); }); } }); }); } public terminate(...args: any[]): any { debugLog("Terminating client subscription ", this.subscriptionId); const callback = args[0]; assert(typeof callback === "function", "expecting a callback function"); if (this.subscriptionId === TERMINATED_SUBSCRIPTION_ID || this.subscriptionId === TERMINATING_SUBSCRIPTION_ID) { // already terminated... just ignore return callback(); } if (isFinite(this.subscriptionId)) { const subscriptionId = this.subscriptionId; this.subscriptionId = TERMINATING_SUBSCRIPTION_ID; this.publishEngine.unregisterSubscription(subscriptionId); if (!this.hasSession) { return this._terminate_step2(callback); } const session = this.session; if (!session) { return callback(new Error("no session")); } session.deleteSubscriptions( { subscriptionIds: [subscriptionId] }, (err: Error | null, response?: DeleteSubscriptionsResponse) => { if (response && response!.results![0] !== StatusCodes.Good) { debugLog("warning: deleteSubscription returned ", response.results); } if (err) { /** * notify the observers that an error has occurred * @event internal_error * @param err the error */ this.emit("internal_error", err); } this._terminate_step2(callback); } ); } else { debugLog("subscriptionId is not value ", this.subscriptionId); assert(this.subscriptionId === PENDING_SUBSCRIPTION_ID); this._terminate_step2(callback); } } /** */ public _nextClientHandle(): number { this.#nextClientHandle += 1; return this.#nextClientHandle; } public async monitor( itemToMonitor: ReadValueIdOptions, requestedParameters: MonitoringParametersOptions, timestampsToReturn: TimestampsToReturn, monitoringMode: MonitoringMode ): Promise<ClientMonitoredItemBase>; public monitor( itemToMonitor: ReadValueIdOptions, requestedParameters: MonitoringParametersOptions, timestampsToReturn: TimestampsToReturn, monitoringMode: MonitoringMode, done: Callback<ClientMonitoredItemBase> ): void; public monitor(...args: any[]): any { const itemToMonitor = args[0] as ReadValueIdOptions; const requestedParameters = args[1] as MonitoringParametersOptions; const timestampsToReturn = args[2] as TimestampsToReturn; const monitoringMode = typeof args[3] === "function" ? MonitoringMode.Reporting : (args[3] as MonitoringMode); const done = (typeof args[3] === "function" ? args[3] : args[4]) as Callback<ClientMonitoredItemBase>; assert(typeof done === "function", "expecting a function here"); itemToMonitor.nodeId = resolveNodeId(itemToMonitor.nodeId!); const monitoredItem = ClientMonitoredItem_create( this, itemToMonitor, requestedParameters, timestampsToReturn, monitoringMode, (err1?: Error | null, monitoredItem2?: ClientMonitoredItem) => { if (err1) { return done && done(err1); } done(err1 || null, monitoredItem); } ); } public async monitorItems( itemsToMonitor: ReadValueIdOptions[], requestedParameters: MonitoringParametersOptions, timestampsToReturn: TimestampsToReturn ): Promise<ClientMonitoredItemGroup>; public monitorItems( itemsToMonitor: ReadValueIdOptions[], requestedParameters: MonitoringParametersOptions, timestampsToReturn: TimestampsToReturn, done: Callback<ClientMonitoredItemGroup> ): void; public monitorItems(...args: any[]): any { const itemsToMonitor = args[0] as ReadValueIdOptions[]; const requestedParameters = args[1] as MonitoringParametersOptions; const timestampsToReturn = args[2] as TimestampsToReturn; const done = args[3] as Callback<ClientMonitoredItemGroup>; const monitoredItemGroup = new ClientMonitoredItemGroupImpl(this, itemsToMonitor, requestedParameters, timestampsToReturn); this._wait_for_subscription_to_be_ready((err?: Error) => { if (err) { return done(err); } monitoredItemGroup._monitor((err1?: Error) => { if (err1) { return done && done(err1); } done(err1!, monitoredItemGroup); }); }); } public _delete_monitored_items(monitoredItems: ClientMonitoredItemBase[], callback: ErrorCallback): void { assert(typeof callback === "function"); assert(Array.isArray(monitoredItems)); assert(this.isActive); for (const monitoredItem of monitoredItems) { this._remove(monitoredItem); } const session = this.session as ClientSessionImpl; session.deleteMonitoredItems( { monitoredItemIds: monitoredItems.map((monitoredItem) => monitoredItem.monitoredItemId), subscriptionId: this.subscriptionId }, (err: Error | null, response?: DeleteMonitoredItemsResponse) => { callback(err!); } ); } public async setPublishingMode(publishingEnabled: boolean): Promise<StatusCode>; public setPublishingMode(publishingEnabled: boolean, callback: Callback<StatusCode>): void; public setPublishingMode(...args: any[]): any { const publishingEnabled = args[0] as boolean; const callback = args[1] as Callback<StatusCode>; assert(typeof callback === "function"); const session = this.session as ClientSessionImpl; if (!session) { return callback(new Error("no session")); } const subscriptionId = this.subscriptionId as SubscriptionId; session.setPublishingMode(publishingEnabled, subscriptionId, (err: Error | null, statusCode?: StatusCode) => { if (err) { return callback(err); } /* c8 ignore next */ if (!statusCode) { return callback(new Error("Internal Error")); } if (statusCode.isNotGood()) { return callback(null, statusCode); } callback(null, StatusCodes.Good); }); } /** * */ public setTriggering( triggeringItem: ClientMonitoredItemBase, linksToAdd: ClientMonitoredItemBase[] | null, linksToRemove?: ClientMonitoredItemBase[] | null ): Promise<SetTriggeringResponse>; public setTriggering( triggeringItem: ClientMonitoredItemBase, linksToAdd: ClientMonitoredItemBase[] | null, linksToRemove: ClientMonitoredItemBase[] | null, callback: Callback<SetTriggeringResponse> ): void; public setTriggering(...args: any[]): any { const triggeringItem = args[0] as ClientMonitoredItemBase; const linksToAdd = args[1] as ClientMonitoredItemBase[] | null; const linksToRemove = args[2] as ClientMonitoredItemBase[] | null; const callback = args[3] as Callback<SetTriggeringResponse>; assert(typeof callback === "function"); const session = this.session as ClientSessionImpl; if (!session) { return callback(new Error("no session")); } const subscriptionId = this.subscriptionId; const triggeringItemId = triggeringItem.monitoredItemId!; const setTriggeringRequest = new SetTriggeringRequest({ linksToAdd: linksToAdd ? linksToAdd.map((i) => i.monitoredItemId!) : null, linksToRemove: linksToRemove ? linksToRemove.map((i) => i.monitoredItemId!) : null, subscriptionId, triggeringItemId }); session.setTriggering(setTriggeringRequest, (err: Error | null, response?: SetTriggeringResponse) => { if (err) { if (response) { // use soft error, no exceptions return callback(null, response); } else { return callback(err); } } // c8 ignore next if (!response) { return callback(new Error("Internal Error")); } callback(null, response); }); } // public subscription service public modify(options: ModifySubscriptionOptions, callback: Callback<ModifySubscriptionResult>): void; public modify(options: ModifySubscriptionOptions): Promise<ModifySubscriptionResult>; public modify(...args: any[]): any { const modifySubscriptionRequest = args[0] as ModifySubscriptionRequestOptions; const callback = args[1] as Callback<ModifySubscriptionResult>; const session = this.session as ClientSessionImpl; if (!session) { return callback(new Error("no session")); } modifySubscriptionRequest.subscriptionId = this.subscriptionId; modifySubscriptionRequest.priority = modifySubscriptionRequest.priority === undefined ? this.priority : modifySubscriptionRequest.priority; modifySubscriptionRequest.requestedLifetimeCount = modifySubscriptionRequest.requestedLifetimeCount === undefined ? this.lifetimeCount : modifySubscriptionRequest.requestedLifetimeCount; modifySubscriptionRequest.requestedMaxKeepAliveCount = modifySubscriptionRequest.requestedMaxKeepAliveCount === undefined ? this.maxKeepAliveCount : modifySubscriptionRequest.requestedMaxKeepAliveCount; modifySubscriptionRequest.requestedPublishingInterval = modifySubscriptionRequest.requestedPublishingInterval === undefined ? this.publishingInterval : modifySubscriptionRequest.requestedPublishingInterval; modifySubscriptionRequest.maxNotificationsPerPublish = modifySubscriptionRequest.maxNotificationsPerPublish === undefined ? this.maxNotificationsPerPublish : modifySubscriptionRequest.maxNotificationsPerPublish; session.modifySubscription(modifySubscriptionRequest, (err: Error | null, response?: ModifySubscriptionResponse) => { if (err || !response) { return callback(err); } this.publishingInterval = response.revisedPublishingInterval; this.lifetimeCount = response.revisedLifetimeCount; this.maxKeepAliveCount = response.revisedMaxKeepAliveCount; callback(null, response); }); } public getMonitoredItems(): Promise<MonitoredItemData>; public getMonitoredItems(callback: Callback<MonitoredItemData>): void; public getMonitoredItems(...args: any[]): any { this.session.getMonitoredItems(this.subscriptionId, args[0]); } public toString(): string { let str = ""; str += "subscriptionId : " + this.subscriptionId + "\n"; str += "publishingInterval : " + this.publishingInterval + "\n"; str += "lifetimeCount : " + this.lifetimeCount + "\n"; str += "maxKeepAliveCount : " + this.maxKeepAliveCount + "\n"; str += "hasTimedOut : " + this.hasTimedOut + "\n"; const timeToLive = this.lifetimeCount * this.publishingInterval; str += "(maxKeepAliveCount*publishingInterval: " + this.publishingInterval * this.maxKeepAliveCount + " ms)\n"; str += "(maxLifetimeCount*publishingInterval: " + timeToLive + " ms)\n"; const lastRequestSentTime = this.publishEngine.lastRequestSentTime; str += "lastRequestSentTime : " + lastRequestSentTime.toString() + "\n"; const duration = Date.now() - lastRequestSentTime.getTime(); const extra = duration - timeToLive > 0 ? chalk.red(" expired since " + (duration - timeToLive) / 1000 + " seconds") : chalk.green(" valid for " + -(duration - timeToLive) / 1000 + " seconds"); str += "timeSinceLast PR : " + duration + "ms" + extra + "\n"; str += "has expired : " + (duration > timeToLive) + "\n"; str += "(session timeout : " + this.session.timeout + " ms)\n"; return str; } /** * returns the approximated remaining life time of this subscription in milliseconds */ public evaluateRemainingLifetime(): number { const now = Date.now(); const timeout = this.publishingInterval * this.lifetimeCount; const lastRequestSentTime = this.publishEngine.lastRequestSentTime; const expiryTime = lastRequestSentTime.getTime() + timeout; return Math.max(0, expiryTime - now); } public _add_monitored_item(clientHandle: ClientHandle, monitoredItem: ClientMonitoredItemBase): void { assert(this.isActive, "subscription must be active and not terminated"); assert(monitoredItem.monitoringParameters.clientHandle === clientHandle); this.#monitoredItems[clientHandle] = monitoredItem; /** * notify the observers that a new monitored item has been added to the subscription. * @event item_added * @param the monitored item. */ this.emit("item_added", monitoredItem); } public _add_monitored_items_group(monitoredItemGroup: ClientMonitoredItemGroupImpl): void { this.#monitoredItemGroups.push(monitoredItemGroup); } public _wait_for_subscription_to_be_ready(done: ErrorCallback): void { let _watchDogCount = 0; const waitForSubscriptionAndMonitor = () => { _watchDogCount++; if (this.subscriptionId === PENDING_SUBSCRIPTION_ID) { // the subscriptionID is not yet known because the server hasn't replied yet // let postpone this call, a little bit, to let things happen setImmediate(waitForSubscriptionAndMonitor); } else if (this.subscriptionId === TERMINATED_SUBSCRIPTION_ID) { // the subscription has been terminated in the meantime // this indicates a potential issue in the code using this api. if (typeof done === "function") { done(new Error("subscription has been deleted")); } } else { done(); } }; setImmediate(waitForSubscriptionAndMonitor); } private __on_publish_response_DataChangeNotification(notification: DataChangeNotification) { assert(notification.schema.name === "DataChangeNotification"); const monitoredItems = notification.monitoredItems || []; let repeated = 0; for (const monitoredItem of monitoredItems) { const monitorItemObj = this.#monitoredItems[monitoredItem.clientHandle]; if (monitorItemObj) { if (monitorItemObj.itemToMonitor.attributeId === AttributeIds.EventNotifier) { warningLog( chalk.yellow("Warning"), chalk.cyan( " Server send a DataChangeNotification for an EventNotifier." + " EventNotificationList was expected" ) ); warningLog( chalk.cyan(" the Server may not be fully OPCUA compliant"), chalk.yellow(". This notification will be ignored.") ); } else { const monitoredItemImpl = monitorItemObj as ClientMonitoredItemImpl; monitoredItemImpl._notify_value_change(monitoredItem.value); } } else { repeated += 1; if (repeated === 1) { warningLog( "Receiving a notification for a unknown monitoredItem with clientHandle ", monitoredItem.clientHandle ); } } } // c8 ignore next if (repeated > 1) { warningLog("previous message repeated", repeated, "times"); } } private __on_publish_response_StatusChangeNotification(notification: StatusChangeNotification) { assert(notification.schema.name === "StatusChangeNotification"); debugLog("Client has received a Status Change Notification ", notification.status.toString()); if (notification.status === StatusCodes.GoodSubscriptionTransferred) { // 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("ClientSubscription#__on_publish_response_StatusChangeNotification : GoodSubscriptionTransferred"); // may be it has been transferred after a reconnection.... in this case should do nothing about it } if (notification.status === StatusCodes.BadTimeout) { // the server tells use that the subscription has timed out .. // this mean that this subscription has been closed on the server side and cannot process any // new PublishRequest. // // from Spec OPCUA Version 1.03 Part 4 - 5.13.1.1 Description : Page 69: // // h. Subscriptions have a lifetime counter that counts the number of consecutive publishing cycles in // which there have been no Publish requests available to send a Publish response for the // Subscription. Any Service call that uses the SubscriptionId or the processing of a Publish // response resets the lifetime counter of this Subscription. When this counter reaches the value // calculated for the lifetime of a Subscription based on the MaxKeepAliveCount parameter in the // CreateSubscription Service (5.13.2), the Subscription is closed. Closing the Subscription causes // its MonitoredItems to be deleted. In addition the Server shall issue a StatusChangeNotification // notificationMessage with the status code BadTimeout. // this.hasTimedOut = true; this.terminate(() => { /* empty */ }); } /** * notify the observers that the server has send a status changed notification (such as BadTimeout ) * @event status_changed */ this.emit("status_changed", notification.status, notification.diagnosticInfo); } private __on_publish_response_EventNotificationList(notification: EventNotificationList) { assert(notification.schema.name === "EventNotificationList"); const events = notification.events || []; for (const event of events) { const monitorItemObj = this.#monitoredItems[event.clientHandle]; assert(monitorItemObj, "Expecting a monitored item"); const monitoredItemImpl = monitorItemObj as ClientMonitoredItemImpl; monitoredItemImpl._notify_event(event.eventFields || []); } } public onNotificationMessage(notificationMessage: NotificationMessage): void { assert(Object.hasOwn(notificationMessage, "sequenceNumber")); this.lastSequenceNumber = notificationMessage.sequenceNumber; this.emit("raw_notification", notificationMessage); const notificationData = (notificationMessage.notificationData || []) as NotificationData[]; if (notificationData.length === 0) { // this is a keep alive message debugLog(chalk.yellow("Client : received a keep alive notification from client")); /** * notify the observers that a keep alive Publish Response has been received from the server. * @event keepalive */ this.emit("keepalive"); } else { /** * notify the observers that some notifications has been received from the server in a PublishResponse * each modified monitored Item * @event received_notifications */ this.emit("received_notifications", notificationMessage); // let publish a global event promoteOpaqueStructureInNotificationData(this.session, notificationData).then(() => { detectLongOperation( () => { // now process all notifications for (const notification of notificationData) { // c8 ignore next if (!notification) { continue; } // DataChangeNotification / StatusChangeNotification / EventNotification switch (notification.schema.name) { case "DataChangeNotification": // now inform each individual monitored item this.__on_publish_response_DataChangeNotification(notification as DataChangeNotification); break; case "StatusChangeNotification": this.__on_publish_response_StatusChangeNotification(notification as StatusChangeNotification); break; case "EventNotificationList": this.__on_publish_response_EventNotificationList(notification as EventNotificationList); break; default: warningLog(" Invalid notification :", notification.toString()); } } }, (duration: number) => { const s = (a: unknown) => { const b = a as { $_slowNotifCount: number; $_maxDuration: number }; b.$_slowNotifCount = b.$_slowNotifCount || 0; b.$_maxDuration = b.$_maxDuration || 0; return b; }; s(this).$_maxDuration = Math.max(s(this).$_maxDuration, duration); if (s(this).$_slowNotifCount > 0 && s(this).$_slowNotifCount % 1000 !== 0) return; s(this).$_slowNotifCount++; warningLog( `[NODE-OPCUA-W32]}: monitored.item event handler takes too much time : operation duration ${duration} ms [repeated ${ s(this).$_slowNotifCount } times]\n please ensure that your monitoredItem event handler is not blocking the event loop.` ); } ); }); } } private _terminate_step2(callback: (err?: Error) => void) { const monitoredItems = Object.values(this.#monitoredItems); for (const monitoredItem of monitoredItems) { this._remove(monitoredItem); } const monitoredItemGroups = this.#monitoredItemGroups; for (const monitoredItemGroup of monitoredItemGroups) { this._removeGroup(monitoredItemGroup); } assert(Object.values(this.#monitoredItems).length === 0); setImmediate(() => { /** * notify the observers that the client subscription has terminated * @event terminated */ this.subscriptionId = TERMINATED_SUBSCRIPTION_ID; this.emit("terminated"); callback(); }); } private _remove(monitoredItem: ClientMonitoredItemBase) { const clientHandle = monitoredItem.monitoringParameters.clientHandle; this._removeMonitoredItem(clientHandle); const priv = monitoredItem as ClientMonitoredItemImpl; priv._terminate_and_emit(); } public _removeMonitoredItem(clientHandle: ClientHandle) { if (this.#monitoredItems[clientHandle]) { delete this.#monitoredItems[clientHandle]; } } public _removeGroup(monitoredItemGroup: ClientMonitoredItemGroup): void { (monitoredItemGroup as any)._terminate_and_emit(); this.#monitoredItemGroups = this.#monitoredItemGroups.filter((obj) => obj !== monitoredItemGroup); } /** * @private * @param itemToMonitor * @param monitoringParameters * @param timestampsToReturn */ public _createMonitoredItem( itemToMonitor: ReadValueIdOptions, monitoringParameters: MonitoringParametersOptions, timestampsToReturn: TimestampsToReturn, monitoringMode: MonitoringMode = MonitoringMode.Reporting ): ClientMonitoredItem { /* c8 ignore next*/ const monitoredItem = new ClientMonitoredItemImpl( this, itemToMonitor, monitoringParameters, timestampsToReturn, monitoringMode ); return monitoredItem; } } export function ClientMonitoredItem_create( subscription: ClientSubscription, itemToMonitor: ReadValueIdOptions, monitoringParameters: MonitoringParametersOptions, timestampsToReturn: TimestampsToReturn, monitoringMode: MonitoringMode = MonitoringMode.Reporting, callback?: (err3?: Error | null, monitoredItem?: ClientMonitoredItem) => void ): ClientMonitoredItem { const subscriptionImpl = subscription as ClientSubscriptionImpl; if (!subscriptionImpl) { throw new Error("Invalid subscription"); } const monitoredItem = new ClientMonitoredItemImpl( subscriptionImpl, itemToMonitor, monitoringParameters, timestampsToReturn, monitoringMode ); setImmediate(() => { subscriptionImpl._wait_for_subscription_to_be_ready((err?: Error) => { if (err) { if (callback) { callback(err); } return; } ClientMonitoredItemToolbox._toolbox_monitor(subscription, timestampsToReturn, [monitoredItem], (err1?: Error) => { if (err1) { monitoredItem._terminate_and_emit(err1); } if (callback) { callback(err1, monitoredItem); } }); }); }); return monitoredItem; } // tslint:disable:no-var-requires // tslint:disable:max-line-length import { withCallback } from "thenify-ex"; const opts = { multiArgs: false }; ClientSubscriptionImpl.prototype.setPublishingMode = withCallback(ClientSubscriptionImpl.prototype.setPublishingMode); ClientSubscriptionImpl.prototype.monitor = withCallback(ClientSubscriptionImpl.prototype.monitor); ClientSubscriptionImpl.prototype.monitorItems = withCallback(ClientSubscriptionImpl.prototype.monitorItems); ClientSubscriptionImpl.prototype.setTriggering = withCallback(ClientSubscriptionImpl.prototype.setTriggering); ClientSubscriptionImpl.prototype.modify = withCallback(ClientSubscriptionImpl.prototype.modify); ClientSubscriptionImpl.prototype.terminate = withCallback(ClientSubscriptionImpl.prototype.terminate); ClientSubscriptionImpl.prototype.getMonitoredItems = withCallback(ClientSubscriptionImpl.prototype.getMonitoredItems); ClientSubscription.create = (clientSession: ClientSession, options: ClientSubscriptionOptions) => { return new ClientSubscriptionImpl(clientSession, options); }; export function __create_subscription(subscription: ClientSubscriptionImpl, callback: ErrorCallback) { // c8 ignore next if (!subscription.hasSession) { return callback(new Error("__create_subscription: subscription has no Session")); } const session = subscription.session; debugLog(chalk.yellow.bold("ClientSubscription created ")); const request = new CreateSubscriptionRequest({ maxNotificationsPerPublish: subscription.maxNotificationsPerPublish, priority: subscription.priority, publishingEnabled: subscription.publishingEnabled, requestedLifetimeCount: subscription.lifetimeCount, requestedMaxKeepAliveCount: subscription.maxKeepAliveCount, requestedPublishingInterval: subscription.publishingInterval }); session.createSubscription(request, (err: Error | null, response?: CreateSubscriptionResponse) => { if (err) { /* c8 ignore next */ subscription.emit("internal_error", err); if (callback) { return callback(err); } return; } /* c8 ignore next */ if (!response) { return callback(new Error("internal error")); } if (!subscription.hasSession) { return callback(new Error("createSubscription has failed = > no session")); } assert(subscription.hasSession); subscription.subscriptionId = response.subscriptionId; subscription.publishingInterval = response.revisedPublishingInterval; subscription.lifetimeCount = response.revisedLifetimeCount; subscription.maxKeepAliveCount = response.revisedMaxKeepAliveCount; subscription.timeoutHint = Math.min((subscription.maxKeepAliveCount + 10) * subscription.publishingInterval * 2, 0x7ffff); displayKeepAliveWarning(subscription.session.timeout, subscription.maxKeepAliveCount, subscription.publishingInterval); ClientSubscription.ignoreNextWarning = false; // c8 ignore next if (doDebug) { debugLog(chalk.yellow.bold("registering callback")); debugLog(chalk.yellow.bold("publishingInterval "), subscription.publishingInterval); debugLog(chalk.yellow.bold("lifetimeCount "), subscription.lifetimeCount); debugLog(chalk.yellow.bold("maxKeepAliveCount "), subscription.maxKeepAliveCount); debugLog(chalk.yellow.bold("publish request timeout hint = "), subscription.timeoutHint); debugLog(chalk.yellow.bold("hasTimedOut "), subscription.hasTimedOut); debugLog(chalk.yellow.bold("timeoutHint for publish request "), subscription.timeoutHint); } subscription.publishEngine.registerSubscription(subscription); if (callback) { callback(); } }); }