import { Injectable, Logger, OnModuleDestroy, OnModuleInit, } from '@nestjs/common'; import { connect, MqttClient } from 'mqtt'; export type MqttMessageListener = (topic: string, payload: Buffer) => void; @Injectable() export class MqttClientService implements OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(MqttClientService.name); private readonly listeners = new Set(); private client: MqttClient | null = null; onModuleInit() { const host = process.env.MQTT_HOST || 'mosquitto'; const port = Number(process.env.MQTT_PORT || 1883); const username = process.env.MQTT_USERNAME; const password = process.env.MQTT_PASSWORD; if (!username || !password) { throw new Error( 'MQTT_USERNAME and MQTT_PASSWORD are required; refusing to use a default broker credential.', ); } this.client = connect(`mqtt://${host}:${port}`, { username, password, clientId: `ulhub-backend-${Math.random().toString(16).slice(2)}`, }); this.client.on('connect', () => { this.logger.log(`Connected to MQTT broker at ${host}:${port}`); // devices/# = legacy device-direct namespace; ul/# = SRS §3.4.2 topic scheme // (app + device-direct). QoS 1 so the broker redelivers durable log records the // backend missed while disconnected (broker loss ≠ capture loss, SRS-SYN-1/7). this.client!.subscribe(['devices/#', 'ul/#'], { qos: 1 }, (err) => { if (err) { this.logger.error('Failed to subscribe to devices/# + ul/#', err); } else { this.logger.log('Subscribed to devices/# and ul/#'); } }); }); this.client.on('message', (topic, payload) => { for (const listener of this.listeners) { listener(topic, payload); } }); this.client.on('error', (error) => { this.logger.error(`MQTT client error: ${error.message}`); }); } async onModuleDestroy() { await this.client?.endAsync(); } onMessage(listener: MqttMessageListener): () => void { this.listeners.add(listener); return () => this.listeners.delete(listener); } publish(topic: string, payload: object) { this.client?.publish(topic, JSON.stringify(payload), { qos: 1 }, (err) => { if (err) { this.logger.error(`Failed to publish to ${topic}: ${err.message}`); } }); } }