vrpc 3.5.1 → 3.5.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vrpc",
3
- "version": "3.5.1",
3
+ "version": "3.5.3",
4
4
  "description": "Variadic Remote Procedure Calls",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -136,6 +136,8 @@ class VrpcClient extends EventEmitter {
136
136
  this._proxies = {}
137
137
  this._callbackIds = new WeakMap()
138
138
  this._emitterListener = new WeakMap()
139
+ this._pendingSubscriptions = new Map()
140
+ this._pendingUnsubscriptions = new Map()
139
141
  if (log === 'console') {
140
142
  this._log = console
141
143
  this._log.debug = () => {}
@@ -188,6 +190,11 @@ class VrpcClient extends EventEmitter {
188
190
  }
189
191
  if (username === undefined) delete options.username
190
192
  if (password === undefined) delete options.password
193
+
194
+ this.on('error', err => {
195
+ this._log.debug(`Encountered MQTT error: ${err.message}`)
196
+ })
197
+
191
198
  this._client = mqtt.connect(this._broker, options)
192
199
 
193
200
  // Persistent lifecycle handlers
@@ -195,6 +202,7 @@ class VrpcClient extends EventEmitter {
195
202
  this._log.warn('MQTT client went offline.')
196
203
  // Clear the cache to ensure a fresh state upon reconnection
197
204
  this._agents = {}
205
+ this._proxies = {}
198
206
  this.emit('offline')
199
207
  })
200
208
 
@@ -211,31 +219,37 @@ class VrpcClient extends EventEmitter {
211
219
  this.emit('error', err)
212
220
  })
213
221
 
214
- this.on('error', err => {
215
- this._log.debug(`Encountered MQTT error: ${err.message}`)
216
- })
217
-
218
- this._client.on('connect', () => {
219
- // This will give us an overview of all remotely available classes
220
- const agent = this._agent === '*' ? '+' : this._agent
221
- // Agent info
222
- this._mqttSubscribe(`${this._domain}/${agent}/__agentInfo__`)
223
- // Class info
224
- if (this._requiresSchema) {
225
- this._mqttSubscribe(`${this._domain}/${agent}/+/__classInfo__`)
226
- } else {
227
- this._mqttSubscribe(`${this._domain}/${agent}/+/__classInfoConcise__`)
228
- }
229
- // RPC responses
230
- this._mqttSubscribe(this._vrpcClientId)
231
-
232
- // Re-subscribe to all cached event topics
233
- this._log.info('Restoring event subscriptions after reconnect...')
234
- for (const topic of Object.keys(this._cachedSubscriptions)) {
235
- this._mqttSubscribe(topic)
236
- }
237
-
222
+ this._client.on('connect', async () => {
223
+ // forward the good news
238
224
  this.emit('connect')
225
+ try {
226
+ // This will give us an overview of all remotely available classes
227
+ const agent = this._agent === '*' ? '+' : this._agent
228
+ // Agent info
229
+ await this._mqttSubscribe(`${this._domain}/${agent}/__agentInfo__`)
230
+ // Class info
231
+ if (this._requiresSchema) {
232
+ await this._mqttSubscribe(`${this._domain}/${agent}/+/__classInfo__`)
233
+ } else {
234
+ await this._mqttSubscribe(
235
+ `${this._domain}/${agent}/+/__classInfoConcise__`
236
+ )
237
+ }
238
+ // RPC responses
239
+ await this._mqttSubscribe(this._vrpcClientId)
240
+
241
+ // Re-subscribe to all cached event topics
242
+ this._log.debug('Restoring event subscriptions after reconnect...')
243
+ const promises = []
244
+ for (const topic of Object.keys(this._cachedSubscriptions)) {
245
+ promises.push(this._mqttSubscribe(topic))
246
+ }
247
+ await Promise.allSettled(promises)
248
+ } catch (err) {
249
+ this._log.error(
250
+ `Encountered issue while setting up subscriptions after connect: ${err.message}`
251
+ )
252
+ }
239
253
  })
240
254
 
241
255
  this._client.on('message', (topic, message) => {
@@ -326,7 +340,6 @@ class VrpcClient extends EventEmitter {
326
340
  resolve()
327
341
  })
328
342
  })
329
- // --------------------------------------------------------------------------------
330
343
  }
