diff --git a/.eslintrc.js b/.eslintrc.js index 2432172..4c47340 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,5 +3,8 @@ module.exports = { "env": { "browser": true, "jest": true + }, + "rules": { + "no-await-in-loop": 0 } }; diff --git a/README.md b/README.md index 885ec5c..7280e25 100644 --- a/README.md +++ b/README.md @@ -24,51 +24,57 @@ import ArduinoCloud from 'arduino-iot-js'; // apiUrl: 'AUTH SERVER URL', // Default is https://auth.arduino.cc // onDisconnect: message => { /* Disconnection callback */ } // } -ArduinoCloud.connect(options).then(connectionId => { +ArduinoCloud.connect(options).then(() => { // Connected }); -ArduinoCloud.disconnect(connectionId).then(() => { +ArduinoCloud.disconnect().then(() => { // Disconnected }); -ArduinoCloud.subscribe(connectionId, topic, cb).then(topic => { +ArduinoCloud.subscribe(topic, cb).then(topic => { // Subscribed to topic, messaged fired in the cb }); -ArduinoCloud.unsubscribe(connectionId, topic).then(topic => { +ArduinoCloud.unsubscribe(topic).then(topic => { // Unsubscribed to topic }); -ArduinoCloud.sendMessage(connectionId, topic, message).then(() => { +ArduinoCloud.sendMessage(topic, message).then(() => { // Message sent }); -ArduinoCloud.openCloudMonitor(connectionId, deviceId, cb).then(topic => { +ArduinoCloud.openCloudMonitor(deviceId, cb).then(topic => { // Cloud monitor messages fired to cb }); -ArduinoCloud.writeCloudMonitor(connectionId, deviceId, message).then(() => { +ArduinoCloud.writeCloudMonitor(deviceId, message).then(() => { // Message sent to cloud monitor }); -ArduinoCloud.closeCloudMonitor(connectionId, deviceId).then(topic => { +ArduinoCloud.closeCloudMonitor(deviceId).then(topic => { // Close cloud monitor }); // Send a property value to a device // - value can be a string, a boolean or a number // - timestamp is a unix timestamp, not required -ArduinoCloud.sendProperty(connectionId, thingId, name, value, timestamp).then(() => { +ArduinoCloud.sendProperty(thingId, name, value, timestamp).then(() => { // Property value sent }); // Register a callback on a property value change // -ArduinoCloud.onPropertyValue(connectionId, thingId, propertyName, updateCb).then(() => { +ArduinoCloud.onPropertyValue(thingId, propertyName, updateCb).then(() => { // updateCb(message) will be called every time a new value is available. Value can be string, number, or a boolean depending on the property type }); +// Re-connect with a new authentication token, keeping the subscriptions +// to the Things topics +ArduinoCloud.updateToken(newToken).then(() => { + // Successful reconnection with the provided new token +}); + ``` ## Run tests diff --git a/src/index.js b/src/index.js index e4cf71d..0ec5a52 100644 --- a/src/index.js +++ b/src/index.js @@ -48,7 +48,8 @@ import CBOR from 'cbor-js'; import ArduinoCloudError from './ArduinoCloudError'; -const connections = {}; +let connection = null; +let connectionOptions = null; const subscribedTopics = {}; const propertyCallback = {}; const arduinoCloudPort = 8443; @@ -82,6 +83,12 @@ const connect = options => new Promise((resolve, reject) => { onConnected: options.onConnected, }; + connectionOptions = opts; + + if (connection) { + return reject(new Error('connection failed: connection already open')); + } + if (!opts.host) { return reject(new Error('connection failed: you need to provide a valid host (broker)')); } @@ -140,15 +147,13 @@ const connect = options => new Promise((resolve, reject) => { if (reconnect === true) { // This is a re-connection: re-subscribe to all topics subscribed before the // connection loss - Object.getOwnPropertySymbols(subscribedTopics).forEach((connectionId) => { - Object.values(subscribedTopics[connectionId]).forEach((subscribeParams) => { - subscribe(connectionId, subscribeParams.topic, subscribeParams.cb) - }); + Object.values(subscribedTopics).forEach((subscribeParams) => { + subscribe(subscribeParams.topic, subscribeParams.cb); }); } if (typeof opts.onConnected === 'function') { - opts.onConnected(reconnect) + opts.onConnected(reconnect); } }; @@ -170,9 +175,8 @@ const connect = options => new Promise((resolve, reject) => { reconnect: true, keepAliveInterval: 30, onSuccess: () => { - const id = Symbol(clientID); - connections[id] = client; - return resolve(id); + connection = client; + return resolve(); }, onFailure: ({ errorCode, errorMessage }) => reject( new ArduinoCloudError(errorCode, errorMessage), @@ -192,13 +196,19 @@ const connect = options => new Promise((resolve, reject) => { }, reject); }); -const disconnect = id => new Promise((resolve, reject) => { - const client = connections[id]; - if (!client) { - return reject(new Error('disconnection failed: client not found')); +const disconnect = () => new Promise((resolve, reject) => { + if (!connection) { + return reject(new Error('disconnection failed: connection closed')); + } + + try { + connection.disconnect(); + } catch (error) { + return reject(error); } - client.disconnect(); + // Remove the connection + connection = null; // Remove property callbacks to allow resubscribing in a later connect() Object.keys(propertyCallback).forEach((topic) => { @@ -209,39 +219,78 @@ const disconnect = id => new Promise((resolve, reject) => { // Clean up subscribed topics - a new connection might not need the same topics Object.keys(subscribedTopics).forEach((topic) => { - if (subscribedTopics[topic]) { - delete subscribedTopics[topic]; - } + delete subscribedTopics[topic]; }); return resolve(); }); -const subscribe = (id, topic, cb) => new Promise((resolve, reject) => { - const client = connections[id]; - if (!client) { - return reject(new Error('disconnection failed: client not found')); +const updateToken = async function updateToken(token) { + // This infinite loop will exit once the reconnection is successful - + // and will pause between each reconnection tentative, every 5 secs. + // eslint-disable-next-line no-constant-condition + while (true) { + try { + if (connection) { + // Disconnect to the connection that is using the old token + connection.disconnect(); + + // Remove the connection + connection = null; + } + + // Reconnect using the new token + const reconnectOptions = Object.assign({}, connectionOptions, { token }); + await connect(reconnectOptions); + + // Re-subscribe to all topics subscribed before the reconnection + Object.values(subscribedTopics).forEach((subscribeParams) => { + subscribe(subscribeParams.topic, subscribeParams.cb); + }); + + if (typeof connectionOptions.onConnected === 'function') { + // Call the connection callback (with the reconnection param set to true) + connectionOptions.onConnected(true); + } + + // Exit the infinite loop + return; + } catch (error) { + // Expose paho-mqtt errors + // eslint-disable-next-line no-console + console.error(error); + + // Something went wrong during the reconnection - retry in 5 secs. + await new Promise((resolve) => { + setTimeout(resolve, 5000); + }); + } } +}; - return client.subscribe(topic, { +const subscribe = (topic, cb) => new Promise((resolve, reject) => { + if (!connection) { + return reject(new Error('subscription failed: connection closed')); + } + + return connection.subscribe(topic, { onSuccess: () => { - if (!client.topics[topic]) { - client.topics[topic] = []; + if (!connection.topics[topic]) { + connection.topics[topic] = []; } - client.topics[topic].push(cb); + connection.topics[topic].push(cb); return resolve(topic); }, onFailure: () => reject(), }); }); -const unsubscribe = (id, topic) => new Promise((resolve, reject) => { - const client = connections[id]; - if (!client) { - return reject(new Error('disconnection failed: client not found')); +const unsubscribe = topic => new Promise((resolve, reject) => { + if (!connection) { + return reject(new Error('disconnection failed: connection closed')); } - return client.unsubscribe(topic, { + return connection.unsubscribe(topic, { onSuccess: () => resolve(topic), onFailure: () => reject(), }); @@ -258,32 +307,31 @@ const arrayBufferToBase64 = (buffer) => { return window.btoa(binary); }; -const sendMessage = (id, topic, message) => new Promise((resolve, reject) => { - const client = connections[id]; - if (!client) { - return reject(new Error('disconnection failed: client not found')); +const sendMessage = (topic, message) => new Promise((resolve, reject) => { + if (!connection) { + return reject(new Error('disconnection failed: connection closed')); } - client.publish(topic, message, 1, false); + connection.publish(topic, message, 1, false); return resolve(); }); -const openCloudMonitor = (id, deviceId, cb) => { +const openCloudMonitor = (deviceId, cb) => { const cloudMonitorOutputTopic = `/a/d/${deviceId}/s/o`; - return subscribe(id, cloudMonitorOutputTopic, cb); + return subscribe(cloudMonitorOutputTopic, cb); }; -const writeCloudMonitor = (id, deviceId, message) => { +const writeCloudMonitor = (deviceId, message) => { const cloudMonitorInputTopic = `/a/d/${deviceId}/s/i`; - return sendMessage(id, cloudMonitorInputTopic, message); + return sendMessage(cloudMonitorInputTopic, message); }; -const closeCloudMonitor = (id, deviceId) => { +const closeCloudMonitor = (deviceId) => { const cloudMonitorOutputTopic = `/a/d/${deviceId}/s/o`; - return unsubscribe(id, cloudMonitorOutputTopic); + return unsubscribe(cloudMonitorOutputTopic); }; -const sendProperty = (connectionId, thingId, name, value, timestamp) => { +const sendProperty = (thingId, name, value, timestamp) => { const propertyInputTopic = `/a/t/${thingId}/e/i`; if (timestamp && !Number.isInteger(timestamp)) { @@ -313,7 +361,7 @@ const sendProperty = (connectionId, thingId, name, value, timestamp) => { break; } - return sendMessage(connectionId, propertyInputTopic, CBOR.encode([cborValue])); + return sendMessage(propertyInputTopic, CBOR.encode([cborValue])); }; const getSenml = (deviceId, name, value, timestamp) => { @@ -355,7 +403,7 @@ const getCborValue = (senMl) => { return arrayBufferToBase64(cborEncoded); }; -const sendPropertyAsDevice = (connectionId, deviceId, thingId, name, value, timestamp) => { +const sendPropertyAsDevice = (deviceId, thingId, name, value, timestamp) => { const propertyInputTopic = `/a/t/${thingId}/e/o`; if (timestamp && !Number.isInteger(timestamp)) { @@ -367,10 +415,10 @@ const sendPropertyAsDevice = (connectionId, deviceId, thingId, name, value, time } const senMlValue = getSenml(deviceId, name, value, timestamp); - return sendMessage(connectionId, propertyInputTopic, CBOR.encode([senMlValue])); + return sendMessage(propertyInputTopic, CBOR.encode([senMlValue])); }; -const onPropertyValue = (connectionId, thingId, name, cb) => { +const onPropertyValue = (thingId, name, cb) => { if (!name) { throw new Error('Invalid property name'); } @@ -379,19 +427,15 @@ const onPropertyValue = (connectionId, thingId, name, cb) => { } const propOutputTopic = `/a/t/${thingId}/e/o`; - if (!subscribedTopics[connectionId]) { - subscribedTopics[connectionId] = {}; - } - - subscribedTopics[connectionId][thingId] = { + subscribedTopics[thingId] = { topic: propOutputTopic, - cb: cb, + cb, }; if (!propertyCallback[propOutputTopic]) { propertyCallback[propOutputTopic] = {}; propertyCallback[propOutputTopic][name] = cb; - subscribe(connectionId, propOutputTopic, cb); + subscribe(propOutputTopic, cb); } else if (propertyCallback[propOutputTopic] && !propertyCallback[propOutputTopic][name]) { propertyCallback[propOutputTopic][name] = cb; } @@ -401,6 +445,7 @@ const onPropertyValue = (connectionId, thingId, name, cb) => { export default { connect, disconnect, + updateToken, subscribe, unsubscribe, sendMessage, diff --git a/test/arduino-cloud.test.js b/test/arduino-cloud.test.js index 5b34493..39ee74d 100644 --- a/test/arduino-cloud.test.js +++ b/test/arduino-cloud.test.js @@ -19,9 +19,8 @@ */ const ArduinoCloud = require('../dist/index.js'); -let connectionId; const deviceId = '1f4ced70-53ad-4b29-b221-1b0abbdfc757'; -const thingId = '2cea8542-d472-4464-859c-4ef4dfc7d1d3' +const thingId = '2cea8542-d472-4464-859c-4ef4dfc7d1d3'; const propertyIntName = 'integer'; const propertyIntValue = 22; @@ -34,27 +33,27 @@ const propertyStrVal = 'ok'; const propertyBoolName = 'boolean'; const propertyBoolVal = true; -it('ArduinoCloud connection', () => { - expect.assertions(1); +it('ArduinoCloud connection', (done) => { /* global token */ - return ArduinoCloud.connect({ + ArduinoCloud.connect({ token, onDisconnect: (message) => { if (message.errorCode !== 0) { throw Error(message); } }, - }).then((id) => { - connectionId = id; - expect(id).toBeDefined(); - }, (error) => { - throw new Error(error); - }); + }) + .then(() => { + done(); + }) + .catch((error) => { + throw new Error(error); + }); }); it('Property name must be a string in sendProperty', (done) => { try { - ArduinoCloud.sendProperty(connectionId, deviceId, undefined, propertyIntValue); + ArduinoCloud.sendProperty(deviceId, undefined, propertyIntValue); } catch (error) { if (error.message === 'Name must be a valid string') { done(); @@ -63,7 +62,7 @@ it('Property name must be a string in sendProperty', (done) => { }); it('Simulate client write to cloud monitor', (done) => { - ArduinoCloud.writeCloudMonitor(connectionId, deviceId, `this is a test ${Math.random()}`).then(() => { + ArduinoCloud.writeCloudMonitor(deviceId, `this is a test ${Math.random()}`).then(() => { done(); }, (error) => { throw new Error(error); @@ -72,7 +71,7 @@ it('Simulate client write to cloud monitor', (done) => { it('Simulate device write to cloud monitor', (done) => { const cloudMonitorInputTopic = `/a/d/${deviceId}/s/o`; - ArduinoCloud.sendMessage(connectionId, cloudMonitorInputTopic, `this is a test ${Math.random()}`).then(() => { + ArduinoCloud.sendMessage(cloudMonitorInputTopic, `this is a test ${Math.random()}`).then(() => { done(); }, (error) => { throw new Error(error); @@ -87,10 +86,10 @@ it('Simulate device write and client read his message from cloud monitor', (done done(); }; - ArduinoCloud.openCloudMonitor(connectionId, deviceId, cb).then(() => { + ArduinoCloud.openCloudMonitor(deviceId, cb).then(() => { // console.log(`Subscribed to topic: ${topic}`); const message = `This is a test ${new Date()}`; - ArduinoCloud.sendMessage(connectionId, cloudMonitorInputTopic, message).then(() => { + ArduinoCloud.sendMessage(cloudMonitorInputTopic, message).then(() => { // console.log(`[${new Date()}] Message sent to monitor: [${message}]`); }, (error) => { throw new Error(error); @@ -101,42 +100,42 @@ it('Simulate device write and client read his message from cloud monitor', (done }); it('Simulate client read integer property sent by device', (done) => { - ArduinoCloud.onPropertyValue(connectionId, thingId, propertyIntName, (value) => { + ArduinoCloud.onPropertyValue(thingId, propertyIntName, (value) => { if (value === propertyIntValue) { done(); } }).then(() => { - ArduinoCloud.sendPropertyAsDevice(connectionId, deviceId, thingId, propertyIntName, propertyIntValue); + ArduinoCloud.sendPropertyAsDevice(deviceId, thingId, propertyIntName, propertyIntValue); }); }); it('Simulate client read float property sent by device', (done) => { - ArduinoCloud.onPropertyValue(connectionId, thingId, propertyFloatName, (value) => { + ArduinoCloud.onPropertyValue(thingId, propertyFloatName, (value) => { if (value === propertyFloatVal) { done(); } }).then(() => { - ArduinoCloud.sendPropertyAsDevice(connectionId, deviceId, thingId, propertyFloatName, propertyFloatVal); + ArduinoCloud.sendPropertyAsDevice(deviceId, thingId, propertyFloatName, propertyFloatVal); }); }); it('Simulate client read string property sent by device', (done) => { - ArduinoCloud.onPropertyValue(connectionId, thingId, propertyStrName, (value) => { + ArduinoCloud.onPropertyValue(thingId, propertyStrName, (value) => { if (value === propertyStrVal) { done(); } }).then(() => { - ArduinoCloud.sendPropertyAsDevice(connectionId, deviceId, thingId, propertyStrName, propertyStrVal); + ArduinoCloud.sendPropertyAsDevice(deviceId, thingId, propertyStrName, propertyStrVal); }); }); it('Simulate client read boolean property sent by device', (done) => { - ArduinoCloud.onPropertyValue(connectionId, thingId, propertyBoolName, (value) => { + ArduinoCloud.onPropertyValue(thingId, propertyBoolName, (value) => { if (value === propertyBoolVal) { - ArduinoCloud.disconnect(connectionId); + ArduinoCloud.disconnect(); done(); } }).then(() => { - ArduinoCloud.sendPropertyAsDevice(connectionId, deviceId, thingId, propertyBoolName, propertyBoolVal); + ArduinoCloud.sendPropertyAsDevice(deviceId, thingId, propertyBoolName, propertyBoolVal); }); });