@meddv/ngx-mqtt
Version:
This library isn't just a wrapper around MQTT.js for angular. It uses observables and takes care of subscription handling and message routing.
421 lines (414 loc) • 17.1 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, NgModule, EventEmitter, Injectable, Inject } from '@angular/core';
import mqtt from 'mqtt';
import { BehaviorSubject, Subject, using, Subscription, merge, Observable } from 'rxjs';
import { publishReplay, publish, filter, refCount } from 'rxjs/operators';
var MqttConnectionState;
(function (MqttConnectionState) {
MqttConnectionState[MqttConnectionState["CLOSED"] = 0] = "CLOSED";
MqttConnectionState[MqttConnectionState["CONNECTING"] = 1] = "CONNECTING";
MqttConnectionState[MqttConnectionState["CONNECTED"] = 2] = "CONNECTED";
})(MqttConnectionState || (MqttConnectionState = {}));
const MQTT_SERVICE_OPTIONS = {
connectOnCreate: true,
hostname: 'localhost',
port: 1884,
path: ''
};
const MqttServiceConfig = new InjectionToken('NgxMqttServiceConfig');
const MqttClientService = new InjectionToken('NgxMqttClientService');
class MqttModule {
static forRoot(config, client) {
return {
ngModule: MqttModule,
providers: [
{
provide: MqttServiceConfig,
useValue: config
},
{
provide: MqttClientService,
useValue: client
}
]
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: MqttModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.1.2", ngImport: i0, type: MqttModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: MqttModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: MqttModule, decorators: [{
type: NgModule
}] });
const connect = mqtt.connect;
// A javascript function that takes two objects and merges them recursively
function mergeDeep(target, ...sources) {
if (!sources.length) {
return target;
}
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) {
Object.assign(target, { [key]: {} });
}
mergeDeep(target[key], source[key]);
}
else {
Object.assign(target, { [key]: source[key] });
}
}
}
return mergeDeep(target, ...sources);
}
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
/**
* With an instance of MqttService, you can observe and subscribe to MQTT in multiple places, e.g. in different components,
* to only subscribe to the broker once per MQTT filter.
* It also handles proper unsubscription from the broker, if the last observable with a filter is closed.
*/
class MqttService {
/**
* The constructor needs [connection options]{@link IMqttServiceOptions} regarding the broker and some
* options to configure behavior of this service, like if the connection to the broker
* should be established on creation of this service or not.
*/
constructor(options, client) {
this.options = options;
/** a map of all mqtt observables by filter */
this.observables = {};
/** the connection state */
this.state = new BehaviorSubject(MqttConnectionState.CLOSED);
/** an observable of the last mqtt message */
this.messages = new Subject();
this._clientId = this._generateClientId();
this._connectTimeout = 10000;
this._reconnectPeriod = 10000;
this._onConnect = new EventEmitter();
this._onReconnect = new EventEmitter();
this._onClose = new EventEmitter();
this._onOffline = new EventEmitter();
this._onError = new EventEmitter();
this._onEnd = new EventEmitter();
this._onMessage = new EventEmitter();
this._onSuback = new EventEmitter();
this._onPacketsend = new EventEmitter();
this._onPacketreceive = new EventEmitter();
this._handleOnConnect = (e) => {
if (this.options.connectOnCreate === true) {
Object.keys(this.observables).forEach((filterString) => {
this.client.subscribe(filterString);
});
}
this.state.next(MqttConnectionState.CONNECTED);
this._onConnect.emit(e);
};
this._handleOnReconnect = () => {
if (this.options.connectOnCreate === true) {
Object.keys(this.observables).forEach((filterString) => {
this.client.subscribe(filterString);
});
}
this.state.next(MqttConnectionState.CONNECTING);
this._onReconnect.emit();
};
this._handleOnClose = () => {
this.state.next(MqttConnectionState.CLOSED);
this._onClose.emit();
};
this._handleOnOffline = () => {
this._onOffline.emit();
};
this._handleOnError = (e) => {
this._onError.emit(e);
console.error(e);
};
this._handleOnEnd = () => {
this._onEnd.emit();
};
this._handleOnMessage = (topic, payload, packet) => {
this._onMessage.emit(packet);
if (packet.cmd === 'publish') {
this.messages.next(packet);
}
};
this._handleOnPacketsend = (e) => {
this._onPacketsend.emit(e);
};
this._handleOnPacketreceive = (e) => {
this._onPacketreceive.emit(e);
};
if (options.connectOnCreate !== false) {
this.connect({}, client);
}
this.state.subscribe();
}
/**
* gets the _clientId
*/
get clientId() {
return this._clientId;
}
/** An EventEmitter to listen to connect messages */
get onConnect() {
return this._onConnect;
}
/** An EventEmitter to listen to reconnect messages */
get onReconnect() {
return this._onReconnect;
}
/** An EventEmitter to listen to close messages */
get onClose() {
return this._onClose;
}
/** An EventEmitter to listen to offline events */
get onOffline() {
return this._onOffline;
}
/** An EventEmitter to listen to error events */
get onError() {
return this._onError;
}
/** An EventEmitter to listen to close messages */
get onEnd() {
return this._onEnd;
}
/** An EventEmitter to listen to message events */
get onMessage() {
return this._onMessage;
}
/** An EventEmitter to listen to packetsend messages */
get onPacketsend() {
return this._onPacketsend;
}
/** An EventEmitter to listen to packetreceive messages */
get onPacketreceive() {
return this._onPacketreceive;
}
/** An EventEmitter to listen to suback events */
get onSuback() {
return this._onSuback;
}
/** An actual connection state **/
get connectionState() {
return this.state.value;
}
/**
* This static method shall be used to determine whether a MQTT
* topic matches a given filter. The matching rules are specified in the MQTT
* standard documentation and in the library test suite.
*
* @param {string} filter A filter may contain wildcards like '#' and '+'.
* @param {string} topic A topic may not contain wildcards.
* @return {boolean} true on match and false otherwise.
*/
static filterMatchesTopic(filterString, topic) {
if (filterString[0] === '#' && topic[0] === '$') {
return false;
}
// Preparation: split and reverse on '/'. The JavaScript split function is sane.
const fs = (filterString || '').split('/').reverse();
const ts = (topic || '').split('/').reverse();
// This function is tail recursive and compares both arrays one element at a time.
const match = () => {
// Cutting of the last element of both the filter and the topic using pop().
const f = fs.pop();
const t = ts.pop();
switch (f) {
// In case the filter level is '#', this is a match no matter whether
// the topic is undefined on this level or not ('#' matches parent element as well!).
case '#':
return true;
// In case the filter level is '+', we shall dive into the recursion only if t is not undefined.
case '+':
return t ? match() : false;
// In all other cases the filter level must match the topic level,
// both must be defined and the filter tail must match the topic
// tail (which is determined by the recursive call of match()).
default:
return f === t && (f === undefined ? true : match());
}
};
return match();
}
/**
* connect manually connects to the mqtt broker.
*/
connect(opts, client) {
const options = mergeDeep(this.options || {}, opts);
const protocol = options.protocol || 'ws';
const hostname = options.hostname || 'localhost';
if (options.url) {
this._url = options.url;
}
else {
this._url = `${protocol}://${hostname}`;
this._url += options.port ? `:${options.port}` : '';
this._url += options.path ? `${options.path}` : '';
}
this.state.next(MqttConnectionState.CONNECTING);
const mergedOptions = mergeDeep({
clientId: this._clientId,
reconnectPeriod: this._reconnectPeriod,
connectTimeout: this._connectTimeout
}, options);
if (this.client) {
this.client.end(true);
}
if (!client) {
this.client = connect(this._url, mergedOptions);
}
else {
this.client = client;
}
this._clientId = mergedOptions.clientId;
this.client.on('connect', this._handleOnConnect);
this.client.on('reconnect', this._handleOnReconnect);
this.client.on('close', this._handleOnClose);
this.client.on('offline', this._handleOnOffline);
this.client.on('error', this._handleOnError);
this.client.stream.on('error', this._handleOnError);
this.client.on('end', this._handleOnEnd);
this.client.on('message', this._handleOnMessage);
this.client.on('packetsend', this._handleOnPacketsend);
this.client.on('packetreceive', this._handleOnPacketreceive);
}
/**
* disconnect disconnects from the mqtt client.
* This method `should` be executed when leaving the application.
*/
disconnect(force = true) {
if (!this.client) {
throw new Error('mqtt client not connected');
}
this.client.end(force);
}
/**
* With this method, you can observe messages for a mqtt topic.
* The observable will only emit messages matching the filter.
* The first one subscribing to the resulting observable executes a mqtt subscribe.
* The last one unsubscribing this filter executes a mqtt unsubscribe.
* Every new subscriber gets the latest message.
*/
observeRetained(filterString, opts = { qos: 1 }) {
return this._generalObserve(filterString, () => publishReplay(1), opts);
}
/**
* With this method, you can observe messages for a mqtt topic.
* The observable will only emit messages matching the filter.
* The first one subscribing to the resulting observable executes a mqtt subscribe.
* The last one unsubscribing this filter executes a mqtt unsubscribe.
*/
observe(filterString, opts = { qos: 1 }) {
return this._generalObserve(filterString, () => publish(), opts);
}
/**
* With this method, you can observe messages for a mqtt topic.
* The observable will only emit messages matching the filter.
* The first one subscribing to the resulting observable executes a mqtt subscribe.
* The last one unsubscribing this filter executes a mqtt unsubscribe.
* Depending on the publish function, the messages will either be replayed after new
* subscribers subscribe or the messages are just passed through
*/
_generalObserve(filterString, publishFn, opts) {
if (!this.client) {
throw new Error('mqtt client not connected');
}
if (!this.observables[filterString]) {
const rejected = new Subject();
this.observables[filterString] = using(
// resourceFactory: Do the actual ref-counting MQTT subscription.
// refcount is decreased on unsubscribe.
() => {
const subscription = new Subscription();
this.client.subscribe(filterString, opts, (err, granted) => {
if (granted) { // granted can be undefined when an error occurs when the client is disconnecting
granted.forEach((granted_) => {
if (granted_.qos === 128) {
delete this.observables[granted_.topic];
this.client.unsubscribe(granted_.topic);
rejected.error(`subscription for '${granted_.topic}' rejected!`);
}
this._onSuback.emit({ filter: filterString, granted: granted_.qos !== 128 });
});
}
});
subscription.add(() => {
delete this.observables[filterString];
this.client.unsubscribe(filterString);
});
return subscription;
},
// observableFactory: Create the observable that is consumed from.
// This part is not executed until the Observable returned by
// `observe` gets actually subscribed.
(subscription) => merge(rejected, this.messages))
.pipe(filter((msg) => MqttService.filterMatchesTopic(filterString, msg.topic)), publishFn(), refCount());
}
return this.observables[filterString];
}
/**
* This method returns an observable for a topic with optional options.
* After subscribing, the actual mqtt publication will be executed and
* the observable will emit an empty value and completes, if publishing was successful
* or throws an error, if the publication fails.
*/
publish(topic, message, options = {}) {
if (!this.client) {
throw new Error('mqtt client not connected');
}
return Observable.create((obs) => {
this.client.publish(topic, message, options, (error) => {
if (error) {
obs.error(error);
}
else {
obs.next();
obs.complete();
}
});
});
}
/**
* This method publishes a message for a topic with optional options.
* If an error occurs, it will throw.
*/
unsafePublish(topic, message, options = {}) {
if (!this.client) {
throw new Error('mqtt client not connected');
}
this.client.publish(topic, message, options, (error) => {
if (error) {
throw (error);
}
});
}
_generateClientId() {
return 'client-' + Math.random().toString(36).substr(2, 19);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: MqttService, deps: [{ token: MqttServiceConfig }, { token: MqttClientService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: MqttService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: MqttService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [MqttServiceConfig]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [MqttClientService]
}] }] });
/*
* Public API Surface of ngx-mqtt
*/
/**
* Generated bundle index. Do not edit.
*/
export { MQTT_SERVICE_OPTIONS, MqttClientService, MqttConnectionState, MqttModule, MqttService, MqttServiceConfig };
//# sourceMappingURL=meddv-ngx-mqtt.mjs.map