Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2de29a1

Browse files
authoredJan 8, 2019
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 16 additions & 10 deletions
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

Lines changed: 98 additions & 53 deletions
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,

‎test/arduino-cloud.test.js

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@
1919
*/
2020
const ArduinoCloud = require('../dist/index.js');
2121

22-
let connectionId;
2322
const deviceId = '1f4ced70-53ad-4b29-b221-1b0abbdfc757';
24-
const thingId = '2cea8542-d472-4464-859c-4ef4dfc7d1d3'
23+
const thingId = '2cea8542-d472-4464-859c-4ef4dfc7d1d3';
2524
const propertyIntName = 'integer';
2625
const propertyIntValue = 22;
2726

@@ -34,27 +33,27 @@ const propertyStrVal = 'ok';
3433
const propertyBoolName = 'boolean';
3534
const propertyBoolVal = true;
3635

37-
it('ArduinoCloud connection', () => {
38-
expect.assertions(1);
36+
it('ArduinoCloud connection', (done) => {
3937
/* global token */
40-
return ArduinoCloud.connect({
38+
ArduinoCloud.connect({
4139
token,
4240
onDisconnect: (message) => {
4341
if (message.errorCode !== 0) {
4442
throw Error(message);
4543
}
4644
},
47-
}).then((id) => {
48-
connectionId = id;
49-
expect(id).toBeDefined();
50-
}, (error) => {
51-
throw new Error(error);
52-
});
45+
})
46+
.then(() => {
47+
done();
48+
})
49+
.catch((error) => {
50+
throw new Error(error);
51+
});
5352
});
5453

5554
it('Property name must be a string in sendProperty', (done) => {
5655
try {
57-
ArduinoCloud.sendProperty(connectionId, deviceId, undefined, propertyIntValue);
56+
ArduinoCloud.sendProperty(deviceId, undefined, propertyIntValue);
5857
} catch (error) {
5958
if (error.message === 'Name must be a valid string') {
6059
done();
@@ -63,7 +62,7 @@ it('Property name must be a string in sendProperty', (done) => {
6362
});
6463

6564
it('Simulate client write to cloud monitor', (done) => {
66-
ArduinoCloud.writeCloudMonitor(connectionId, deviceId, `this is a test ${Math.random()}`).then(() => {
65+
ArduinoCloud.writeCloudMonitor(deviceId, `this is a test ${Math.random()}`).then(() => {
6766
done();
6867
}, (error) => {
6968
throw new Error(error);
@@ -72,7 +71,7 @@ it('Simulate client write to cloud monitor', (done) => {
7271

7372
it('Simulate device write to cloud monitor', (done) => {
7473
const cloudMonitorInputTopic = `/a/d/${deviceId}/s/o`;
75-
ArduinoCloud.sendMessage(connectionId, cloudMonitorInputTopic, `this is a test ${Math.random()}`).then(() => {
74+
ArduinoCloud.sendMessage(cloudMonitorInputTopic, `this is a test ${Math.random()}`).then(() => {
7675
done();
7776
}, (error) => {
7877
throw new Error(error);
@@ -87,10 +86,10 @@ it('Simulate device write and client read his message from cloud monitor', (done
8786
done();
8887
};
8988

90-
ArduinoCloud.openCloudMonitor(connectionId, deviceId, cb).then(() => {
89+
ArduinoCloud.openCloudMonitor(deviceId, cb).then(() => {
9190
// console.log(`Subscribed to topic: ${topic}`);
9291
const message = `This is a test ${new Date()}`;
93-
ArduinoCloud.sendMessage(connectionId, cloudMonitorInputTopic, message).then(() => {
92+
ArduinoCloud.sendMessage(cloudMonitorInputTopic, message).then(() => {
9493
// console.log(`[${new Date()}] Message sent to monitor: [${message}]`);
9594
}, (error) => {
9695
throw new Error(error);
@@ -101,42 +100,42 @@ it('Simulate device write and client read his message from cloud monitor', (done
101100
});
102101

103102
it('Simulate client read integer property sent by device', (done) => {
104-
ArduinoCloud.onPropertyValue(connectionId, thingId, propertyIntName, (value) => {
103+
ArduinoCloud.onPropertyValue(thingId, propertyIntName, (value) => {
105104
if (value === propertyIntValue) {
106105
done();
107106
}
108107
}).then(() => {
109-
ArduinoCloud.sendPropertyAsDevice(connectionId, deviceId, thingId, propertyIntName, propertyIntValue);
108+
ArduinoCloud.sendPropertyAsDevice(deviceId, thingId, propertyIntName, propertyIntValue);
110109
});
111110
});
112111

113112
it('Simulate client read float property sent by device', (done) => {
114-
ArduinoCloud.onPropertyValue(connectionId, thingId, propertyFloatName, (value) => {
113+
ArduinoCloud.onPropertyValue(thingId, propertyFloatName, (value) => {
115114
if (value === propertyFloatVal) {
116115
done();
117116
}
118117
}).then(() => {
119-
ArduinoCloud.sendPropertyAsDevice(connectionId, deviceId, thingId, propertyFloatName, propertyFloatVal);
118+
ArduinoCloud.sendPropertyAsDevice(deviceId, thingId, propertyFloatName, propertyFloatVal);
120119
});
121120
});
122121

123122
it('Simulate client read string property sent by device', (done) => {
124-
ArduinoCloud.onPropertyValue(connectionId, thingId, propertyStrName, (value) => {
123+
ArduinoCloud.onPropertyValue(thingId, propertyStrName, (value) => {
125124
if (value === propertyStrVal) {
126125
done();
127126
}
128127
}).then(() => {
129-
ArduinoCloud.sendPropertyAsDevice(connectionId, deviceId, thingId, propertyStrName, propertyStrVal);
128+
ArduinoCloud.sendPropertyAsDevice(deviceId, thingId, propertyStrName, propertyStrVal);
130129
});
131130
});
132131

133132
it('Simulate client read boolean property sent by device', (done) => {
134-
ArduinoCloud.onPropertyValue(connectionId, thingId, propertyBoolName, (value) => {
133+
ArduinoCloud.onPropertyValue(thingId, propertyBoolName, (value) => {
135134
if (value === propertyBoolVal) {
136-
ArduinoCloud.disconnect(connectionId);
135+
ArduinoCloud.disconnect();
137136
done();
138137
}
139138
}).then(() => {
140-
ArduinoCloud.sendPropertyAsDevice(connectionId, deviceId, thingId, propertyBoolName, propertyBoolVal);
139+
ArduinoCloud.sendPropertyAsDevice(deviceId, thingId, propertyBoolName, propertyBoolVal);
141140
});
142141
});

0 commit comments

Comments
 (0)
Please sign in to comment.