@getanthill/datastore
Version:
Event-Sourced Datastore
178 lines (136 loc) • 3.98 kB
text/typescript
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;
}
}