@seriousme/opifex
Version:
MQTT client & server for Deno & NodeJS
188 lines • 6.59 kB
JavaScript
import { Deferred, logger, MemoryStore, PacketType, } from "./deps.js";
import { Context } from "./context.js";
/**
* Generates a random client ID with the given prefix
* @param prefix - The prefix to use for the client ID
* @returns A string containing the prefix followed by a random number
*/
function generateClientId(prefix) {
return `${prefix}-${Math.random().toString().slice(-10)}`;
}
/**
* Implements exponential backoff sleep with optional randomization
* based on https://dthain.blogspot.com/2009/02/exponential-backoff-in-distributed.html
* @param random - Whether to add randomization to the delay
* @param attempt - The attempt number (used to calculate delay)
* @returns Promise that resolves after the calculated delay
*/
function backOffSleep(random, attempt) {
const factor = 1.5;
const min = 1000;
const max = 5000;
const randomness = 1 + (random ? Math.random() : 0);
const delay = Math.floor(Math.min(randomness * min * (factor ** attempt), max));
logger.debug({ delay });
return new Promise((resolve) => setTimeout(resolve, delay));
}
/** the default MQTT URL to connect to */
export const DEFAULT_URL = "mqtt://localhost:1883/";
const DEFAULT_KEEPALIVE = 60; // 60 seconds
const DEFAULT_RETRIES = 3; // on first connect
const CLIENTID_PREFIX = "opifex"; // on first connect
/**
* The Client class provides an MQTT Client that can be used to connect to
* a MQTT broker and publish/subscribe messages.
*
* The Client class is not meant to be used directly, but
* instead should be subclassed and the subclass should
* override the createConn() method to provide a
* connection type that is supported by the subclass.
*/
export class Client {
clientIdPrefix = CLIENTID_PREFIX;
numberOfRetries = DEFAULT_RETRIES;
url = new URL(DEFAULT_URL);
keepAlive = DEFAULT_KEEPALIVE;
autoReconnect = true;
caCerts;
cert;
key;
clientId;
ctx = new Context(new MemoryStore());
connectPacket;
/**
* Creates a new MQTT client instance
*/
constructor() {
this.clientId = generateClientId(this.clientIdPrefix);
this.numberOfRetries = DEFAULT_RETRIES;
}
/**
* Creates a new connection to the MQTT broker
* @param protocol - The protocol to use (mqtt, mqtts, etc)
* @param _hostname - The hostname to connect to
* @param _port - The port to connect to
* @param _caCerts - Optional CA certificates
* @param _cert - Optional client certificate
* @param _key - Optional client key
* @returns Promise resolving to a SockConn connection
*/
createConn(protocol, _hostname, _port, _caCerts, _cert, _key) {
// if you need to support alternative connection types just
// overload this method in your subclass
throw `Unsupported protocol: ${protocol}`;
}
/**
* Handles the connection process including retries and reconnection
* @returns Promise that resolves when connection is established or fails
*/
async doConnect() {
if (!this.connectPacket) {
return;
}
let isReconnect = false;
let attempt = 1;
let lastMessage = new Error();
let tryConnect = true;
while (tryConnect) {
logger.debug(`${isReconnect ? "re" : ""}connecting`);
try {
const conn = await this.createConn(this.url.protocol, this.url.hostname, Number(this.url.port) ?? undefined, this.caCerts, this.cert, this.key);
// if we get this far we have a connection
tryConnect =
(await this.ctx.handleConnection(conn, this.connectPacket)) &&
this.autoReconnect;
logger.debug({ tryConnect });
isReconnect = true;
this.connectPacket.clean = false;
this.ctx.close();
}
catch (err) {
if (err instanceof Error) {
lastMessage = err;
}
logger.debug(lastMessage);
if (!isReconnect && attempt > this.numberOfRetries) {
tryConnect = false;
}
else {
await backOffSleep(true, attempt++);
}
}
}
if (isReconnect === false) {
this.ctx.unresolvedConnect?.reject(lastMessage);
}
}
/**
* Connects to the MQTT broker
* @param params - Connection parameters
* @returns Promise resolving to authentication result
*/
connect(params = {}) {
this.url = params?.url || this.url;
this.numberOfRetries = params.numberOfRetries || this.numberOfRetries;
this.caCerts = params?.caCerts;
this.cert = params?.cert;
this.key = params?.key;
const options = Object.assign({
keepAlive: this.keepAlive,
clientId: this.clientId,
}, params?.options);
this.connectPacket = {
type: PacketType.connect,
...options,
};
const deferred = new Deferred();
this.ctx.unresolvedConnect = deferred;
this.doConnect();
return deferred.promise;
}
/**
* Disconnects from the MQTT broker
* @returns Promise that resolves when disconnected
*/
async disconnect() {
await this.ctx.disconnect();
}
/**
* Publishes a message to the MQTT broker
* @param params - Publish parameters including topic and payload
* @returns Promise that resolves when published
*/
async publish(params) {
const packet = {
type: PacketType.publish,
...params,
};
await this.ctx.publish(packet);
}
/**
* Subscribes to topics on the MQTT broker
* @param params - Subscribe parameters including topics
* @returns Promise that resolves when subscribed
*/
async subscribe(params) {
const packet = {
type: PacketType.subscribe,
id: 0, //placeholder
...params,
};
await this.ctx.subscribe(packet);
}
/**
* Gets an async iterator for received messages
* @returns AsyncGenerator yielding received publish packets
*/
async *messages() {
yield* this.ctx.incoming;
}
/**
* Closes the message stream
* @param reason - Optional reason for closing
*/
closeMessages(reason) {
this.ctx.incoming.close(reason);
}
}
//# sourceMappingURL=client.js.map