/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ /** * Node.js 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 crt_native from './binding'; import { NativeResourceMixin } from "./native_resource"; import { BufferedEventEmitter } from '../common/event'; import * as io from "./io"; import * as http from './http'; import * as mqtt5_packet from "../common/mqtt5_packet"; import * as mqtt5 from "../common/mqtt5"; import * as mqtt_shared from "../common/mqtt_shared"; import {CrtError} from "./error"; export { HttpProxyOptions } from './http'; export * from "../common/mqtt5"; export * from '../common/mqtt5_packet'; /** * Websocket handshake http request transformation function signature */ export type WebsocketHandshakeTransform = (request: http.HttpRequest, done: (error_code?: number) => void) => void; /** * Information about the client's queue of operations */ export interface ClientStatistics { /** * Total number of operations submitted to the client that have not yet been completed. Unacked operations * are a subset of this. */ incompleteOperationCount : number; /** * Total packet size of operations submitted to the client that have not yet been completed. Unacked operations * are a subset of this. */ incompleteOperationSize : number; /** * Total number of operations that have been sent to the server and are waiting for a corresponding ACK before * they can be completed. */ unackedOperationCount : number; /** * Total packet size of operations that have been sent to the server and are waiting for a corresponding ACK before * they can be completed. */ unackedOperationSize : number; }; /** * Controls how disconnects affect the queued and in-progress operations tracked by the client. Also controls * how operations are handled while the client is not connected. In particular, if the client is not connected, * then any operation that would be failed on disconnect (according to these rules) will be rejected. */ export enum ClientOperationQueueBehavior { /** Same as FailQos0PublishOnDisconnect */ Default = 0, /** * Re-queues QoS 1+ publishes on disconnect; un-acked publishes go to the front while unprocessed publishes stay * in place. All other operations (QoS 0 publishes, subscribe, unsubscribe) are failed. */ FailNonQos1PublishOnDisconnect = 1, /** * QoS 0 publishes that are not complete at the time of disconnection are failed. Un-acked QoS 1+ publishes are * re-queued at the head of the line for immediate retransmission on a session resumption. All other operations * are requeued in original order behind any retransmissions. */ FailQos0PublishOnDisconnect = 2, /** * All operations that are not complete at the time of disconnection are failed, except operations that * the MQTT5 spec requires to be retransmitted (un-acked QoS1+ publishes). */ FailAllOnDisconnect = 3, } /** * Additional controls for client behavior with respect to operation validation and flow control; these checks * go beyond the MQTT5 spec to respect limits of specific MQTT brokers. */ export enum ClientExtendedValidationAndFlowControl { /** * Do not do any additional validation or flow control */ None = 0, /** * Apply additional client-side validation and operational flow control that respects the * default AWS IoT Core limits. * * Currently applies the following additional validation: * * 1. No more than 8 subscriptions per SUBSCRIBE packet * 1. Topics and topic filters have a maximum of 7 slashes (8 segments), not counting any AWS rules prefix * 1. Topics must be <= 256 bytes in length * 1. Client id must be <= 128 bytes in length * * Also applies the following flow control: * * 1. Outbound throughput throttled to 512KB/s * 1. Outbound publish TPS throttled to 100 */ AwsIotCoreDefaults = 1, } /** * 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; /** * Time interval to wait after sending a CONNECT request for a CONNACK to arrive. If one does not arrive, the * connection will be shut down. */ connackTimeoutMs? : 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; /** * Controls how disconnects affect the queued and in-progress operations tracked by the client. Also controls * how new operations are handled while the client is not connected. In particular, if the client is not connected, * then any operation that would be failed on disconnect (according to these rules) will also be rejected. * * @group Node-only */ offlineQueueBehavior? : ClientOperationQueueBehavior; /** * Time interval to wait after sending a PINGREQ for a PINGRESP to arrive. If one does not arrive, the client will * close the current connection. * * @group Node-only */ pingTimeoutMs? : number; /** * Time interval to wait for an ack after sending a QoS 1+ PUBLISH, SUBSCRIBE, or UNSUBSCRIBE before * failing the operation. * * @group Node-only */ ackTimeoutSeconds? : 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 /** * Client bootstrap to use. In almost all cases, this can be left undefined. * * @group Node-only */ clientBootstrap?: io.ClientBootstrap; /** * Controls socket properties of the underlying MQTT connections made by the client. Leave undefined to use * defaults (no TCP keep alive, 10 second socket timeout). * * @group Node-only */ socketOptions?: io.SocketOptions; /** * TLS context for secure socket connections. * If undefined, then a plaintext connection will be used. * * @group Node-only */ tlsCtx?: io.ClientTlsContext; /** * This callback allows a custom transformation of the HTTP request that acts as the websocket handshake. * Websockets will be used if this is set to a valid transformation callback. To use websockets but not perform * a transformation, just set this as a trivial completion callback. If undefined, the connection will be made * with direct MQTT. * * @group Node-only */ websocketHandshakeTransform?: WebsocketHandshakeTransform; /** * Configures (tunneling) HTTP proxy usage when establishing MQTT connections * * @group Node-only */ httpProxyOptions?: http.HttpProxyOptions; /** * Additional controls for client behavior with respect to operation validation and flow control; these checks * go beyond the base MQTT5 spec to respect limits of specific MQTT brokers. * * @group Node-only */ extendedValidationAndFlowControlOptions? : ClientExtendedValidationAndFlowControl; } /** * Node.js specific MQTT5 client implementation * * Not all parts of the MQTT5 spec are supported. We currently do not support: * * * AUTH packets and the authentication fields in the CONNECT packet * * QoS 2 * * [MQTT5 Client User Guide](https://www.github.com/awslabs/aws-crt-nodejs/blob/main/MQTT5-UserGuide.md) * * This client is based on native resources. When finished with the client, you must call close() to dispose of * them or they will leak. * */ export class Mqtt5Client extends NativeResourceMixin(BufferedEventEmitter) implements mqtt5.IMqtt5Client { /** * Client constructor * * @param config The configuration for this client */ constructor(config: Mqtt5ClientConfig) { super(); this._super(crt_native.mqtt5_client_new( this, config, (client: Mqtt5Client) => { Mqtt5Client._s_on_stopped(client); }, (client: Mqtt5Client) => { Mqtt5Client._s_on_attempting_connect(client); }, (client: Mqtt5Client, connack : mqtt5_packet.ConnackPacket, settings: mqtt5.NegotiatedSettings) => { Mqtt5Client._s_on_connection_success(client, connack, settings); }, (client: Mqtt5Client, errorCode: number, connack? : mqtt5_packet.ConnackPacket) => { Mqtt5Client._s_on_connection_failure(client, new CrtError(errorCode), connack); }, (client: Mqtt5Client, errorCode: number, disconnect? : mqtt5_packet.DisconnectPacket) => { Mqtt5Client._s_on_disconnection(client, new CrtError(errorCode), disconnect); }, (client: Mqtt5Client, message : mqtt5_packet.PublishPacket) => { Mqtt5Client._s_on_message_received(client, message); }, config.clientBootstrap ? config.clientBootstrap.native_handle() : null, config.socketOptions ? config.socketOptions.native_handle() : null, config.tlsCtx ? config.tlsCtx.native_handle() : null, config.httpProxyOptions ? config.httpProxyOptions.create_native_handle() : null )); } /** * Triggers cleanup of native resources associated with the MQTT5 client. Once this has been invoked, callbacks * and events are not guaranteed to be received. * * This must be called when finished with a client; otherwise, native resources will leak. It is not safe * to invoke any further operations on the client after close() has been called. * * For a running client, safe and proper shutdown can be accomplished by * * ```ts * const stopped = once(client, "stopped"); * client.stop(); * await stopped; * client.close(); * ``` * * This is an asynchronous operation. * * @group Node-only */ close() { crt_native.mqtt5_client_close(this.native_handle()); } /** * 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() { crt_native.mqtt5_client_start(this.native_handle()); } /** * 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) { crt_native.mqtt5_client_stop(this.native_handle(), disconnectPacket); } /** * 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 { return new Promise((resolve, reject) => { function curriedPromiseCallback(client: Mqtt5Client, errorCode: number, suback?: mqtt5_packet.SubackPacket){ return Mqtt5Client._s_on_suback_callback(resolve, reject, client, errorCode, suback); } try { crt_native.mqtt5_client_subscribe(this.native_handle(), packet, curriedPromiseCallback); } catch (e) { reject(e); } }); } /** * 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 { return new Promise((resolve, reject) => { function curriedPromiseCallback(client: Mqtt5Client, errorCode: number, unsuback?: mqtt5_packet.UnsubackPacket){ return Mqtt5Client._s_on_unsuback_callback(resolve, reject, client, errorCode, unsuback); } try { crt_native.mqtt5_client_unsubscribe(this.native_handle(), packet, curriedPromiseCallback); } catch (e) { reject(e); } }); } /** * 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 { return new Promise((resolve, reject) => { if (packet && packet.payload) { packet.payload = mqtt_shared.normalize_payload(packet.payload); } function curriedPromiseCallback(client: Mqtt5Client, errorCode: number, result: mqtt5.PublishCompletionResult){ return Mqtt5Client._s_on_puback_callback(resolve, reject, client, errorCode, result); } try { crt_native.mqtt5_client_publish(this.native_handle(), packet, curriedPromiseCallback); } catch (e) { reject(e); } }); } /** * Queries a small set of numerical statistics about the current state of the client's operation queue * * @group Node-only */ getOperationalStatistics() : ClientStatistics { return crt_native.mqtt5_client_get_queue_statistics(this.native_handle()); } /** * Queries a small set of numerical statistics about the current state of the client's operation queue * @deprecated use getOperationalStatistics instead * * @group Node-only */ getQueueStatistics() : ClientStatistics { return this.getOperationalStatistics(); } /** * Event emitted when the client encounters a serious error condition, such as invalid input, napi failures, and * other potentially unrecoverable situations. * * Listener type: {@link ErrorEventListener} * * @event */ static ERROR : string = 'error'; /** * 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 serious error condition, such as invalid input, napi failures, and other potentially * unrecoverable situations. * * @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 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 helper functions * * Callbacks come through static functions so that the native threadsafe function objects do not * capture the client object itself, simplifying the number of strong references to the client floating around. */ private static _s_on_stopped(client: Mqtt5Client) { process.nextTick(() => { let stoppedEvent: mqtt5.StoppedEvent = {}; client.emit(Mqtt5Client.STOPPED, stoppedEvent); }); } private static _s_on_attempting_connect(client: Mqtt5Client) { process.nextTick(() => { let attemptingConnectEvent: mqtt5.AttemptingConnectEvent = {}; client.emit(Mqtt5Client.ATTEMPTING_CONNECT, attemptingConnectEvent); }); } private static _s_on_connection_success(client: Mqtt5Client, connack: mqtt5_packet.ConnackPacket, settings: mqtt5.NegotiatedSettings) { let connectionSuccessEvent: mqtt5.ConnectionSuccessEvent = { connack: connack, settings: settings }; process.nextTick(() => { client.emit(Mqtt5Client.CONNECTION_SUCCESS, connectionSuccessEvent); }); } private static _s_on_connection_failure(client: Mqtt5Client, error: CrtError, connack?: mqtt5_packet.ConnackPacket) { let connectionFailureEvent: mqtt5.ConnectionFailureEvent = { error: error }; if (connack !== null && connack !== undefined) { connectionFailureEvent.connack = connack; } process.nextTick(() => { client.emit(Mqtt5Client.CONNECTION_FAILURE, connectionFailureEvent); }); } private static _s_on_disconnection(client: Mqtt5Client, error: CrtError, disconnect?: mqtt5_packet.DisconnectPacket) { let disconnectionEvent: mqtt5.DisconnectionEvent = { error: error }; if (disconnect !== null && disconnect !== undefined) { disconnectionEvent.disconnect = disconnect; } process.nextTick(() => { client.emit(Mqtt5Client.DISCONNECTION, disconnectionEvent); }); } private static _s_on_suback_callback(resolve : (value: (mqtt5_packet.SubackPacket | PromiseLike)) => void, reject : (reason?: any) => void, client: Mqtt5Client, errorCode: number, suback?: mqtt5_packet.SubackPacket) { if (errorCode == 0 && suback !== undefined) { resolve(suback); } else { reject(io.error_code_to_string(errorCode)); } } private static _s_on_unsuback_callback(resolve : (value: (mqtt5_packet.UnsubackPacket | PromiseLike)) => void, reject : (reason?: any) => void, client: Mqtt5Client, errorCode: number, unsuback?: mqtt5_packet.UnsubackPacket) { if (errorCode == 0 && unsuback !== undefined) { resolve(unsuback); } else { reject(io.error_code_to_string(errorCode)); } } private static _s_on_puback_callback(resolve : (value: (mqtt5.PublishCompletionResult | PromiseLike)) => void, reject : (reason?: any) => void, client: Mqtt5Client, errorCode: number, result:mqtt5.PublishCompletionResult) { if (errorCode == 0) { resolve(result); } else { reject(io.error_code_to_string(errorCode)); } } private static _s_on_message_received(client: Mqtt5Client, message : mqtt5_packet.PublishPacket) { let messageReceivedEvent: mqtt5.MessageReceivedEvent = { message: message }; process.nextTick(() => { client.emit(Mqtt5Client.MESSAGE_RECEIVED, messageReceivedEvent); }); } }