@robot.com/better-mqtt
Version:
A modern, TypeScript-first MQTT client library that provides a better developer experience with async iterators, shared subscriptions, and React hooks. Better MQTT is a wrapper around the excellent [mqtt.js](https://github.com/mqttjs/MQTT.js) library, enh
848 lines (633 loc) • 21.4 kB
Markdown
# Better MQTT
A modern, TypeScript-first MQTT client library that provides a better developer experience with async iterators, shared subscriptions, and React hooks. Better MQTT is a wrapper around the excellent [mqtt.js](https://github.com/mqttjs/MQTT.js) library, enhancing it with modern JavaScript features and a more developer-friendly API.
## Features
- 🚀 **Modern TypeScript API** - Built with TypeScript and modern JavaScript features
- 🔄 **Async Iterators** - Subscribe to topics and iterate over messages using `for await...of`
- 📡 **Shared Subscriptions** - Support for MQTT shared subscriptions (`$share/group/topic`) and queue subscriptions (`$queue/topic`)
- 🎯 **Topic Matching** - Extract parameters from wildcard topics (`+` and `#`)
- ⚡ **Multiple Parsers** - Built-in parsers for strings, JSON, and binary data
- 🎣 **React Hooks** - First-class React integration with hooks for MQTT operations
- 🔌 **Event Emitter** - Built on top of `ee-ts` for reliable event handling
- 🧪 **Fully Tested** - Comprehensive test coverage for all features
- ⚡ **Async Operations** - Both synchronous and asynchronous methods for all operations
## Installation
```bash
npm install @robot.com/better-mqtt
# or
pnpm add @robot.com/better-mqtt
# or
yarn add @robot.com/better-mqtt
# or
bun add @robot.com/better-mqtt
```
## Quick Start
### Basic Connection and Publishing
```typescript
import { BetterMQTT } from '@robot.com/better-mqtt'
// Connect to MQTT broker
const mqtt = await BetterMQTT.connectAsync({
host: 'localhost',
port: 1883,
clientId: 'my-client'
})
// Publish messages
mqtt.publish('sensors/temperature', '23.5')
mqtt.publishJson('sensors/humidity', { value: 65, unit: '%' })
// Clean up
mqtt.end()
```
### Subscribing and Receiving Messages
```typescript
// Subscribe to a topic
const subscription = mqtt.subscribeString('sensors/temperature')
// Method 1: Using async iterator
for await (const message of subscription) {
console.log(`Temperature: ${message.content}°C`)
console.log(`Topic: ${message.topic}`)
console.log(`Parameters: ${message.params}`)
// Break after first message
break
}
// Method 2: Using event listener
subscription.on('message', (message) => {
console.log(`Temperature: ${message.content}°C`)
})
// Unsubscribe when done
subscription.end()
```
### Wildcard Topics with Parameter Extraction
```typescript
// Subscribe to wildcard topic with single level wildcard (+)
const subscription = mqtt.subscribeString('sensors/+/status')
// Publish to matching topics
mqtt.publish('sensors/temperature/status', 'online')
mqtt.publish('sensors/humidity/status', 'offline')
// Extract parameters from wildcards
for await (const message of subscription) {
console.log(`Sensor: ${message.params[0]}`) // 'temperature' or 'humidity'
console.log(`Status: ${message.content}`) // 'online' or 'offline'
console.log(`Full topic: ${message.topic}`) // 'sensors/temperature/status'
}
// Subscribe to multi-level wildcard (#)
const multiLevelSub = mqtt.subscribeString('sensors/+/data/#')
mqtt.publish('sensors/temperature/data/room1/2024', '23.5°C')
for await (const message of multiLevelSub) {
console.log(`Sensor: ${message.params[0]}`) // 'temperature'
console.log(`Data path: ${message.params[1]}`) // 'room1/2024'
console.log(`Value: ${message.content}`) // '23.5°C'
}
```
### JSON Messages
```typescript
interface SensorData {
value: number
unit: string
timestamp: number
}
// Subscribe with JSON parser
const subscription = mqtt.subscribeJson<SensorData>('sensors/data')
// Publish JSON data
mqtt.publishJson('sensors/data', {
message: 'test message',
value: 23.5,
unit: '°C',
timestamp: Date.now()
})
for await (const message of subscription) {
const data = message.content // Typed as SensorData
console.log(`${data.value}${data.unit} at ${new Date(data.timestamp)}`)
}
```
### Async Operations
```typescript
// Async connection
const mqtt = await BetterMQTT.connectAsync({
host: 'localhost',
port: 1883,
clientId: 'async-client'
})
// Async subscription
const subscription = await mqtt.subscribeStringAsync('test/topic')
// Async publishing
await mqtt.publishAsync('test/topic', 'async message')
await mqtt.publishJsonAsync('test/json', { data: 'async json' })
// Process messages
for await (const message of subscription) {
console.log('Received:', message.content)
break
}
```
### Shared Subscriptions
```typescript
// Subscribe to shared topic (load balancing across multiple clients)
const subscription1 = mqtt1.subscribeString('$share/group1/commands/robot')
const subscription2 = mqtt2.subscribeString('$share/group1/commands/robot')
// Only one client in the group will receive each message
// This ensures load balancing across multiple workers
for await (const message of subscription1) {
console.log(`Worker 1 received command: ${message.content}`)
}
// Publish multiple messages to test load balancing
await mqtt1.publishAsync('commands/robot', 'command 1')
await mqtt1.publishAsync('commands/robot', 'command 2')
await mqtt1.publishAsync('commands/robot', 'command 3')
await mqtt1.publishAsync('commands/robot', 'command 4')
// Each message will be received by only one of the subscribers
// ensuring no duplicate processing
```
### Queue Subscriptions
```typescript
// Subscribe to queue topic (round-robin distribution)
const subscription = mqtt.subscribeString('$queue/notifications/urgent')
// Messages are distributed round-robin among subscribers
for await (const message of subscription) {
console.log(`Urgent notification: ${message.content}`)
}
```
### Multiple Subscriptions to Same Topic
```typescript
// Multiple clients can subscribe to the same topic
const sub1 = await mqtt1.subscribeStringAsync('test/better-mqtt/subscribe_3')
const sub2 = await mqtt2.subscribeStringAsync('test/better-mqtt/subscribe_3')
// Both will receive the same message
mqtt1.publish('test/better-mqtt/subscribe_3', 'test message')
// Process in first subscription
for await (const message of sub1) {
console.log(`Sub1 received: ${message.content}`)
break
}
// Process in second subscription
for await (const message of sub2) {
console.log(`Sub2 received: ${message.content}`)
break
}
```
### Unsubscribing and Cleanup
```typescript
// Subscribe to a topic
const subscription = await mqtt.subscribeStringAsync('test/topic')
// Publish and receive a message
mqtt.publish('test/topic', 'test message')
for await (const message of subscription) {
console.log('Received:', message.content)
break
}
// Unsubscribe from the topic
mqtt.unsubscribe(subscription)
// Publish another message (should not be received)
mqtt.publish('test/topic', 'this message should not be received')
// Verify no more messages are received
let received = false
for await (const _ of subscription) {
received = true
}
console.log('Message received after unsubscribe:', received) // false
```
## React Integration
### Provider Setup
```tsx
import { BetterMQTTProvider } from '@robot.com/better-mqtt/react'
function App() {
return (
<BetterMQTTProvider options={{
host: 'localhost',
port: 1883,
clientId: 'react-app'
}}>
<MyComponent />
</BetterMQTTProvider>
)
}
```
### Using MQTT in Components
```tsx
import { useMQTT, useMQTTStatus, useMQTTSubscription } from '@robot.com/better-mqtt/react'
function SensorDisplay() {
const mqtt = useMQTT()
const status = useMQTTStatus()
// Subscribe to temperature updates
useMQTTSubscription(
'sensors/temperature',
(message) => message.toString('utf8'),
(temperature) => {
console.log(`Temperature: ${temperature}°C`)
}
)
// Publish commands
const sendCommand = () => {
mqtt.publish('commands/robot', 'start')
}
return (
<div>
<p>MQTT Status: {status}</p>
<button onClick={sendCommand}>Send Command</button>
</div>
)
}
```
### Error Handling
```tsx
import { useMQTTError } from '@robot.com/better-mqtt/react'
function ErrorBoundary() {
const error = useMQTTError((err) => {
console.error('MQTT Error:', err)
})
if (error) {
return <div>Connection Error: {error.message}</div>
}
return null
}
```
## API Reference
### BetterMQTT Class
#### Constructor
```typescript
new BetterMQTT(client: MqttClient)
```
#### Static Methods
##### `connect(options: IClientOptions): BetterMQTT`
Creates a new BetterMQTT instance with synchronous connection.
##### `connectAsync(options: IClientOptions): Promise<BetterMQTT>`
Creates a new BetterMQTT instance with asynchronous connection.
#### Instance Methods
##### `subscribe<T>(topic: string, parser: MessageParser<T>): Subscription<T>`
Subscribes to a topic with a custom message parser.
##### `subscribeString(topic: string): Subscription<string>`
Subscribes to a topic with string message parsing.
##### `subscribeJson<T>(topic: string): Subscription<T>`
Subscribes to a topic with JSON message parsing.
##### `subscribeBinary(topic: string): Subscription<Buffer>`
Subscribes to a topic with binary message parsing.
##### `subscribeAsync<T>(topic: string, parser: MessageParser<T>): Promise<Subscription<T>>`
Asynchronously subscribes to a topic with a custom message parser.
##### `subscribeStringAsync(topic: string): Promise<Subscription<string>>`
Asynchronously subscribes to a topic with string message parsing.
##### `subscribeJsonAsync<T>(topic: string): Promise<Subscription<T>>`
Asynchronously subscribes to a topic with JSON message parsing.
##### `subscribeBinaryAsync(topic: string): Promise<Subscription<Buffer>>`
Asynchronously subscribes to a topic with binary message parsing.
##### `publish(topic: string, message: string | Buffer, opts?: { qos?: 0 | 1 | 2 }): void`
Publishes a message to a topic.
##### `publishAsync(topic: string, message: string | Buffer): Promise<void>`
Asynchronously publishes a message to a topic.
##### `publishJson<T>(topic: string, message: T): void`
Publishes a JSON message to a topic.
##### `publishJsonAsync<T>(topic: string, message: T): Promise<void>`
Asynchronously publishes a JSON message to a topic.
##### `unsubscribe(subscription: Subscription<unknown>): void`
Unsubscribes from a topic.
##### `end(): void`
Closes the MQTT connection.
#### Properties
##### `status: 'online' | 'offline'`
Current connection status.
##### `client: MqttClient`
Underlying MQTT client instance.
#### Events
##### `status(status: 'online' | 'offline')`
Emitted when connection status changes.
##### `error(error: Error | ErrorWithReasonCode)`
Emitted when an error occurs.
##### `end()`
Emitted when the connection ends.
### Subscription Class
#### Properties
##### `topic: string`
The subscribed topic.
##### `parser: (message: Buffer) => T`
The message parser function.
#### Methods
##### `end(): void`
Ends the subscription.
#### Events
##### `message(message: BetterMQTTMessage<T>)`
Emitted when a message is received.
##### `end()`
Emitted when the subscription ends.
##### `error(error: Error)`
Emitted when an error occurs.
#### Async Iterator
Subscriptions implement the async iterator protocol:
```typescript
for await (const message of subscription) {
// Process message
}
```
### Message Parsers
#### Built-in Parsers
##### `stringParser(message: Buffer): string`
Converts Buffer to UTF-8 string.
##### `jsonParser<T>(message: Buffer): T`
Parses Buffer as JSON.
##### `binaryParser(message: Buffer): Buffer`
Returns Buffer as-is.
#### Custom Parsers
```typescript
function customParser(message: Buffer): MyType {
// Custom parsing logic
return parseMyType(message)
}
const subscription = mqtt.subscribe('my/topic', customParser)
```
### Topic Matching
The `matchTopic` function extracts parameters from wildcard topics:
```typescript
import { matchTopic } from '@robot.com/better-mqtt'
// Single level wildcard
const match = matchTopic('sensors/temp123/status', 'sensors/+/status')
console.log(match?.params) // ['temp123']
// Multi-level wildcard
const match2 = matchTopic('sensors/temp/room1/status', 'sensors/+/#')
console.log(match2?.params) // ['temp', 'room1/status']
// Shared subscription
const match3 = matchTopic('commands/robot', '$share/group1/commands/+')
console.log(match3?.params) // ['robot']
// Queue subscription
const match4 = matchTopic('notifications/urgent', '$queue/notifications/+')
console.log(match4?.params) // ['urgent']
```
## Examples
### Robot Control System
```typescript
import { BetterMQTT } from '@robot.com/better-mqtt'
class RobotController {
private mqtt: BetterMQTT
constructor() {
this.mqtt = BetterMQTT.connect({
host: 'robot-broker.local',
clientId: 'robot-controller'
})
this.setupSubscriptions()
}
private setupSubscriptions() {
// Listen for movement commands
const movementSub = this.mqtt.subscribeJson<{
direction: 'forward' | 'backward' | 'left' | 'right'
speed: number
}>('robot/movement')
// Listen for sensor data
const sensorSub = this.mqtt.subscribeJson<{
temperature: number
battery: number
position: { x: number; y: number }
}>('robot/sensors/+')
// Process messages
this.processMovementCommands(movementSub)
this.processSensorData(sensorSub)
}
private async processMovementCommands(subscription: Subscription) {
for await (const message of subscription) {
const { direction, speed } = message.content
console.log(`Moving ${direction} at speed ${speed}`)
// Execute robot movement
}
}
private async processSensorData(subscription: Subscription) {
for await (const message of subscription) {
const sensorType = message.params[0] // Extract from wildcard
const data = message.content
console.log(`${sensorType} sensor:`, data)
// Publish processed data
this.mqtt.publishJson(`robot/processed/${sensorType}`, {
...data,
timestamp: Date.now(),
processed: true
})
}
}
public sendCommand(command: string) {
this.mqtt.publish('robot/commands', command)
}
public cleanup() {
this.mqtt.end()
}
}
```
### IoT Device Monitor
```typescript
import { BetterMQTT } from '@robot.com/better-mqtt'
class IoTMonitor {
private mqtt: BetterMQTT
private devices = new Map<string, DeviceStatus>()
constructor() {
this.mqtt = BetterMQTT.connect({
host: 'iot-broker.local',
clientId: 'monitor'
})
this.setupMonitoring()
}
private setupMonitoring() {
// Monitor all device statuses
const statusSub = this.mqtt.subscribeString('devices/+/status')
// Monitor device data
const dataSub = this.mqtt.subscribeJson('devices/+/data')
this.monitorDeviceStatus(statusSub)
this.monitorDeviceData(dataSub)
}
private async monitorDeviceStatus(subscription: Subscription) {
for await (const message of subscription) {
const deviceId = message.params[0]
const status = message.content
this.devices.set(deviceId, { status, lastSeen: Date.now() })
// Alert if device goes offline
if (status === 'offline') {
this.mqtt.publishJson('alerts/device', {
deviceId,
type: 'offline',
timestamp: Date.now()
})
}
}
}
private async monitorDeviceData(subscription: Subscription) {
for await (const message of subscription) {
const deviceId = message.params[0]
const data = message.content
// Process and store device data
console.log(`Device ${deviceId} data:`, data)
// Forward to data processing pipeline
this.mqtt.publishJson('data/processed', {
deviceId,
data,
timestamp: Date.now()
})
}
}
public getDeviceStatus(deviceId: string) {
return this.devices.get(deviceId)
}
public getAllDevices() {
return Array.from(this.devices.entries())
}
}
```
### Load Balanced Worker System
```typescript
import { BetterMQTT } from '@robot.com/better-mqtt'
class Worker {
private mqtt: BetterMQTT
private workerId: string
constructor(workerId: string) {
this.workerId = workerId
this.mqtt = BetterMQTT.connect({
host: 'worker-broker.local',
clientId: `worker-${workerId}`
})
this.setupWorkQueue()
}
private setupWorkQueue() {
// Subscribe to shared work queue
const workQueue = this.mqtt.subscribeString('$share/workers/jobs/process')
this.processJobs(workQueue)
}
private async processJobs(subscription: Subscription) {
for await (const message of subscription) {
const job = message.content
console.log(`Worker ${this.workerId} processing job: ${job}`)
// Process the job
await this.processJob(job)
// Publish completion
this.mqtt.publishJson('jobs/completed', {
workerId: this.workerId,
job,
timestamp: Date.now()
})
}
}
private async processJob(job: string) {
// Simulate job processing
await new Promise(resolve => setTimeout(resolve, 1000))
}
public cleanup() {
this.mqtt.end()
}
}
// Create multiple workers
const worker1 = new Worker('worker-1')
const worker2 = new Worker('worker-2')
const worker3 = new Worker('worker-3')
// Jobs will be distributed among workers automatically
```
## Error Handling
### Connection Errors
```typescript
const mqtt = BetterMQTT.connect({
host: 'invalid-host',
port: 1883
})
mqtt.on('error', (error) => {
console.error('Connection error:', error.message)
if (error.code === 'ENOTFOUND') {
console.log('Host not found, retrying...')
// Implement retry logic
}
})
```
### Subscription Errors
```typescript
const subscription = mqtt.subscribeString('test/topic')
subscription.on('error', (error) => {
console.error('Subscription error:', error.message)
// Handle subscription-specific errors
})
```
### Message Parsing Errors
```typescript
function safeJsonParser<T>(message: Buffer): T | null {
try {
return JSON.parse(message.toString('utf8'))
} catch (error) {
console.error('Failed to parse JSON:', error)
return null
}
}
const subscription = mqtt.subscribe('test/topic', safeJsonParser)
for await (const message of subscription) {
if (message.content === null) {
console.log('Skipping invalid message')
continue
}
// Process valid message
console.log('Valid message:', message.content)
}
```
## Best Practices
### 1. Always Clean Up Subscriptions
```typescript
const subscription = mqtt.subscribeString('my/topic')
try {
for await (const message of subscription) {
// Process message
if (shouldStop) break
}
} finally {
subscription.end() // Clean up
}
```
### 2. Use Shared Subscriptions for Load Balancing
```typescript
// Instead of multiple clients subscribing to the same topic
const subscription = mqtt.subscribeString('$share/workers/queue/jobs')
// This ensures only one worker processes each job
```
### 3. Handle Connection Status
```typescript
mqtt.on('status', (status) => {
if (status === 'offline') {
console.log('Connection lost, implementing reconnection...')
// Implement reconnection logic
} else if (status === 'online') {
console.log('Reconnected successfully')
// Restore subscriptions
}
})
```
### 4. Use Appropriate QoS Levels
```typescript
// High reliability for critical messages
mqtt.publish('alerts/critical', 'System failure', { qos: 2 })
// Lower overhead for frequent updates
mqtt.publish('sensors/temperature', '23.5', { qos: 0 })
```
### 5. Implement Proper Error Boundaries in React
```tsx
function MQTTErrorBoundary({ children }: { children: ReactNode }) {
const error = useMQTTError((err) => {
// Log error to monitoring service
console.error('MQTT Error:', err)
})
if (error) {
return <div>Connection Error: {error.message}</div>
}
return <>{children}</>
}
```
### 6. Use Async Methods for Better Performance
```typescript
// For better performance in async contexts
const subscription = await mqtt.subscribeStringAsync('my/topic')
await mqtt.publishAsync('my/topic', 'message')
// Instead of synchronous versions
const subscription = mqtt.subscribeString('my/topic')
mqtt.publish('my/topic', 'message')
```
### 7. Handle Wildcard Parameters Safely
```typescript
const subscription = mqtt.subscribeString('sensors/+/data/+')
for await (const message of subscription) {
// Always check params length before accessing
if (message.params.length >= 2) {
const sensorType = message.params[0]
const dataType = message.params[1]
console.log(`${sensorType} sensor ${dataType}: ${message.content}`)
}
}
```
## Acknowledgments
Better MQTT is built on top of the excellent [mqtt.js](https://github.com/mqttjs/MQTT.js) library, which provides the core MQTT functionality. We extend our thanks to the mqtt.js contributors for their outstanding work on the underlying MQTT client implementation.
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
MIT License - see the [LICENSE](../../LICENSE) file for details.
## AI Slop Warning
This docs was partially generated by AI. All the code (including tests) was written by hand.