UNPKG

node-opcua-client

Version:

pure nodejs OPCUA SDK - module client

352 lines 18.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClientSidePublishEngine = void 0; /** * @module node-opcua-client-private */ const chalk_1 = __importDefault(require("chalk")); const node_opcua_assert_1 = require("node-opcua-assert"); const node_opcua_date_time_1 = require("node-opcua-date-time"); const node_opcua_debug_1 = require("node-opcua-debug"); const node_opcua_service_subscription_1 = require("node-opcua-service-subscription"); 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); /** * A client side implementation to deal with publish service. * * @class ClientSidePublishEngine * The ClientSidePublishEngine encapsulates the mechanism to * deal with a OPCUA Server and constantly sending PublishRequest * The ClientSidePublishEngine also performs notification acknowledgements. * Finally, ClientSidePublishEngine dispatch PublishResponse to the correct * Subscription id callback * * @private */ class ClientSidePublishEngine { static publishRequestCountInPipeline = 5; timeoutHint; activeSubscriptionCount; nbPendingPublishRequests; nbMaxPublishRequestsAcceptedByServer; isSuspended; session; subscriptionAcknowledgements; /** * @internal * @private */ subscriptionMap; lastRequestSentTime = (0, node_opcua_date_time_1.getMinOPCUADate)(); constructor(session) { this.session = session; this.subscriptionAcknowledgements = []; this.subscriptionMap = {}; this.timeoutHint = 10000; // 10 s by default this.activeSubscriptionCount = 0; // number of pending Publish request sent to the server and awaited for being processed by the server this.nbPendingPublishRequests = 0; // the maximum number of publish requests we think that the server can queue. // we will adjust this value . this.nbMaxPublishRequestsAcceptedByServer = 1000; this.isSuspended = false; (0, node_opcua_assert_1.assert)(this.session, "Session must exist"); } /** * the number of active subscriptions managed by this publish engine. * @property subscriptionCount * @type {Number} */ get subscriptionCount() { return Object.keys(this.subscriptionMap).length; } suspend(suspendedState) { if (this.isSuspended === suspendedState) { // nothing to do ... return; } this.isSuspended = suspendedState; if (!this.isSuspended) { this.replenish_publish_request_queue(); } } acknowledge_notification(subscriptionId, sequenceNumber) { this.subscriptionAcknowledgements.push({ subscriptionId, sequenceNumber }); } cleanup_acknowledgment_for_subscription(subscriptionId) { this.subscriptionAcknowledgements = this.subscriptionAcknowledgements.filter((a) => a.subscriptionId !== subscriptionId); } /** * @private */ send_publish_request() { if (this.isSuspended) { return; } if (this.nbPendingPublishRequests >= this.nbMaxPublishRequestsAcceptedByServer) { return; } const session = this.session; if (session && !session.isChannelValid()) { // wait for channel to be valid setTimeout(() => { if (this.subscriptionCount) { this.send_publish_request(); } }, 100); } else { setImmediate(() => { if (!this.session || this.isSuspended) { // session has been terminated or suspended return; } this.internalSendPublishRequest(); }); } } /** * @private */ terminate() { debugLog("Terminated ClientPublishEngine "); this.session = null; } /** * @private */ registerSubscription(subscription) { debugLog("ClientSidePublishEngine#registerSubscription ", subscription.subscriptionId); const _subscription = subscription; (0, node_opcua_assert_1.assert)(arguments.length === 1); (0, node_opcua_assert_1.assert)(isFinite(subscription.subscriptionId)); (0, node_opcua_assert_1.assert)(!Object.hasOwn(this.subscriptionMap, subscription.subscriptionId)); // already registered ? (0, node_opcua_assert_1.assert)(typeof _subscription.onNotificationMessage === "function"); (0, node_opcua_assert_1.assert)(isFinite(subscription.timeoutHint)); this.activeSubscriptionCount += 1; this.subscriptionMap[subscription.subscriptionId] = _subscription; this.timeoutHint = Math.min(Math.max(this.timeoutHint, subscription.timeoutHint), 0x7ffffff); debugLog(" setting timeoutHint = ", this.timeoutHint, subscription.timeoutHint); this.replenish_publish_request_queue(); } /** * @private */ replenish_publish_request_queue() { // Spec 1.03 part 4 5.13.5 Publish // [..] in high latency networks, the Client may wish to pipeline Publish requests // to ensure cyclic reporting from the Server. Pipe-lining involves sending more than one Publish // request for each Subscription before receiving a response. For example, if the network introduces a // delay between the Client and the Server of 5 seconds and the publishing interval for a Subscription // is one second, then the Client will have to issue Publish requests every second instead of waiting for // a response to be received before sending the next request. this.send_publish_request(); // send more than one publish request to server to cope with latency for (let i = 0; i < ClientSidePublishEngine.publishRequestCountInPipeline - 1; i++) { this.send_publish_request(); } } /** * * @param subscriptionId * @private */ unregisterSubscription(subscriptionId) { debugLog("ClientSidePublishEngine#unregisterSubscription ", subscriptionId); (0, node_opcua_assert_1.assert)(isFinite(subscriptionId) && subscriptionId > 0); this.activeSubscriptionCount -= 1; // note : it is possible that we get here while the server has already requested // a session shutdown ... in this case it is possible that subscriptionId is already // removed if (Object.hasOwn(this.subscriptionMap, subscriptionId)) { delete this.subscriptionMap[subscriptionId]; } else { debugLog("ClientSidePublishEngine#unregisterSubscription cannot find subscription ", subscriptionId); } } getSubscriptionIds() { return Object.keys(this.subscriptionMap).map((a) => parseInt(a, 10)); } /*** * get the client subscription from Id */ getSubscription(subscriptionId) { (0, node_opcua_assert_1.assert)(isFinite(subscriptionId) && subscriptionId > 0); (0, node_opcua_assert_1.assert)(Object.hasOwn(this.subscriptionMap, subscriptionId)); return this.subscriptionMap[subscriptionId]; } hasSubscription(subscriptionId) { (0, node_opcua_assert_1.assert)(isFinite(subscriptionId) && subscriptionId > 0); return Object.hasOwn(this.subscriptionMap, subscriptionId); } internalSendPublishRequest() { (0, node_opcua_assert_1.assert)(this.session, "ClientSidePublishEngine terminated ?"); this.nbPendingPublishRequests += 1; debugLog(chalk_1.default.yellow("sending publish request "), this.nbPendingPublishRequests); const subscriptionAcknowledgements = this.subscriptionAcknowledgements; this.subscriptionAcknowledgements = []; // as started in the spec (Spec 1.02 part 4 page 81 5.13.2.2 Function DequeuePublishReq()) // the server will dequeue the PublishRequest in first-in first-out order // and will validate if the publish request is still valid by checking the timeoutHint in the RequestHeader. // If the request timed out, the server will send a BadTimeout service result for the request and de-queue // another publish request. // // in Part 4. page 144 Request Header the timeoutHint is described this way. // timeoutHint UInt32 This timeout in milliseconds is used in the Client side Communication Stack to // set the timeout on a per-call base. // For a Server this timeout is only a hint and can be used to cancel long running // operations to free resources. If the Server detects a timeout, he can cancel the // operation by sending the Service result BadTimeout. The Server should wait // at minimum the timeout after he received the request before cancelling the operation. // The value of 0 indicates no timeout. // In issue#40 (MonitoredItem on changed not fired), we have found that some server might wrongly interpret // the timeoutHint of the request header ( and will bang a BadTimeout regardless if client send timeoutHint=0) // as a work around here , we force the timeoutHint to be set to a suitable value. // // see https://github.com/node-opcua/node-opcua/issues/141 // This suitable value shall be at least the time between two keep alive signal that the server will send. // (i.e revisedLifetimeCount * revisedPublishingInterval) // also ( part 3 - Release 1.03 page 140) // The Server shall check the timeoutHint parameter of a PublishRequest before processing a PublishResponse. // If the request timed out, a BadTimeout Service result is sent and another PublishRequest is used. // The value of 0 indicates no timeout // in our case: (0, node_opcua_assert_1.assert)(this.nbPendingPublishRequests > 0); const calculatedTimeout = Math.min(0x7fffffff, this.nbPendingPublishRequests * this.timeoutHint); const publishRequest = new node_opcua_service_subscription_1.PublishRequest({ requestHeader: { timeoutHint: calculatedTimeout }, // see note subscriptionAcknowledgements }); let active = true; const session = this.session; session.publish(publishRequest, (err, response) => { this.nbPendingPublishRequests -= 1; this.lastRequestSentTime = new Date(); if (err) { debugLog(chalk_1.default.cyan("ClientSidePublishEngine.prototype.internalSendPublishRequest callback : "), chalk_1.default.yellow(err.message)); debugLog("'" + err.message + "'"); if (err.message.match("not connected")) { debugLog(chalk_1.default.bgWhite.red(" WARNING : CLIENT IS NOT CONNECTED :" + " MAY BE RECONNECTION IS IN PROGRESS")); debugLog("this.activeSubscriptionCount =", this.activeSubscriptionCount); // the previous publish request has ended up with an error because // the connection has failed ... // There is no need to send more publish request for the time being until reconnection is completed active = false; } // c8 ignore next if (err.message.match(/BadNoSubscription/) && this.activeSubscriptionCount >= 1) { // there is something wrong happening here. // the server tells us that there is no subscription for this session // but the client have some active subscription left. // This could happen if the client has missed or not received the StatusChange Notification debugLog(chalk_1.default.bgWhite.red(" WARNING: server tells that there is no Subscription, but client disagree")); debugLog("this.activeSubscriptionCount =", this.activeSubscriptionCount); active = false; } if (err.message.match(/BadSessionClosed|BadSessionIdInvalid/)) { // // server has closed the session .... // may be the session timeout is shorted than the subscription life time // and the client does not send intermediate keepAlive request to keep the connection working. // debugLog(chalk_1.default.bgWhite.red(" WARNING : Server tells that the session has closed ...")); debugLog(" the ClientSidePublishEngine shall now be disabled," + " as server will reject any further request"); // close all active subscription.... active = false; } if (err.message.match(/BadTooManyPublishRequests/)) { // preventing queue overflow // ------------------------- // if the client send too many publish requests that the server can queue, the server returns // a Service result of BadTooManyPublishRequests. // // let adjust the nbMaxPublishRequestsAcceptedByServer value so we never overflow the server // with extraneous publish requests in the future. // this.nbMaxPublishRequestsAcceptedByServer = Math.min(this.nbPendingPublishRequests, this.nbMaxPublishRequestsAcceptedByServer); active = false; if (this.nbPendingPublishRequests < 10) { warningLog(chalk_1.default.bgWhite.red(" warning : server tells that too many publish request has been send ...")); warningLog(" On our side nbPendingPublishRequests = ", this.nbPendingPublishRequests); warningLog(" => nbMaxPublishRequestsAcceptedByServer =", this.nbMaxPublishRequestsAcceptedByServer); } } if (err.message.match(/BadSecureChannelIdInvalid/)) { // This can happen transiently during session transfer to a new // SecureChannel (e.g. after server certificate change). The // PublishRequest arrived on the new channel before ActivateSession // completed the session transfer. We should pause and let the // reconnection flow replenish publish requests once the session // transfer completes. debugLog(chalk_1.default.bgWhite.yellow(" WARNING: BadSecureChannelIdInvalid on PublishRequest" + " - session transfer may be in progress")); active = false; } } else { // c8 ignore next if (doDebug) { debugLog(chalk_1.default.cyan("ClientSidePublishEngine.prototype.internalSendPublishRequest callback ")); } this._receive_publish_response(response); } // feed the server with a new publish Request to the server if (!this.isSuspended && active && this.activeSubscriptionCount > 0) { if (err && err.message.match(/Connection Break/)) { // do not renew when connection is broken } else { this.send_publish_request(); } } }); } _receive_publish_response(response) { debugLog(chalk_1.default.yellow("receive publish response")); // the id of the subscription sending the notification message const subscriptionId = response.subscriptionId; // the sequence numbers available in this subscription // for retransmission and not acknowledged by the client // -- var available_seq = response.availableSequenceNumbers; // has the server more notification for us ? // -- var moreNotifications = response.moreNotifications; const notificationMessage = response.notificationMessage; // notificationMessage.sequenceNumber // notificationMessage.publishTime // notificationMessage.notificationData[] notificationMessage.notificationData = notificationMessage.notificationData || []; if (notificationMessage.notificationData.length !== 0) { this.acknowledge_notification(subscriptionId, notificationMessage.sequenceNumber); } // else { // this is a keep-alive notification // in this case , we shall not acknowledge notificationMessage.sequenceNumber // which is only an information of what will be the future sequenceNumber. // } const subscription = this.subscriptionMap[subscriptionId]; if (subscription && this.session !== null) { try { // delegate notificationData to the subscription callback subscription.onNotificationMessage(notificationMessage); } catch (err) { // c8 ignore next if (doDebug) { debugLog(err); debugLog("Exception in onNotificationMessage"); } } } else { debugLog(" ignoring notificationMessage", notificationMessage, " for subscription", subscriptionId); debugLog(" because there is no subscription."); debugLog(" or because there is no session for the subscription (session terminated ?)."); } } } exports.ClientSidePublishEngine = ClientSidePublishEngine; //# sourceMappingURL=client_publish_engine.js.map