Skip to content

Commit 2de29a1

Browse files
authored
Merge pull request #8 from arduino/single-connection
Single connection
2 parents 2fee57d + 7425161 commit 2de29a1

File tree

4 files changed

+141
-88
lines changed

4 files changed

+141
-88
lines changed

.eslintrc.js

+3
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@ module.exports = {
33
"env": {
44
"browser": true,
55
"jest": true
6+
},
7+
"rules": {
8+
"no-await-in-loop": 0
69
}
710
};

README.md

+16-10
Original file line numberDiff line numberDiff line change
@@ -24,51 +24,57 @@ import ArduinoCloud from 'arduino-iot-js';
2424
// apiUrl: 'AUTH SERVER URL', // Default is https://auth.arduino.cc
2525
// onDisconnect: message => { /* Disconnection callback */ }
2626
// }
27-
ArduinoCloud.connect(options).then(connectionId => {
27+
ArduinoCloud.connect(options).then(() => {
2828
// Connected
2929
});
3030

31-
ArduinoCloud.disconnect(connectionId).then(() => {
31+
ArduinoCloud.disconnect().then(() => {
3232
// Disconnected
3333
});
3434

35-
ArduinoCloud.subscribe(connectionId, topic, cb).then(topic => {
35+
ArduinoCloud.subscribe(topic, cb).then(topic => {
3636
// Subscribed to topic, messaged fired in the cb
3737
});
3838

39-
ArduinoCloud.unsubscribe(connectionId, topic).then(topic => {
39+
ArduinoCloud.unsubscribe(topic).then(topic => {
4040
// Unsubscribed to topic
4141
});
4242

43-
ArduinoCloud.sendMessage(connectionId, topic, message).then(() => {
43+
ArduinoCloud.sendMessage(topic, message).then(() => {
4444
// Message sent
4545
});
4646

47-
ArduinoCloud.openCloudMonitor(connectionId, deviceId, cb).then(topic => {
47+
ArduinoCloud.openCloudMonitor(deviceId, cb).then(topic => {
4848
// Cloud monitor messages fired to cb
4949
});
5050

51-
ArduinoCloud.writeCloudMonitor(connectionId, deviceId, message).then(() => {
51+
ArduinoCloud.writeCloudMonitor(deviceId, message).then(() => {
5252
// Message sent to cloud monitor
5353
});
5454

55-
ArduinoCloud.closeCloudMonitor(connectionId, deviceId).then(topic => {
55+
ArduinoCloud.closeCloudMonitor(deviceId).then(topic => {
5656
// Close cloud monitor
5757
});
5858

5959
// Send a property value to a device
6060
// - value can be a string, a boolean or a number
6161
// - timestamp is a unix timestamp, not required
62-
ArduinoCloud.sendProperty(connectionId, thingId, name, value, timestamp).then(() => {
62+
ArduinoCloud.sendProperty(thingId, name, value, timestamp).then(() => {
6363
// Property value sent
6464
});
6565

6666
// Register a callback on a property value change
6767
//
68-
ArduinoCloud.onPropertyValue(connectionId, thingId, propertyName, updateCb).then(() => {
68+
ArduinoCloud.onPropertyValue(thingId, propertyName, updateCb).then(() => {
6969
// 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
7070
});
7171

72+
// Re-connect with a new authentication token, keeping the subscriptions
73+
// to the Things topics
74+
ArduinoCloud.updateToken(newToken).then(() => {
75+
// Successful reconnection with the provided new token
76+
});
77+
7278
```
7379

7480
## Run tests

src/index.js

+98-53
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ import CBOR from 'cbor-js';
4848

4949
import ArduinoCloudError from './ArduinoCloudError';
5050

51-
const connections = {};
51+
let connection = null;
52+
let connectionOptions = null;
5253
const subscribedTopics = {};
5354
const propertyCallback = {};
5455
const arduinoCloudPort = 8443;
@@ -82,6 +83,12 @@ const connect = options => new Promise((resolve, reject) => {
8283
onConnected: options.onConnected,
8384
};
8485

86+
connectionOptions = opts;
87+
88+
if (connection) {
89+
return reject(new Error('connection failed: connection already open'));
90+
}
91+
8592
if (!opts.host) {
8693
return reject(new Error('connection failed: you need to provide a valid host (broker)'));
8794
}
@@ -140,15 +147,13 @@ const connect = options => new Promise((resolve, reject) => {
140147
if (reconnect === true) {
141148
// This is a re-connection: re-subscribe to all topics subscribed before the
142149
// connection loss
143-
Object.getOwnPropertySymbols(subscribedTopics).forEach((connectionId) => {
144-
Object.values(subscribedTopics[connectionId]).forEach((subscribeParams) => {
145-
subscribe(connectionId, subscribeParams.topic, subscribeParams.cb)
146-
});
150+
Object.values(subscribedTopics).forEach((subscribeParams) => {
151+
subscribe(subscribeParams.topic, subscribeParams.cb);
147152
});
148153
}
149154

150155
if (typeof opts.onConnected === 'function') {
151-
opts.onConnected(reconnect)
156+
opts.onConnected(reconnect);
152157
}
153158
};
154159

@@ -170,9 +175,8 @@ const connect = options => new Promise((resolve, reject) => {
170175
reconnect: true,
171176
keepAliveInterval: 30,
172177
onSuccess: () => {
173-
const id = Symbol(clientID);
174-
connections[id] = client;
175-
return resolve(id);
178+
connection = client;
179+
return resolve();
176180
},
177181
onFailure: ({ errorCode, errorMessage }) => reject(
178182
new ArduinoCloudError(errorCode, errorMessage),
@@ -192,13 +196,19 @@ const connect = options => new Promise((resolve, reject) => {
192196
}, reject);
193197
});
194198

195-
const disconnect = id => new Promise((resolve, reject) => {
196-
const client = connections[id];
197-
if (!client) {
198-
return reject(new Error('disconnection failed: client not found'));
199+
const disconnect = () => new Promise((resolve, reject) => {
200+
if (!connection) {
201+
return reject(new Error('disconnection failed: connection closed'));
202+
}
203+
204+
try {
205+
connection.disconnect();
206+
} catch (error) {
207+
return reject(error);
199208
}
200209

201-
client.disconnect();
210+
// Remove the connection
211+
connection = null;
202212

203213
// Remove property callbacks to allow resubscribing in a later connect()
204214
Object.keys(propertyCallback).forEach((topic) => {
@@ -209,39 +219,78 @@ const disconnect = id => new Promise((resolve, reject) => {
209219

210220
// Clean up subscribed topics - a new connection might not need the same topics
211221
Object.keys(subscribedTopics).forEach((topic) => {
212-
if (subscribedTopics[topic]) {
213-
delete subscribedTopics[topic];
214-
}
222+
delete subscribedTopics[topic];
215223
});
216224

217225
return resolve();
218226
});
219227

220-
const subscribe = (id, topic, cb) => new Promise((resolve, reject) => {
221-
const client = connections[id];
222-
if (!client) {
223-
return reject(new Error('disconnection failed: client not found'));
228+
const updateToken = async function updateToken(token) {
229+
// This infinite loop will exit once the reconnection is successful -
230+
// and will pause between each reconnection tentative, every 5 secs.
231+
// eslint-disable-next-line no-constant-condition
232+
while (true) {
233+
try {
234+
if (connection) {
235+
// Disconnect to the connection that is using the old token
236+
connection.disconnect();
237+
238+
// Remove the connection
239+
connection = null;
240+
}
241+
242+
// Reconnect using the new token
243+
const reconnectOptions = Object.assign({}, connectionOptions, { token });
244+
await connect(reconnectOptions);
245+
246+
// Re-subscribe to all topics subscribed before the reconnection
247+
Object.values(subscribedTopics).forEach((subscribeParams) => {
248+
subscribe(subscribeParams.topic, subscribeParams.cb);
249+
});
250+
251+
if (typeof connectionOptions.onConnected === 'function') {
252+
// Call the connection callback (with the reconnection param set to true)
253+
connectionOptions.onConnected(true);
254+
}
255+
256+
// Exit the infinite loop
257+
return;
258+
} catch (error) {
259+
// Expose paho-mqtt errors
260+
// eslint-disable-next-line no-console
261+
console.error(error);
262+
263+
// Something went wrong during the reconnection - retry in 5 secs.
264+
await new Promise((resolve) => {
265+
setTimeout(resolve, 5000);
266+
});
267+
}
224268
}
269+
};
225270

226-
return client.subscribe(topic, {
271+
const subscribe = (topic, cb) => new Promise((resolve, reject) => {
272+
if (!connection) {
273+
return reject(new Error('subscription failed: connection closed'));
274+
}
275+
276+
return connection.subscribe(topic, {
227277
onSuccess: () => {
228-
if (!client.topics[topic]) {
229-
client.topics[topic] = [];
278+
if (!connection.topics[topic]) {
279+
connection.topics[topic] = [];
230280
}
231-
client.topics[topic].push(cb);
281+
connection.topics[topic].push(cb);
232282
return resolve(topic);
233283
},
234284
onFailure: () => reject(),
235285
});
236286
});
237287

238-
const unsubscribe = (id, topic) => new Promise((resolve, reject) => {
239-
const client = connections[id];
240-
if (!client) {
241-
return reject(new Error('disconnection failed: client not found'));
288+
const unsubscribe = topic => new Promise((resolve, reject) => {
289+
if (!connection) {
290+
return reject(new Error('disconnection failed: connection closed'));
242291
}
243292

244-
return client.unsubscribe(topic, {
293+
return connection.unsubscribe(topic, {
245294
onSuccess: () => resolve(topic),
246295
onFailure: () => reject(),
247296
});
@@ -258,32 +307,31 @@ const arrayBufferToBase64 = (buffer) => {
258307
return window.btoa(binary);
259308
};
260309

261-
const sendMessage = (id, topic, message) => new Promise((resolve, reject) => {
262-
const client = connections[id];
263-
if (!client) {
264-
return reject(new Error('disconnection failed: client not found'));
310+
const sendMessage = (topic, message) => new Promise((resolve, reject) => {
311+
if (!connection) {
312+
return reject(new Error('disconnection failed: connection closed'));
265313
}
266314

267-
client.publish(topic, message, 1, false);
315+
connection.publish(topic, message, 1, false);
268316
return resolve();
269317
});
270318

271-
const openCloudMonitor = (id, deviceId, cb) => {
319+
const openCloudMonitor = (deviceId, cb) => {
272320
const cloudMonitorOutputTopic = `/a/d/${deviceId}/s/o`;
273-
return subscribe(id, cloudMonitorOutputTopic, cb);
321+
return subscribe(cloudMonitorOutputTopic, cb);
274322
};
275323

276-
const writeCloudMonitor = (id, deviceId, message) => {
324+
const writeCloudMonitor = (deviceId, message) => {
277325
const cloudMonitorInputTopic = `/a/d/${deviceId}/s/i`;
278-
return sendMessage(id, cloudMonitorInputTopic, message);
326+
return sendMessage(cloudMonitorInputTopic, message);
279327
};
280328

281-
const closeCloudMonitor = (id, deviceId) => {
329+
const closeCloudMonitor = (deviceId) => {
282330
const cloudMonitorOutputTopic = `/a/d/${deviceId}/s/o`;
283-
return unsubscribe(id, cloudMonitorOutputTopic);
331+
return unsubscribe(cloudMonitorOutputTopic);
284332
};
285333

286-
const sendProperty = (connectionId, thingId, name, value, timestamp) => {
334+
const sendProperty = (thingId, name, value, timestamp) => {
287335
const propertyInputTopic = `/a/t/${thingId}/e/i`;
288336

289337
if (timestamp && !Number.isInteger(timestamp)) {
@@ -313,7 +361,7 @@ const sendProperty = (connectionId, thingId, name, value, timestamp) => {
313361
break;
314362
}
315363

316-
return sendMessage(connectionId, propertyInputTopic, CBOR.encode([cborValue]));
364+
return sendMessage(propertyInputTopic, CBOR.encode([cborValue]));
317365
};
318366

319367
const getSenml = (deviceId, name, value, timestamp) => {
@@ -355,7 +403,7 @@ const getCborValue = (senMl) => {
355403
return arrayBufferToBase64(cborEncoded);
356404
};
357405

358-
const sendPropertyAsDevice = (connectionId, deviceId, thingId, name, value, timestamp) => {
406+
const sendPropertyAsDevice = (deviceId, thingId, name, value, timestamp) => {
359407
const propertyInputTopic = `/a/t/${thingId}/e/o`;
360408

361409
if (timestamp && !Number.isInteger(timestamp)) {
@@ -367,10 +415,10 @@ const sendPropertyAsDevice = (connectionId, deviceId, thingId, name, value, time
367415
}
368416

369417
const senMlValue = getSenml(deviceId, name, value, timestamp);
370-
return sendMessage(connectionId, propertyInputTopic, CBOR.encode([senMlValue]));
418+
return sendMessage(propertyInputTopic, CBOR.encode([senMlValue]));
371419
};
372420

373-
const onPropertyValue = (connectionId, thingId, name, cb) => {
421+
const onPropertyValue = (thingId, name, cb) => {
374422
if (!name) {
375423
throw new Error('Invalid property name');
376424
}
@@ -379,19 +427,15 @@ const onPropertyValue = (connectionId, thingId, name, cb) => {
379427
}
380428
const propOutputTopic = `/a/t/${thingId}/e/o`;
381429

382-
if (!subscribedTopics[connectionId]) {
383-
subscribedTopics[connectionId] = {};
384-
}
385-
386-
subscribedTopics[connectionId][thingId] = {
430+
subscribedTopics[thingId] = {
387431
topic: propOutputTopic,
388-
cb: cb,
432+
cb,
389433
};
390434

391435
if (!propertyCallback[propOutputTopic]) {
392436
propertyCallback[propOutputTopic] = {};
393437
propertyCallback[propOutputTopic][name] = cb;
394-
subscribe(connectionId, propOutputTopic, cb);
438+
subscribe(propOutputTopic, cb);
395439
} else if (propertyCallback[propOutputTopic] && !propertyCallback[propOutputTopic][name]) {
396440
propertyCallback[propOutputTopic][name] = cb;
397441
}
@@ -401,6 +445,7 @@ const onPropertyValue = (connectionId, thingId, name, cb) => {
401445
export default {
402446
connect,
403447
disconnect,
448+
updateToken,
404449
subscribe,
405450
unsubscribe,
406451
sendMessage,

0 commit comments

Comments
 (0)