vrpc 3.0.0-alpha.3 → 3.0.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.
@@ -16,7 +16,7 @@ Author: Dr. Burkhard C. Heisen (https://github.com/heisenware/vrpc)
16
16
 
17
17
 
18
18
  Licensed under the MIT License <http://opensource.org/licenses/MIT>.
19
- Copyright (c) 2018 - 2021 Dr. Burkhard C. Heisen <burkhard.heisen@heisenware.com>.
19
+ Copyright (c) 2018 - 2022 Dr. Burkhard C. Heisen <burkhard.heisen@heisenware.com>.
20
20
 
21
21
  Permission is hereby granted, free of charge, to any person obtaining a copy
22
22
  of this software and associated documentation files (the "Software"), to deal
@@ -59,9 +59,9 @@ class VrpcClient extends EventEmitter {
59
59
  *
60
60
  * @constructor
61
61
  * @param {Object} options
62
- * @param {String} options.token Access token as generated by: https://app.vrpc.io
63
- * @param {String} options.username MQTT username (if no token is used)
64
- * @param {String} options.password MQTT password (if no token is provided)
62
+ * @param {String} [options.username='<user>@<hostname>-<platform>-js'] MQTT username
63
+ * @param {String} [options.password] MQTT password (if no token is provided)
64
+ * @param {String} [options.token] Access token
65
65
  * @param {String} options.domain Sets the domain
66
66
  * @param {String} [options.agent="*"] Sets default agent
67
67
  * @param {String} [options.broker="mqtts://vrpc.io:8883"] Broker url in form: `<scheme>://<host>:<port>`
@@ -70,14 +70,14 @@ class VrpcClient extends EventEmitter {
70
70
  * @param {Boolean} [options.bestEffort=false] If true, message will be sent with best effort, i.e. no caching if offline
71
71
  * @example
72
72
  * const client = new VrpcClient({
73
- * domain: 'public.vrpc',
73
+ * domain: 'vrpc',
74
74
  * broker: 'mqtt://vrpc.io'
75
75
  * })
76
76
  */
77
77
  constructor ({
78
78
  token,
79
- username,
80
79
  password,
80
+ username = VrpcClient._generateUserName(),
81
81
  domain,
82
82
  agent = '*',
83
83
  broker = 'mqtts://vrpc.io:8883',
@@ -116,7 +116,7 @@ class VrpcClient extends EventEmitter {
116
116
  this._invokeId = 0
117
117
  this._proxyId = 0
118
118
  this._client = null
119
- this._cachedSubscriptions = new Map()
119
+ this._cachedSubscriptions = {}
120
120
  this._proxies = {}
121
121
  if (log === 'console') {
122
122
  this._log = console
@@ -152,7 +152,7 @@ class VrpcClient extends EventEmitter {
152
152
  let username = this._username
153
153
  let password = this._password
154
154
  if (this._token) {
155
- username = '__token__'
155
+ username = username || VrpcClient._generateUserName()
156
156
  password = this._token
157
157
  }
158
158
  const options = {
@@ -162,6 +162,7 @@ class VrpcClient extends EventEmitter {
162
162
  keepalive: 30,
163
163
  clientId: this._mqttClientId,
164
164
  rejectUnauthorized: false,
165
+ connectTimeout: this._timeout,
165
166
  will: {
166
167
  topic: `${this._vrpcClientId}/__clientInfo__`,
167
168
  payload: JSON.stringify({ status: 'offline' })
@@ -229,10 +230,12 @@ class VrpcClient extends EventEmitter {
229
230
  staticFunctions,
230
231
  meta
231
232
  } = json
232
- if (removed.length !== 0)
233
+ if (removed.length !== 0) {
233
234
  this.emit('instanceGone', removed, { domain, agent, className })
234
- if (added.length !== 0)
235
+ }
236
+ if (added.length !== 0) {
235
237
  this.emit('instanceNew', added, { domain, agent, className })
238
+ }
236
239
  this.emit('class', {
237
240
  domain,
238
241
  agent,
@@ -248,31 +251,29 @@ class VrpcClient extends EventEmitter {
248
251
  this._eventEmitter.emit(i, { a, e, r })
249
252
  }
250
253
  })
251
- await new Promise((resolve, reject) => {
252
- const timer = setTimeout(() => {
254
+ return new Promise((resolve, reject) => {
255
+ this._client.once('offline', () => {
256
+ this._client.end()
253
257
  reject(new Error(`Connection trial timed out (> ${this._timeout} ms)`))
254
- }, this._timeout)
255
- this.once('connect', () => {
256
- clearTimeout(timer)
257
- resolve()
258
258
  })
259
+ this._client.once('connect', resolve)
259
260
  })
260
261
  }
261
262
 
262
263
  /**
263
264
  * Creates a new remote instance and provides a proxy to it.
264
265
  *
265
- * Remote instances can be "named" or "anonymous". Named instances are
266
- * shareable and re-attachable across clients as long as they are not
267
- * explicitly deleted. Life-cycle changes of named instances are available
268
- * under the `class`, `instanceNew`, and `instanceGone` events. A named
269
- * instance is created when specifying the `instance` option.
266
+ * Remote instances can be "shared" or "isolated". Shared instances are
267
+ * visible and re-attachable across clients as long as they are not
268
+ * explicitly deleted. Life-cycle changes of shared instances are available
269
+ * under the `class`, `instanceNew`, and `instanceGone` events. A shared
270
+ * instance is created by default (`isIsolated: false`).
270
271
  *
271
- * When the `instance` option is not provided, the created proxy is the only
272
- * object capable of issuing remote function calls. The remote instance stays
273
- * invisible to other clients.
272
+ * When the `isIsolated` option is true, the remote instance stays invisible
273
+ * to other clients and the corresponding proxy object is the only way to
274
+ * issue commands.
274
275
  *
275
- * **NOTE** When creating a named instance that already exists, the new proxy will
276
+ * **NOTE** When creating an instance that already exists, the new proxy will
276
277
  * simply attach to (and not re-create) it - just like `getInstance()` was
277
278
  * called.
278
279
  *
@@ -280,28 +281,32 @@ class VrpcClient extends EventEmitter {
280
281
  * @param {String} options.className Name of the class which should be
281
282
  * instantiated
282
283
  * @param {String} [options.instance] Name of the created instance. If not
283
- * provided an (invisible) id will be generated
284
+ * provided an id will be generated
284
285
  * @param {Array} [options.args] Positional arguments for the constructor call
285
286
  * @param {String} [options.agent] Agent name. If not provided class default
286
287
  * is used
287
- * @param {bool} [options.cacheProxy=false] If true the proxy object for a given
288
- * instance is cached and used in subsequent calls
289
- * @returns {Promise<Proxy>} Object reflecting a proxy to the original one
290
- * handled by the agent
288
+ * @param {bool} [options.cacheProxy=false] If true the proxy object for a
289
+ * given instance is cached and (re-)used in subsequent calls
290
+ * @param {bool} [options.isIsolated=false] If true the created proxy will be
291
+ * visible only to the client who created it
292
+ * @returns {Promise<Proxy>} Object reflecting a proxy to the original object
293
+ * which is handled by the agent
291
294
  * @example
292
- * // create anonymous instance
295
+ * // create isolated instance
293
296
  * const proxy1 = await client.create({
294
- * className: 'Foo'
297
+ * className: 'Foo',
298
+ * instance: 'myPersonalInstance',
299
+ * isIsolated: true
295
300
  * })
296
- * // create named instance
301
+ * // create shared instance
297
302
  * const proxy2 = await client.create({
298
303
  * className: 'Foo',
299
- * instance: 'myFooInstance'
304
+ * instance: 'aSharedFooInstance'
300
305
  * })
301
- * // create named instance providing three constructor arguments
306
+ * // create shared instance providing three constructor arguments
302
307
  * const proxy3 = await client.create({
303
308
  * className: 'Bar',
304
- * instance: 'myBarInstance',
309
+ * instance: 'mySharedBarInstance',
305
310
  * args: [42, "second argument", { some: 'option' }]
306
311
  * })
307
312
  */
@@ -317,9 +322,9 @@ class VrpcClient extends EventEmitter {
317
322
  const json = {
318
323
  c: className,
319
324
  f: isIsolated ? '__createIsolated__' : '__createShared__',
325
+ a: [instance, ...args],
320
326
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
321
327
  s: `${this._domain}/${os.hostname()}/${this._instance}`,
322
- a: [instance, ...args],
323
328
  v: VRPC_PROTOCOL_VERSION
324
329
  }
325
330
  const proxy = await this._getProxy(agent, className, json)
@@ -364,15 +369,15 @@ class VrpcClient extends EventEmitter {
364
369
  const json = {
365
370
  c: className,
366
371
  f: '__delete__',
372
+ a: [instance],
367
373
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
368
374
  s: `${this._domain}/${os.hostname()}/${this._instance}`,
369
- a: [instance],
370
375
  v: VRPC_PROTOCOL_VERSION
371
376
  }
372
377
  const topic = `${this._domain}/${agent}/${className}/__static__/__delete__`
373
378
  this._mqttPublish(topic, JSON.stringify(json))
374
379
  if (this._proxies[instance]) delete this._proxies[instance]
375
- return this._handleAgentAnswer(json)
380
+ return this._handleAgentAnswer(json, agent)
376
381
  }
377
382
 
378
383
  /**
@@ -391,20 +396,25 @@ class VrpcClient extends EventEmitter {
391
396
  args = [],
392
397
  agent = this._agent
393
398
  } = {}) {
394
- const wrapped = this._wrapArguments(className, functionName, ...args)
399
+ const wrapped = this._wrapArguments({
400
+ agent,
401
+ className,
402
+ functionName,
403
+ args
404
+ })
395
405
  if (!wrapped) return // Skipping remote call -> handled locally
396
406
  const json = {
397
407
  c: className,
398
408
  f: functionName,
409
+ a: wrapped,
399
410
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
400
411
  s: this._vrpcClientId,
401
- a: wrapped,
402
412
  v: VRPC_PROTOCOL_VERSION
403
413
  }
404
414
  const topic = `${this._domain}/${agent}/${className}/__static__/${functionName}`
405
415
  await this._waitUntilClassIsOnline(agent, className)
406
416
  this._mqttPublish(topic, JSON.stringify(json))
407
- return this._handleAgentAnswer(json)
417
+ return this._handleAgentAnswer(json, agent)
408
418
  }
409
419
 
410
420
  /**
@@ -436,41 +446,43 @@ class VrpcClient extends EventEmitter {
436
446
  const json = {
437
447
  c: className,
438
448
  f: functionName,
449
+ a: [],
439
450
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
440
451
  s: this._vrpcClientId,
441
- a: [],
442
452
  v: VRPC_PROTOCOL_VERSION
443
453
  }
444
454
  if (agent === '*') {
445
455
  const result = []
446
456
  const agents = this.getAvailableAgents()
447
- for (const x of agents) {
448
- const topic = `${this._domain}/${x}/${className}/__static__/__callAll__`
449
- const wrapped = this._wrapArguments(
450
- `${x}-${className}`,
457
+ for (const agent of agents) {
458
+ const topic = `${this._domain}/${agent}/${className}/__static__/__callAll__`
459
+ const wrapped = this._wrapArguments({
460
+ agent,
461
+ className,
451
462
  functionName,
452
- ...args
453
- )
463
+ args
464
+ })
454
465
  if (!wrapped) continue // using cache
455
466
  json.a = [functionName, ...wrapped]
456
- await this._waitUntilClassIsOnline(x, className)
467
+ await this._waitUntilClassIsOnline(agent, className)
457
468
  this._mqttPublish(topic, JSON.stringify(json))
458
- const tmp = await this._handleAgentAnswer(json)
469
+ const tmp = await this._handleAgentAnswer(json, agent)
459
470
  result.push(...tmp)
460
471
  }
461
472
  return result
462
473
  }
463
474
  const topic = `${this._domain}/${agent}/${className}/__static__/__callAll__`
464
- const wrapped = this._wrapArguments(
465
- `${agent}-${className}`,
475
+ const wrapped = this._wrapArguments({
476
+ agent,
477
+ className,
466
478
  functionName,
467
- ...args
468
- )
479
+ args
480
+ })
469
481
  if (!wrapped) return // using cache
470
482
  json.a = [functionName, ...wrapped]
471
483
  await this._waitUntilClassIsOnline(agent, className)
472
484
  this._mqttPublish(topic, JSON.stringify(json))
473
- return this._handleAgentAnswer(json)
485
+ return this._handleAgentAnswer(json, agent)
474
486
  }
475
487
 
476
488
  /**
@@ -530,7 +542,7 @@ class VrpcClient extends EventEmitter {
530
542
  }
531
543
 
532
544
  /**
533
- * Retrieves all (named) instances on specific class and agent.
545
+ * Retrieves all (shared) instances on specific class and agent.
534
546
  *
535
547
  * @param {Object} [options]
536
548
  * @param {String} options.className Class name
@@ -648,6 +660,10 @@ class VrpcClient extends EventEmitter {
648
660
  return new Promise(resolve => this._client.end(false, {}, resolve))
649
661
  }
650
662
 
663
+ static _generateUserName () {
664
+ return `${os.userInfo().username}@${os.hostname()}-${os.platform()}-js`
665
+ }
666
+
651
667
  _createMqttClientId () {
652
668
  const clientInfo =
653
669
  os.arch() +
@@ -664,7 +680,7 @@ class VrpcClient extends EventEmitter {
664
680
  .createHash('md5')
665
681
  .update(clientInfo)
666
682
  .digest('hex')
667
- .substr(0, 12)
683
+ .substring(0, 12)
668
684
  return `vc3${this._instance}${md5}` // 3 + 8 + 12 = 23 (max clientId)
669
685
  }
670
686
 
@@ -793,35 +809,120 @@ class VrpcClient extends EventEmitter {
793
809
  // Remove overloads
794
810
  const uniqueFuncs = new Set(functions)
795
811
  // Build proxy
796
- uniqueFuncs.forEach(name => {
797
- proxy[name] = async (...args) => {
798
- const wrapped = this._wrapArguments(proxyId, name, ...args)
799
- if (!wrapped) return // Skipping remote call -> handled locally
812
+ uniqueFuncs.forEach(functionName => {
813
+ // as we are messing with Node.js' event system we have to intercept here
814
+ if (functionName === 'removeAllListeners') {
815
+ proxy[functionName] = async eventName => {
816
+ if (!eventName) {
817
+ throw new Error('VRPC does not support removing all listeners')
818
+ }
819
+ const topic = `${this._domain}/${agent}/${className}/${instance}:${eventName}`
820
+ const id = `__e__${topic}`
821
+ if (this._cachedSubscriptions[topic]) {
822
+ this._eventEmitter.removeAllListeners(id)
823
+ this._mqttUnsubscribe(topic)
824
+ delete this._cachedSubscriptions[topic]
825
+ }
826
+ try {
827
+ const json = {
828
+ c: instance,
829
+ f: functionName,
830
+ a: [eventName],
831
+ i: `${this._instance}-${this._invokeId++ %
832
+ Number.MAX_SAFE_INTEGER}`,
833
+ s: this._vrpcClientId,
834
+ v: VRPC_PROTOCOL_VERSION
835
+ }
836
+ this._mqttPublish(
837
+ `${targetTopic}/${functionName}`,
838
+ JSON.stringify(json)
839
+ )
840
+ return this._handleAgentAnswer(json, agent)
841
+ } catch (err) {
842
+ throw new Error(
843
+ `Could not remotely call "${functionName}" because: ${err.message}`
844
+ )
845
+ }
846
+ }
847
+ return
848
+ }
849
+ proxy[functionName] = async (...args) => {
850
+ const wrapped = this._wrapArguments({
851
+ agent,
852
+ className,
853
+ instance,
854
+ proxyId,
855
+ functionName,
856
+ args
857
+ })
858
+ if (!wrapped) return true // Skipping remote call -> handled locally
800
859
  try {
801
860
  const json = {
802
861
  c: instance,
803
- f: name,
862
+ f: functionName,
863
+ a: wrapped,
804
864
  i: `${this._instance}-${this._invokeId++ %
805
865
  Number.MAX_SAFE_INTEGER}`,
806
866
  s: this._vrpcClientId,
807
- a: wrapped,
808
867
  v: VRPC_PROTOCOL_VERSION
809
868
  }
810
- this._mqttPublish(`${targetTopic}/${name}`, JSON.stringify(json))
811
- return this._handleAgentAnswer(json)
869
+ this._mqttPublish(
870
+ `${targetTopic}/${functionName}`,
871
+ JSON.stringify(json)
872
+ )
873
+ return this._handleAgentAnswer(json, agent)
812
874
  } catch (err) {
813
875
  throw new Error(
814
- `Could not remotely call "${name}" because: ${err.message}`
876
+ `Could not remotely call "${functionName}" because: ${err.message}`
815
877
  )
816
878
  }
817
879
  }
818
880
  })
881
+ if (!uniqueFuncs.has('vrpcOn') && !uniqueFuncs.has('vrpcOff')) {
882
+ proxy.vrpcOn = async (functionName, ...args) => {
883
+ if (!uniqueFuncs.has(functionName)) throw new Error('Bad magic')
884
+ const wrapped = this._wrapArguments({
885
+ agent,
886
+ className,
887
+ instance,
888
+ proxyId,
889
+ functionName: 'vrpcOn',
890
+ args: [functionName, ...args]
891
+ })
892
+ if (!wrapped) return // Skipping remote call -> handled locally
893
+ try {
894
+ const json = {
895
+ c: instance,
896
+ f: functionName,
897
+ a: wrapped.slice(1), // first argument was remote function name
898
+ i: `${this._instance}-${this._invokeId++ %
899
+ Number.MAX_SAFE_INTEGER}`,
900
+ s: this._vrpcClientId,
901
+ v: VRPC_PROTOCOL_VERSION
902
+ }
903
+ this._mqttPublish(
904
+ `${targetTopic}/${functionName}`,
905
+ JSON.stringify(json)
906
+ )
907
+ return this._handleAgentAnswer(json, agent)
908
+ } catch (err) {
909
+ throw new Error(
910
+ `Could not remotely call "${functionName}" because: ${err.message}`
911
+ )
912
+ }
913
+ }
914
+ proxy.vrpcOff = functionName => {
915
+ const topic = `${this._domain}/${agent}/${className}/${instance}-${functionName}`
916
+ const id = `__e__${topic}`
917
+ this._eventEmitter.removeAllListeners(id)
918
+ }
919
+ }
819
920
  return proxy
820
921
  }
821
922
 
822
- async _handleAgentAnswer ({ i, c, f }) {
923
+ async _handleAgentAnswer ({ i, c, f }, agent) {
823
924
  return new Promise((resolve, reject) => {
824
- const msg = `Function call "${c}::${f}()" timed out (> ${this._timeout} ms)`
925
+ const msg = `Function call "${c}::${f}()" on agent "${agent}" timed out (> ${this._timeout} ms)`
825
926
  const timer = setTimeout(() => {
826
927
  this._eventEmitter.removeAllListeners(i)
827
928
  reject(new Error(msg))
@@ -829,13 +930,15 @@ class VrpcClient extends EventEmitter {
829
930
  this._eventEmitter.once(i, data => {
830
931
  clearTimeout(timer)
831
932
  if (data.e) {
832
- reject(new Error(data.e))
933
+ reject(new Error(`[vrpc ${agent}-${c}-${f}]: ${data.e}`))
833
934
  } else {
834
935
  const ret = data.r
835
936
  // Handle functions returning a promise
836
- if (typeof ret === 'string' && ret.substr(0, 5) === '__p__') {
937
+ if (typeof ret === 'string' && ret.substring(0, 5) === '__p__') {
837
938
  this._eventEmitter.once(ret, promiseData => {
838
- if (promiseData.e) reject(new Error(promiseData.e))
939
+ // TODO improve error message here, like above
940
+ if (promiseData.e)
941
+ reject(new Error(`[vrpc ${agent}-${c}-${f}]: ${promiseData.e}`))
839
942
  else resolve(promiseData.r)
840
943
  })
841
944
  } else {
@@ -866,79 +969,130 @@ class VrpcClient extends EventEmitter {
866
969
  })
867
970
  }
868
971
 
869
- _wrapArguments (remoteId, functionName, ...args) {
972
+ _wrapArguments ({ agent, className, instance, proxyId, functionName, args }) {
870
973
  const wrapped = []
871
- let isHandledLocally = false
872
- args.forEach((x, i) => {
974
+ for (const [i, x] of args.entries()) {
873
975
  // Check whether provided argument is a function
874
976
  if (this._isFunction(x)) {
875
- // Check special case of an event emitter registration
876
- // We test three conditions:
877
- // 1) functionName must be "on"
878
- // 2) callback is second argument
879
- // 3) first argument was string
880
- if (functionName === 'on' && i === 1 && typeof args[0] === 'string') {
881
- const id = this._addEventSubscription(remoteId, args[0], x)
882
- if (!id) isHandledLocally = true
977
+ const callback = x
978
+ if (
979
+ functionName === 'vrpcOn' ||
980
+ (functionName === 'on' && i === 1 && typeof args[0] === 'string')
981
+ ) {
982
+ // special case of an (remote-)event emitter registration
983
+ // we test three conditions:
984
+ // 1) functionName must be "on"
985
+ // 2) callback is second argument
986
+ // 3) first argument was string
987
+ const event = args[0]
988
+ const id = this._onRemoteEvent({
989
+ agent,
990
+ className,
991
+ instance,
992
+ event,
993
+ callback
994
+ })
995
+ // indicate that no network call is required
996
+ if (id === null) return null
883
997
  wrapped.push(id)
998
+ continue
884
999
  } else if (
885
1000
  (functionName === 'off' || functionName === 'removeListener') &&
886
1001
  i === 1 &&
887
1002
  typeof args[0] === 'string'
888
1003
  ) {
889
- const id = this._removeEventSubscription(remoteId, args[0], x)
890
- if (!id) isHandledLocally = true
1004
+ const event = args[0]
1005
+ const id = this._offRemoteEvent({
1006
+ agent,
1007
+ className,
1008
+ instance,
1009
+ event,
1010
+ callback
1011
+ })
1012
+ // indicate that no network call is required
1013
+ if (id === null) return null
891
1014
  wrapped.push(id)
892
- // Regular function callback
1015
+ continue
893
1016
  } else {
1017
+ // Regular function callback (can be static or member function)
1018
+ const remoteId = proxyId || `${agent}-${className}`
894
1019
  const id = `__f__${remoteId}-${functionName}-${i}-${this._invokeId++ %
895
1020
  Number.MAX_SAFE_INTEGER}`
896
1021
  wrapped.push(id)
897
- this._eventEmitter.once(id, ({ a }) => {
898
- x.apply(null, a) // This is the actual function call
899
- })
1022
+ this._eventEmitter.once(id, ({ a }) => x.apply(null, a))
1023
+ continue
900
1024
  }
901
- } else if (this._isEmitter(x)) {
1025
+ }
1026
+ // special case of an EventEmitter provided as argument
1027
+ if (this._isEmitter(x)) {
902
1028
  const { emitter, event } = x
903
- const id = `__f__${remoteId}-${functionName}-${i}-${event}`
904
- wrapped.push(id)
905
- this._eventEmitter.on(id, ({ a }) => {
906
- emitter.emit(event, ...a)
1029
+ const id = this._onRemoteEvent({
1030
+ agent,
1031
+ className,
1032
+ instance,
1033
+ event,
1034
+ callback: (...args) => emitter.emit(event, ...args)
907
1035
  })
908
- } else {
909
- wrapped.push(x)
1036
+ // indicate that no network call is required
1037
+ if (id === null) return null
1038
+ wrapped.push(id)
1039
+ continue
910
1040
  }
911
- })
912
- if (isHandledLocally) return null
1041
+ // default behavior is to not touch
1042
+ wrapped.push(x)
1043
+ }
913
1044
  return wrapped
914
1045
  }
915
1046
 
916
- _addEventSubscription (remoteId, event, callback) {
917
- const id = `__f__${remoteId}-${event}`
918
- if (this._cachedSubscriptions.has(id)) {
919
- this._cachedSubscriptions.get(id).on(id, callback)
920
- return
1047
+ _onRemoteEvent ({ agent, className, instance, event, callback }) {
1048
+ // the topic on which any remote event will be published to
1049
+ const topic = instance
1050
+ ? `${this._domain}/${agent}/${className}/${instance}:${event}`
1051
+ : `${this._domain}/${agent}/${className}/${event}`
1052
+ // prepare a special id for the agent to know that events should be
1053
+ // published to the provided topic after the prefix
1054
+ const id = `__e__${topic}`
1055
+ // call our proxy callback when remote events were received
1056
+ const handler = ({ a }) => callback.apply(null, a)
1057
+ this._eventEmitter.on(id, handler)
1058
+ if (!this._cachedSubscriptions[topic]) {
1059
+ // when not yet subscribed to this topic do it now
1060
+ this._mqttSubscribe(topic)
1061
+ this._cachedSubscriptions[topic] = [{ callback, handler }]
1062
+ return id
921
1063
  }
922
- const emitter = new EventEmitter()
923
- emitter.on(id, callback)
924
- this._cachedSubscriptions.set(id, emitter)
925
- this._eventEmitter.on(id, ({ a }) => {
926
- emitter.emit(id, ...a)
927
- })
928
- return id
1064
+ // otherwise just add the new callback-handler pair
1065
+ this._cachedSubscriptions[topic].push({ callback, handler })
1066
+ // indicate that no network call is required
1067
+ return null
929
1068
  }
930
1069
 
931
- _removeEventSubscription (remoteId, event, callback) {
932
- const id = `__f__${remoteId}-${event}`
933
- if (this._cachedSubscriptions.has(id)) {
934
- const emitter = this._cachedSubscriptions.get(id)
935
- emitter.removeListener(id, callback)
936
- if (emitter.listenerCount(id) === 0) {
937
- this._cachedSubscriptions.delete(id)
938
- this._eventEmitter.removeAllListeners(id)
1070
+ _offRemoteEvent ({ agent, className, instance, event, callback }) {
1071
+ const topic = instance
1072
+ ? `${this._domain}/${agent}/${className}/${instance}:${event}`
1073
+ : `${this._domain}/${agent}/${className}/${event}`
1074
+ const id = `__e__${topic}`
1075
+ if (this._cachedSubscriptions[topic]) {
1076
+ const index = this._cachedSubscriptions[topic].findIndex(
1077
+ x => x.callback === callback
1078
+ )
1079
+ if (index === -1) {
1080
+ this._log.warn(`Failed removing event listener ${id}`)
1081
+ return null
1082
+ }
1083
+ const [{ handler }] = this._cachedSubscriptions[topic].splice(index, 1)
1084
+ this._eventEmitter.removeListener(id, handler)
1085
+ if (this._cachedSubscriptions[topic].length === 0) {
1086
+ this._mqttUnsubscribe(topic)
1087
+ delete this._cachedSubscriptions[topic]
939
1088
  return id
940
1089
  }
1090
+ // indicate that no network call is required
1091
+ return null
1092
+ } else {
1093
+ this._log.warn(`Can not unsubscribe from non-existing event: ${event}`)
941
1094
  }
1095
+ return id
942
1096
  }
943
1097
 
944
1098
  _stripSignature (method) {
@@ -1045,7 +1199,7 @@ class VrpcClient extends EventEmitter {
1045
1199
  * @param {String} info.domain - Domain name
1046
1200
  * @param {String} info.agent - Agent name
1047
1201
  * @param {String} info.className - Class name
1048
- * @param {Array.<String>} info.instances - Array of named instances
1202
+ * @param {Array.<String>} info.instances - Array of instances
1049
1203
  * @param {Array.<String>} info.memberFunctions - Array of member functions
1050
1204
  * @param {Array.<String>} info.staticFunctions - Array of static functions
1051
1205
  * @param {MetaData} info.meta - Object associating further information to functions