azure-iot-device
Version:
Azure IoT device SDK
395 lines • 23.9 kB
JavaScript
/* eslint-disable @typescript-eslint/adjacent-overload-signatures */
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.InternalClient = void 0;
const events_1 = require("events");
const fs = require("fs");
const dbg = require("debug");
const debug = dbg('azure-iot-device:InternalClient');
const debugErrors = dbg('azure-iot-device:InternalClient:Errors');
const azure_iot_common_1 = require("azure-iot-common");
const azure_iot_common_2 = require("azure-iot-common");
const device_method_1 = require("./device_method");
const twin_1 = require("./twin");
/**
* @private
* Default maximum operation timeout for client operations: 4 minutes.
*/
const MAX_OPERATION_TIMEOUT = 240000;
function safeCallback(callback, error, result) {
if (callback)
callback(error, result);
}
/**
* @private
*/
class InternalClient extends events_1.EventEmitter {
constructor(transport, connStr) {
/*Codes_SRS_NODE_INTERNAL_CLIENT_05_001: [The Client constructor shall throw ReferenceError if the transport argument is falsy.]*/
if (!transport)
throw new ReferenceError('transport is \'' + transport + '\'');
super();
this._userRegisteredMethodListener = false;
if (connStr) {
throw new azure_iot_common_1.errors.InvalidOperationError('the connectionString parameter of the constructor is not used - users of the SDK should be using the `fromConnectionString` factory method.');
}
this._transport = transport;
this._transport.on('error', (err) => {
// errors right now bubble up through the disconnect handler.
// ultimately we would like to get rid of that disconnect event and rely on the error event instead
debugErrors('Transport error: ' + err);
});
/*Codes_SRS_NODE_INTERNAL_CLIENT_41_001: [A `connect` event will be emitted when a `connected` event is received from the transport.]*/
this._transport.on('connected', () => this.emit('connect'));
this._methodCallbackMap = {};
this._disconnectHandler = (err) => {
if (err) {
debugErrors('transport disconnect event: ' + err);
}
else {
debug('transport disconnect event: no error');
}
if (err && this._retryPolicy.shouldRetry(err)) {
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_098: [If the transport emits a `disconnect` event while the client is subscribed to direct methods the retry policy shall be used to reconnect and re-enable the feature using the transport `enableMethods` method.]*/
if (this._userRegisteredMethodListener) {
debug('re-enabling Methods link');
this._enableMethods((err) => {
if (err) {
debugErrors('Error enabling methods: ' + err);
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_100: [If the retry policy fails to reestablish the direct methods functionality a `disconnect` event shall be emitted with a `results.Disconnected` object.]*/
this.emit('disconnect', new azure_iot_common_1.results.Disconnected(err));
}
});
}
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_099: [If the transport emits a `disconnect` event while the client is subscribed to desired properties updates the retry policy shall be used to reconnect and re-enable the feature using the transport `enableTwinDesiredPropertiesUpdates` method.]*/
if (this._twin && this._twin.userRegisteredDesiredPropertiesListener) {
debug('re-enabling Twin');
this._twin.enableTwinDesiredPropertiesUpdates((err) => {
if (err) {
debugErrors('Error enabling twin: ' + err);
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_101: [If the retry policy fails to reestablish the twin desired properties updates functionality a `disconnect` event shall be emitted with a `results.Disconnected` object.]*/
this.emit('disconnect', new azure_iot_common_1.results.Disconnected(err));
}
});
}
}
else {
this.emit('disconnect', new azure_iot_common_1.results.Disconnected(err));
}
};
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_045: [If the transport successfully establishes a connection the `open` method shall subscribe to the `disconnect` event of the transport.]*/
this._transport.on('disconnect', this._disconnectHandler);
this._retryPolicy = new azure_iot_common_2.ExponentialBackOffWithJitter();
this._maxOperationTimeout = MAX_OPERATION_TIMEOUT;
}
/*Codes_SRS_NODE_INTERNAL_CLIENT_05_016: [When a Client method encounters an error in the transport, the callback function (indicated by the done argument) shall be invoked with the following arguments:
err - the standard JavaScript Error object, with a response property that points to a transport-specific response object, and a responseBody property that contains the body of the transport response.]*/
/*Codes_SRS_NODE_INTERNAL_CLIENT_05_017: [With the exception of receive, when a Client method completes successfully, the callback function (indicated by the done argument) shall be invoked with the following arguments:
err - null
response - a transport-specific response object]*/
updateSharedAccessSignature(sharedAccessSignature, updateSasCallback) {
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_031: [The updateSharedAccessSignature method shall throw a ReferenceError if the sharedAccessSignature parameter is falsy.]*/
if (!sharedAccessSignature)
throw new ReferenceError('sharedAccessSignature is falsy');
const retryOp = new azure_iot_common_2.RetryOperation('updateSharedAccessSignature', this._retryPolicy, this._maxOperationTimeout);
retryOp.retry((opCallback) => {
this._transport.updateSharedAccessSignature(sharedAccessSignature, opCallback);
}, (err, result) => {
if (!err) {
this.emit('_sharedAccessSignatureUpdated');
}
safeCallback(updateSasCallback, err, result);
});
}
open(openCallback) {
return (0, azure_iot_common_1.callbackToPromise)((_callback) => {
const retryOp = new azure_iot_common_2.RetryOperation('open', this._retryPolicy, this._maxOperationTimeout);
retryOp.retry((opCallback) => {
this._transport.connect(opCallback);
}, (connectErr, connectResult) => {
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_060: [The `open` method shall call the `_callback` callback with a null error object and a `results.Connected()` result object if the transport is already connected, doesn't need to connect or has just connected successfully.]*/
safeCallback(_callback, connectErr, connectResult);
});
}, openCallback);
}
sendEvent(message, sendEventCallback) {
return (0, azure_iot_common_1.callbackToPromise)((_callback) => {
const retryOp = new azure_iot_common_2.RetryOperation('sendEvent', this._retryPolicy, this._maxOperationTimeout);
retryOp.retry((opCallback) => {
/*Codes_SRS_NODE_INTERNAL_CLIENT_05_007: [The sendEvent method shall send the event indicated by the message argument via the transport associated with the Client instance.]*/
this._transport.sendEvent(message, opCallback);
}, (err, result) => {
safeCallback(_callback, err, result);
});
}, sendEventCallback);
}
sendEventBatch(messages, sendEventBatchCallback) {
return (0, azure_iot_common_1.callbackToPromise)((_callback) => {
const retryOp = new azure_iot_common_2.RetryOperation('sendEventBatch', this._retryPolicy, this._maxOperationTimeout);
retryOp.retry((opCallback) => {
/*Codes_SRS_NODE_INTERNAL_CLIENT_05_008: [The sendEventBatch method shall send the list of events (indicated by the messages argument) via the transport associated with the Client instance.]*/
this._transport.sendEventBatch(messages, opCallback);
}, (err, result) => {
safeCallback(_callback, err, result);
});
}, sendEventBatchCallback);
}
close(closeCallback) {
return (0, azure_iot_common_1.callbackToPromise)((_callback) => {
this._closeTransport((err, result) => {
safeCallback(_callback, err, result);
});
}, closeCallback);
}
setTransportOptions(options, done) {
return (0, azure_iot_common_1.callbackToPromise)((_callback) => {
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_024: [The ‘setTransportOptions’ method shall throw a ‘ReferenceError’ if the options object is falsy] */
if (!options)
throw new ReferenceError('options cannot be falsy.');
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_025: [The ‘setTransportOptions’ method shall throw a ‘NotImplementedError’ if the transport doesn’t implement a ‘setOption’ method.] */
if (typeof this._transport.setOptions !== 'function')
throw new azure_iot_common_1.errors.NotImplementedError('setOptions does not exist on this transport');
const clientOptions = {
http: {
receivePolicy: options
}
};
const retryOp = new azure_iot_common_2.RetryOperation('setTransportOptions', this._retryPolicy, this._maxOperationTimeout);
retryOp.retry((opCallback) => {
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_021: [The ‘setTransportOptions’ method shall call the ‘setOptions’ method on the transport object.]*/
this._transport.setOptions(clientOptions, opCallback);
}, (err) => {
if (err) {
safeCallback(_callback, err);
}
else {
safeCallback(_callback, null, new azure_iot_common_1.results.TransportConfigured());
}
});
}, done);
}
setOptions(options, done) {
return (0, azure_iot_common_1.callbackToPromise)((_callback) => {
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_042: [The `setOptions` method shall throw a `ReferenceError` if the options object is falsy.]*/
if (!options)
throw new ReferenceError('options cannot be falsy.');
/*Codes_SRS_NODE_INTERNAL_CLIENT_06_001: [The `setOptions` method shall assume the `ca` property is the name of an already existent file and it will attempt to read that file as a pem into a string value and pass the string to config object `ca` property. Otherwise, it is assumed to be a pem string.] */
if (options.ca) {
// eslint-disable-next-line security/detect-non-literal-fs-filename
fs.readFile(options.ca, 'utf8', (err, contents) => {
if (!err) {
const localOptions = {};
for (const k in options) {
localOptions[k] = options[k];
}
localOptions.ca = contents;
this._invokeSetOptions(localOptions, _callback);
}
else {
this._invokeSetOptions(options, _callback);
}
});
}
else {
this._invokeSetOptions(options, _callback);
}
}, done);
}
complete(message, completeCallback) {
return (0, azure_iot_common_1.callbackToPromise)((_callback) => {
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_016: [The ‘complete’ method shall throw a ReferenceError if the ‘message’ parameter is falsy.] */
if (!message)
throw new ReferenceError('message is \'' + message + '\'');
const retryOp = new azure_iot_common_2.RetryOperation('complete', this._retryPolicy, this._maxOperationTimeout);
retryOp.retry((opCallback) => {
this._transport.complete(message, opCallback);
}, (err, result) => {
safeCallback(_callback, err, result);
});
}, completeCallback);
}
reject(message, rejectCallback) {
return (0, azure_iot_common_1.callbackToPromise)((_callback) => {
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_018: [The reject method shall throw a ReferenceError if the ‘message’ parameter is falsy.] */
if (!message)
throw new ReferenceError('message is \'' + message + '\'');
const retryOp = new azure_iot_common_2.RetryOperation('reject', this._retryPolicy, this._maxOperationTimeout);
retryOp.retry((opCallback) => {
this._transport.reject(message, opCallback);
}, (err, result) => {
safeCallback(_callback, err, result);
});
}, rejectCallback);
}
abandon(message, abandonCallback) {
return (0, azure_iot_common_1.callbackToPromise)((_callback) => {
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_017: [The abandon method shall throw a ReferenceError if the ‘message’ parameter is falsy.] */
if (!message)
throw new ReferenceError('message is \'' + message + '\'');
const retryOp = new azure_iot_common_2.RetryOperation('abandon', this._retryPolicy, this._maxOperationTimeout);
retryOp.retry((opCallback) => {
this._transport.abandon(message, opCallback);
}, (err, result) => {
safeCallback(_callback, err, result);
});
}, abandonCallback);
}
getTwin(done) {
return (0, azure_iot_common_1.callbackToPromise)((_callback) => {
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_094: [If this is the first call to `getTwin` the method shall instantiate a new `Twin` object and pass it the transport currently in use.]*/
if (!this._twin) {
this._twin = new twin_1.Twin(this._transport, this._retryPolicy, this._maxOperationTimeout);
}
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_095: [The `getTwin` method shall call the `get()` method on the `Twin` object currently in use and pass it its `done` argument for a callback.]*/
this._twin.get(_callback);
}, done);
}
/**
* Sets the retry policy used by the client on all operations. The default is {@link azure-iot-common.ExponentialBackoffWithJitter|ExponentialBackoffWithJitter}.
*
* @param policy {RetryPolicy} The retry policy that should be used for all future operations.
*/
setRetryPolicy(policy) {
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_083: [The `setRetryPolicy` method shall throw a `ReferenceError` if the policy object is falsy.]*/
if (!policy) {
throw new ReferenceError('\'policy\' cannot be \'' + policy + '\'');
}
else {
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_084: [The `setRetryPolicy` method shall throw an `ArgumentError` if the policy object doesn't have a `shouldRetry` method.]*/
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_085: [The `setRetryPolicy` method shall throw an `ArgumentError` if the policy object doesn't have a `nextRetryTimeout` method.]*/
if (typeof policy.shouldRetry !== 'function' || typeof policy.nextRetryTimeout !== 'function') {
throw new azure_iot_common_1.errors.ArgumentError('A policy object must have a maxTimeout property that is a number and a nextRetryTimeout method.');
}
}
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_086: [Any operation happening after a `setRetryPolicy` call should use the policy set during that call.]*/
this._retryPolicy = policy;
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_096: [The `setRetryPolicy` method shall call the `setRetryPolicy` method on the twin if it is set and pass it the `policy` object.]*/
if (this._twin) {
this._twin.setRetryPolicy(policy);
}
}
_onDeviceMethod(methodName, callback) {
//
// We want to always retain that the we want to have this feature enabled because the API (.on) doesn't really
// provide for the capability to say it failed. It can certainly fail because a network operation is required to
// enable.
// By saving this off, we are strictly honoring that the feature is enabled. If it doesn't turn on we signal via
// the emitted 'error' that something bad happened.
// But if we ever again attain a connected state, this feature will be operational.
//
this._userRegisteredMethodListener = true;
// validate input args
this._validateDeviceMethodInputs(methodName, callback);
this._methodCallbackMap[methodName] = callback;
this._addMethodCallback(methodName, callback);
this._enableMethods((err) => {
if (err) {
this.emit('error', err);
}
});
}
_invokeSetOptions(options, done) {
// Making this an operation that can be retried because we cannot assume the transport's behavior (whether it's going to disconnect/reconnect, etc).
const retryOp = new azure_iot_common_2.RetryOperation('_invokeSetOptions', this._retryPolicy, this._maxOperationTimeout);
retryOp.retry((opCallback) => {
this._transport.setOptions(options, opCallback);
}, (err) => {
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_043: [The `done` callback shall be invoked no parameters when it has successfully finished setting the client and/or transport options.]*/
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_044: [The `done` callback shall be invoked with a standard javascript `Error` object and no result object if the client could not be configured as requested.]*/
safeCallback(done, err);
});
}
_validateDeviceMethodInputs(methodName, callback) {
// Codes_SRS_NODE_INTERNAL_CLIENT_13_020: [ onDeviceMethod shall throw a ReferenceError if methodName is falsy. ]
if (!methodName) {
throw new ReferenceError('methodName cannot be \'' + methodName + '\'');
}
// Codes_SRS_NODE_INTERNAL_CLIENT_13_024: [ onDeviceMethod shall throw a TypeError if methodName is not a string. ]
if (typeof (methodName) !== 'string') {
throw new TypeError('methodName\'s type is \'' + typeof (methodName) + '\'. A string was expected.');
}
// Codes_SRS_NODE_INTERNAL_CLIENT_13_022: [ onDeviceMethod shall throw a ReferenceError if callback is falsy. ]
if (!callback) {
throw new ReferenceError('callback cannot be \'' + callback + '\'');
}
// Codes_SRS_NODE_INTERNAL_CLIENT_13_025: [ onDeviceMethod shall throw a TypeError if callback is not a Function. ]
if (typeof (callback) !== 'function') {
throw new TypeError('callback\'s type is \'' + typeof (callback) + '\'. A function reference was expected.');
}
// Codes_SRS_NODE_INTERNAL_CLIENT_13_023: [ onDeviceMethod shall throw an Error if a listener is already subscribed for a given method call. ]
if (this._methodCallbackMap[methodName]) {
throw new Error('A handler for this method has already been registered with the client.');
}
}
_addMethodCallback(methodName, callback) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
this._transport.onDeviceMethod(methodName, (message) => {
// build the response object
const response = new device_method_1.DeviceMethodResponse(message.requestId, self._transport);
// build the request object
try {
const request = new device_method_1.DeviceMethodRequest(message.requestId, message.methods.methodName, message.body);
// Codes_SRS_NODE_INTERNAL_CLIENT_13_001: [ The onDeviceMethod method shall cause the callback function to be invoked when a cloud-to-device method invocation signal is received from the IoT Hub service. ]
callback(request, response);
}
catch (err) {
response.send(400, 'Invalid request format: ' + err.message, (err) => {
if (err) {
debugErrors('Error sending invalid request response back to application: ' + err);
}
});
}
});
}
_enableMethods(callback) {
debug('enabling methods');
const retryOp = new azure_iot_common_2.RetryOperation('_enableMethods', this._retryPolicy, this._maxOperationTimeout);
retryOp.retry((opCallback) => {
this._transport.enableMethods(opCallback);
}, (err) => {
if (!err) {
debug('enabled methods');
}
else {
debugErrors('Error while enabling methods: ' + err);
}
callback(err);
});
}
// Currently there is no code making use of this function, because there is no "removeDeviceMethod" corresponding to "onDeviceMethod"
// private _disableMethods(callback: (err?: Error) => void): void {
// if (this._methodsEnabled) {
// this._transport.disableMethods((err) => {
// if (!err) {
// this._methodsEnabled = false;
// }
// callback(err);
// });
// } else {
// callback();
// }
// }
_closeTransport(closeCallback) {
const onDisconnected = (err, result) => {
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_056: [The `close` method shall not throw if the `closeCallback` is not passed.]*/
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_055: [The `close` method shall call the `closeCallback` function when done with either a single Error object if it failed or null and a results.Disconnected object if successful.]*/
safeCallback(closeCallback, err, result);
};
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_046: [The `close` method shall remove the listener that has been attached to the transport `disconnect` event.]*/
this._transport.removeListener('disconnect', this._disconnectHandler);
/*Codes_SRS_NODE_INTERNAL_CLIENT_16_001: [The `close` function shall call the transport's `disconnect` function if it exists.]*/
this._transport.disconnect((disconnectError, disconnectResult) => {
onDisconnected(disconnectError, disconnectResult);
});
}
}
exports.InternalClient = InternalClient;
// SAS token created by the client have a lifetime of 60 minutes, renew every 45 minutes
/**
* @private
*/
InternalClient.sasRenewalInterval = 2700000;
//# sourceMappingURL=internal_client.js.map