From 223e24b2af776f029e4e2ee790d5b820b8088168 Mon Sep 17 00:00:00 2001 From: Luca Barbetti Date: Wed, 19 Dec 2018 12:10:57 +0100 Subject: [PATCH 1/5] Clean up subscribed topics upon disconnect --- src/index.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/index.js b/src/index.js index e4cf71d..0b1d276 100644 --- a/src/index.js +++ b/src/index.js @@ -142,13 +142,13 @@ const connect = options => new Promise((resolve, reject) => { // connection loss Object.getOwnPropertySymbols(subscribedTopics).forEach((connectionId) => { Object.values(subscribedTopics[connectionId]).forEach((subscribeParams) => { - subscribe(connectionId, subscribeParams.topic, subscribeParams.cb) + subscribe(connectionId, subscribeParams.topic, subscribeParams.cb); }); }); } if (typeof opts.onConnected === 'function') { - opts.onConnected(reconnect) + opts.onConnected(reconnect); } }; @@ -198,7 +198,14 @@ const disconnect = id => new Promise((resolve, reject) => { return reject(new Error('disconnection failed: client not found')); } - client.disconnect(); + try { + client.disconnect(); + } catch (error) { + return reject(error); + } + + // Remove the connection + delete connections[id]; // Remove property callbacks to allow resubscribing in a later connect() Object.keys(propertyCallback).forEach((topic) => { @@ -208,19 +215,14 @@ 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[id]; 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')); + return reject(new Error('subscription failed: client not found')); } return client.subscribe(topic, { @@ -385,7 +387,7 @@ const onPropertyValue = (connectionId, thingId, name, cb) => { subscribedTopics[connectionId][thingId] = { topic: propOutputTopic, - cb: cb, + cb, }; if (!propertyCallback[propOutputTopic]) { From 87a16f999d4968751f3784888b154d57486bd90c Mon Sep 17 00:00:00 2001 From: Luca Barbetti Date: Wed, 19 Dec 2018 15:51:16 +0100 Subject: [PATCH 2/5] Drop support for multiple connections --- README.md | 20 ++++---- src/index.js | 96 ++++++++++++++++++-------------------- test/arduino-cloud.test.js | 49 ++++++++++--------- 3 files changed, 80 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 885ec5c..ca32a27 100644 --- a/README.md +++ b/README.md @@ -24,48 +24,48 @@ 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 }); diff --git a/src/index.js b/src/index.js index 0b1d276..e42493d 100644 --- a/src/index.js +++ b/src/index.js @@ -48,7 +48,7 @@ import CBOR from 'cbor-js'; import ArduinoCloudError from './ArduinoCloudError'; -const connections = {}; +let connection = null; const subscribedTopics = {}; const propertyCallback = {}; const arduinoCloudPort = 8443; @@ -82,6 +82,10 @@ const connect = options => new Promise((resolve, reject) => { onConnected: options.onConnected, }; + 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,10 +144,8 @@ 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); }); } @@ -170,9 +172,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,20 +193,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 { - client.disconnect(); + connection.disconnect(); } catch (error) { return reject(error); } // Remove the connection - delete connections[id]; + connection = null; // Remove property callbacks to allow resubscribing in a later connect() Object.keys(propertyCallback).forEach((topic) => { @@ -215,35 +215,36 @@ const disconnect = id => new Promise((resolve, reject) => { }); // Clean up subscribed topics - a new connection might not need the same topics - delete subscribedTopics[id]; + Object.keys(subscribedTopics).forEach((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('subscription failed: client not found')); +const subscribe = (topic, cb) => new Promise((resolve, reject) => { + if (!connection) { + return reject(new Error('subscription failed: connection closed')); } - return client.subscribe(topic, { + 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(), }); @@ -260,32 +261,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)) { @@ -315,7 +315,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) => { @@ -357,7 +357,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)) { @@ -369,10 +369,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'); } @@ -381,11 +381,7 @@ 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, }; @@ -393,7 +389,7 @@ const onPropertyValue = (connectionId, thingId, name, 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; } 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); }); }); From 275bcdbebe22cde4e2464c93aa50bfe134d7fc0e Mon Sep 17 00:00:00 2001 From: Luca Barbetti Date: Wed, 19 Dec 2018 18:26:56 +0100 Subject: [PATCH 3/5] Add the updateToken method to reconnect after a token refresh --- README.md | 6 ++++++ src/index.js | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/README.md b/README.md index ca32a27..7280e25 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,12 @@ 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 e42493d..a43a50a 100644 --- a/src/index.js +++ b/src/index.js @@ -49,6 +49,7 @@ import CBOR from 'cbor-js'; import ArduinoCloudError from './ArduinoCloudError'; let connection = null; +let connectionOptions = null; const subscribedTopics = {}; const propertyCallback = {}; const arduinoCloudPort = 8443; @@ -82,6 +83,8 @@ const connect = options => new Promise((resolve, reject) => { onConnected: options.onConnected, }; + connectionOptions = opts; + if (connection) { return reject(new Error('connection failed: connection already open')); } @@ -222,6 +225,40 @@ const disconnect = () => new Promise((resolve, reject) => { return resolve(); }); +const updateToken = token => new Promise((resolve, reject) => { + if (!connection) { + return reject(new Error('disconnection failed: connection closed')); + } + + try { + // Disconnect to the connection using the old token + connection.disconnect(); + } catch (error) { + // Ignore disconnection errors that comes out when Paho is reconnecting + } + + // Remove the connection + connection = null; + + return resolve(); +}) + .then(() => { + // Reconnect using the new token + const reconnectOptions = Object.assign({}, connectionOptions, { token }); + return connect(reconnectOptions); + }) + .then(() => { + // 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); + } + }); + const subscribe = (topic, cb) => new Promise((resolve, reject) => { if (!connection) { return reject(new Error('subscription failed: connection closed')); @@ -399,6 +436,7 @@ const onPropertyValue = (thingId, name, cb) => { export default { connect, disconnect, + updateToken, subscribe, unsubscribe, sendMessage, From 4dcdc373511f212757ef4813a632ff4c56a7e14b Mon Sep 17 00:00:00 2001 From: Luca Barbetti Date: Fri, 21 Dec 2018 14:23:57 +0100 Subject: [PATCH 4/5] Retry the token renewal process every 30 secs in case of failure --- src/index.js | 76 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/src/index.js b/src/index.js index a43a50a..cac7159 100644 --- a/src/index.js +++ b/src/index.js @@ -225,39 +225,65 @@ const disconnect = () => new Promise((resolve, reject) => { return resolve(); }); -const updateToken = token => new Promise((resolve, reject) => { +const updateToken = token => new Promise(((updateTokenResolve, updateTokenReject) => { if (!connection) { - return reject(new Error('disconnection failed: connection closed')); + return updateTokenReject(new Error('disconnection failed: connection closed')); } - try { - // Disconnect to the connection using the old token - connection.disconnect(); - } catch (error) { - // Ignore disconnection errors that comes out when Paho is reconnecting - } + // Wrap the update token process into a single promise-returning function + const updateTokenPromise = () => new Promise((resolve) => { + try { + // Disconnect to the connection using the old token + connection.disconnect(); + } catch (error) { + // Ignore disconnection errors that comes out when Paho is reconnecting + } - // Remove the connection - connection = null; + // Remove the connection + connection = null; - return resolve(); -}) - .then(() => { - // Reconnect using the new token - const reconnectOptions = Object.assign({}, connectionOptions, { token }); - return connect(reconnectOptions); + return resolve(); }) - .then(() => { - // Re-subscribe to all topics subscribed before the reconnection - Object.values(subscribedTopics).forEach((subscribeParams) => { - subscribe(subscribeParams.topic, subscribeParams.cb); + .then(() => { + // Reconnect using the new token + const reconnectOptions = Object.assign({}, connectionOptions, { token }); + return connect(reconnectOptions); + }) + .then(() => { + // 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); + } }); - if (typeof connectionOptions.onConnected === 'function') { - // Call the connection callback (with the reconnection param set to true) - connectionOptions.onConnected(true); - } - }); + let updateTokenInterval = null; + + // It runs the token update. If it succeed, clears the interval and + // exits updateToken. + const updateTokenIntervalFunction = () => { + updateTokenPromise() + .then(() => { + // Token update went well - exiting + clearInterval(updateTokenInterval); + return updateTokenResolve(); + }) + .catch(() => { + // Ignore reconnection errors - keep trying to reconnect + }); + }; + + // Try to refresh the token every 30 secs + updateTokenInterval = setInterval(updateTokenIntervalFunction, 30000); + + // Try immediately to refresh the token - if it fails the next + // tentative will be started by the setInterval + updateTokenIntervalFunction(); +})); const subscribe = (topic, cb) => new Promise((resolve, reject) => { if (!connection) { From 7425161074e9d09e000438db5134337f8a494c91 Mon Sep 17 00:00:00 2001 From: Luca Barbetti Date: Fri, 4 Jan 2019 17:44:45 +0100 Subject: [PATCH 5/5] Rewrite the updateToken function as asyncFunction, prevent duplicate connections --- .eslintrc.js | 3 +++ src/index.js | 69 ++++++++++++++++++++-------------------------------- 2 files changed, 29 insertions(+), 43 deletions(-) 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/src/index.js b/src/index.js index cac7159..0ec5a52 100644 --- a/src/index.js +++ b/src/index.js @@ -225,31 +225,24 @@ const disconnect = () => new Promise((resolve, reject) => { return resolve(); }); -const updateToken = token => new Promise(((updateTokenResolve, updateTokenReject) => { - if (!connection) { - return updateTokenReject(new Error('disconnection failed: connection closed')); - } - - // Wrap the update token process into a single promise-returning function - const updateTokenPromise = () => new Promise((resolve) => { +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 { - // Disconnect to the connection using the old token - connection.disconnect(); - } catch (error) { - // Ignore disconnection errors that comes out when Paho is reconnecting - } + if (connection) { + // Disconnect to the connection that is using the old token + connection.disconnect(); - // Remove the connection - connection = null; + // Remove the connection + connection = null; + } - return resolve(); - }) - .then(() => { // Reconnect using the new token const reconnectOptions = Object.assign({}, connectionOptions, { token }); - return connect(reconnectOptions); - }) - .then(() => { + await connect(reconnectOptions); + // Re-subscribe to all topics subscribed before the reconnection Object.values(subscribedTopics).forEach((subscribeParams) => { subscribe(subscribeParams.topic, subscribeParams.cb); @@ -259,31 +252,21 @@ const updateToken = token => new Promise(((updateTokenResolve, updateTokenReject // Call the connection callback (with the reconnection param set to true) connectionOptions.onConnected(true); } - }); - - let updateTokenInterval = null; - - // It runs the token update. If it succeed, clears the interval and - // exits updateToken. - const updateTokenIntervalFunction = () => { - updateTokenPromise() - .then(() => { - // Token update went well - exiting - clearInterval(updateTokenInterval); - return updateTokenResolve(); - }) - .catch(() => { - // Ignore reconnection errors - keep trying to reconnect - }); - }; - // Try to refresh the token every 30 secs - updateTokenInterval = setInterval(updateTokenIntervalFunction, 30000); + // Exit the infinite loop + return; + } catch (error) { + // Expose paho-mqtt errors + // eslint-disable-next-line no-console + console.error(error); - // Try immediately to refresh the token - if it fails the next - // tentative will be started by the setInterval - updateTokenIntervalFunction(); -})); + // Something went wrong during the reconnection - retry in 5 secs. + await new Promise((resolve) => { + setTimeout(resolve, 5000); + }); + } + } +}; const subscribe = (topic, cb) => new Promise((resolve, reject) => { if (!connection) {