902 lines
34 KiB
TypeScript
902 lines
34 KiB
TypeScript
/*
|
|
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
* SPDX-License-Identifier: Apache-2.0.
|
|
*/
|
|
|
|
/**
|
|
* Browser specific MQTT5 client implementation
|
|
*
|
|
* [MQTT5 Client User Guide](https://www.github.com/awslabs/aws-crt-nodejs/blob/main/MQTT5-UserGuide.md)
|
|
*
|
|
* @packageDocumentation
|
|
* @module mqtt5
|
|
* @mergeTarget
|
|
*
|
|
*/
|
|
|
|
import {BufferedEventEmitter} from "../common/event";
|
|
import * as mqtt from "mqtt"; /* The mqtt-js external dependency */
|
|
import * as mqtt5 from "../common/mqtt5";
|
|
import {OutboundTopicAliasBehaviorType} from "../common/mqtt5";
|
|
import * as mqtt5_packet from "../common/mqtt5_packet"
|
|
import {CrtError} from "./error";
|
|
import * as WebsocketUtils from "./ws";
|
|
import * as mqtt_utils from "./mqtt5_utils";
|
|
import * as mqtt_shared from "../common/mqtt_shared";
|
|
import * as auth from "./auth";
|
|
|
|
export * from "../common/mqtt5";
|
|
export * from '../common/mqtt5_packet';
|
|
|
|
/**
|
|
* Factory function that allows the user to completely control the url used to form the websocket handshake
|
|
* request.
|
|
*/
|
|
export type Mqtt5WebsocketUrlFactory = () => string;
|
|
|
|
/**
|
|
* Type of url to construct when establishing an MQTT5 connection over websockets
|
|
*/
|
|
export enum Mqtt5WebsocketUrlFactoryType {
|
|
|
|
/**
|
|
* Websocket connection over plain-text with no additional handshake transformation
|
|
*/
|
|
Ws = 1,
|
|
|
|
/**
|
|
* Websocket connection over TLS with no additional handshake transformation
|
|
*/
|
|
Wss = 2,
|
|
|
|
/**
|
|
* Websocket connection over TLS with a handshake signed by the Aws Sigv4 signing process
|
|
*/
|
|
Sigv4 = 3,
|
|
|
|
/**
|
|
* Websocket connection whose url is formed by a user-supplied callback function
|
|
*/
|
|
Custom = 4
|
|
}
|
|
|
|
/**
|
|
* Websocket factory options discriminated union variant for untransformed connections over plain-text
|
|
*/
|
|
export interface Mqtt5WebsocketUrlFactoryWsOptions {
|
|
urlFactory: Mqtt5WebsocketUrlFactoryType.Ws;
|
|
};
|
|
|
|
/**
|
|
* Websocket factory options discriminated union variant for untransformed connections over TLS
|
|
*/
|
|
export interface Mqtt5WebsocketUrlFactoryWssOptions {
|
|
urlFactory: Mqtt5WebsocketUrlFactoryType.Wss;
|
|
};
|
|
|
|
/**
|
|
* Websocket factory options discriminated union variant for untransformed connections over TLS signed by
|
|
* the AWS Sigv4 signing process.
|
|
*/
|
|
export interface Mqtt5WebsocketUrlFactorySigv4Options {
|
|
urlFactory : Mqtt5WebsocketUrlFactoryType.Sigv4;
|
|
|
|
/**
|
|
* AWS Region to sign against.
|
|
*/
|
|
region?: string;
|
|
|
|
/**
|
|
* Provider to source AWS credentials from
|
|
*/
|
|
credentialsProvider: auth.CredentialsProvider;
|
|
}
|
|
|
|
/**
|
|
* Websocket factory options discriminated union variant for arbitrarily transformed handshake urls.
|
|
*/
|
|
export interface Mqtt5WebsocketUrlFactoryCustomOptions {
|
|
urlFactory: Mqtt5WebsocketUrlFactoryType.Custom;
|
|
|
|
customUrlFactory: Mqtt5WebsocketUrlFactory;
|
|
};
|
|
|
|
/**
|
|
* Union of all websocket factory option possibilities.
|
|
*/
|
|
export type Mqtt5WebsocketUrlFactoryOptions = Mqtt5WebsocketUrlFactoryWsOptions | Mqtt5WebsocketUrlFactoryWssOptions | Mqtt5WebsocketUrlFactorySigv4Options | Mqtt5WebsocketUrlFactoryCustomOptions;
|
|
|
|
/**
|
|
* Browser-specific websocket configuration options for connection establishment
|
|
*/
|
|
export interface Mqtt5WebsocketConfig {
|
|
|
|
/**
|
|
* Options determining how the websocket url is created.
|
|
*/
|
|
urlFactoryOptions : Mqtt5WebsocketUrlFactoryOptions;
|
|
|
|
/**
|
|
* Opaque options set passed through to the underlying websocket implementation regardless of url factory.
|
|
* Use this to control proxy settings amongst other things.
|
|
*/
|
|
wsOptions?: any;
|
|
}
|
|
|
|
/**
|
|
* Configuration options for mqtt5 client creation.
|
|
*/
|
|
export interface Mqtt5ClientConfig {
|
|
|
|
/**
|
|
* Host name of the MQTT server to connect to.
|
|
*/
|
|
hostName: string;
|
|
|
|
/**
|
|
* Network port of the MQTT server to connect to.
|
|
*/
|
|
port: number;
|
|
|
|
/**
|
|
* Controls how the MQTT5 client should behave with respect to MQTT sessions.
|
|
*/
|
|
sessionBehavior? : mqtt5.ClientSessionBehavior;
|
|
|
|
/**
|
|
* Controls how the reconnect delay is modified in order to smooth out the distribution of reconnection attempt
|
|
* timepoints for a large set of reconnecting clients.
|
|
*/
|
|
retryJitterMode? : mqtt5.RetryJitterType;
|
|
|
|
/**
|
|
* Minimum amount of time to wait to reconnect after a disconnect. Exponential backoff is performed with jitter
|
|
* after each connection failure.
|
|
*/
|
|
minReconnectDelayMs? : number;
|
|
|
|
/**
|
|
* Maximum amount of time to wait to reconnect after a disconnect. Exponential backoff is performed with jitter
|
|
* after each connection failure.
|
|
*/
|
|
maxReconnectDelayMs? : number;
|
|
|
|
/**
|
|
* Amount of time that must elapse with an established connection before the reconnect delay is reset to the minimum.
|
|
* This helps alleviate bandwidth-waste in fast reconnect cycles due to permission failures on operations.
|
|
*/
|
|
minConnectedTimeToResetReconnectDelayMs? : number;
|
|
|
|
/**
|
|
* All configurable options with respect to the CONNECT packet sent by the client, including the will. These
|
|
* connect properties will be used for every connection attempt made by the client.
|
|
*/
|
|
connectProperties?: mqtt5_packet.ConnectPacket;
|
|
|
|
/**
|
|
* Overall time interval to wait to establish an MQTT connection. If a complete MQTT connection (from socket
|
|
* establishment all the way up to CONNACK receipt) has not been established before this timeout expires,
|
|
* the connection attempt will be considered a failure.
|
|
*/
|
|
connectTimeoutMs? : number;
|
|
|
|
/**
|
|
* Additional controls for client behavior with respect to topic alias usage.
|
|
*
|
|
* If this setting is left undefined, then topic aliasing behavior will be disabled.
|
|
*/
|
|
topicAliasingOptions? : mqtt5.TopicAliasingOptions
|
|
|
|
/**
|
|
* Options for the underlying websocket connection
|
|
*
|
|
* @group Browser-only
|
|
*/
|
|
websocketOptions?: Mqtt5WebsocketConfig;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*
|
|
* Mqtt-js only supports reconnect on a fixed delay.
|
|
*
|
|
* This helper class allows for variable time-delay rescheduling of reconnect attempts by implementing the
|
|
* reconnect delay options supported by the native client. Variable-delay reconnect actually happens by configuring
|
|
* the mqtt-js client to have a much longer reconnect delay than our configured maximum and then letting this class
|
|
* "interrupt" that long reconnect delay with the real, shorter wait-then-connect each time.
|
|
*/
|
|
class ReconnectionScheduler {
|
|
private connectionFailureCount: number;
|
|
private lastReconnectDelay: number | undefined;
|
|
private resetConnectionFailureCountTask : ReturnType<typeof setTimeout> | undefined;
|
|
private reconnectionTask : ReturnType<typeof setTimeout> | undefined;
|
|
|
|
constructor(private browserClient: mqtt.MqttClient, private clientConfig: Mqtt5ClientConfig) {
|
|
this.connectionFailureCount = 0;
|
|
this.lastReconnectDelay = 0;
|
|
this.resetConnectionFailureCountTask = undefined;
|
|
this.reconnectionTask = undefined;
|
|
this.lastReconnectDelay = undefined;
|
|
}
|
|
|
|
/**
|
|
* Invoked by the client when a successful connection is established. Schedules the task that will reset the
|
|
* delay if a configurable amount of time elapses with a good connection.
|
|
*/
|
|
onSuccessfulConnection() : void {
|
|
this.clearTasks();
|
|
|
|
this.resetConnectionFailureCountTask = setTimeout(() => {
|
|
this.connectionFailureCount = 0;
|
|
this.lastReconnectDelay = undefined;
|
|
}, this.clientConfig.minConnectedTimeToResetReconnectDelayMs ?? mqtt_utils.DEFAULT_MIN_CONNECTED_TIME_TO_RESET_RECONNECT_DELAY_MS);
|
|
}
|
|
|
|
/**
|
|
* Invoked by the client after a disconnection or connection failure occurs. Schedules the next reconnect
|
|
* task.
|
|
*/
|
|
onConnectionFailureOrDisconnection() : void {
|
|
this.clearTasks();
|
|
|
|
let nextDelay : number = this.calculateNextReconnectDelay();
|
|
|
|
this.lastReconnectDelay = nextDelay;
|
|
this.connectionFailureCount += 1;
|
|
|
|
this.reconnectionTask = setTimeout(async () => {
|
|
let wsOptions = this.clientConfig.websocketOptions;
|
|
if (wsOptions && wsOptions.urlFactoryOptions.urlFactory == Mqtt5WebsocketUrlFactoryType.Sigv4) {
|
|
let sigv4Options = wsOptions.urlFactoryOptions as Mqtt5WebsocketUrlFactorySigv4Options;
|
|
if (sigv4Options.credentialsProvider) {
|
|
await sigv4Options.credentialsProvider.refreshCredentials();
|
|
}
|
|
}
|
|
this.browserClient.reconnect();
|
|
}, nextDelay);
|
|
}
|
|
|
|
/**
|
|
* Resets any reconnect/clear-delay tasks.
|
|
*/
|
|
clearTasks() : void {
|
|
if (this.reconnectionTask) {
|
|
clearTimeout(this.reconnectionTask);
|
|
}
|
|
|
|
if (this.resetConnectionFailureCountTask) {
|
|
clearTimeout(this.resetConnectionFailureCountTask);
|
|
}
|
|
}
|
|
|
|
private randomInRange(min: number, max: number) : number {
|
|
return min + (max - min) * Math.random();
|
|
}
|
|
|
|
/**
|
|
* Computes the next reconnect delay based on the Jitter/Retry configuration.
|
|
* Implements jitter calculations in https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
|
|
* @private
|
|
*/
|
|
private calculateNextReconnectDelay() : number {
|
|
const jitterType : mqtt5.RetryJitterType = this.clientConfig.retryJitterMode ?? mqtt5.RetryJitterType.Default;
|
|
const [minDelay, maxDelay] : [number, number] = mqtt_utils.getOrderedReconnectDelayBounds(this.clientConfig.minReconnectDelayMs, this.clientConfig.maxReconnectDelayMs);
|
|
const clampedFailureCount : number = Math.min(52, this.connectionFailureCount);
|
|
let delay : number = 0;
|
|
|
|
if (jitterType == mqtt5.RetryJitterType.None) {
|
|
delay = minDelay * Math.pow(2, clampedFailureCount);
|
|
} else if (jitterType == mqtt5.RetryJitterType.Decorrelated && this.lastReconnectDelay) {
|
|
delay = this.randomInRange(minDelay, 3 * this.lastReconnectDelay);
|
|
} else {
|
|
delay = this.randomInRange(minDelay, Math.min(maxDelay, minDelay * Math.pow(2, clampedFailureCount)));
|
|
}
|
|
|
|
delay = Math.min(maxDelay, delay);
|
|
this.lastReconnectDelay = delay;
|
|
|
|
return delay;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Elements of a simple state machine that allows us to adapt the mqtt-js control model to our mqtt5 client
|
|
* control model (start/stop).
|
|
*
|
|
* @internal
|
|
*/
|
|
enum Mqtt5ClientState {
|
|
Stopped = 0,
|
|
Running = 1,
|
|
Stopping = 2,
|
|
Restarting = 3,
|
|
}
|
|
|
|
/**
|
|
* Elements of a simple state machine that allows us to adapt the mqtt-js event set to our mqtt5 client's
|
|
* lifecycle event set.
|
|
*
|
|
* @internal
|
|
*/
|
|
enum Mqtt5ClientLifecycleEventState {
|
|
None = 0,
|
|
Connecting = 1,
|
|
Connected = 2,
|
|
Disconnected = 3,
|
|
}
|
|
|
|
/**
|
|
* Browser specific MQTT5 client implementation
|
|
*
|
|
* [MQTT5 Client User Guide](https://www.github.com/awslabs/aws-crt-nodejs/blob/main/MQTT5-UserGuide.md)
|
|
*/
|
|
export class Mqtt5Client extends BufferedEventEmitter implements mqtt5.IMqtt5Client {
|
|
private browserClient?: mqtt.MqttClient;
|
|
private state : Mqtt5ClientState;
|
|
private lifecycleEventState : Mqtt5ClientLifecycleEventState;
|
|
private lastDisconnect? : mqtt5_packet.DisconnectPacket;
|
|
private lastError? : Error;
|
|
private reconnectionScheduler? : ReconnectionScheduler;
|
|
private mqttJsConfig : mqtt.IClientOptions;
|
|
private topicAliasBindings : Map<number, string>;
|
|
|
|
/**
|
|
* Client constructor
|
|
*
|
|
* @param config The configuration for this client
|
|
*/
|
|
constructor(private config: Mqtt5ClientConfig) {
|
|
super();
|
|
|
|
this.mqttJsConfig = mqtt_utils.create_mqtt_js_client_config_from_crt_client_config(this.config);
|
|
this.state = Mqtt5ClientState.Stopped;
|
|
this.lifecycleEventState = Mqtt5ClientLifecycleEventState.None;
|
|
this.topicAliasBindings = new Map<number, string>();
|
|
}
|
|
|
|
/**
|
|
* Triggers cleanup of native resources associated with the MQTT5 client. On the browser, the implementation is
|
|
* an empty function.
|
|
*/
|
|
close() {}
|
|
|
|
/**
|
|
* Notifies the MQTT5 client that you want it to maintain connectivity to the configured endpoint.
|
|
* The client will attempt to stay connected using the properties of the reconnect-related parameters
|
|
* in the mqtt5 client configuration.
|
|
*
|
|
* This is an asynchronous operation.
|
|
*/
|
|
start() {
|
|
if (this.state == Mqtt5ClientState.Stopped) {
|
|
this.lifecycleEventState = Mqtt5ClientLifecycleEventState.Connecting;
|
|
this.lastDisconnect = undefined;
|
|
|
|
/* pause event emission until everything is fully-initialized */
|
|
this.cork();
|
|
this.emit('attemptingConnect');
|
|
|
|
const create_websocket_stream = (client: mqtt.MqttClient) => WebsocketUtils.create_mqtt5_websocket_stream(this.config);
|
|
this.browserClient = new mqtt.MqttClient(create_websocket_stream, this.mqttJsConfig);
|
|
|
|
// hook up events
|
|
this.browserClient.on('end', () => {this._on_stopped_internal();});
|
|
this.browserClient.on('reconnect', () => {this.on_attempting_connect();});
|
|
this.browserClient.on('connect', (connack: mqtt.IConnackPacket) => {this.on_connection_success(connack);});
|
|
this.browserClient.on('message', (topic: string, payload: Buffer, packet: mqtt.IPublishPacket) => { this.on_message(topic, payload, packet);});
|
|
this.browserClient.on('error', (error: Error) => { this.on_browser_client_error(error); });
|
|
this.browserClient.on('close', () => { this.on_browser_close(); });
|
|
this.browserClient.on('disconnect', (packet: mqtt.IDisconnectPacket) => { this.on_browser_disconnect_packet(packet); });
|
|
|
|
this.reconnectionScheduler = new ReconnectionScheduler(this.browserClient, this.config);
|
|
|
|
this.state = Mqtt5ClientState.Running;
|
|
|
|
/* unpause event emission */
|
|
this.uncork();
|
|
} else if (this.state == Mqtt5ClientState.Stopping) {
|
|
this.state = Mqtt5ClientState.Restarting;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notifies the MQTT5 client that you want it to end connectivity to the configured endpoint, disconnecting any
|
|
* existing connection and halting reconnection attempts.
|
|
*
|
|
* This is an asynchronous operation. Once the process completes, no further events will be emitted until the client
|
|
* has {@link start} invoked. Invoking {@link start start()} after a {@link stop stop()} will always result in
|
|
* a new MQTT session.
|
|
*
|
|
* @param disconnectPacket (optional) properties of a DISCONNECT packet to send as part of the shutdown process
|
|
*/
|
|
stop(disconnectPacket?: mqtt5_packet.DisconnectPacket) {
|
|
if (this.state == Mqtt5ClientState.Running) {
|
|
if (disconnectPacket) {
|
|
this.browserClient?.end(true, mqtt_utils.transform_crt_disconnect_to_mqtt_js_disconnect(disconnectPacket));
|
|
} else {
|
|
this.browserClient?.end(true);
|
|
}
|
|
this.state = Mqtt5ClientState.Stopping;
|
|
} else if (this.state == Mqtt5ClientState.Restarting) {
|
|
this.state = Mqtt5ClientState.Stopping;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Subscribe to one or more topic filters by queuing a SUBSCRIBE packet to be sent to the server.
|
|
*
|
|
* @param packet SUBSCRIBE packet to send to the server
|
|
* @returns a promise that will be rejected with an error or resolved with the SUBACK response
|
|
*/
|
|
async subscribe(packet: mqtt5_packet.SubscribePacket) : Promise<mqtt5_packet.SubackPacket> {
|
|
return new Promise<mqtt5_packet.SubackPacket>((resolve, reject) => {
|
|
|
|
try {
|
|
if (!this.browserClient) {
|
|
reject(new Error("Client is stopped and cannot subscribe"));
|
|
return;
|
|
}
|
|
|
|
if (!packet) {
|
|
reject(new Error("Invalid subscribe packet"));
|
|
return;
|
|
}
|
|
|
|
let subMap: mqtt.ISubscriptionMap = mqtt_utils.transform_crt_subscribe_to_mqtt_js_subscription_map(packet);
|
|
let subOptions: mqtt.IClientSubscribeOptions = mqtt_utils.transform_crt_subscribe_to_mqtt_js_subscribe_options(packet);
|
|
|
|
// @ts-ignore
|
|
this.browserClient.subscribe(subMap, subOptions, (error, grants) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
const suback: mqtt5_packet.SubackPacket = mqtt_utils.transform_mqtt_js_subscription_grants_to_crt_suback(grants);
|
|
resolve(suback);
|
|
});
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Unsubscribe from one or more topic filters by queuing an UNSUBSCRIBE packet to be sent to the server.
|
|
*
|
|
* @param packet UNSUBSCRIBE packet to send to the server
|
|
* @returns a promise that will be rejected with an error or resolved with the UNSUBACK response
|
|
*/
|
|
async unsubscribe(packet: mqtt5_packet.UnsubscribePacket) : Promise<mqtt5_packet.UnsubackPacket> {
|
|
|
|
return new Promise<mqtt5_packet.UnsubackPacket>((resolve, reject) => {
|
|
|
|
try {
|
|
if (!this.browserClient) {
|
|
reject(new Error("Client is stopped and cannot unsubscribe"));
|
|
return;
|
|
}
|
|
|
|
if (!packet) {
|
|
reject(new Error("Invalid unsubscribe packet"));
|
|
return;
|
|
}
|
|
|
|
let topicFilters: string[] = packet.topicFilters;
|
|
let unsubOptions: Object = mqtt_utils.transform_crt_unsubscribe_to_mqtt_js_unsubscribe_options(packet);
|
|
|
|
this.browserClient.unsubscribe(topicFilters, unsubOptions, (error, packet) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* sigh, mqtt-js doesn't emit the unsuback packet, we have to make something up that won't reflect
|
|
* reality.
|
|
*/
|
|
if (!packet || packet.cmd !== 'unsuback') {
|
|
/* this is a complete lie */
|
|
let unsuback: mqtt5_packet.UnsubackPacket = {
|
|
type: mqtt5_packet.PacketType.Unsuback,
|
|
reasonCodes: topicFilters.map((filter: string, index: number, array: string[]): mqtt5_packet.UnsubackReasonCode => {
|
|
return mqtt5_packet.UnsubackReasonCode.Success;
|
|
})
|
|
};
|
|
resolve(unsuback);
|
|
} else {
|
|
const unsuback: mqtt5_packet.UnsubackPacket = mqtt_utils.transform_mqtt_js_unsuback_to_crt_unsuback(packet as mqtt.IUnsubackPacket);
|
|
resolve(unsuback);
|
|
}
|
|
});
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
private reset_topic_aliases() {
|
|
this.topicAliasBindings.clear();
|
|
}
|
|
|
|
private bind_topic_alias(alias: number, topic: string) {
|
|
this.topicAliasBindings.set(alias, topic);
|
|
}
|
|
|
|
private is_topic_alias_bound(alias: number, topic: string) {
|
|
if (!topic) {
|
|
return false;
|
|
}
|
|
|
|
return this.topicAliasBindings.get(alias) === topic;
|
|
}
|
|
|
|
/**
|
|
* Send a message to subscribing clients by queuing a PUBLISH packet to be sent to the server.
|
|
*
|
|
* @param packet PUBLISH packet to send to the server
|
|
* @returns a promise that will be rejected with an error or resolved with the PUBACK response (QoS 1), or
|
|
* undefined (QoS 0)
|
|
*/
|
|
async publish(packet: mqtt5_packet.PublishPacket) : Promise<mqtt5.PublishCompletionResult> {
|
|
return new Promise<mqtt5.PublishCompletionResult>((resolve, reject) => {
|
|
|
|
try {
|
|
if (!this.browserClient) {
|
|
reject(new Error("Client is stopped and cannot publish"));
|
|
return;
|
|
}
|
|
|
|
if (!packet) {
|
|
reject(new Error("Invalid publish packet"));
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Out topic aliasing contract and mqtt-js's don't quite match, so we do some fixup here.
|
|
*
|
|
* In our manual mode, the contract is that you must *always* submit both the topic and the alias.
|
|
*
|
|
* In Mqtt-js's manual mode, the alias will only be used if it's been previously bound and you don't
|
|
* submit an alias in the publish (this is not reasonable behavior, but it's not under out control).
|
|
* So when we're in manual aliasing mode, we track all the current bindings and strip out the alias
|
|
* when there's a match, signaling to mqtt-js that the alias binding should be used.
|
|
*/
|
|
if ((this.config.topicAliasingOptions?.outboundBehavior ?? OutboundTopicAliasBehaviorType.Default) == OutboundTopicAliasBehaviorType.Manual) {
|
|
if (packet.topicAlias && this.lifecycleEventState == Mqtt5ClientLifecycleEventState.Connected) {
|
|
if (this.is_topic_alias_bound(packet.topicAlias, packet.topicName)) {
|
|
delete (packet.topicAlias);
|
|
}
|
|
|
|
this.bind_topic_alias(packet.topicAlias, packet.topicName);
|
|
}
|
|
} else {
|
|
delete (packet.topicAlias);
|
|
}
|
|
|
|
let publishOptions : mqtt.IClientPublishOptions = mqtt_utils.transform_crt_publish_to_mqtt_js_publish_options(packet);
|
|
let qos : mqtt5_packet.QoS = packet.qos;
|
|
|
|
let payload = mqtt_shared.normalize_payload(packet.payload);
|
|
this.browserClient.publish(packet.topicName, payload, publishOptions, (error, completionPacket) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
switch (qos) {
|
|
case mqtt5_packet.QoS.AtMostOnce:
|
|
resolve(undefined);
|
|
break;
|
|
|
|
case mqtt5_packet.QoS.AtLeastOnce:
|
|
if (!completionPacket) {
|
|
reject(new Error("Invalid puback packet from mqtt-js"));
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* sadly, mqtt-js returns the original publish packet when the puback is a success, so we have
|
|
* to create a fake puback instead. This means we won't reflect any reason string or
|
|
* user properties that might have been present in the real puback.
|
|
*/
|
|
if (completionPacket.cmd !== "puback") {
|
|
resolve({
|
|
type: mqtt5_packet.PacketType.Puback,
|
|
reasonCode: mqtt5_packet.PubackReasonCode.Success
|
|
})
|
|
}
|
|
|
|
const puback: mqtt5_packet.PubackPacket = mqtt_utils.transform_mqtt_js_puback_to_crt_puback(completionPacket as mqtt.IPubackPacket);
|
|
resolve(puback);
|
|
break;
|
|
|
|
default:
|
|
/* Technically, mqtt-js supports QoS 2 but we don't yet model it in the CRT types */
|
|
reject(new Error("Unsupported QoS value"));
|
|
break;
|
|
}
|
|
});
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Queries whether the client is currently connected
|
|
*
|
|
* @returns whether the client is currently connected
|
|
*/
|
|
isConnected() : boolean {
|
|
return this.lifecycleEventState == Mqtt5ClientLifecycleEventState.Connected;
|
|
}
|
|
|
|
/**
|
|
* Event emitted when the client encounters a disruptive error condition. Not currently used.
|
|
*
|
|
* Listener type: {@link ErrorEventListener}
|
|
*
|
|
* @event
|
|
*/
|
|
static ERROR : string = 'error';
|
|
|
|
/**
|
|
* Event emitted when the client encounters a transient error event that will not disrupt promises based on
|
|
* lifecycle events. Currently, mqtt-js client error events are relayed to this event.
|
|
*
|
|
* Listener type: {@link ErrorEventListener}
|
|
*
|
|
* @event
|
|
* @group Browser-only
|
|
*/
|
|
static INFO : string = 'info';
|
|
|
|
/**
|
|
* Event emitted when an MQTT PUBLISH packet is received by the client.
|
|
*
|
|
* Listener type: {@link MessageReceivedEventListener}
|
|
*
|
|
* @event
|
|
*/
|
|
static MESSAGE_RECEIVED : string = 'messageReceived';
|
|
|
|
/**
|
|
* Event emitted when the client begins a connection attempt.
|
|
*
|
|
* Listener type: {@link AttemptingConnectEventListener}
|
|
*
|
|
* @event
|
|
*/
|
|
static ATTEMPTING_CONNECT : string = 'attemptingConnect';
|
|
|
|
/**
|
|
* Event emitted when the client successfully establishes an MQTT connection. Only emitted after
|
|
* an {@link ATTEMPTING_CONNECT attemptingConnect} event.
|
|
*
|
|
* Listener type: {@link ConnectionSuccessEventListener}
|
|
*
|
|
* @event
|
|
*/
|
|
static CONNECTION_SUCCESS : string = 'connectionSuccess';
|
|
|
|
/**
|
|
* Event emitted when the client fails to establish an MQTT connection. Only emitted after
|
|
* an {@link ATTEMPTING_CONNECT attemptingConnect} event.
|
|
*
|
|
* Listener type: {@link ConnectionFailureEventListener}
|
|
*
|
|
* @event
|
|
*/
|
|
static CONNECTION_FAILURE : string = 'connectionFailure';
|
|
|
|
/**
|
|
* Event emitted when the client's current connection is closed for any reason. Only emitted after
|
|
* a {@link CONNECTION_SUCCESS connectionSuccess} event.
|
|
*
|
|
* Listener type: {@link DisconnectionEventListener}
|
|
*
|
|
* @event
|
|
*/
|
|
static DISCONNECTION : string = 'disconnection';
|
|
|
|
/**
|
|
* Event emitted when the client finishes shutdown as a result of the user invoking {@link stop}.
|
|
*
|
|
* Listener type: {@link StoppedEventListener}
|
|
*
|
|
* @event
|
|
*/
|
|
static STOPPED : string = 'stopped';
|
|
|
|
/**
|
|
* Registers a listener for the client's {@link ERROR error} event. An {@link ERROR error} event is emitted when
|
|
* the client encounters a disruptive error condition.
|
|
*
|
|
* @param event the type of event to listen to
|
|
* @param listener the event listener to add
|
|
*/
|
|
on(event: 'error', listener: mqtt5.ErrorEventListener): this;
|
|
|
|
/**
|
|
* Registers a listener for the client's {@link INFO info} event. An {@link INFO info} event is emitted when
|
|
* the client encounters a transient error event that will not disrupt promises based on lifecycle events.
|
|
* Currently, mqtt-js client error events are relayed to this event.
|
|
*
|
|
* @param event the type of event to listen to
|
|
* @param listener the event listener to add
|
|
*
|
|
* @group Browser-only
|
|
*/
|
|
on(event: 'info', listener: mqtt5.ErrorEventListener): this;
|
|
|
|
/**
|
|
* Registers a listener for the client's {@link MESSAGE_RECEIVED messageReceived} event. A
|
|
* {@link MESSAGE_RECEIVED messageReceived} event is emitted when an MQTT PUBLISH packet is received by the
|
|
* client.
|
|
*
|
|
* @param event the type of event to listen to
|
|
* @param listener the event listener to add
|
|
*/
|
|
on(event: 'messageReceived', listener: mqtt5.MessageReceivedEventListener): this;
|
|
|
|
/**
|
|
* Registers a listener for the client's {@link ATTEMPTING_CONNECT attemptingConnect} event. A
|
|
* {@link ATTEMPTING_CONNECT attemptingConnect} event is emitted every time the client begins a connection attempt.
|
|
*
|
|
* @param event the type of event to listen to
|
|
* @param listener the event listener to add
|
|
*/
|
|
on(event: 'attemptingConnect', listener: mqtt5.AttemptingConnectEventListener): this;
|
|
|
|
/**
|
|
* Registers a listener for the client's {@link CONNECTION_SUCCESS connectionSuccess} event. A
|
|
* {@link CONNECTION_SUCCESS connectionSuccess} event is emitted every time the client successfully establishes
|
|
* an MQTT connection.
|
|
*
|
|
* @param event the type of event to listen to
|
|
* @param listener the event listener to add
|
|
*/
|
|
on(event: 'connectionSuccess', listener: mqtt5.ConnectionSuccessEventListener): this;
|
|
|
|
/**
|
|
* Registers a listener for the client's {@link CONNECTION_FAILURE connectionFailure} event. A
|
|
* {@link CONNECTION_FAILURE connectionFailure} event is emitted every time the client fails to establish an
|
|
* MQTT connection.
|
|
*
|
|
* @param event the type of event to listen to
|
|
* @param listener the event listener to add
|
|
*/
|
|
on(event: 'connectionFailure', listener: mqtt5.ConnectionFailureEventListener): this;
|
|
|
|
/**
|
|
* Registers a listener for the client's {@link DISCONNECTION disconnection} event. A
|
|
* {@link DISCONNECTION disconnection} event is emitted when the client's current MQTT connection is closed
|
|
* for any reason.
|
|
*
|
|
* @param event the type of event to listen to
|
|
* @param listener the event listener to add
|
|
*/
|
|
on(event: 'disconnection', listener: mqtt5.DisconnectionEventListener): this;
|
|
|
|
/**
|
|
* Registers a listener for the client's {@link STOPPED stopped} event. A
|
|
* {@link STOPPED stopped} event is emitted when the client finishes shutdown as a
|
|
* result of the user invoking {@link stop}.
|
|
*
|
|
* @param event the type of event to listen to
|
|
* @param listener the event listener to add
|
|
*/
|
|
on(event: 'stopped', listener: mqtt5.StoppedEventListener): this;
|
|
|
|
on(event: string | symbol, listener: (...args: any[]) => void): this {
|
|
super.on(event, listener);
|
|
return this;
|
|
}
|
|
|
|
private on_browser_disconnect_packet(packet: mqtt.IDisconnectPacket) {
|
|
this.lastDisconnect = mqtt_utils.transform_mqtt_js_disconnect_to_crt_disconnect(packet);
|
|
}
|
|
|
|
private on_browser_close() {
|
|
let lastDisconnect : mqtt5_packet.DisconnectPacket | undefined = this.lastDisconnect;
|
|
let lastError : Error | undefined = this.lastError;
|
|
|
|
if (this.lifecycleEventState == Mqtt5ClientLifecycleEventState.Connected) {
|
|
this.lifecycleEventState = Mqtt5ClientLifecycleEventState.Disconnected;
|
|
this.reconnectionScheduler?.onConnectionFailureOrDisconnection();
|
|
|
|
let disconnectionEvent : mqtt5.DisconnectionEvent = {
|
|
error: new CrtError(lastError?.toString() ?? "disconnected")
|
|
}
|
|
|
|
if (lastDisconnect !== undefined) {
|
|
disconnectionEvent.disconnect = lastDisconnect;
|
|
}
|
|
|
|
setTimeout(() => {
|
|
this.emit(Mqtt5Client.DISCONNECTION, disconnectionEvent);
|
|
}, 0);
|
|
} else if (this.lifecycleEventState == Mqtt5ClientLifecycleEventState.Connecting) {
|
|
this.lifecycleEventState = Mqtt5ClientLifecycleEventState.Disconnected;
|
|
this.reconnectionScheduler?.onConnectionFailureOrDisconnection();
|
|
|
|
let connectionFailureEvent: mqtt5.ConnectionFailureEvent = {
|
|
error: new CrtError(lastError?.toString() ?? "connectionFailure")
|
|
};
|
|
|
|
setTimeout(() => {
|
|
this.emit(Mqtt5Client.CONNECTION_FAILURE, connectionFailureEvent);
|
|
}, 0);
|
|
}
|
|
|
|
this.lastDisconnect = undefined;
|
|
this.lastError = undefined;
|
|
}
|
|
|
|
private on_browser_client_error(error: Error) {
|
|
this.lastError = error;
|
|
setTimeout(() => {
|
|
this.emit(Mqtt5Client.INFO, new CrtError(error));
|
|
}, 0);
|
|
}
|
|
|
|
private on_attempting_connect () {
|
|
this.lifecycleEventState = Mqtt5ClientLifecycleEventState.Connecting;
|
|
|
|
let attemptingConnectEvent: mqtt5.AttemptingConnectEvent = {};
|
|
|
|
setTimeout(() => {
|
|
this.emit(Mqtt5Client.ATTEMPTING_CONNECT, attemptingConnectEvent);
|
|
}, 0);
|
|
}
|
|
|
|
private on_connection_success (connack: mqtt.IConnackPacket) {
|
|
this.lifecycleEventState = Mqtt5ClientLifecycleEventState.Connected;
|
|
this.reset_topic_aliases();
|
|
|
|
this.reconnectionScheduler?.onSuccessfulConnection();
|
|
|
|
let crt_connack : mqtt5_packet.ConnackPacket = mqtt_utils.transform_mqtt_js_connack_to_crt_connack(connack);
|
|
let settings : mqtt5.NegotiatedSettings = mqtt_utils.create_negotiated_settings(this.config, crt_connack);
|
|
|
|
let connectionSuccessEvent: mqtt5.ConnectionSuccessEvent = {
|
|
connack: crt_connack,
|
|
settings: settings
|
|
};
|
|
|
|
setTimeout(() => {
|
|
this.emit(Mqtt5Client.CONNECTION_SUCCESS, connectionSuccessEvent);
|
|
}, 0);
|
|
}
|
|
|
|
private _on_stopped_internal() {
|
|
this.reconnectionScheduler?.clearTasks();
|
|
this.reconnectionScheduler = undefined;
|
|
this.browserClient = undefined;
|
|
this.lifecycleEventState = Mqtt5ClientLifecycleEventState.None;
|
|
this.lastDisconnect = undefined;
|
|
this.lastError = undefined;
|
|
|
|
if (this.state == Mqtt5ClientState.Restarting) {
|
|
this.state = Mqtt5ClientState.Stopped;
|
|
this.start();
|
|
} else if (this.state != Mqtt5ClientState.Stopped) {
|
|
this.state = Mqtt5ClientState.Stopped;
|
|
this.emit(Mqtt5Client.STOPPED);
|
|
}
|
|
}
|
|
|
|
private on_message = (topic: string, payload: Buffer, packet: mqtt.IPublishPacket) => {
|
|
let crtPublish : mqtt5_packet.PublishPacket = mqtt_utils.transform_mqtt_js_publish_to_crt_publish(packet);
|
|
|
|
let messageReceivedEvent: mqtt5.MessageReceivedEvent = {
|
|
message: crtPublish
|
|
};
|
|
|
|
setTimeout(() => {
|
|
this.emit(Mqtt5Client.MESSAGE_RECEIVED, messageReceivedEvent);
|
|
}, 0);
|
|
}
|
|
}
|