nope-js-node
Version:
NoPE Runtime for Nodejs. For Browser-Support please use nope-browser
280 lines (279 loc) • 11.2 kB
JavaScript
"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;