UNPKG

nope-js-node

Version:

NoPE Runtime for Nodejs. For Browser-Support please use nope-browser

280 lines (279 loc) 11.2 kB
"use strict"; /** * @author Martin Karkowski * @email m.karkowski@zema.de * @create date 2021-08-03 17:32:16 * @modify date 2021-08-03 21:14:12 * @desc [description] */ Object.defineProperty(exports, "__esModule", { value: true }); exports.MQTTLayer = void 0; const mqtt_1 = require("mqtt"); const mqtt_pattern_1 = require("mqtt-pattern"); const os_1 = require("os"); const idMethods_1 = require("../../helpers/idMethods"); const objectMethods_1 = require("../../helpers/objectMethods"); const stringMethods_1 = require("../../helpers/stringMethods"); const getLogger_1 = require("../../logger/getLogger"); const index_browser_1 = require("../../logger/index.browser"); const nopeObservable_1 = require("../../observables/nopeObservable"); function _mqttMatch(subscription, offered) { let _subscription = (0, stringMethods_1.replaceAll)(subscription, objectMethods_1.SPLITCHAR, "/"); let _offered = (0, stringMethods_1.replaceAll)(offered, objectMethods_1.SPLITCHAR, "/"); // Perform the Match let res = (0, mqtt_pattern_1.matches)(_subscription, _offered); if (res) { // If it is matching => Quit method return res; } // Check if the Topic matches the data, based on a shortend Topic if (_offered.split("/").length > _subscription.split("/").length && subscription.indexOf("+") === -1) { // Shorten the offered Topic _offered = _offered .split("/") .slice(0, _subscription.split("/").length) .join("/"); // Repreform the Matching res = (0, mqtt_pattern_1.matches)(_subscription, _offered); } else if (_offered.split("/").length < _subscription.split("/").length && subscription.indexOf("+") === -1) { // Shorten the Subscription _subscription = _subscription .split("/") .slice(0, _offered.split("/").length) .join("/"); // Repreform the Matching res = (0, mqtt_pattern_1.matches)(_subscription, _offered); } // TODO: Fix // Return the Result return res; } /** * Default implementation of an {@link ICommunicationInterface}. * * This layer will use mqtt to connect and transport messages. * * Defaultly all messages will be subscribed on the following topics: * - `+/nope/<eventname>` * * Defaultly all messages will be published on the following topics: * - `<preTopic>/nope/<eventname>` * - `preTopic` is set to the hostname. * * The Layer is able to forward data, events etc to default ports. * Asume data is emitted using the `dataChanged` emit. If the flag * `forwardToCustomTopics` is set to true, the path of the data will * directly forward to mqtt. */ class MQTTLayer { /** * Creates an instance of MQTTLayer. * @param {string} uri Uri of the Broker. e.g. `mqtt://localhost:1883` or `ws://localhost:9000`. * @param {ValidLoggerDefinition} [logger="info"] Logger level * @param {string} [preTopic=hostname()] Defaultly all messages will be published on the following topics: `<preTopic>/nope/<eventname>`. `preTopic` is defaultly set to the hostname of the node in which `NoPE` is running. * @param {(0 | 1 | 2)} [qos=2] The QOS of mqtt. see https://www.hivemq.com/blog/mqtt-essentials-part-6-mqtt-quality-of-service-levels/ for more details. Default = Exactly once. Otherwise there might be an issue. * @param {boolean} [forwardToCustomTopics=true] The Layer is able to forward data, events etc to default ports. This flag enables this behavior * @memberof MQTTLayer */ constructor(uri, logger = "info", preTopic = (0, os_1.hostname)(), qos = 2, forwardToCustomTopics = true) { this.uri = uri; this.preTopic = preTopic; this.qos = qos; this.forwardToCustomTopics = forwardToCustomTopics; // Make shure we use the http before connecting. this.uri = this.uri.startsWith("mqtt://") ? this.uri : "mqtt://" + this.uri; this.connected = new nopeObservable_1.NopeObservable(); this.connected.setContent(false); this._cbs = new Map(); this._logger = (0, getLogger_1.defineNopeLogger)(logger, "core.layer.mqtt"); this._logger.info("connecting to:", this.uri); this.considerConnection = true; this.id = (0, idMethods_1.generateId)(); this.receivesOwnMessages = true; // Create a Broker and use the provided ID this._client = (0, mqtt_1.connect)(this.uri); const _this = this; this._client.on("connect", () => { _this.connected.setContent(true); }); this._client.on("disconnect", () => { _this.connected.setContent(false); }); this._client.on("message", (topic, payload) => { var _a; const data = JSON.parse(payload.toString("utf-8")); for (const subscription of _this._cbs.keys()) { // Test if the Topic matches if (_mqttMatch(subscription, topic)) { if (((_a = _this._logger) === null || _a === void 0 ? void 0 : _a.enabledFor(index_browser_1.DEBUG)) && !topic.includes("nope/StatusChanged")) { _this._logger.debug("received", topic, data, _this._cbs.get(subscription).size); } for (const callback of _this._cbs.get(subscription)) { // Callback callback(data); } return; } } }); } /** * See {@link ICommunicationInterface.on} */ async on(eventname, cb) { return await this._on(`+/nope/${eventname}`, cb); } /** * See {@link ICommunicationInterface.emit} */ async emit(eventname, data) { await this._emit(`${this.preTopic}/nope/${eventname}`, data); if (this.forwardToCustomTopics) { switch (eventname) { case "dataChanged": { let topic = data.path; topic = this._adaptTopic(topic); await this._emit(topic, data.data); break; } case "event": { let topic = data.path; topic = this._adaptTopic(topic); await this._emit(topic, data.data); break; } case "rpcRequest": { let topic = data.functionId; topic = this._adaptTopic(topic); await this._emit(topic, data.params); break; } } } } _adaptTopic(topic) { return (0, stringMethods_1.replaceAll)(topic, ".", "/"); } /** * Internal Function to subscribe to a topic using a specific callback * @param topic the topic, which should be subscribed to * @param callback the callback to call * @returns */ _on(topic, callback) { const _this = this; const _topic = `${this._adaptTopic(topic)}`; return new Promise((resolve, reject) => { var _a; if (!_this._cbs.has(_topic)) { // No subscription is present: // create the subscription. _this._cbs.set(_topic, new Set()); if ((_a = _this._logger) === null || _a === void 0 ? void 0 : _a.enabledFor(index_browser_1.DEBUG)) { _this._logger.debug("subscribing :", _topic); } // Call the Subscription on MQTT _this._client.subscribe(_topic, { qos: _this.qos }, (err) => { if (err) { reject(err); } else { resolve(); } }); // Store the callback _this._cbs.get(_topic).add(callback); } else { // A susbcription is allready present: // Store the callback _this._cbs.get(_topic).add(callback); resolve(); } }); } /** * Internal function to remove a susbcription from a topic. * To be precise, we only remove the callback. * @param topic the topic, which should be unsubscribed * @param callback the callback to unsubscribe * @returns */ _off(topic, callback) { const _this = this; const _topic = this._adaptTopic(topic); return new Promise((resolve, reject) => { var _a; if (_this._cbs.has(_topic)) { _this._cbs.get(_topic).delete(callback); if (_this._cbs.get(_topic).size === 0) { _this._cbs.delete(_topic); if ((_a = _this._logger) === null || _a === void 0 ? void 0 : _a.enabledFor(index_browser_1.INFO)) { _this._logger.info("unsubscribing :", _topic); } _this._client.unsubscribe(_topic, {}, (err) => { if (err) { reject(err); } else { resolve(); } }); return; } } resolve(); }); } /** * Internal function to publish data on the given topic * @param topic The topic to publish the data on * @param data The data to publish * @returns */ _emit(topic, data) { const _this = this; const _topic = this._adaptTopic(topic); return new Promise((resolve, reject) => { var _a; // Publish the event try { if (((_a = _this._logger) === null || _a === void 0 ? void 0 : _a.enabledFor(index_browser_1.DEBUG)) && !_topic.startsWith(_this.preTopic + "/nope/StatusChanged")) { _this._logger.debug("emitting: ", _topic); } _this._client.publish(_topic, JSON.stringify(data), { qos: _this.qos }, (err) => { if (err) { reject(err); } else { resolve(); } }); } catch (e) { reject(e); } }); } /** * Function to dispose the Interface. * @returns nothing */ dispose() { const _this = this; return new Promise((resolve, reject) => { this._client.end(true, {}, (err) => { if (err) reject(err); else resolve(); }); }); } } exports.MQTTLayer = MQTTLayer;