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