vrpc 3.0.0-alpha.3 → 3.0.0

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
@@ -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
@@ -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
@@ -664,7 +676,7 @@ class VrpcClient extends EventEmitter {
664
676
  .createHash('md5')
665
677
  .update(clientInfo)
666
678
  .digest('hex')
667
- .substr(0, 12)
679
+ .substring(0, 12)
668
680
  return `vc3${this._instance}${md5}` // 3 + 8 + 12 = 23 (max clientId)
669
681
  }
670
682
 
@@ -793,35 +805,120 @@ class VrpcClient extends EventEmitter {
793
805
  // Remove overloads
794
806
  const uniqueFuncs = new Set(functions)
795
807
  // 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
808
+ uniqueFuncs.forEach(functionName => {
809
+ // as we are messing with Node.js' event system we have to intercept here
810
+ if (functionName === 'removeAllListeners') {
811
+ proxy[functionName] = async eventName => {
812
+ if (!eventName) {
813
+ throw new Error('VRPC does not support removing all listeners')
814
+ }
815
+ const topic = `${this._domain}/${agent}/${className}/${instance}:${eventName}`
816
+ const id = `__e__${topic}`
817
+ if (this._cachedSubscriptions[topic]) {
818
+ this._eventEmitter.removeAllListeners(id)
819
+ this._mqttUnsubscribe(topic)
820
+ delete this._cachedSubscriptions[topic]
821
+ }
822
+ try {
823
+ const json = {
824
+ c: instance,
825
+ f: functionName,
826
+ a: [eventName],
827
+ i: `${this._instance}-${this._invokeId++ %
828
+ Number.MAX_SAFE_INTEGER}`,
829
+ s: this._vrpcClientId,
830
+ v: VRPC_PROTOCOL_VERSION
831
+ }
832
+ this._mqttPublish(
833
+ `${targetTopic}/${functionName}`,
834
+ JSON.stringify(json)
835
+ )
836
+ return this._handleAgentAnswer(json, agent)
837
+ } catch (err) {
838
+ throw new Error(
839
+ `Could not remotely call "${functionName}" because: ${err.message}`
840
+ )
841
+ }
842
+ }
843
+ return
844
+ }
845
+ proxy[functionName] = async (...args) => {
846
+ const wrapped = this._wrapArguments({
847
+ agent,
848
+ className,
849
+ instance,
850
+ proxyId,
851
+ functionName,
852
+ args
853
+ })
854
+ if (!wrapped) return true // Skipping remote call -> handled locally
800
855
  try {
801
856
  const json = {
802
857
  c: instance,
803
- f: name,
858
+ f: functionName,
859
+ a: wrapped,
804
860
  i: `${this._instance}-${this._invokeId++ %
805
861
  Number.MAX_SAFE_INTEGER}`,
806
862
  s: this._vrpcClientId,
807
- a: wrapped,
808
863
  v: VRPC_PROTOCOL_VERSION
809
864
  }
810
- this._mqttPublish(`${targetTopic}/${name}`, JSON.stringify(json))
811
- return this._handleAgentAnswer(json)
865
+ this._mqttPublish(
866
+ `${targetTopic}/${functionName}`,
867
+ JSON.stringify(json)
868
+ )
869
+ return this._handleAgentAnswer(json, agent)
812
870
  } catch (err) {
813
871
  throw new Error(
814
- `Could not remotely call "${name}" because: ${err.message}`
872
+ `Could not remotely call "${functionName}" because: ${err.message}`
815
873
  )
816
874
  }
817
875
  }
818
876
  })
877
+ if (!uniqueFuncs.has('vrpcOn') && !uniqueFuncs.has('vrpcOff')) {
878
+ proxy.vrpcOn = async (functionName, ...args) => {
879
+ if (!uniqueFuncs.has(functionName)) throw new Error('Bad magic')
880
+ const wrapped = this._wrapArguments({
881
+ agent,
882
+ className,
883
+ instance,
884
+ proxyId,
885
+ functionName: 'vrpcOn',
886
+ args: [functionName, ...args]
887
+ })
888
+ if (!wrapped) return // Skipping remote call -> handled locally
889
+ try {
890
+ const json = {
891
+ c: instance,
892
+ f: functionName,
893
+ a: wrapped.slice(1), // first argument was remote function name
894
+ i: `${this._instance}-${this._invokeId++ %
895
+ Number.MAX_SAFE_INTEGER}`,
896
+ s: this._vrpcClientId,
897
+ v: VRPC_PROTOCOL_VERSION
898
+ }
899
+ this._mqttPublish(
900
+ `${targetTopic}/${functionName}`,
901
+ JSON.stringify(json)
902
+ )
903
+ return this._handleAgentAnswer(json, agent)
904
+ } catch (err) {
905
+ throw new Error(
906
+ `Could not remotely call "${functionName}" because: ${err.message}`
907
+ )
908
+ }
909
+ }
910
+ proxy.vrpcOff = functionName => {
911
+ const topic = `${this._domain}/${agent}/${className}/${instance}-${functionName}`
912
+ const id = `__e__${topic}`
913
+ this._eventEmitter.removeAllListeners(id)
914
+ }
915
+ }
819
916
  return proxy
820
917
  }
