@reactoo/watchtogether-sdk-js 2.7.49 → 2.7.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -189,7 +189,7 @@
189
189
  return Instance.iot.iotLogout()
190
190
  }
191
191
 
192
- function itoLogin() {
192
+ function iotLogin() {
193
193
  return Instance.iot.iotLogin()
194
194
  }
195
195
 
@@ -197,6 +197,10 @@
197
197
  return Instance.iot.subscribe(topic)
198
198
  }
199
199
 
200
+ function updateCredentials() {
201
+ return Instance.iot.__updateCredentials()
202
+ }
203
+
200
204
  let Instance = WatchTogetherSDK({debug:true})({instanceType:'reactooDemo'});
201
205
 
202
206
  Instance.auth.$on('login', (r) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactoo/watchtogether-sdk-js",
3
- "version": "2.7.49",
3
+ "version": "2.7.50",
4
4
  "description": "Javascript SDK for Reactoo",
5
5
  "main": "src/index.js",
6
6
  "unpkg": "dist/watchtogether-sdk.min.js",
package/src/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import Room from './modules/wt-room';
4
4
  import Auth from './modules/wt-auth';
5
- import Iot from './modules/wt-iot';
5
+ import Iot from './modules/wt-iot2';
6
6
 
7
7
  import auth from './models/auth';
8
8
  import room from './models/room';
@@ -0,0 +1,164 @@
1
+ import { mqtt, iot } from 'aws-iot-device-sdk-v2';
2
+ console.log('Worker: Starting up');
3
+
4
+ let connection = null;
5
+
6
+ self.onmessage = function(event) {
7
+ const { type, params, topic, message } = event.data;
8
+ console.log(`Worker: Received message of type: ${type}`);
9
+
10
+ switch (type) {
11
+ case 'connect':
12
+ connect(params);
13
+ break;
14
+ case 'disconnect':
15
+ disconnect();
16
+ break;
17
+ case 'is_connected':
18
+ isConnected();
19
+ break;
20
+ case 'clear_topics':
21
+ // No action needed in the worker
22
+ break;
23
+ case 'subscribe':
24
+ subscribe(topic);
25
+ break;
26
+ case 'unsubscribe':
27
+ unsubscribe(topic);
28
+ break;
29
+ case 'send':
30
+ send(topic, message);
31
+ break;
32
+ }
33
+ };
34
+
35
+ function connect(params) {
36
+ console.log('Worker: Attempting to connect');
37
+ const { apiMqttUrl, apiMqttClientId, region, accessKeyId, secretAccessKey, sessionToken } = params;
38
+
39
+ const configBuilder = iot.AwsIotMqttConnectionConfigBuilder.new_with_websockets();
40
+
41
+ configBuilder.with_clean_session(true);
42
+ configBuilder.with_client_id(apiMqttClientId);
43
+ configBuilder.with_endpoint(apiMqttUrl);
44
+ configBuilder.with_credentials(region, accessKeyId, secretAccessKey, sessionToken);
45
+ configBuilder.with_keep_alive_seconds(30);
46
+ configBuilder.with_ping_timeout_ms(3000);
47
+ configBuilder.with_reconnect_max_sec(5);
48
+ configBuilder.with_reconnect_min_sec(1);
49
+
50
+ const config = configBuilder.build();
51
+
52
+ const client = new mqtt.MqttClient();
53
+ connection = client.new_connection(config);
54
+
55
+ setupConnectionListeners();
56
+
57
+ connection.connect()
58
+ .then(() => {
59
+ console.log('Worker: Connection successful');
60
+ self.postMessage({ type: 'connect_result', data: {success: true} });
61
+ })
62
+ .catch((error) => {
63
+ console.error('Worker: Connection failed', error);
64
+ self.postMessage({ type: 'connect_result', data: {success: false, error: error.message} });
65
+ });
66
+ }
67
+
68
+ function disconnect() {
69
+ if (connection) {
70
+ connection.disconnect()
71
+ .then(() => {
72
+ self.postMessage({ type: 'disconnect_result', data: {success: true} });
73
+ })
74
+ .catch((error) => {
75
+ self.postMessage({ type: 'disconnect_result', data: { success: false, error: error.message} });
76
+ });
77
+ } else {
78
+ self.postMessage({ type: 'disconnect_result', data: {success: true} });
79
+ }
80
+ }
81
+
82
+ function isConnected() {
83
+ const connected = connection && connection.currentState === 0 && connection.desiredState === 0;
84
+ self.postMessage({ type: 'is_connected_result', data:{connected} });
85
+ }
86
+
87
+ function subscribe(topic) {
88
+ if (connection && connection.currentState === 0 && connection.desiredState === 0) {
89
+ connection.subscribe(topic, mqtt.QoS.AtLeastOnce)
90
+ .then(() => {
91
+ self.postMessage({ type: 'subscribe_result', data: {success: true} });
92
+ })
93
+ .catch((error) => {
94
+ self.postMessage({ type: 'subscribe_result', data: {success: false, error: error.message} });
95
+ });
96
+ } else {
97
+ self.postMessage({ type: 'subscribe_result', data: {success: false, error: 'Not connected'} });
98
+ }
99
+ }
100
+
101
+ function unsubscribe(topic) {
102
+ if (connection && connection.currentState === 0 && connection.desiredState === 0) {
103
+ connection.unsubscribe(topic)
104
+ .then(() => {
105
+ self.postMessage({ type: 'unsubscribe_result', data: {success: true} });
106
+ })
107
+ .catch((error) => {
108
+ self.postMessage({ type: 'unsubscribe_result', data: {success: false, error: error.message} });
109
+ });
110
+ } else {
111
+ self.postMessage({ type: 'unsubscribe_result', data: {success: false, error: 'Not connected'} });
112
+ }
113
+ }
114
+
115
+ function send(topic, message) {
116
+ if (connection && connection.currentState === 0 && connection.desiredState === 0) {
117
+ connection.publish(topic, message, mqtt.QoS.AtLeastOnce, false);
118
+ } else {
119
+ console.error('Cannot send message: Not connected');
120
+ }
121
+ }
122
+
123
+ function setupConnectionListeners() {
124
+ connection.on('connect', () => {
125
+ self.postMessage({ type: 'connect' });
126
+ });
127
+
128
+ connection.on('disconnect', () => {
129
+ self.postMessage({ type: 'disconnect' });
130
+ });
131
+
132
+ connection.on('error', (error) => {
133
+ self.postMessage({ type: 'error', data: error });
134
+ });
135
+
136
+ connection.on('interrupt', (error) => {
137
+ self.postMessage({ type: 'interrupt', data: error });
138
+ });
139
+
140
+ connection.on('resume', (error) => {
141
+ self.postMessage({ type: 'resume', data: error });
142
+ });
143
+
144
+ connection.on('message', (topic, payload) => {
145
+ self.postMessage({ type: 'message', data: { topic, payload } });
146
+ });
147
+
148
+ connection.on('connection_success', (error) => {
149
+ self.postMessage({ type: 'connection_success', data: error });
150
+ });
151
+
152
+ connection.on('connection_failure', (error) => {
153
+ self.postMessage({ type: 'connection_failure', data: error });
154
+ });
155
+
156
+ // Add a general error handler for uncaught exceptions
157
+ self.addEventListener('error', (error) => {
158
+ console.error('Worker: Uncaught error', error);
159
+ self.postMessage({ type: 'uncaught_error', error: error.message });
160
+ });
161
+ }
162
+
163
+ // Add this at the end of the file
164
+ console.log('Worker: Setup complete');
@@ -0,0 +1,355 @@
1
+ import emitter from './wt-emitter';
2
+ import { decodeJanusDisplay } from "./wt-utils";
3
+ import Worker from './wt-iot-worker.worker.js';
4
+
5
+ class Iot {
6
+ constructor(enableDebugFlag) {
7
+ Object.assign(this, emitter());
8
+ this.decoder = new TextDecoder('utf-8');
9
+ this.log = Iot.noop;
10
+ this.credentialsExpirationCheckIntervalId = null;
11
+ this.currentCredentialsExpirationStamp = null;
12
+ this.lastConnectParams = null;
13
+ this.subscribedTopics = new Set();
14
+ this.initWorker();
15
+
16
+ if (enableDebugFlag) {
17
+ this.enableDebug();
18
+ }
19
+ }
20
+
21
+ static noop() {}
22
+
23
+ enableDebug() {
24
+ this.log = console.log.bind(console);
25
+ }
26
+
27
+ initWorker() {
28
+ this.worker = new Worker();
29
+ this.worker.onmessage = this.handleWorkerMessage.bind(this);
30
+ }
31
+
32
+ connect(apiMqttUrl, apiMqttClientId, region, accessKeyId, secretAccessKey, sessionToken, expiration) {
33
+ this.log('iot connect called, we disconnect first just to be sure');
34
+ return this.disconnect()
35
+ .catch(() => {
36
+ // we dont care if disconnect fails
37
+ return Promise.resolve();
38
+ })
39
+ .finally(() => {
40
+ this.log('iot connect');
41
+ this.startCredentialsExpirationCheck(expiration);
42
+ this.lastConnectParams = { apiMqttUrl, apiMqttClientId, region, accessKeyId, secretAccessKey, sessionToken, expiration };
43
+
44
+ return new Promise((resolve, reject) => {
45
+ const timeoutId = setTimeout(() => {
46
+ this.off('worker:connect_result');
47
+ reject(new Error('Connection timeout'));
48
+ }, 5000);
49
+
50
+ const handleConnectResult = (event) => {
51
+ clearTimeout(timeoutId);
52
+ this.off('worker:connect_result', handleConnectResult);
53
+ if (event.success) {
54
+ resolve();
55
+ } else {
56
+ reject(new Error(event.error));
57
+ }
58
+ };
59
+
60
+ this.on('worker:connect_result', handleConnectResult);
61
+
62
+ this.worker.postMessage({
63
+ type: 'connect',
64
+ params: this.lastConnectParams
65
+ });
66
+ });
67
+ });
68
+ }
69
+
70
+ disconnect() {
71
+ this.log('iot disconnect');
72
+ this.stopCredentialsExpirationCheck();
73
+ return new Promise((resolve, reject) => {
74
+ const timeoutId = setTimeout(() => {
75
+ this.off('worker:disconnect_result');
76
+ reject(new Error('Disconnect timeout'));
77
+ }, 5000);
78
+
79
+ const handleDisconnectResult = (event) => {
80
+ clearTimeout(timeoutId);
81
+ this.off('worker:disconnect_result', handleDisconnectResult);
82
+ if (event.success) {
83
+ resolve();
84
+ } else {
85
+ reject(new Error(event.error));
86
+ }
87
+ };
88
+ this.on('worker:disconnect_result', handleDisconnectResult);
89
+ this.worker.postMessage({ type: 'disconnect' });
90
+ });
91
+ }
92
+
93
+ isConnected() {
94
+ return new Promise((resolve) => {
95
+ const handleIsConnectedResult = (event) => {
96
+ this.off('worker:is_connected_result', handleIsConnectedResult);
97
+ resolve(event.connected);
98
+ };
99
+
100
+ this.on('worker:is_connected_result', handleIsConnectedResult);
101
+ this.worker.postMessage({ type: 'is_connected' });
102
+ });
103
+ }
104
+
105
+ clearTopics() {
106
+ this.subscribedTopics.clear();
107
+ this.worker.postMessage({ type: 'clear_topics' });
108
+ }
109
+
110
+ subscribe(topic) {
111
+ this.log('iot subscribe', topic);
112
+ if (typeof topic === 'string' && topic.trim() !== '') {
113
+ return new Promise((resolve, reject) => {
114
+ const handleSubscribeResult = (event) => {
115
+ this.off('worker:subscribe_result', handleSubscribeResult);
116
+ if (event.success) {
117
+ this.subscribedTopics.add(topic);
118
+ resolve();
119
+ } else {
120
+ reject(new Error(event.error));
121
+ }
122
+ };
123
+
124
+ this.on('worker:subscribe_result', handleSubscribeResult);
125
+ this.worker.postMessage({ type: 'subscribe', topic });
126
+ });
127
+ } else {
128
+ this.log('Invalid topic:', topic);
129
+ return Promise.reject(new Error('Invalid topic'));
130
+ }
131
+ }
132
+
133
+ unsubscribe(topic) {
134
+ this.log('iot unsubscribe', topic);
135
+ if (typeof topic === 'string' && topic.trim() !== '') {
136
+ return new Promise((resolve, reject) => {
137
+ const handleUnsubscribeResult = (event) => {
138
+ this.off('worker:unsubscribe_result', handleUnsubscribeResult);
139
+ if (event.success) {
140
+ this.subscribedTopics.delete(topic);
141
+ resolve();
142
+ } else {
143
+ reject(new Error(event.error));
144
+ }
145
+ };
146
+
147
+ this.on('worker:unsubscribe_result', handleUnsubscribeResult);
148
+ this.worker.postMessage({ type: 'unsubscribe', topic });
149
+ });
150
+ } else {
151
+ this.log('Invalid topic:', topic);
152
+ return Promise.reject(new Error('Invalid topic'));
153
+ }
154
+ }
155
+
156
+ send(topic, message) {
157
+ this.log('iot send', topic, message);
158
+ let msg = typeof message === 'object' ? JSON.stringify(message) : message;
159
+ this.worker.postMessage({ type: 'send', topic, message: msg });
160
+ }
161
+
162
+ handleWorkerMessage(event) {
163
+ const { type, data } = event.data;
164
+ switch (type) {
165
+ case 'message':
166
+ this.handleMessage(data.topic, new Uint8Array(data.payload));
167
+ break;
168
+ case 'connect':
169
+ case 'disconnect':
170
+ case 'error':
171
+ case 'interrupt':
172
+ case 'resume':
173
+ case 'connection_success':
174
+ case 'connection_failure':
175
+ this.emit(type, data);
176
+ break;
177
+ case 'connect_result':
178
+ case 'disconnect_result':
179
+ case 'is_connected_result':
180
+ case 'subscribe_result':
181
+ case 'unsubscribe_result':
182
+ this.emit(`worker:${type}`, data);
183
+ break;
184
+ }
185
+ }
186
+
187
+ handleMessage(topic, payload) {
188
+ const topicParts = topic.split('/');
189
+ let message;
190
+ try {
191
+ message = JSON.parse(this.decoder.decode(payload));
192
+ } catch (error) {
193
+ this.log('Error parsing message:', error);
194
+ return;
195
+ }
196
+
197
+ if(message.display) {
198
+ const decodedDisplay = decodeJanusDisplay(message.display);
199
+ if(decodedDisplay.userId) {
200
+ message = {...message, userId: decodedDisplay.userId, role: decodedDisplay.role, start: decodedDisplay.start};
201
+ }
202
+ }
203
+
204
+ if(topicParts[0] === 'user') { // user
205
+ const userId = topicParts[1].replace("_", ':');
206
+ this.emit('message', {userId, ...message, event: message.event ? `user:${message.event}` : 'user'});
207
+ } else if(topicParts[0] === 'wt') {
208
+ const event = message.event;
209
+ const roomId = topicParts[2];
210
+ if(topicParts[1] === 'room') { // room
211
+ if(
212
+ event === 'message' ||
213
+ event === 'template_updated' ||
214
+ event === 'record_start' ||
215
+ event === 'record_stop' ||
216
+ event === 'record_configured' ||
217
+ event === 'record_livestream_available' ||
218
+ event === 'record_livestream_kick' ||
219
+ event === 'user_update_displayname' ||
220
+ event === 'user_update_avatar' ||
221
+ event === 'user_update_bio' ||
222
+ event === 'user_update_customattributes' ||
223
+ event === 'user_update_privateattributes' ||
224
+ event === 'channel_changed' ||
225
+ event === "instance_homepage_changed" ||
226
+ event === "instance_settings_changed" ||
227
+ event === "externalmix_changed" ||
228
+ event === "video_uploaded" ||
229
+ event === "change_user_devices" ||
230
+ event === "queue" ||
231
+ event === "title_changed" ||
232
+ event === "videowall_changed" ||
233
+ event === 'left' || //user removed room a.k.a. left the room
234
+ event === 'kicked' ||
235
+ event === 'banned' ||
236
+ event === 'unbanned' ||
237
+ event === 'approved' ||
238
+ event === 'muted' ||
239
+ event === 'unmuted' ||
240
+ event === 'messageRemoved' ||
241
+ event === 'messageReported' ||
242
+ event === 'chatClear' ||
243
+ event === 'handRaised' ||
244
+ event === 'handLowered' ||
245
+ event === 'handsCleared' ||
246
+ event === 'volume_set' ||
247
+ event === 'asset_created'
248
+ ) {
249
+ this.emit('message', {event, ...message, roomId})
250
+ }
251
+ else if(event === 'joined' || event === 'leaving') {
252
+ this.emit('message', {event, ...message, isObserver:!!message.isObserver, roomId});
253
+ }
254
+ }
255
+ else if(topicParts[1] === 'instanceroom') { // instance
256
+ if(event === 'add_room' || event === 'remove_room' || event === 'set_room' || event === "instance_homepage_changed" || event === 'instance_settings_changed') {
257
+ this.emit('message', {event, ...message});
258
+ }
259
+ }
260
+ else if(topicParts[1] === 'externalmix') {
261
+ const event = message.event;
262
+ this.emit('message', {event, ...message});
263
+ }
264
+ else if(topicParts[1] === 'asset') {
265
+ this.emit('message', {event: 'asset', assetId: topicParts[2], ...message});
266
+ }
267
+ } else if(topicParts[0] === 'wtr' || topicParts[0] === 'gwtr') {
268
+ const recorderId = topicParts[1];
269
+ const sessionId = topicParts[2];
270
+ if(topicParts[3] === 'control') {
271
+ this.emit('message', {event: 'recorder_control', ...message, recorderId, sessionId});
272
+ } // recorder control
273
+ else if(topicParts[3] === 'monitor') {
274
+ this.emit('message', {event: 'recorder_monitor', ...message, recorderId, sessionId});
275
+ } // recorder monitor
276
+ }
277
+ }
278
+
279
+ startCredentialsExpirationCheck(expiration) {
280
+ this.stopCredentialsExpirationCheck();
281
+ this.currentCredentialsExpirationStamp = new Date(expiration).getTime();
282
+ this.credentialsExpirationCheckIntervalId = setInterval(() => {
283
+ const currentTimeStamp = new Date().getTime();
284
+ // update 15 minutes before expiration
285
+ if(this.currentCredentialsExpirationStamp - currentTimeStamp <= 900000) {
286
+ this.log('iot credentials expired, updating');
287
+ this.emit('updateCredentials');
288
+ }
289
+ }, 5000);
290
+ }
291
+
292
+ stopCredentialsExpirationCheck() {
293
+ clearInterval(this.credentialsExpirationCheckIntervalId);
294
+ this.credentialsExpirationCheckIntervalId = null;
295
+ }
296
+
297
+ updateWebSocketCredentials(accessKeyId, secretAccessKey, sessionToken, expiration) {
298
+ this.log('iot updateWebSocketCredentials');
299
+ this.lastConnectParams = {...this.lastConnectParams, accessKeyId, secretAccessKey, sessionToken, expiration };
300
+ const currentTopics = new Set(this.subscribedTopics);
301
+ return this.connect(
302
+ this.lastConnectParams.apiMqttUrl,
303
+ this.lastConnectParams.apiMqttClientId,
304
+ this.lastConnectParams.region,
305
+ accessKeyId,
306
+ secretAccessKey,
307
+ sessionToken,
308
+ expiration
309
+ ).then(() => {
310
+ // Resubscribe to topics
311
+ currentTopics.forEach(topic => this.subscribe(topic));
312
+ return true;
313
+ });
314
+ }
315
+
316
+ checkConnection() {
317
+ return new Promise((resolve, reject) => {
318
+ if (this.subscribedTopics.size === 0) {
319
+ reject(new Error('No subscribed topics available for connection check', {cause: -1}));
320
+ return;
321
+ }
322
+
323
+ let suitableTopic = Array.from(this.subscribedTopics).find(topic => topic.indexOf('user') > -1);
324
+
325
+ if (!suitableTopic) {
326
+ reject(new Error('No suitable topic found for connection check', {cause: -1}));
327
+ return;
328
+ }
329
+
330
+ const testMessage = {
331
+ type: 'keep_alive',
332
+ timestamp: Date.now()
333
+ };
334
+
335
+ const timeoutId = setTimeout(() => {
336
+ this.off('message', checkMessageHandler);
337
+ reject(new Error('Connection check timeout'));
338
+ }, 5000);
339
+
340
+ const checkMessageHandler = (message) => {
341
+ if (message.type === 'keep_alive' && message.timestamp === testMessage.timestamp) {
342
+ clearTimeout(timeoutId);
343
+ this.off('message', checkMessageHandler);
344
+ resolve();
345
+ }
346
+ };
347
+
348
+ this.on('message', checkMessageHandler);
349
+
350
+ this.send(suitableTopic, testMessage);
351
+ });
352
+ }
353
+ }
354
+
355
+ export default Iot;
package/webpack.config.js CHANGED
@@ -22,9 +22,9 @@ module.exports = (env, argv) => {
22
22
  libraryTarget: 'umd',
23
23
  libraryExport: 'default',
24
24
  umdNamedDefine: true,
25
- globalObject: "typeof self !== 'undefined' ? self : this"
25
+ globalObject: "typeof self !== 'undefined' ? self : this",
26
+ publicPath: '',
26
27
  },
27
- // Remove the experiments section
28
28
  module: {
29
29
  rules: [
30
30
  {
@@ -36,7 +36,9 @@ module.exports = (env, argv) => {
36
36
  },
37
37
  {
38
38
  test: /\.worker\.js$/,
39
- use: { loader: 'worker-loader' }
39
+ use: [
40
+ { loader: 'worker-loader', options: { inline: 'no-fallback' } },
41
+ ]
40
42
  }
41
43
  ]
42
44
  },
@@ -44,6 +46,11 @@ module.exports = (env, argv) => {
44
46
  new webpack.BannerPlugin(banner),
45
47
  new webpack.ProvidePlugin({
46
48
  process: 'process/browser',
49
+ Buffer: ['buffer', 'Buffer'],
50
+ }),
51
+ new webpack.DefinePlugin({
52
+ 'typeof window': JSON.stringify('object'),
53
+ 'window': 'self',
47
54
  }),
48
55
  ],
49
56
  optimization: {
@@ -1,84 +0,0 @@
1
- import { mqtt, iot } from 'aws-iot-device-sdk-v2';
2
-
3
- let connection = null;
4
-
5
- self.onmessage = function(e) {
6
- const { action, payload } = e.data;
7
-
8
- switch (action) {
9
- case 'connect':
10
- connect(payload);
11
- break;
12
- case 'disconnect':
13
- disconnect();
14
- break;
15
- case 'subscribe':
16
- subscribe(payload.topic);
17
- break;
18
- case 'unsubscribe':
19
- unsubscribe(payload.topic);
20
- break;
21
- case 'send':
22
- send(payload.topic, payload.message);
23
- break;
24
- }
25
- };
26
-
27
- function connect({ apiMqttUrl, apiMqttClientId, region, accessKeyId, secretAccessKey, sessionToken }) {
28
- const configBuilder = iot.AwsIotMqttConnectionConfigBuilder.new_with_websockets();
29
-
30
- configBuilder.with_clean_session(true);
31
- configBuilder.with_client_id(apiMqttClientId);
32
- configBuilder.with_endpoint(apiMqttUrl);
33
- configBuilder.with_credentials(region, accessKeyId, secretAccessKey, sessionToken);
34
- configBuilder.with_keep_alive_seconds(30);
35
- configBuilder.with_ping_timeout_ms(3000);
36
-
37
- const config = configBuilder.build();
38
-
39
- const client = new mqtt.MqttClient();
40
- connection = client.new_connection(config);
41
-
42
- connection.on('connect', () => {
43
- self.postMessage({ event: 'connect' });
44
- });
45
-
46
- connection.on('disconnect', () => {
47
- self.postMessage({ event: 'disconnect' });
48
- });
49
-
50
- connection.on('error', (error) => {
51
- self.postMessage({ event: 'error', error: error.toString() });
52
- });
53
-
54
- connection.on('message', (topic, payload) => {
55
- self.postMessage({ event: 'message', topic, payload: new Uint8Array(payload) });
56
- });
57
-
58
- connection.connect();
59
- }
60
-
61
- function disconnect() {
62
- if (connection) {
63
- connection.disconnect();
64
- connection = null;
65
- }
66
- }
67
-
68
- function subscribe(topic) {
69
- if (connection) {
70
- connection.subscribe(topic, mqtt.QoS.AtLeastOnce);
71
- }
72
- }
73
-
74
- function unsubscribe(topic) {
75
- if (connection) {
76
- connection.unsubscribe(topic);
77
- }
78
- }
79
-
80
- function send(topic, message) {
81
- if (connection) {
82
- connection.publish(topic, message, mqtt.QoS.AtLeastOnce);
83
- }
84
- }