vrpc 3.0.0-alpha.2 → 3.0.2

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
@@ -70,7 +70,7 @@ 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
  */
@@ -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,6 @@ class VrpcClient extends EventEmitter {
152
152
  let username = this._username
153
153
  let password = this._password
154
154
  if (this._token) {
155
- username = '__token__'
156
155
  password = this._token
157
156
  }
158
157
  const options = {
@@ -162,6 +161,7 @@ class VrpcClient extends EventEmitter {
162
161
  keepalive: 30,
163
162
  clientId: this._mqttClientId,
164
163
  rejectUnauthorized: false,
164
+ connectTimeout: this._timeout,
165
165
  will: {
166
166
  topic: `${this._vrpcClientId}/__clientInfo__`,
167
167
  payload: JSON.stringify({ status: 'offline' })
@@ -229,10 +229,12 @@ class VrpcClient extends EventEmitter {
229
229
  staticFunctions,
230
230
  meta
231
231
  } = json
232
- if (removed.length !== 0)
232
+ if (removed.length !== 0) {
233
233
  this.emit('instanceGone', removed, { domain, agent, className })
234
- if (added.length !== 0)
234
+ }
235
+ if (added.length !== 0) {
235
236
  this.emit('instanceNew', added, { domain, agent, className })
237
+ }
236
238
  this.emit('class', {
237
239
  domain,
238
240
  agent,
@@ -248,31 +250,29 @@ class VrpcClient extends EventEmitter {
248
250
  this._eventEmitter.emit(i, { a, e, r })
249
251
  }
250
252
  })
251
- await new Promise((resolve, reject) => {
252
- const timer = setTimeout(() => {
253
+ return new Promise((resolve, reject) => {
254
+ this._client.once('offline', () => {
255
+ this._client.end()
253
256
  reject(new Error(`Connection trial timed out (> ${this._timeout} ms)`))
254
- }, this._timeout)
255
- this.once('connect', () => {
256
- clearTimeout(timer)
257
- resolve()
258
257
  })
258
+ this._client.once('connect', resolve)
259
259
  })
260
260
  }
261
261
 
262
262
  /**
263
263
  * Creates a new remote instance and provides a proxy to it.
264
264
  *
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.
265
+ * Remote instances can be "shared" or "isolated". Shared instances are
266
+ * visible and re-attachable across clients as long as they are not
267
+ * explicitly deleted. Life-cycle changes of shared instances are available
268
+ * under the `class`, `instanceNew`, and `instanceGone` events. A shared
269
+ * instance is created by default (`isIsolated: false`).
270
270
  *
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.
271
+ * When the `isIsolated` option is true, the remote instance stays invisible
272
+ * to other clients and the corresponding proxy object is the only way to
273
+ * issue commands.
274
274
  *
275
- * **NOTE** When creating a named instance that already exists, the new proxy will
275
+ * **NOTE** When creating an instance that already exists, the new proxy will
276
276
  * simply attach to (and not re-create) it - just like `getInstance()` was
277
277
  * called.
278
278
  *
@@ -280,28 +280,32 @@ class VrpcClient extends EventEmitter {
280
280
  * @param {String} options.className Name of the class which should be
281
281
  * instantiated
282
282
  * @param {String} [options.instance] Name of the created instance. If not
283
- * provided an (invisible) id will be generated
283
+ * provided an id will be generated
284
284
  * @param {Array} [options.args] Positional arguments for the constructor call
285
285
  * @param {String} [options.agent] Agent name. If not provided class default
286
286
  * 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
287
+ * @param {bool} [options.cacheProxy=false] If true the proxy object for a
288
+ * given instance is cached and (re-)used in subsequent calls
289
+ * @param {bool} [options.isIsolated=false] If true the created proxy will be
290
+ * visible only to the client who created it
291
+ * @returns {Promise<Proxy>} Object reflecting a proxy to the original object
292
+ * which is handled by the agent
291
293
  * @example
292
- * // create anonymous instance
294
+ * // create isolated instance
293
295
  * const proxy1 = await client.create({
294
- * className: 'Foo'
296
+ * className: 'Foo',
297
+ * instance: 'myPersonalInstance',
298
+ * isIsolated: true
295
299
  * })
296
- * // create named instance
300
+ * // create shared instance
297
301
  * const proxy2 = await client.create({
298
302
  * className: 'Foo',
299
- * instance: 'myFooInstance'
303
+ * instance: 'aSharedFooInstance'
300
304
  * })
301
- * // create named instance providing three constructor arguments
305
+ * // create shared instance providing three constructor arguments
302
306
  * const proxy3 = await client.create({
303
307
  * className: 'Bar',
304
- * instance: 'myBarInstance',
308
+ * instance: 'mySharedBarInstance',
305
309
  * args: [42, "second argument", { some: 'option' }]
306
310
  * })
307
311
  */
@@ -317,9 +321,9 @@ class VrpcClient extends EventEmitter {
317
321
  const json = {
318
322
  c: className,
319
323
  f: isIsolated ? '__createIsolated__' : '__createShared__',
324
+ a: [instance, ...args],
320
325
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
321
326
  s: `${this._domain}/${os.hostname()}/${this._instance}`,
322
- a: [instance, ...args],
323
327
  v: VRPC_PROTOCOL_VERSION
324
328
  }
325
329
  const proxy = await this._getProxy(agent, className, json)
@@ -336,37 +340,15 @@ class VrpcClient extends EventEmitter {
336
340
  *
337
341
  * @param {String} instance The instance to be retrieved
338
342
  * @param {Object} [options] Explicitly define agent and class
339
- * @param {String} options.className Name of the instance's class
340
- * @param {String} options.agent Agent name. If not provided class default is used
341
- * @param {bool} options.noWait If true immediately fail if instance could not be found in local cache
343
+ * @param {String} [options.className] Name of the instance's class
344
+ * @param {String} [options.agent] Agent name. If not provided class default is used as priority hit
345
+ * @param {bool} [options.noWait=false] If true immediately fail if instance could not be found in local cache
342
346
  * @returns {Promise<Proxy>} Proxy object reflecting the remotely existing instance
343
347
  */
344
- async getInstance (instance, options) {
348
+ async getInstance (instance, options = {}) {
345
349
  if (this._proxies[instance]) return this._proxies[instance]
346
- if (typeof instance === 'string') {
347
- const {
348
- agent,
349
- className,
350
- instance: instanceString
351
- } = await this._getInstanceData(instance, options)
352
- return this._createProxy(agent, className, instanceString)
353
- } else {
354
- // deprecate this
355
- this._indicateDeprecation(
356
- 'VrpcClient.getInstance(): ' +
357
- 'Using an object as single argument is deprecated, ' +
358
- 'use "getInstance(instance, options)" instead.'
359
- )
360
- const fakedOptions = { ...instance }
361
- const fakedInstance = fakedOptions.instance
362
- delete fakedOptions.instance
363
- const {
364
- agent,
365
- className,
366
- instance: instanceString
367
- } = await this._getInstanceData(fakedInstance, fakedOptions)
368
- return this._createProxy(agent, className, instanceString)
369
- }
350
+ const { agent, className } = await this._getInstanceData(instance, options)
351
+ return this._createProxy(agent, className, instance)
370
352
  }
371
353
 
372
354
  /**
@@ -378,36 +360,23 @@ class VrpcClient extends EventEmitter {
378
360
  * @param {String} instance The instance to be deleted
379
361
  * @param {Object} [options] Explicitly define agent and class
380
362
  * @param {String} options.className Name of the instance's class
381
- * @param {String} options.agent Agent name. If not provided class default is used
363
+ * @param {String} options.agent Agent name. If not provided class default is used as priority hit
382
364
  * @returns {Promise<Boolean>} true if successful, false otherwise
383
365
  */
384
- async delete (instance, options) {
385
- let instanceData = { agent: this._agent }
386
- if (typeof instance === 'string') {
387
- instanceData = await this._getInstanceData(instance)
388
- if (options) instanceData = { ...options, ...instanceData }
389
- } else {
390
- // deprecate this
391
- this._indicateDeprecation(
392
- 'VrpcClient.delete(): ' +
393
- 'Using an object as single argument is deprecated, ' +
394
- 'use "delete(instance, options)" instead.'
395
- )
396
- instanceData = { ...instanceData, ...instance }
397
- }
398
- const { agent, className, instance: instanceString } = instanceData
366
+ async delete (instance, options = {}) {
367
+ const { agent, className } = await this._getInstanceData(instance, options)
399
368
  const json = {
400
369
  c: className,
401
370
  f: '__delete__',
371
+ a: [instance],
402
372
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
403
373
  s: `${this._domain}/${os.hostname()}/${this._instance}`,
404
- a: [instanceString],
405
374
  v: VRPC_PROTOCOL_VERSION
406
375
  }
407
376
  const topic = `${this._domain}/${agent}/${className}/__static__/__delete__`
408
377
  this._mqttPublish(topic, JSON.stringify(json))
409
378
  if (this._proxies[instance]) delete this._proxies[instance]
410
- return this._handleAgentAnswer(json)
379
+ return this._handleAgentAnswer(json, agent)
411
380
  }
412
381
 
413
382
  /**
@@ -426,20 +395,25 @@ class VrpcClient extends EventEmitter {
426
395
  args = [],
427
396
  agent = this._agent
428
397
  } = {}) {
429
- const wrapped = this._wrapArguments(className, functionName, ...args)
398
+ const wrapped = this._wrapArguments({
399
+ agent,
400
+ className,
401
+ functionName,
402
+ args
403
+ })
430
404
  if (!wrapped) return // Skipping remote call -> handled locally
431
405
  const json = {
432
406
  c: className,
433
407
  f: functionName,
408
+ a: wrapped,
434
409
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
435
410
  s: this._vrpcClientId,
436
- a: wrapped,
437
411
  v: VRPC_PROTOCOL_VERSION
438
412
  }
439
413
  const topic = `${this._domain}/${agent}/${className}/__static__/${functionName}`
440
414
  await this._waitUntilClassIsOnline(agent, className)
441
415
  this._mqttPublish(topic, JSON.stringify(json))
442
- return this._handleAgentAnswer(json)
416
+ return this._handleAgentAnswer(json, agent)
443
417
  }
444
418
 
445
419
  /**
@@ -471,41 +445,43 @@ class VrpcClient extends EventEmitter {
471
445
  const json = {
472
446
  c: className,
473
447
  f: functionName,
448
+ a: [],
474
449
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
475
450
  s: this._vrpcClientId,
476
- a: [],
477
451
  v: VRPC_PROTOCOL_VERSION
478
452
  }
479
453
  if (agent === '*') {
480
454
  const result = []
481
455
  const agents = this.getAvailableAgents()
482
- for (const x of agents) {
483
- const topic = `${this._domain}/${x}/${className}/__static__/__callAll__`
484
- const wrapped = this._wrapArguments(
485
- `${x}-${className}`,
456
+ for (const agent of agents) {
457
+ const topic = `${this._domain}/${agent}/${className}/__static__/__callAll__`
458
+ const wrapped = this._wrapArguments({
459
+ agent,
460
+ className,
486
461
  functionName,
487
- ...args
488
- )
462
+ args
463
+ })
489
464
  if (!wrapped) continue // using cache
490
465
  json.a = [functionName, ...wrapped]
491
- await this._waitUntilClassIsOnline(x, className)
466
+ await this._waitUntilClassIsOnline(agent, className)
492
467
  this._mqttPublish(topic, JSON.stringify(json))
493
- const tmp = await this._handleAgentAnswer(json)
468
+ const tmp = await this._handleAgentAnswer(json, agent)
494
469
  result.push(...tmp)
495
470
  }
496
471
  return result
497
472
  }
498
473
  const topic = `${this._domain}/${agent}/${className}/__static__/__callAll__`
499
- const wrapped = this._wrapArguments(
500
- `${agent}-${className}`,
474
+ const wrapped = this._wrapArguments({
475
+ agent,
476
+ className,
501
477
  functionName,
502
- ...args
503
- )
478
+ args
479
+ })
504
480
  if (!wrapped) return // using cache
505
481
  json.a = [functionName, ...wrapped]
506
482
  await this._waitUntilClassIsOnline(agent, className)
507
483
  this._mqttPublish(topic, JSON.stringify(json))
508
- return this._handleAgentAnswer(json)
484
+ return this._handleAgentAnswer(json, agent)
509
485
  }
510
486
 
511
487
  /**
@@ -535,19 +511,7 @@ class VrpcClient extends EventEmitter {
535
511
  * @param {Boolean} [options.mustBeOnline=true] Only retrieve currently online agents
536
512
  * @returns {Array} Array of agent names.
537
513
  */
538
- getAvailableAgents (options = {}) {
539
- /// DEPRECATION INFORMATION AND WORKAROUND - TO BE REMOVED IN 3.x
540
- // `options = {}` was `domain = this._domain` before
541
- if (typeof options === 'string') {
542
- this._indicateDeprecation(
543
- 'VrpcClient.getAvailableAgents(): ' +
544
- 'Providing a domain here has no effect and will be an error in future.'
545
- )
546
- options = { mustBeOnline: true }
547
- }
548
- ///
549
-
550
- const { mustBeOnline = true } = options
514
+ getAvailableAgents ({ mustBeOnline = true } = {}) {
551
515
  const agents = []
552
516
  if (mustBeOnline) {
553
517
  // loop agents
@@ -568,34 +532,7 @@ class VrpcClient extends EventEmitter {
568
532
  * @param {Boolean} [options.mustBeOnline=true] Only retrieve currently online classes
569
533
  * @returns {Array} Array of class names.
570
534
  */
571
- getAvailableClasses (
572
- options = { agent: this._agent, mustBeOnline: true },
573
- domain
574
- ) {
575
- /// DEPRECATION INFORMATION AND WORKAROUND - TO BE REMOVED IN 3.x
576
- let agent = this._agent
577
- let mustBeOnline = true
578
- // `options = {}` was `agent = this._agent` before
579
- if (typeof options === 'string') {
580
- this._indicateDeprecation(
581
- 'VrpcClient.getAvailableClasses(): ' +
582
- 'setting the agent as string argument is deprecated.`' +
583
- 'Use `{ agent: <myAgent> }` instead.'
584
- )
585
- agent = options
586
- } else {
587
- agent = options.agent
588
- mustBeOnline = options.mustBeOnline
589
- }
590
- // was `domain = this._domain` before
591
- if (domain) {
592
- this._indicateDeprecation(
593
- 'VrpcClient.getAvailableClasses(): ' +
594
- 'Providing a domain here has no effect and will be an error in future.'
595
- )
596
- }
597
- ///
598
-
535
+ getAvailableClasses ({ agent = this._agent, mustBeOnline = true } = {}) {
599
536
  if (agent === '*') throw new Error('Agent must be specified')
600
537
  if (!this._agents[agent]) return []
601
538
  const { classes, status } = this._agents[agent]
@@ -604,43 +541,19 @@ class VrpcClient extends EventEmitter {
604
541
  }
605
542
 
606
543
  /**
607
- * Retrieves all (named) instances on specific class and agent.
544
+ * Retrieves all (shared) instances on specific class and agent.
608
545
  *
609
- * @param {String} className Class name
610
546
  * @param {Object} [options]
547
+ * @param {String} options.className Class name
611
548
  * @param {String} [options.agent] Agent name. If not provided class default is used
612
549
  * @param {Boolean} [options.mustBeOnline=true] Only retrieve currently online classes
613
550
  * @returns {Array} Array of instance names
614
551
  */
615
- getAvailableInstances (
552
+ getAvailableInstances ({
616
553
  className,
617
- options = { agent: this._agent, mustBeOnline: true },
618
- domain
619
- ) {
620
- /// DEPRECATION INFORMATION AND WORKAROUND - TO BE REMOVED IN 3.x
621
- let agent = this._agent
622
- let mustBeOnline = true
623
- // `options = {}` was `agent = this._agent` before
624
- if (typeof options === 'string') {
625
- this._indicateDeprecation(
626
- 'VrpcClient.getAvailableInstances(): ' +
627
- 'Setting the agent as string argument is deprecated.`' +
628
- 'Use `{ agent: <myAgent> }` instead.'
629
- )
630
- agent = options
631
- } else {
632
- agent = options.agent
633
- mustBeOnline = options.mustBeOnline
634
- }
635
- // was `domain = this._domain` before
636
- if (domain) {
637
- this._indicateDeprecation(
638
- 'VrpcClient.getAvailableInstances(): ' +
639
- 'Providing a domain here has no effect and will be an error in future.'
640
- )
641
- }
642
- ///
643
-
554
+ agent = this._agent,
555
+ mustBeOnline = true
556
+ } = {}) {
644
557
  if (agent === '*') throw new Error('Agent must be specified')
645
558
  if (!this._agents[agent]) return []
646
559
  const { classes, status } = this._agents[agent]
@@ -651,41 +564,17 @@ class VrpcClient extends EventEmitter {
651
564
  /**
652
565
  * Retrieves all member functions of specific class and agent.
653
566
  *
654
- * @param {String} className Class name
655
567
  * @param {Object} [options]
568
+ * @param {String} options.className Class name
656
569
  * @param {String} [options.agent] Agent name. If not provided class default is used
657
570
  * @param {Boolean} [options.mustBeOnline=true] Only retrieve currently online classes
658
571
  * @returns {Array} Array of member function names
659
572
  */
660
- getAvailableMemberFunctions (
573
+ getAvailableMemberFunctions ({
661
574
  className,
662
- options = { agent: this._agent, mustBeOnline: true },
663
- domain
664
- ) {
665
- /// DEPRECATION INFORMATION AND WORKAROUND - TO BE REMOVED IN 3.x
666
- let agent = this._agent
667
- let mustBeOnline = true
668
- // `options = {}` was `agent = this._agent` before
669
- if (typeof options === 'string') {
670
- this._indicateDeprecation(
671
- 'VrpcClient.getAvailableMemberFunctions(): ' +
672
- 'Setting the agent as string argument is deprecated.`' +
673
- 'Use `{ agent: <myAgent> }` instead.'
674
- )
675
- agent = options
676
- } else {
677
- agent = options.agent
678
- mustBeOnline = options.mustBeOnline
679
- }
680
- // was `domain = this._domain` before
681
- if (domain) {
682
- this._indicateDeprecation(
683
- 'VrpcClient.getAvailableMemberFunctions(): ' +
684
- 'Providing a domain here has no effect and will be an error in future.'
685
- )
686
- }
687
- ///
688
-
575
+ agent = this._agent,
576
+ mustBeOnline = true
577
+ } = {}) {
689
578
  if (agent === '*') throw new Error('Agent must be specified')
690
579
  if (!this._agents[agent]) return []
691
580
  const { classes, status } = this._agents[agent]
@@ -698,41 +587,17 @@ class VrpcClient extends EventEmitter {
698
587
  /**
699
588
  * Retrieves all static functions of specific class and agent.
700
589
  *
701
- * @param {String} className Class name
702
590
  * @param {Object} [options]
591
+ * @param {String} options.className Class name
703
592
  * @param {String} [options.agent] Agent name. If not provided class default is used
704
593
  * @param {Boolean} [options.mustBeOnline=true] Only retrieve currently online classes
705
594
  * @returns {Array} Array of static function names
706
595
  */
707
- getAvailableStaticFunctions (
596
+ getAvailableStaticFunctions ({
708
597
  className,
709
- options = { agent: this._agent, mustBeOnline: true },
710
- domain
711
- ) {
712
- /// DEPRECATION INFORMATION AND WORKAROUND - TO BE REMOVED IN 3.x
713
- let agent = this._agent
714
- let mustBeOnline = true
715
- // `options = {}` was `agent = this._agent` before
716
- if (typeof options === 'string') {
717
- this._indicateDeprecation(
718
- 'VrpcClient.getAvailableStaticFunctions(): ' +
719
- 'Setting the agent as string argument is deprecated.`' +
720
- 'Use `{ agent: <myAgent> }` instead.'
721
- )
722
- agent = options
723
- } else {
724
- agent = options.agent
725
- mustBeOnline = options.mustBeOnline
726
- }
727
- // was `domain = this._domain` before
728
- if (domain) {
729
- this._indicateDeprecation(
730
- 'VrpcClient.getAvailableStaticFunctions(): ' +
731
- 'Providing a domain here has no effect and will be an error in future.'
732
- )
733
- }
734
- ///
735
-
598
+ agent = this._agent,
599
+ mustBeOnline = true
600
+ } = {}) {
736
601
  if (agent === '*') throw new Error('Agent must be specified')
737
602
  if (!this._agents[agent]) return []
738
603
  const { classes, status } = this._agents[agent]
@@ -810,7 +675,7 @@ class VrpcClient extends EventEmitter {
810
675
  .createHash('md5')
811
676
  .update(clientInfo)
812
677
  .digest('hex')
813
- .substr(0, 12)
678
+ .substring(0, 12)
814
679
  return `vc3${this._instance}${md5}` // 3 + 8 + 12 = 23 (max clientId)
815
680
  }
816
681
 
@@ -939,34 +804,120 @@ class VrpcClient extends EventEmitter {
939
804
  // Remove overloads
940
805
  const uniqueFuncs = new Set(functions)
941
806
  // Build proxy
942
- uniqueFuncs.forEach(name => {
943
- proxy[name] = async (...args) => {
944
- const wrapped = this._wrapArguments(proxyId, name, ...args)
945
- if (!wrapped) return // Skipping remote call -> handled locally
807
+ uniqueFuncs.forEach(functionName => {
808
+ // as we are messing with Node.js' event system we have to intercept here
809
+ if (functionName === 'removeAllListeners') {
810
+ proxy[functionName] = async eventName => {
811
+ if (!eventName) {
812
+ throw new Error('VRPC does not support removing all listeners')
813
+ }
814
+ const topic = `${this._domain}/${agent}/${className}/${instance}:${eventName}`
815
+ const id = `__e__${topic}`
816
+ if (this._cachedSubscriptions[topic]) {
817
+ this._eventEmitter.removeAllListeners(id)
818
+ this._mqttUnsubscribe(topic)
819
+ delete this._cachedSubscriptions[topic]
820
+ }
821
+ try {
822
+ const json = {
823
+ c: instance,
824
+ f: functionName,
825
+ a: [eventName],
826
+ i: `${this._instance}-${this._invokeId++ %
827
+ Number.MAX_SAFE_INTEGER}`,
828
+ s: this._vrpcClientId,
829
+ v: VRPC_PROTOCOL_VERSION
830
+ }
831
+ this._mqttPublish(
832
+ `${targetTopic}/${functionName}`,
833
+ JSON.stringify(json)
834
+ )
835
+ return this._handleAgentAnswer(json, agent)
836
+ } catch (err) {
837
+ throw new Error(
838
+ `Could not remotely call "${functionName}" because: ${err.message}`
839
+ )
840
+ }
841
+ }
842
+ return
843
+ }
844
+ proxy[functionName] = async (...args) => {
845
+ const wrapped = this._wrapArguments({
846
+ agent,
847
+ className,
848
+ instance,
849
+ proxyId,
850
+ functionName,
851
+ args
852
+ })
853
+ if (!wrapped) return true // Skipping remote call -> handled locally
946
854
  try {
947
855
  const json = {
948
856
  c: instance,
949
- f: name,
950
- i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
951
- s: this._vrpcClientId,
857
+ f: functionName,
952
858
  a: wrapped,
859
+ i: `${this._instance}-${this._invokeId++ %
860
+ Number.MAX_SAFE_INTEGER}`,
861
+ s: this._vrpcClientId,
953
862
  v: VRPC_PROTOCOL_VERSION
954
863
  }
955
- this._mqttPublish(`${targetTopic}/${name}`, JSON.stringify(json))
956
- return this._handleAgentAnswer(json)
864
+ this._mqttPublish(
865
+ `${targetTopic}/${functionName}`,
866
+ JSON.stringify(json)
867
+ )
868
+ return this._handleAgentAnswer(json, agent)
957
869
  } catch (err) {
958
870
  throw new Error(
959
- `Could not remotely call "${name}" because: ${err.message}`
871
+ `Could not remotely call "${functionName}" because: ${err.message}`
960
872
  )
961
873
  }
962
874
  }
963
875
  })
876
+ if (!uniqueFuncs.has('vrpcOn') && !uniqueFuncs.has('vrpcOff')) {
877
+ proxy.vrpcOn = async (functionName, ...args) => {
878
+ if (!uniqueFuncs.has(functionName)) throw new Error('Bad magic')
879
+ const wrapped = this._wrapArguments({
880
+ agent,
881
+ className,
882
+ instance,
883
+ proxyId,
884
+ functionName: 'vrpcOn',
885
+ args: [functionName, ...args]
886
+ })
887
+ if (!wrapped) return // Skipping remote call -> handled locally
888
+ try {
889
+ const json = {
890
+ c: instance,
891
+ f: functionName,
892
+ a: wrapped.slice(1), // first argument was remote function name
893
+ i: `${this._instance}-${this._invokeId++ %
894
+ Number.MAX_SAFE_INTEGER}`,
895
+ s: this._vrpcClientId,
896
+ v: VRPC_PROTOCOL_VERSION
897
+ }
898
+ this._mqttPublish(
899
+ `${targetTopic}/${functionName}`,
900
+ JSON.stringify(json)
901
+ )
902
+ return this._handleAgentAnswer(json, agent)
903
+ } catch (err) {
904
+ throw new Error(
905
+ `Could not remotely call "${functionName}" because: ${err.message}`
906
+ )
907
+ }
908
+ }
909
+ proxy.vrpcOff = functionName => {
910
+ const topic = `${this._domain}/${agent}/${className}/${instance}-${functionName}`
911
+ const id = `__e__${topic}`
912
+ this._eventEmitter.removeAllListeners(id)
913
+ }
914
+ }
964
915
  return proxy
965
916
  }
966
917
 
967
- async _handleAgentAnswer ({ i, c, f }) {
918
+ async _handleAgentAnswer ({ i, c, f }, agent) {
968
919
  return new Promise((resolve, reject) => {
969
- const msg = `Function call "${c}::${f}()" timed out (> ${this._timeout} ms)`
920
+ const msg = `Function call "${c}::${f}()" on agent "${agent}" timed out (> ${this._timeout} ms)`
970
921
  const timer = setTimeout(() => {
971
922
  this._eventEmitter.removeAllListeners(i)
972
923
  reject(new Error(msg))
@@ -974,13 +925,14 @@ class VrpcClient extends EventEmitter {
974
925
  this._eventEmitter.once(i, data => {
975
926
  clearTimeout(timer)
976
927
  if (data.e) {
977
- reject(new Error(data.e))
928
+ reject(new Error(`[vrpc ${agent}-${c}-${f}]: ${data.e}`))
978
929
  } else {
979
930
  const ret = data.r
980
931
  // Handle functions returning a promise
981
- if (typeof ret === 'string' && ret.substr(0, 5) === '__p__') {
932
+ if (typeof ret === 'string' && ret.substring(0, 5) === '__p__') {
982
933
  this._eventEmitter.once(ret, promiseData => {
983
- if (promiseData.e) reject(new Error(promiseData.e))
934
+ // TODO improve error message here, like above
935
+ if (promiseData.e) reject(new Error(`[vrpc ${agent}-${c}-${f}]: ${promiseData.e}`))
984
936
  else resolve(promiseData.r)
985
937
  })
986
938
  } else {
@@ -1011,79 +963,128 @@ class VrpcClient extends EventEmitter {
1011
963
  })
1012
964
  }
1013
965
 
1014
- _wrapArguments (remoteId, functionName, ...args) {
966
+ _wrapArguments ({ agent, className, instance, proxyId, functionName, args }) {
1015
967
  const wrapped = []
1016
- let isHandledLocally = false
1017
- args.forEach((x, i) => {
968
+ for (const [i, x] of args.entries()) {
1018
969
  // Check whether provided argument is a function
1019
970
  if (this._isFunction(x)) {
1020
- // Check special case of an event emitter registration
1021
- // We test three conditions:
1022
- // 1) functionName must be "on"
1023
- // 2) callback is second argument
1024
- // 3) first argument was string
1025
- if (functionName === 'on' && i === 1 && typeof args[0] === 'string') {
1026
- const id = this._addEventSubscription(remoteId, args[0], x)
1027
- if (!id) isHandledLocally = true
971
+ const callback = x
972
+ if (
973
+ functionName === 'vrpcOn' ||
974
+ (functionName === 'on' && i === 1 && typeof args[0] === 'string')
975
+ ) {
976
+ // special case of an (remote-)event emitter registration
977
+ // we test three conditions:
978
+ // 1) functionName must be "on"
979
+ // 2) callback is second argument
980
+ // 3) first argument was string
981
+ const event = args[0]
982
+ const id = this._onRemoteEvent({
983
+ agent,
984
+ className,
985
+ instance,
986
+ event,
987
+ callback
988
+ })
989
+ // indicate that no network call is required
990
+ if (id === null) return null
1028
991
  wrapped.push(id)
992
+ continue
1029
993
  } else if (
1030
994
  (functionName === 'off' || functionName === 'removeListener') &&
1031
995
  i === 1 &&
1032
996
  typeof args[0] === 'string'
1033
997
  ) {
1034
- const id = this._removeEventSubscription(remoteId, args[0], x)
1035
- if (!id) isHandledLocally = true
998
+ const event = args[0]
999
+ const id = this._offRemoteEvent({
1000
+ agent,
1001
+ className,
1002
+ instance,
1003
+ event,
1004
+ callback
1005
+ })
1006
+ // indicate that no network call is required
1007
+ if (id === null) return null
1036
1008
  wrapped.push(id)
1037
- // Regular function callback
1009
+ continue
1038
1010
  } else {
1011
+ // Regular function callback (can be static or member function)
1012
+ const remoteId = proxyId || `${agent}-${className}`
1039
1013
  const id = `__f__${remoteId}-${functionName}-${i}-${this._invokeId++ %
1040
1014
  Number.MAX_SAFE_INTEGER}`
1041
1015
  wrapped.push(id)
1042
- this._eventEmitter.once(id, ({ a }) => {
1043
- x.apply(null, a) // This is the actual function call
1044
- })
1016
+ this._eventEmitter.once(id, ({ a }) => x.apply(null, a))
1017
+ continue
1045
1018
  }
1046
- } else if (this._isEmitter(x)) {
1019
+ }
1020
+ // special case of an EventEmitter provided as argument
1021
+ if (this._isEmitter(x)) {
1047
1022
  const { emitter, event } = x
1048
- const id = `__f__${remoteId}-${functionName}-${i}-${event}`
1049
- wrapped.push(id)
1050
- this._eventEmitter.on(id, ({ a }) => {
1051
- emitter.emit(event, ...a)
1023
+ const id = this._onRemoteEvent({
1024
+ agent,
1025
+ className,
1026
+ instance,
1027
+ event,
1028
+ callback: (...args) => emitter.emit(event, ...args)
1052
1029
  })
1053
- } else {
1054
- wrapped.push(x)
1030
+ // indicate that no network call is required
1031
+ if (id === null) return null
1032
+ wrapped.push(id)
1033
+ continue
1055
1034
  }
1056
- })
1057
- if (isHandledLocally) return null
1035
+ // default behavior is to not touch
1036
+ wrapped.push(x)
1037
+ }
1058
1038
  return wrapped
1059
1039
  }
1060
1040
 
1061
- _addEventSubscription (remoteId, event, callback) {
1062
- const id = `__f__${remoteId}-${event}`
1063
- if (this._cachedSubscriptions.has(id)) {
1064
- this._cachedSubscriptions.get(id).on(id, callback)
1065
- return
1041
+ _onRemoteEvent ({ agent, className, instance, event, callback }) {
1042
+ // the topic on which any remote event will be published to
1043
+ const topic = instance
1044
+ ? `${this._domain}/${agent}/${className}/${instance}:${event}`
1045
+ : `${this._domain}/${agent}/${className}/${event}`
1046
+ // prepare a special id for the agent to know that events should be
1047
+ // published to the provided topic after the prefix
1048
+ const id = `__e__${topic}`
1049
+ // call our proxy callback when remote events were received
1050
+ const handler = ({ a }) => callback.apply(null, a)
1051
+ this._eventEmitter.on(id, handler)
1052
+ if (!this._cachedSubscriptions[topic]) {
1053
+ // when not yet subscribed to this topic do it now
1054
+ this._mqttSubscribe(topic)
1055
+ this._cachedSubscriptions[topic] = [{ callback, handler }]
1056
+ return id
1066
1057
  }
1067
- const emitter = new EventEmitter()
1068
- emitter.on(id, callback)
1069
- this._cachedSubscriptions.set(id, emitter)
1070
- this._eventEmitter.on(id, ({ a }) => {
1071
- emitter.emit(id, ...a)
1072
- })
1073
- return id
1058
+ // otherwise just add the new callback-handler pair
1059
+ this._cachedSubscriptions[topic].push({ callback, handler })
1060
+ // indicate that no network call is required
1061
+ return null
1074
1062
  }
1075
1063
 
1076
- _removeEventSubscription (remoteId, event, callback) {
1077
- const id = `__f__${remoteId}-${event}`
1078
- if (this._cachedSubscriptions.has(id)) {
1079
- const emitter = this._cachedSubscriptions.get(id)
1080
- emitter.removeListener(id, callback)
1081
- if (emitter.listenerCount(id) === 0) {
1082
- this._cachedSubscriptions.delete(id)
1083
- this._eventEmitter.removeAllListeners(id)
1064
+ _offRemoteEvent ({ agent, className, instance, event, callback }) {
1065
+ const topic = instance
1066
+ ? `${this._domain}/${agent}/${className}/${instance}:${event}`
1067
+ : `${this._domain}/${agent}/${className}/${event}`
1068
+ const id = `__e__${topic}`
1069
+ if (this._cachedSubscriptions[topic]) {
1070
+ const index = this._cachedSubscriptions[topic].findIndex(x => x.callback === callback)
1071
+ if (index === -1) {
1072
+ this._log.warn(`Failed removing event listener ${id}`)
1073
+ return null
1074
+ }
1075
+ const [{ handler }] = this._cachedSubscriptions[topic].splice(index, 1)
1076
+ this._eventEmitter.removeListener(id, handler)
1077
+ if (this._cachedSubscriptions[topic].length === 0) {
1078
+ this._mqttUnsubscribe(topic)
1079
+ delete this._cachedSubscriptions[topic]
1084
1080
  return id
1085
1081
  }
1082
+ // indicate that no network call is required
1083
+ return null
1084
+ } else {
1085
+ this._log.warn(`Can not unsubscribe from non-existing event: ${event}`)
1086
1086
  }
1087
+ return id
1087
1088
  }
1088
1089
 
1089
1090
  _stripSignature (method) {
@@ -1111,7 +1112,7 @@ class VrpcClient extends EventEmitter {
1111
1112
  )
1112
1113
  }
1113
1114
 
1114
- async _getInstanceData (instance, options = {}) {
1115
+ async _getInstanceData (instance, options) {
1115
1116
  const instanceData = this._getInstanceFromCache(instance, options)
1116
1117
  if (!instanceData) {
1117
1118
  if (options.noWait) {
@@ -1123,6 +1124,21 @@ class VrpcClient extends EventEmitter {
1123
1124
  }
1124
1125
 
1125
1126
  _getInstanceFromCache (instance, options = {}) {
1127
+ // when no agent is specified, but an explicit class default exists give
1128
+ // such instances priority in being found
1129
+ if (!options.agent && this._agent !== '*') {
1130
+ const { classes, status } = this._agents[this._agent]
1131
+ if (!classes || status === 'offline') {
1132
+ for (const className in classes) {
1133
+ if (options.className && className !== options.className) continue
1134
+ const { instances } = classes[className]
1135
+ if (!instances) continue
1136
+ if (instances.includes(instance)) {
1137
+ return { agent, className, instance }
1138
+ }
1139
+ }
1140
+ }
1141
+ }
1126
1142
  // loop agents
1127
1143
  for (const agent in this._agents) {
1128
1144
  if (options.agent && agent !== options.agent) continue
@@ -1175,7 +1191,7 @@ class VrpcClient extends EventEmitter {
1175
1191
  * @param {String} info.domain - Domain name
1176
1192
  * @param {String} info.agent - Agent name
1177
1193
  * @param {String} info.className - Class name
1178
- * @param {Array.<String>} info.instances - Array of named instances
1194
+ * @param {Array.<String>} info.instances - Array of instances
1179
1195
  * @param {Array.<String>} info.memberFunctions - Array of member functions
1180
1196
  * @param {Array.<String>} info.staticFunctions - Array of static functions
1181
1197
  * @param {MetaData} info.meta - Object associating further information to functions