821
918
 
822
- async _handleAgentAnswer ({ i, c, f }) {
919
+ async _handleAgentAnswer ({ i, c, f }, agent) {
823
920
  return new Promise((resolve, reject) => {
824
- const msg = `Function call "${c}::${f}()" timed out (> ${this._timeout} ms)`
921
+ const msg = `Function call "${c}::${f}()" on agent "${agent}" timed out (> ${this._timeout} ms)`
825
922
  const timer = setTimeout(() => {
826
923
  this._eventEmitter.removeAllListeners(i)
827
924
  reject(new Error(msg))
@@ -829,13 +926,14 @@ class VrpcClient extends EventEmitter {
829
926
  this._eventEmitter.once(i, data => {
830
927
  clearTimeout(timer)
831
928
  if (data.e) {
832
- reject(new Error(data.e))
929
+ reject(new Error(`[vrpc ${agent}-${c}-${f}]: ${data.e}`))
833
930
  } else {
834
931
  const ret = data.r
835
932
  // Handle functions returning a promise
836
- if (typeof ret === 'string' && ret.substr(0, 5) === '__p__') {
933
+ if (typeof ret === 'string' && ret.substring(0, 5) === '__p__') {
837
934
  this._eventEmitter.once(ret, promiseData => {
838
- if (promiseData.e) reject(new Error(promiseData.e))
935
+ // TODO improve error message here, like above
936
+ if (promiseData.e) reject(new Error(`[vrpc ${agent}-${c}-${f}]: ${promiseData.e}`))
839
937
  else resolve(promiseData.r)
840
938
  })
841
939
  } else {
@@ -866,79 +964,128 @@ class VrpcClient extends EventEmitter {
866
964
  })
867
965
  }
868
966
 
869
- _wrapArguments (remoteId, functionName, ...args) {
967
+ _wrapArguments ({ agent, className, instance, proxyId, functionName, args }) {
870
968
  const wrapped = []
871
- let isHandledLocally = false
872
- args.forEach((x, i) => {
969
+ for (const [i, x] of args.entries()) {
873
970
  // Check whether provided argument is a function
874
971
  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
972
+ const callback = x
973
+ if (
974
+ functionName === 'vrpcOn' ||
975
+ (functionName === 'on' && i === 1 && typeof args[0] === 'string')
976
+ ) {
977
+ // special case of an (remote-)event emitter registration
978
+ // we test three conditions:
979
+ // 1) functionName must be "on"
980
+ // 2) callback is second argument
981
+ // 3) first argument was string
982
+ const event = args[0]
983
+ const id = this._onRemoteEvent({
984
+ agent,
985
+ className,
986
+ instance,
987
+ event,
988
+ callback
989
+ })
990
+ // indicate that no network call is required
991
+ if (id === null) return null
883
992
  wrapped.push(id)
993
+ continue
884
994
  } else if (
885
995
  (functionName === 'off' || functionName === 'removeListener') &&
886
996
  i === 1 &&
887
997
  typeof args[0] === 'string'
888
998
  ) {
889
- const id = this._removeEventSubscription(remoteId, args[0], x)
890
- if (!id) isHandledLocally = true
999
+ const event = args[0]
1000
+ const id = this._offRemoteEvent({
1001
+ agent,
1002
+ className,
1003
+ instance,
1004
+ event,
1005
+ callback
1006
+ })
1007
+ // indicate that no network call is required
1008
+ if (id === null) return null
891
1009
  wrapped.push(id)
892
- // Regular function callback
1010
+ continue
893
1011
  } else {
1012
+ // Regular function callback (can be static or member function)
1013
+ const remoteId = proxyId || `${agent}-${className}`
894
1014
  const id = `__f__${remoteId}-${functionName}-${i}-${this._invokeId++ %
895
1015
  Number.MAX_SAFE_INTEGER}`
896
1016
  wrapped.push(id)
897
- this._eventEmitter.once(id, ({ a }) => {
898
- x.apply(null, a) // This is the actual function call
899
- })
1017
+ this._eventEmitter.once(id, ({ a }) => x.apply(null, a))
1018
+ continue
900
1019
  }
901
- } else if (this._isEmitter(x)) {
1020
+ }
1021
+ // special case of an EventEmitter provided as argument
1022
+ if (this._isEmitter(x)) {
902
1023
  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)
1024
+ const id = this._onRemoteEvent({
1025
+ agent,
1026
+ className,
1027
+ instance,
1028
+ event,
1029
+ callback: (...args) => emitter.emit(event, ...args)
907
1030
  })
908
- } else {
909
- wrapped.push(x)
1031
+ // indicate that no network call is required
1032
+ if (id === null) return null
1033
+ wrapped.push(id)
1034
+ continue
910
1035
  }
911
- })
912
- if (isHandledLocally) return null
1036
+ // default behavior is to not touch
1037
+ wrapped.push(x)
1038
+ }
913
1039
  return wrapped
914
1040
  }
915
1041
 
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
1042
+ _onRemoteEvent ({ agent, className, instance, event, callback }) {
1043
+ // the topic on which any remote event will be published to
1044
+ const topic = instance
1045
+ ? `${this._domain}/${agent}/${className}/${instance}:${event}`
1046
+ : `${this._domain}/${agent}/${className}/${event}`
1047
+ // prepare a special id for the agent to know that events should be
1048
+ // published to the provided topic after the prefix
1049
+ const id = `__e__${topic}`
1050
+ // call our proxy callback when remote events were received
1051
+ const handler = ({ a }) => callback.apply(null, a)
1052
+ this._eventEmitter.on(id, handler)
1053
+ if (!this._cachedSubscriptions[topic]) {
1054
+ // when not yet subscribed to this topic do it now
1055
+ this._mqttSubscribe(topic)
1056
+ this._cachedSubscriptions[topic] = [{ callback, handler }]
1057
+ return id
921
1058
  }
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
1059
+ // otherwise just add the new callback-handler pair
1060
+ this._cachedSubscriptions[topic].push({ callback, handler })
1061
+ // indicate that no network call is required
1062
+ return null
929
1063
  }
930
1064
 
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)
1065
+ _offRemoteEvent ({ agent, className, instance, event, callback }) {
1066
+ const topic = instance
1067
+ ? `${this._domain}/${agent}/${className}/${instance}:${event}`
1068
+ : `${this._domain}/${agent}/${className}/${event}`
1069
+ const id = `__e__${topic}`
1070
+ if (this._cachedSubscriptions[topic]) {
1071
+ const index = this._cachedSubscriptions[topic].findIndex(x => x.callback === callback)
1072
+ if (index === -1) {
1073
+ this._log.warn(`Failed removing event listener ${id}`)
1074
+ return null
1075
+ }
1076
+ const [{ handler }] = this._cachedSubscriptions[topic].splice(index, 1)
1077
+ this._eventEmitter.removeListener(id, handler)
1078
+ if (this._cachedSubscriptions[topic].length === 0) {
1079
+ this._mqttUnsubscribe(topic)
1080
+ delete this._cachedSubscriptions[topic]
939
1081
  return id
940
1082
  }
1083
+ // indicate that no network call is required
1084
+ return null
1085
+ } else {
1086
+ this._log.warn(`Can not unsubscribe from non-existing event: ${event}`)
941
1087
  }
1088
+ return id
942
1089
  }
943
1090
 
944
1091
  _stripSignature (method) {
@@ -1045,7 +1192,7 @@ class VrpcClient extends EventEmitter {
1045
1192
  * @param {String} info.domain - Domain name
1046
1193
  * @param {String} info.agent - Agent name
1047
1194
  * @param {String} info.className - Class name
1048
- * @param {Array.<String>} info.instances - Array of named instances
1195
+ * @param {Array.<String>} info.instances - Array of instances
1049
1196
  * @param {Array.<String>} info.memberFunctions - Array of member functions
1050
1197
  * @param {Array.<String>} info.staticFunctions - Array of static functions
1051
1198
  * @param {MetaData} info.meta - Object associating further information to functions
@@ -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
@@ -41,7 +41,7 @@ const EventEmitter = require('events')
41
41
  const { nanoid } = require('nanoid')
42
42
 
43
43
  /**
44
- * Client capable of creating proxy objects and locally calling
44
+ * Client capable of creating proxy classes and objects to locally call
45
45
  * functions as provided through native addons.
46
46
  */
47
47
  class VrpcNative {
@@ -64,6 +64,15 @@ class VrpcNative {
64
64
  })
65
65
  }
66
66
 
67
+ /**
68
+ * Provides a proxy class to an existing one in the native addon
69
+ *
70
+ * You can use the returned class in the usual way. Static function calls
71
+ * are forwarded to the native addon, as are any instantiations and member
72
+ * function calls.
73
+ * @param {String} className The name of the class
74
+ * @returns Proxy Class
75
+ */
67
76
  getClass (className) {
68
77
  if (!this.getAvailableClasses().includes(className)) {
69
78
  throw new Error(`Native addon does not provide class: ${className}`)
@@ -226,6 +235,11 @@ class VrpcNative {
226
235
  return Klass
227
236
  }
228
237
 
238
+ /**
239
+ * Deletes a proxy object and its underlying instance
240
+ * @param {Object} proxy A proxy object
241
+ * @returns True in case of success, false otherwise
242
+ */
229
243
  delete (proxy) {
230
244
  if (!typeof proxy === 'object' || !proxy.vrpcProxyId) {
231
245
  throw new Error('Provided argument seems not to be a VRPC proxy')
@@ -238,6 +252,15 @@ class VrpcNative {
238
252
  return JSON.parse(this._adapter.call(JSON.stringify(json))).r
239
253
  }
240
254
 
255
+ /**
256
+ * Secondary option to call a static function (when creation of a proxy class
257
+ * seems to be too much overhead)
258
+ *
259
+ * @param {String} className The class on which the static function should be called
260
+ * @param {String} functionName Name of the static function
261
+ * @param {...any} args The function arguments
262
+ * @returns The output of the underlying static function
263
+ */
241
264
  callStatic (className, functionName, ...args) {
242
265
  if (functionName === 'vrpcOff') {
243
266
  const id = `__f__${className}-vrpcOn:${args[0]}`