vrpc 3.0.0-alpha.1 → 3.0.1

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)
@@ -336,37 +341,15 @@ class VrpcClient extends EventEmitter {
336
341
  *
337
342
  * @param {String} instance The instance to be retrieved
338
343
  * @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
344
+ * @param {String} [options.className] Name of the instance's class
345
+ * @param {String} [options.agent] Agent name. If not provided class default is used as priority hit
346
+ * @param {bool} [options.noWait=false] If true immediately fail if instance could not be found in local cache
342
347
  * @returns {Promise<Proxy>} Proxy object reflecting the remotely existing instance
343
348
  */
344
- async getInstance (instance, options) {
349
+ async getInstance (instance, options = {}) {
345
350
  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
- }
351
+ const { agent, className } = await this._getInstanceData(instance, options)
352
+ return this._createProxy(agent, className, instance)
370
353
  }
371
354
 
372
355
  /**
@@ -378,36 +361,23 @@ class VrpcClient extends EventEmitter {
378
361
  * @param {String} instance The instance to be deleted
379
362
  * @param {Object} [options] Explicitly define agent and class
380
363
  * @param {String} options.className Name of the instance's class
381
- * @param {String} options.agent Agent name. If not provided class default is used
364
+ * @param {String} options.agent Agent name. If not provided class default is used as priority hit
382
365
  * @returns {Promise<Boolean>} true if successful, false otherwise
383
366
  */
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
367
+ async delete (instance, options = {}) {
368
+ const { agent, className } = await this._getInstanceData(instance, options)
399
369
  const json = {
400
370
  c: className,
401
371
  f: '__delete__',
372
+ a: [instance],
402
373
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
403
374
  s: `${this._domain}/${os.hostname()}/${this._instance}`,
404
- a: [instanceString],
405
375
  v: VRPC_PROTOCOL_VERSION
406
376
  }
407
377
  const topic = `${this._domain}/${agent}/${className}/__static__/__delete__`
408
378
  this._mqttPublish(topic, JSON.stringify(json))
409
379
  if (this._proxies[instance]) delete this._proxies[instance]
410
- return this._handleAgentAnswer(json)
380
+ return this._handleAgentAnswer(json, agent)
411
381
  }
412
382
 
413
383
  /**
@@ -426,20 +396,25 @@ class VrpcClient extends EventEmitter {
426
396
  args = [],
427
397
  agent = this._agent
428
398
  } = {}) {
429
- const wrapped = this._wrapArguments(className, functionName, ...args)
399
+ const wrapped = this._wrapArguments({
400
+ agent,
401
+ className,
402
+ functionName,
403
+ args
404
+ })
430
405
  if (!wrapped) return // Skipping remote call -> handled locally
431
406
  const json = {
432
407
  c: className,
433
408
  f: functionName,
409
+ a: wrapped,
434
410
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
435
411
  s: this._vrpcClientId,
436
- a: wrapped,
437
412
  v: VRPC_PROTOCOL_VERSION
438
413
  }
439
414
  const topic = `${this._domain}/${agent}/${className}/__static__/${functionName}`
440
415
  await this._waitUntilClassIsOnline(agent, className)
441
416
  this._mqttPublish(topic, JSON.stringify(json))
442
- return this._handleAgentAnswer(json)
417
+ return this._handleAgentAnswer(json, agent)
443
418
  }
444
419
 
445
420
  /**
@@ -471,41 +446,43 @@ class VrpcClient extends EventEmitter {
471
446
  const json = {
472
447
  c: className,
473
448
  f: functionName,
449
+ a: [],
474
450
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
475
451
  s: this._vrpcClientId,
476
- a: [],
477
452
  v: VRPC_PROTOCOL_VERSION
478
453
  }
479
454
  if (agent === '*') {
480
455
  const result = []
481
456
  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}`,
457
+ for (const agent of agents) {
458
+ const topic = `${this._domain}/${agent}/${className}/__static__/__callAll__`
459
+ const wrapped = this._wrapArguments({
460
+ agent,
461
+ className,
486
462
  functionName,
487
- ...args
488
- )
463
+ args
464
+ })
489
465
  if (!wrapped) continue // using cache
490
466
  json.a = [functionName, ...wrapped]
491
- await this._waitUntilClassIsOnline(x, className)
467
+ await this._waitUntilClassIsOnline(agent, className)
492
468
  this._mqttPublish(topic, JSON.stringify(json))
493
- const tmp = await this._handleAgentAnswer(json)
469
+ const tmp = await this._handleAgentAnswer(json, agent)
494
470
  result.push(...tmp)
495
471
  }
496
472
  return result
497
473
  }
498
474
  const topic = `${this._domain}/${agent}/${className}/__static__/__callAll__`
499
- const wrapped = this._wrapArguments(
500
- `${agent}-${className}`,
475
+ const wrapped = this._wrapArguments({
476
+ agent,
477
+ className,
501
478
  functionName,
502
- ...args
503
- )
479
+ args
480
+ })
504
481
  if (!wrapped) return // using cache
505
482
  json.a = [functionName, ...wrapped]
506
483
  await this._waitUntilClassIsOnline(agent, className)
507
484
  this._mqttPublish(topic, JSON.stringify(json))
508
- return this._handleAgentAnswer(json)
485
+ return this._handleAgentAnswer(json, agent)
509
486
  }
510
487
 
511
488
  /**
@@ -535,19 +512,7 @@ class VrpcClient extends EventEmitter {
535
512
  * @param {Boolean} [options.mustBeOnline=true] Only retrieve currently online agents
536
513
  * @returns {Array} Array of agent names.
537
514
  */
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
515
+ getAvailableAgents ({ mustBeOnline = true } = {}) {
551
516
  const agents = []
552
517
  if (mustBeOnline) {
553
518
  // loop agents
@@ -568,34 +533,7 @@ class VrpcClient extends EventEmitter {
568
533
  * @param {Boolean} [options.mustBeOnline=true] Only retrieve currently online classes
569
534
  * @returns {Array} Array of class names.
570
535
  */
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
-
536
+ getAvailableClasses ({ agent = this._agent, mustBeOnline = true } = {}) {
599
537
  if (agent === '*') throw new Error('Agent must be specified')
600
538
  if (!this._agents[agent]) return []
601
539
  const { classes, status } = this._agents[agent]
@@ -604,43 +542,19 @@ class VrpcClient extends EventEmitter {
604
542
  }
605
543
 
606
544
  /**
607
- * Retrieves all (named) instances on specific class and agent.
545
+ * Retrieves all (shared) instances on specific class and agent.
608
546
  *
609
- * @param {String} className Class name
610
547
  * @param {Object} [options]
548
+ * @param {String} options.className Class name
611
549
  * @param {String} [options.agent] Agent name. If not provided class default is used
612
550
  * @param {Boolean} [options.mustBeOnline=true] Only retrieve currently online classes
613
551
  * @returns {Array} Array of instance names
614
552
  */
615
- getAvailableInstances (
553
+ getAvailableInstances ({
616
554
  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
-
555
+ agent = this._agent,
556
+ mustBeOnline = true
557
+ } = {}) {
644
558
  if (agent === '*') throw new Error('Agent must be specified')
645
559
  if (!this._agents[agent]) return []
646
560
  const { classes, status } = this._agents[agent]
@@ -651,41 +565,17 @@ class VrpcClient extends EventEmitter {
651
565
  /**
652
566
  * Retrieves all member functions of specific class and agent.
653
567
  *
654
- * @param {String} className Class name
655
568
  * @param {Object} [options]
569
+ * @param {String} options.className Class name
656
570
  * @param {String} [options.agent] Agent name. If not provided class default is used
657
571
  * @param {Boolean} [options.mustBeOnline=true] Only retrieve currently online classes
658
572
  * @returns {Array} Array of member function names
659
573
  */
660
- getAvailableMemberFunctions (
574
+ getAvailableMemberFunctions ({
661
575
  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
-
576
+ agent = this._agent,
577
+ mustBeOnline = true
578
+ } = {}) {
689
579
  if (agent === '*') throw new Error('Agent must be specified')
690
580
  if (!this._agents[agent]) return []
691
581
  const { classes, status } = this._agents[agent]
@@ -698,41 +588,17 @@ class VrpcClient extends EventEmitter {
698
588
  /**
699
589
  * Retrieves all static functions of specific class and agent.
700
590
  *
701
- * @param {String} className Class name
702
591
  * @param {Object} [options]
592
+ * @param {String} options.className Class name
703
593
  * @param {String} [options.agent] Agent name. If not provided class default is used
704
594
  * @param {Boolean} [options.mustBeOnline=true] Only retrieve currently online classes
705
595
  * @returns {Array} Array of static function names
706
596
  */
707
- getAvailableStaticFunctions (
597
+ getAvailableStaticFunctions ({
708
598
  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
-
599
+ agent = this._agent,
600
+ mustBeOnline = true
601
+ } = {}) {
736
602
  if (agent === '*') throw new Error('Agent must be specified')
737
603
  if (!this._agents[agent]) return []
738
604
  const { classes, status } = this._agents[agent]
@@ -810,9 +676,8 @@ class VrpcClient extends EventEmitter {
810
676
  .createHash('md5')
811
677
  .update(clientInfo)
812
678
  .digest('hex')
813
- .substr(0, 13)
814
- // FIXME (3.x): use vrpcp -> vrpcc
815
- return `vc${this._instance}${md5}` // 2 + 8 + 13 = 23 (max clientId)
679
+ .substring(0, 12)
680
+ return `vc3${this._instance}${md5}` // 3 + 8 + 12 = 23 (max clientId)
816
681
  }
817
682
 
818
683
  _createVrpcClientId () {
@@ -940,34 +805,120 @@ class VrpcClient extends EventEmitter {
940
805
  // Remove overloads
941
806
  const uniqueFuncs = new Set(functions)
942
807
  // Build proxy
943
- uniqueFuncs.forEach(name => {
944
- proxy[name] = async (...args) => {
945
- const wrapped = this._wrapArguments(proxyId, name, ...args)
946
- 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
947
855
  try {
948
856
  const json = {
949
857
  c: instance,
950
- f: name,
951
- i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
952
- s: this._vrpcClientId,
858
+ f: functionName,
953
859
  a: wrapped,
860
+ i: `${this._instance}-${this._invokeId++ %
861
+ Number.MAX_SAFE_INTEGER}`,
862
+ s: this._vrpcClientId,
954
863
  v: VRPC_PROTOCOL_VERSION
955
864
  }
956
- this._mqttPublish(`${targetTopic}/${name}`, JSON.stringify(json))
957
- return this._handleAgentAnswer(json)
865
+ this._mqttPublish(
866
+ `${targetTopic}/${functionName}`,
867
+ JSON.stringify(json)
868
+ )
869
+ return this._handleAgentAnswer(json, agent)
958
870
  } catch (err) {
959
871
  throw new Error(
960
- `Could not remotely call "${name}" because: ${err.message}`
872
+ `Could not remotely call "${functionName}" because: ${err.message}`
961
873
  )
962
874
  }
963
875
  }
964
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
+ }
965
916
  return proxy
966
917
  }
967
918
 
968
- async _handleAgentAnswer ({ i, c, f }) {
919
+ async _handleAgentAnswer ({ i, c, f }, agent) {
969
920
  return new Promise((resolve, reject) => {
970
- 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)`
971
922
  const timer = setTimeout(() => {
972
923
  this._eventEmitter.removeAllListeners(i)
973
924
  reject(new Error(msg))
@@ -975,13 +926,14 @@ class VrpcClient extends EventEmitter {
975
926
  this._eventEmitter.once(i, data => {
976
927
  clearTimeout(timer)
977
928
  if (data.e) {
978
- reject(new Error(data.e))
929
+ reject(new Error(`[vrpc ${agent}-${c}-${f}]: ${data.e}`))
979
930
  } else {
980
931
  const ret = data.r
981
932
  // Handle functions returning a promise
982
- if (typeof ret === 'string' && ret.substr(0, 5) === '__p__') {
933
+ if (typeof ret === 'string' && ret.substring(0, 5) === '__p__') {
983
934
  this._eventEmitter.once(ret, promiseData => {
984
- 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}`))
985
937
  else resolve(promiseData.r)
986
938
  })
987
939
  } else {
@@ -1012,79 +964,128 @@ class VrpcClient extends EventEmitter {
1012
964
  })
1013
965
  }
1014
966
 
1015
- _wrapArguments (remoteId, functionName, ...args) {
967
+ _wrapArguments ({ agent, className, instance, proxyId, functionName, args }) {
1016
968
  const wrapped = []
1017
- let isHandledLocally = false
1018
- args.forEach((x, i) => {
969
+ for (const [i, x] of args.entries()) {
1019
970
  // Check whether provided argument is a function
1020
971
  if (this._isFunction(x)) {
1021
- // Check special case of an event emitter registration
1022
- // We test three conditions:
1023
- // 1) functionName must be "on"
1024
- // 2) callback is second argument
1025
- // 3) first argument was string
1026
- if (functionName === 'on' && i === 1 && typeof args[0] === 'string') {
1027
- const id = this._addEventSubscription(remoteId, args[0], x)
1028
- 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
1029
992
  wrapped.push(id)
993
+ continue
1030
994
  } else if (
1031
995
  (functionName === 'off' || functionName === 'removeListener') &&
1032
996
  i === 1 &&
1033
997
  typeof args[0] === 'string'
1034
998
  ) {
1035
- const id = this._removeEventSubscription(remoteId, args[0], x)
1036
- 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
1037
1009
  wrapped.push(id)
1038
- // Regular function callback
1010
+ continue
1039
1011
  } else {
1012
+ // Regular function callback (can be static or member function)
1013
+ const remoteId = proxyId || `${agent}-${className}`
1040
1014
  const id = `__f__${remoteId}-${functionName}-${i}-${this._invokeId++ %
1041
1015
  Number.MAX_SAFE_INTEGER}`
1042
1016
  wrapped.push(id)
1043
- this._eventEmitter.once(id, ({ a }) => {
1044
- x.apply(null, a) // This is the actual function call
1045
- })
1017
+ this._eventEmitter.once(id, ({ a }) => x.apply(null, a))
1018
+ continue
1046
1019
  }
1047
- } else if (this._isEmitter(x)) {
1020
+ }
1021
+ // special case of an EventEmitter provided as argument
1022
+ if (this._isEmitter(x)) {
1048
1023
  const { emitter, event } = x
1049
- const id = `__f__${remoteId}-${functionName}-${i}-${event}`
1050
- wrapped.push(id)
1051
- this._eventEmitter.on(id, ({ a }) => {
1052
- emitter.emit(event, ...a)
1024
+ const id = this._onRemoteEvent({
1025
+ agent,
1026
+ className,
1027
+ instance,
1028
+ event,
1029
+ callback: (...args) => emitter.emit(event, ...args)
1053
1030
  })
1054
- } else {
1055
- wrapped.push(x)
1031
+ // indicate that no network call is required
1032
+ if (id === null) return null
1033
+ wrapped.push(id)
1034
+ continue
1056
1035
  }
1057
- })
1058
- if (isHandledLocally) return null
1036
+ // default behavior is to not touch
1037
+ wrapped.push(x)
1038
+ }
1059
1039
  return wrapped
1060
1040
  }
1061
1041
 
1062
- _addEventSubscription (remoteId, event, callback) {
1063
- const id = `__f__${remoteId}-${event}`
1064
- if (this._cachedSubscriptions.has(id)) {
1065
- this._cachedSubscriptions.get(id).on(id, callback)
1066
- 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
1067
1058
  }
1068
- const emitter = new EventEmitter()
1069
- emitter.on(id, callback)
1070
- this._cachedSubscriptions.set(id, emitter)
1071
- this._eventEmitter.on(id, ({ a }) => {
1072
- emitter.emit(id, ...a)
1073
- })
1074
- 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
1075
1063
  }
1076
1064
 
1077
- _removeEventSubscription (remoteId, event, callback) {
1078
- const id = `__f__${remoteId}-${event}`
1079
- if (this._cachedSubscriptions.has(id)) {
1080
- const emitter = this._cachedSubscriptions.get(id)
1081
- emitter.removeListener(id, callback)
1082
- if (emitter.listenerCount(id) === 0) {
1083
- this._cachedSubscriptions.delete(id)
1084
- 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]
1085
1081
  return id
1086
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}`)
1087
1087
  }
1088
+ return id
1088
1089
  }
1089
1090
 
1090
1091
  _stripSignature (method) {
@@ -1112,7 +1113,7 @@ class VrpcClient extends EventEmitter {
1112
1113
  )
1113
1114
  }
1114
1115
 
1115
- async _getInstanceData (instance, options = {}) {
1116
+ async _getInstanceData (instance, options) {
1116
1117
  const instanceData = this._getInstanceFromCache(instance, options)
1117
1118
  if (!instanceData) {
1118
1119
  if (options.noWait) {
@@ -1124,6 +1125,21 @@ class VrpcClient extends EventEmitter {
1124
1125
  }
1125
1126
 
1126
1127
  _getInstanceFromCache (instance, options = {}) {
1128
+ // when no agent is specified, but an explicit class default exists give
1129
+ // such instances priority in being found
1130
+ if (!options.agent && this._agent !== '*') {
1131
+ const { classes, status } = this._agents[this._agent]
1132
+ if (!classes || status === 'offline') {
1133
+ for (const className in classes) {
1134
+ if (options.className && className !== options.className) continue
1135
+ const { instances } = classes[className]
1136
+ if (!instances) continue
1137
+ if (instances.includes(instance)) {
1138
+ return { agent, className, instance }
1139
+ }
1140
+ }
1141
+ }
1142
+ }
1127
1143
  // loop agents
1128
1144
  for (const agent in this._agents) {
1129
1145
  if (options.agent && agent !== options.agent) continue
@@ -1176,7 +1192,7 @@ class VrpcClient extends EventEmitter {
1176
1192
  * @param {String} info.domain - Domain name
1177
1193
  * @param {String} info.agent - Agent name
1178
1194
  * @param {String} info.className - Class name
1179
- * @param {Array.<String>} info.instances - Array of named instances
1195
+ * @param {Array.<String>} info.instances - Array of instances
1180
1196
  * @param {Array.<String>} info.memberFunctions - Array of member functions
1181
1197
  * @param {Array.<String>} info.staticFunctions - Array of static functions
1182
1198
  * @param {MetaData} info.meta - Object associating further information to functions