331
344
 
332
345
  /**
@@ -455,25 +468,7 @@ class VrpcClient extends EventEmitter {
455
468
  const topic = `${this._domain}/${agent}/${className}/__static__/__delete__`
456
469
  this._mqttPublish(topic, JSON.stringify(json))
457
470
  if (this._proxies[instance]) delete this._proxies[instance]
458
- const result = await this._handleAgentAnswer(json, agent)
459
- // Now that deletion is confirmed, proactively update all local caches
460
- if (result) {
461
- // 1. Remove from the proxy cache
462
- if (this._proxies[instance]) {
463
- delete this._proxies[instance]
464
- }
465
- // 2. Remove from the main agent/instance cache
466
- if (this._agents[agent] && this._agents[agent].classes) {
467
- const classInfo = this._agents[agent].classes[className]
468
- if (classInfo && classInfo.instances) {
469
- const index = classInfo.instances.indexOf(instance)
470
- if (index > -1) {
471
- classInfo.instances.splice(index, 1)
472
- }
473
- }
474
- }
475
- }
476
- return result
471
+ return this._handleAgentAnswer(json, agent)
477
472
  }
478
473
 
479
474
  /**
@@ -495,7 +490,7 @@ class VrpcClient extends EventEmitter {
495
490
  if (!this._isConnected()) {
496
491
  throw new Error('Client is not connected')
497
492
  }
498
- const wrapped = this._wrapArguments({
493
+ const wrapped = await this._wrapArguments({
499
494
  agent,
500
495
  className,
501
496
  functionName,
@@ -558,7 +553,7 @@ class VrpcClient extends EventEmitter {
558
553
  const agents = this.getAvailableAgents()
559
554
  for (const agent of agents) {
560
555
  const topic = `${this._domain}/${agent}/${className}/__static__/__callAll__`
561
- const wrapped = this._wrapArguments({
556
+ const wrapped = await this._wrapArguments({
562
557
  agent,
563
558
  className,
564
559
  functionName,
@@ -574,7 +569,7 @@ class VrpcClient extends EventEmitter {
574
569
  return result
575
570
  }
576
571
  const topic = `${this._domain}/${agent}/${className}/__static__/__callAll__`
577
- const wrapped = this._wrapArguments({
572
+ const wrapped = await this._wrapArguments({
578
573
  agent,
579
574
  className,
580
575
  functionName,
@@ -847,68 +842,77 @@ class VrpcClient extends EventEmitter {
847
842
  )
848
843
  }
849
844
 
850
- _mqttSubscribe (topic, options) {
851
- this._client.subscribe(
852
- topic,
853
- { qos: this._qos, ...options },
854
- (err, granted) => {
855
- if (err) {
856
- this._log.warn(
857
- `Could not subscribe to topic(s): '${topic}', because: ${err.message}`
858
- )
859
- if (err.message === 'client disconnecting') {
860
- // This most certainly relates to a bug in MQTT.js (#1284)
861
- this._log.error(`Failed to subscribe, because: ${err.message}`)
862
- this.emit('error', err)
863
- this._log.info('Forcing reconnect now...')
864
- this._client.disconnecting = false
865
- this._client.reconnect()
866
- }
867
- } else {
868
- const topicArray = Array.isArray(topic) ? topic : [topic]
869
- const erroneousGranted = granted
870
- .filter(x => x.qos === 128)
871
- .map(x => x.topic)
872
- if (erroneousGranted.length > 0) {
873
- err = new Error(
874
- `Could not subscribe all ${topicArray.length} topic(s) but got error qos=128 on following ${erroneousGranted.length} topic(s): ${erroneousGranted}`
875
- )
876
- err.code = 'SUBSCRIBE_FAILED'
877
- err.subscribeOptions = options
878
- this._log.error(err)
879
- this.emit('error', err)
880
- }
881
- const reducedQos = granted.filter(x => x.qos < this._qos)
882
- if (reducedQos.length > 0) {
883
- err = new Error(
884
- `Could not subscribe all ${
885
- topicArray.length
886
- } topic(s) at desired qos=${
887
- this._qos
888
- } but got reduced qos on following ${
889
- reducedQos.length
890
- } topic(s): ${JSON.stringify(reducedQos)}`
845
+ async _mqttSubscribe (topic, options) {
846
+ return new Promise((resolve, reject) => {
847
+ this._client.subscribe(
848
+ topic,
849
+ { qos: this._qos, ...options },
850
+ (err, granted) => {
851
+ if (err) {
852
+ this._log.warn(
853
+ `Could not subscribe to topic(s): '${topic}', because: ${err.message}`
891
854
  )
892
- err.code = 'SUBSCRIBE_REDUCED_QOS'
893
- err.subscribeOptions = options
894
- this._log.warn(err)
895
- this.emit('error', err)
896
- }
897
- if (granted.length === 0) {
898
- this._log.debug(`Already subscribed to: ${topic}`)
855
+ if (err.message === 'client disconnecting') {
856
+ // This most certainly relates to a bug in MQTT.js (#1284)
857
+ const msg = `Failed to subscribe, because: ${err.message}`
858
+ this._log.error(msg)
859
+ this.emit('error', err)
860
+ this._log.info('Forcing reconnect now...')
861
+ this._client.disconnecting = false
862
+ this._client.reconnect()
863
+ reject(new Error(msg))
864
+ }
865
+ } else {
866
+ const topicArray = Array.isArray(topic) ? topic : [topic]
867
+ const erroneousGranted = granted
868
+ .filter(x => x.qos === 128)
869
+ .map(x => x.topic)
870
+ if (erroneousGranted.length > 0) {
871
+ err = new Error(
872
+ `Could not subscribe all ${topicArray.length} topic(s) but got error qos=128 on following ${erroneousGranted.length} topic(s): ${erroneousGranted}`
873
+ )
874
+ err.code = 'SUBSCRIBE_FAILED'
875
+ err.subscribeOptions = options
876
+ this._log.error(err.message)
877
+ this.emit('error', err)
878
+ }
879
+ const reducedQos = granted.filter(x => x.qos < this._qos)
880
+ if (reducedQos.length > 0) {
881
+ err = new Error(
882
+ `Could not subscribe all ${
883
+ topicArray.length
884
+ } topic(s) at desired qos=${
885
+ this._qos
886
+ } but got reduced qos on following ${
887
+ reducedQos.length
888
+ } topic(s): ${JSON.stringify(reducedQos)}`
889
+ )
890
+ err.code = 'SUBSCRIBE_REDUCED_QOS'
891
+ err.subscribeOptions = options
892
+ this._log.warn(err.message)
893
+ this.emit('error', err)
894
+ }
895
+ if (granted.length === 0) {
896
+ this._log.debug(`Already subscribed to: ${topic}`)
897
+ }
898
+ resolve()
899
899
  }
900
900
  }
901
- }
902
- )
901
+ )
902
+ })
903
903
  }
904
904
 
905
- _mqttUnsubscribe (topic, options) {
906
- this._client.unsubscribe(topic, options, err => {
907
- if (err) {
908
- this._log.warn(
909
- `Could not unsubscribe from topic: ${topic} because: ${err.message}`
910
- )
911
- }
905
+ async _mqttUnsubscribe (topic, options) {
906
+ return new Promise((resolve, reject) => {
907
+ this._client.unsubscribe(topic, options, err => {
908
+ if (err) {
909
+ this._log.warn(
910
+ `Could not unsubscribe from topic: ${topic} because: ${err.message}`
911
+ )
912
+ }
913
+ // we resolve in any case to let the upstream logic flow
914
+ resolve()
915
+ })
912
916
  })
913
917
  }
914
918
 
@@ -1001,7 +1005,7 @@ class VrpcClient extends EventEmitter {
1001
1005
  const id = `__e__${topic}`
1002
1006
  if (this._cachedSubscriptions[topic]) {
1003
1007
  this._eventEmitter.removeAllListeners(id)
1004
- this._mqttUnsubscribe(topic)
1008
+ await this._mqttUnsubscribe(topic)
1005
1009
  delete this._cachedSubscriptions[topic]
1006
1010
  }
1007
1011
  try {
@@ -1029,7 +1033,7 @@ class VrpcClient extends EventEmitter {
1029
1033
  return
1030
1034
  }
1031
1035
  proxy[functionName] = async (...args) => {
1032
- const wrapped = this._wrapArguments({
1036
+ const wrapped = await this._wrapArguments({
1033
1037
  agent,
1034
1038
  className,
1035
1039
  instance,
@@ -1064,7 +1068,7 @@ class VrpcClient extends EventEmitter {
1064
1068
  if (!uniqueFuncs.has('vrpcOn') && !uniqueFuncs.has('vrpcOff')) {
1065
1069
  proxy.vrpcOn = async (functionName, ...args) => {
1066
1070
  if (!uniqueFuncs.has(functionName)) throw new Error('Bad magic')
1067
- const wrapped = this._wrapArguments({
1071
+ const wrapped = await this._wrapArguments({
1068
1072
  agent,
1069
1073
  className,
1070
1074
  instance,
@@ -1169,7 +1173,14 @@ class VrpcClient extends EventEmitter {
1169
1173
  })
1170
1174
  }
1171
1175
 
1172
- _wrapArguments ({ agent, className, instance, proxyId, functionName, args }) {
1176
+ async _wrapArguments ({
1177
+ agent,
1178
+ className,
1179
+ instance,
1180
+ proxyId,
1181
+ functionName,
1182
+ args
1183
+ }) {
1173
1184
  const wrapped = []
1174
1185
  for (const [i, x] of args.entries()) {
1175
1186
  // Check whether provided argument is a function
@@ -1185,7 +1196,7 @@ class VrpcClient extends EventEmitter {
1185
1196
  // 2) callback is second argument
1186
1197
  // 3) first argument was string
1187
1198
  const event = args[0]
1188
- const id = this._onRemoteEvent({
1199
+ const id = await this._onRemoteEvent({
1189
1200
  agent,
1190
1201
  className,
1191
1202
  instance,
@@ -1200,7 +1211,7 @@ class VrpcClient extends EventEmitter {
1200
1211
  typeof args[0] === 'string'
1201
1212
  ) {
1202
1213
  const event = args[0]
1203
- const id = this._offRemoteEvent({
1214
+ const id = await this._offRemoteEvent({
1204
1215
  agent,
1205
1216
  className,
1206
1217
  instance,
@@ -1235,7 +1246,7 @@ class VrpcClient extends EventEmitter {
1235
1246
  this._emitterListener.set(emitter, new Map([[event, callback]]))
1236
1247
  }
1237
1248
 
1238
- const id = this._onRemoteEvent({
1249
+ const id = await this._onRemoteEvent({
1239
1250
  agent,
1240
1251
  className,
1241
1252
  instance,
@@ -1251,7 +1262,7 @@ class VrpcClient extends EventEmitter {
1251
1262
  return wrapped
1252
1263
  }
1253
1264
 
1254
- _onRemoteEvent ({ agent, className, instance, event, callback }) {
1265
+ async _onRemoteEvent ({ agent, className, instance, event, callback }) {
1255
1266
  const callbackId = this._getCallbackId(callback)
1256
1267
  const eventCallback = `${event}-${callbackId}`
1257
1268
 
@@ -1261,10 +1272,15 @@ class VrpcClient extends EventEmitter {
1261
1272
  : `${this._domain}/${agent}/${className}/${eventCallback}`
1262
1273
 
1263
1274
  // prepare a special id for the agent to know that events should be
1264
- // published to the provided topic after the prefix and before the suffix
1275
+ // published to the provided topic after the prefix
1265
1276
 
1266
1277
  const id = `__e__${topic}`
1267
1278
 
1279
+ // If a subscription for this topic is already in progress, wait for it to finish.
1280
+ if (this._pendingSubscriptions.has(topic)) {
1281
+ await this._pendingSubscriptions.get(topic)
1282
+ }
1283
+
1268
1284
  // call our proxy callback when remote events were received
1269
1285
  const handler = ({ a }) => callback.apply(null, a)
1270
1286
  if (this._eventEmitter.listenerCount(id) === 0) {
@@ -1272,19 +1288,29 @@ class VrpcClient extends EventEmitter {
1272
1288
  }
1273
1289
  if (!this._cachedSubscriptions[topic]) {
1274
1290
  // when not yet subscribed to this topic do it now
1275
- this._mqttSubscribe(topic)
1276
- this._cachedSubscriptions[topic] = [{ callback, handler }]
1277
- return id
1291
+ const subscribePromise = this._mqttSubscribe(topic)
1292
+ this._pendingSubscriptions.set(topic, subscribePromise) // set lock
1293
+
1294
+ try {
1295
+ await subscribePromise
1296
+ this._cachedSubscriptions[topic] = [{ callback, handler }]
1297
+ } finally {
1298
+ this._pendingSubscriptions.delete(topic) // release lock
1299
+ }
1300
+ } else {
1301
+ // otherwise just add the new callback-handler pair
1302
+ this._cachedSubscriptions[topic].push({ callback, handler })
1278
1303
  }
1279
- // otherwise just add the new callback-handler pair
1280
- this._cachedSubscriptions[topic].push({ callback, handler })
1281
1304
  return id
1282
1305
  }
1283
1306
 
1284
- _offRemoteEvent ({ agent, className, instance, event, callback }) {
1307
+ async _offRemoteEvent ({ agent, className, instance, event, callback }) {
1285
1308
  const callbackId = this._getCallbackId(callback, {
1286
1309
  readOnly: true
1287
1310
  })
1311
+ // If the callback was never registered, we can't do anything.
1312
+ if (!callbackId) return null
1313
+
1288
1314
  const eventCallback = `${event}-${callbackId}`
1289
1315
  const topic = instance
1290
1316
  ? `${this._domain}/${agent}/${className}/${instance}:${eventCallback}`
@@ -1292,6 +1318,10 @@ class VrpcClient extends EventEmitter {
1292
1318
 
1293
1319
  const id = `__e__${topic}`
1294
1320
 
1321
+ if (this._pendingUnsubscriptions.has(topic)) {
1322
+ await this._pendingUnsubscriptions.get(topic)
1323
+ }
1324
+
1295
1325
  if (this._cachedSubscriptions[topic]) {
1296
1326
  const index = this._cachedSubscriptions[topic].findIndex(
1297
1327
  x => x.callback === callback
@@ -1302,16 +1332,21 @@ class VrpcClient extends EventEmitter {
1302
1332
  }
1303
1333
  const [{ handler }] = this._cachedSubscriptions[topic].splice(index, 1)
1304
1334
  if (this._cachedSubscriptions[topic].length === 0) {
1305
- this._eventEmitter.removeListener(id, handler)
1306
- this._mqttUnsubscribe(topic)
1307
1335
  delete this._cachedSubscriptions[topic]
1308
- return id
1336
+ const unsubscribePromise = this._mqttUnsubscribe(topic)
1337
+ this._pendingUnsubscriptions.set(topic, unsubscribePromise) // set lock
1338
+
1339
+ try {
1340
+ await unsubscribePromise
1341
+ this._eventEmitter.removeListener(id, handler)
1342
+ } finally {
1343
+ this._pendingUnsubscriptions.delete(topic) // release lock
1344
+ }
1345
+ } else {
1346
+ this._log.warn(`Can not unsubscribe from non-existing event: ${event}`)
1309
1347
  }
1310
1348
  return id
1311
- } else {
1312
- this._log.warn(`Can not unsubscribe from non-existing event: ${event}`)
1313
1349
  }
1314
- return id
1315
1350
  }
1316
1351
 
1317
1352
  _clearCachedSubscriptions ({ lostAgent, lostInstance }) {
@@ -1327,11 +1362,12 @@ class VrpcClient extends EventEmitter {
1327
1362
  )
1328
1363
  obsoleteTopics.forEach(topic => {
1329
1364
  const id = `__e__${topic}`
1330
- this._log.info(`Clearing subscriptions for obsolete topic: ${topic}`)
1365
+ this._log.debug(`Clearing subscriptions for obsolete topic: ${topic}`)
1331
1366
  const subscriptions = this._cachedSubscriptions[topic]
1332
1367
  subscriptions.forEach(({ handler }) => {
1333
1368
  this._eventEmitter.removeListener(id, handler)
1334
1369
  })
1370
+ // this async function will never reject
1335
1371
  this._mqttUnsubscribe(topic)
1336
1372
  delete this._cachedSubscriptions[topic]
1337
1373
  })