UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

178 lines (136 loc) 3.98 kB
import type { JSONSchemaType } from 'ajv'; import type { Access, AnyObject } from '../typings'; import MQTT, { AsyncMqttClient } from 'async-mqtt'; import merge from 'lodash/merge'; import get from 'lodash/get'; import unset from 'lodash/unset'; import omit from 'lodash/omit'; import Broker from './broker'; export default class MQTTClient extends Broker { static ERRORS = { NOT_CONNECTED: new Error('MQTT not connected yet'), }; private _client: AsyncMqttClient | null = null; get client() { if (this._client === null) { throw MQTTClient.ERRORS.NOT_CONNECTED; } return this._client; } async connect(): Promise<MQTTClient> { if (this._client !== null) { return this; } this._client = await MQTT.connectAsync( this.config.url, merge({}, this.config.options, { protocolVersion: 5, clean: true, }), ); this._client.on('message', this.onMessage.bind(this)); return this; } async end(): Promise<MQTTClient | undefined> { if (this._client === null) { return; } await this._client.end(); this._client = null; return this; } authenticate(tokens: Access[], handler: Function) { return (event: any, route: any, headers: any, opts: any) => { const token = headers?.['authorization']; if (tokens.find((t) => t.token === token)) { return handler(event, route, headers, opts); } this.telemetry?.logger.debug( '[events#authenticate] Authentication failed', { event, }, ); }; } onMessage(topic: string, message: any, packet: any) { const route = this.getRoute(topic); if (route === null) { return; } const event = JSON.parse(message.toString()); const headers = packet?.properties?.userProperties || {}; this.telemetry?.logger.debug('[services#mqtt] Handling event', { topic, event, headers: omit(headers, 'authorization'), }); const isValid = route.validate(event); if (!isValid) { this.logOnInvalid(event, route); return; } /* istanbul ignore next */ const ack = async () => null; /* istanbul ignore next */ const nack = async () => null; this.emit(route.original, event, this.parseTopic(topic, route), headers, { source: 'mqtt', original: packet, delivery: 0, ack, nack, }); } publish(topic: string, payload: any, options: any = {}) { let _options = merge( {}, { properties: { userProperties: get( this.config, 'options.properties.userProperties', {}, ), }, }, options, ); if ( Object.keys(get(_options, 'properties.userProperties', {})).length === 0 ) { _options = unset(_options, 'properies.userProperties'); } return this.client.publish( this.topicWithNamespace(topic), JSON.stringify(payload), _options, ); } subscribe(topic: string, schema: JSONSchemaType<AnyObject>) { const mappedTopic = this.mapTopic(topic, schema); this.topics.set(mappedTopic.topic, mappedTopic); this.addChannelToSpec(topic, schema); return this.client.subscribe( `$share/${this.config.group}/${mappedTopic.topic}`, ); } async next(topic: string, timeout = 1000): Promise<string> { let resolve: (value: string | PromiseLike<string>) => void; let _timeout: string | number | NodeJS.Timeout; const promise = new Promise<string>((_resolve, _reject) => { resolve = _resolve; _timeout = setTimeout( () => _reject(new Error('[mqtt#next] Message timeout')), timeout, ); }); const handler = (event: string | PromiseLike<string>) => { clearTimeout(_timeout); resolve(event); this.removeListener(topic, handler); }; this.on(topic, handler); return promise; } }