vrpc 2.4.0 → 3.0.0-alpha.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.
package/vrpc/VrpcAgent.js CHANGED
@@ -6,13 +6,14 @@ const { ArgumentParser } = require('argparse')
6
6
  const VrpcAdapter = require('./VrpcAdapter')
7
7
  const EventEmitter = require('events')
8
8
 
9
+ const VRPC_PROTOCOL_VERSION = 3
10
+
9
11
  /**
10
12
  * Agent capable of making existing code available to remote control by clients.
11
13
  *
12
14
  * @extends EventEmitter
13
15
  */
14
16
  class VrpcAgent extends EventEmitter {
15
-
16
17
  /**
17
18
  * Constructs an agent by parsing command line arguments
18
19
  *
@@ -41,79 +42,47 @@ class VrpcAgent extends EventEmitter {
41
42
  add_help: true,
42
43
  description: 'VRPC Node.js Agent'
43
44
  })
44
- parser.add_argument(
45
- '-a',
46
- '--agent',
47
- {
48
- help: 'Agent name',
49
- default: agent || VrpcAgent._generateAgentName()
50
- }
51
- )
52
- parser.add_argument(
53
- '-d',
54
- '--domain',
55
- {
56
- help: 'Domain name',
57
- default: domain || 'public.vrpc'
58
- }
59
- )
60
- parser.add_argument(
61
- '-t',
62
- '--token',
63
- {
64
- help: 'access token',
65
- default: token
66
- }
67
- )
68
- parser.add_argument(
69
- '-b',
70
- '--broker',
71
- {
72
- help: 'Broker url',
73
- default: broker || 'mqtts://vrpc.io:8883'
74
- }
75
- )
76
- parser.add_argument(
77
- '-u',
78
- '--username',
79
- {
80
- help: 'Username',
81
- default: username
82
- }
83
- )
84
- parser.add_argument(
85
- '-P',
86
- '--password',
87
- {
88
- help: 'Password',
89
- default: password
90
- }
91
- )
92
- parser.add_argument(
93
- '--bestEffort',
94
- {
95
- help: 'Calls function on best-effort. Improves performance but may fail under unstable network connections.',
96
- action: 'store_true'
97
- }
98
- )
99
- parser.add_argument(
100
- '-V', '-v',
101
- '--userVersion',
102
- {
103
- help: 'User defined agent version. May be checked on the remote side for compatibility checks.',
104
- required: false,
105
- default: version,
106
- dest: 'version'
107
- }
108
- )
109
- parser.add_argument(
110
- '--version',
111
- {
112
- help: 'Returns the VRPC version this client was built with.',
113
- action: 'version',
114
- version: 'v2.3.2'
115
- }
116
- )
45
+ parser.add_argument('-a', '--agent', {
46
+ help: 'Agent name',
47
+ default: agent || VrpcAgent._generateAgentName()
48
+ })
49
+ parser.add_argument('-d', '--domain', {
50
+ help: 'Domain name',
51
+ default: domain || 'public.vrpc'
52
+ })
53
+ parser.add_argument('-t', '--token', {
54
+ help: 'access token',
55
+ default: token
56
+ })
57
+ parser.add_argument('-b', '--broker', {
58
+ help: 'Broker url',
59
+ default: broker || 'mqtts://vrpc.io:8883'
60
+ })
61
+ parser.add_argument('-u', '--username', {
62
+ help: 'Username',
63
+ default: username
64
+ })
65
+ parser.add_argument('-P', '--password', {
66
+ help: 'Password',
67
+ default: password
68
+ })
69
+ parser.add_argument('--bestEffort', {
70
+ help:
71
+ 'Calls function on best-effort. Improves performance but may fail under unstable network connections.',
72
+ action: 'store_true'
73
+ })
74
+ parser.add_argument('-V', '-v', '--userVersion', {
75
+ help:
76
+ 'User defined agent version. May be checked on the remote side for compatibility checks.',
77
+ required: false,
78
+ default: version,
79
+ dest: 'version'
80
+ })
81
+ parser.add_argument('--version', {
82
+ help: 'Returns the VRPC version this client was built with.',
83
+ action: 'version',
84
+ version: 'v2.3.2'
85
+ })
117
86
  const args = parser.parse_args()
118
87
  return new VrpcAgent(args)
119
88
  }
@@ -149,8 +118,7 @@ class VrpcAgent extends EventEmitter {
149
118
  log = 'console',
150
119
  bestEffort = false,
151
120
  version = ''
152
- } = {}
153
- ) {
121
+ } = {}) {
154
122
  super()
155
123
  this._validateDomain(domain)
156
124
  this._validateAgent(agent)
@@ -172,11 +140,11 @@ class VrpcAgent extends EventEmitter {
172
140
  this._baseTopic = `${this._domain}/${this._agent}`
173
141
  VrpcAdapter.onCallback(this._handleVrpcCallback.bind(this))
174
142
  // maps clientId to instanceId
175
- this._unnamedInstances = new Map()
176
- this._namedInstances = new Map()
143
+ this._isolatedInstances = new Map()
144
+ this._sharedInstances = new Map()
177
145
 
178
146
  // Handle the internal error event in case the user forgot to implement it
179
- this.on('error', (err) => {
147
+ this.on('error', err => {
180
148
  this._log.debug(`Encountered an error: ${err.message}`)
181
149
  })
182
150
  }
@@ -197,8 +165,11 @@ class VrpcAgent extends EventEmitter {
197
165
  * rejects
198
166
  */
199
167
  async serve () {
200
- const md5 = crypto.createHash('md5')
201
- .update(this._domain + this._agent).digest('hex').substr(0, 18)
168
+ const md5 = crypto
169
+ .createHash('md5')
170
+ .update(this._domain + this._agent)
171
+ .digest('hex')
172
+ .substr(0, 18)
202
173
  let username = this._username
203
174
  let password = this._password
204
175
  if (this._token) {
@@ -225,10 +196,10 @@ class VrpcAgent extends EventEmitter {
225
196
  this._log.info('Connecting to MQTT server...')
226
197
  const wasEnded = await this._clearPersistedSession()
227
198
  if (wasEnded) return
228
- this._client = mqtt.connect(
229
- this._broker,
230
- { ...this._options, clean: false }
231
- )
199
+ this._client = mqtt.connect(this._broker, {
200
+ ...this._options,
201
+ clean: false
202
+ })
232
203
  this._client.on('connect', this._handleConnect.bind(this))
233
204
  this._client.on('reconnect', this._handleReconnect.bind(this))
234
205
  this._client.on('error', this._handleError.bind(this))
@@ -269,30 +240,35 @@ class VrpcAgent extends EventEmitter {
269
240
  await new Promise(resolve => this._client.end(false, {}, resolve))
270
241
  await this._clearPersistedSession()
271
242
  } catch (err) {
272
- this._log.error(
273
- err,
274
- `Problem during disconnecting agent: ${err.message}`
275
- )
243
+ this._log.error(err, `Problem during disconnecting agent: ${err.message}`)
276
244
  }
277
245
  }
278
246
 
279
247
  static _generateAgentName () {
280
248
  const { username } = os.userInfo()
281
- const pathId = crypto.createHash('md5').update(path.resolve()).digest('hex').substr(0, 4)
249
+ const pathId = crypto
250
+ .createHash('md5')
251
+ .update(path.resolve())
252
+ .digest('hex')
253
+ .substr(0, 4)
282
254
  return `${username}-${pathId}@${os.hostname()}-${os.platform()}-js`
283
255
  }
284
256
 
285
257
  _validateDomain (domain) {
286
258
  if (!domain) throw new Error('The domain must be specified')
287
259
  if (domain.match(/[+/#*]/)) {
288
- throw new Error('The domain must NOT contain any of those characters: "+", "/", "#", "*"')
260
+ throw new Error(
261
+ 'The domain must NOT contain any of those characters: "+", "/", "#", "*"'
262
+ )
289
263
  }
290
264
  }
291
265
 
292
266
  _validateAgent (agent) {
293
267
  if (!agent) throw new Error('The agent must be specified')
294
268
  if (agent.match(/[+/#*]/)) {
295
- throw new Error('The agent must NOT contain any of those characters: "+", "/", "#", "*"')
269
+ throw new Error(
270
+ 'The agent must NOT contain any of those characters: "+", "/", "#", "*"'
271
+ )
296
272
  }
297
273
  }
298
274
 
@@ -300,7 +276,8 @@ class VrpcAgent extends EventEmitter {
300
276
  return JSON.stringify({
301
277
  status,
302
278
  hostname: os.hostname(),
303
- version: this._version
279
+ version: this._version,
280
+ v: VRPC_PROTOCOL_VERSION
304
281
  })
305
282
  }
306
283
 
@@ -309,11 +286,8 @@ class VrpcAgent extends EventEmitter {
309
286
  */
310
287
  async _clearPersistedSession () {
311
288
  // Clear potentially existing persisted sessions
312
- const client = mqtt.connect(
313
- this._broker,
314
- { ...this._options, clean: true }
315
- )
316
- client.on('error', (err) => this.emit('error', err))
289
+ const client = mqtt.connect(this._broker, { ...this._options, clean: true })
290
+ client.on('error', err => this.emit('error', err))
317
291
  client.on('offline', () => this.emit('offline'))
318
292
  client.on('reconnect', () => this.emit('reconnect'))
319
293
  return new Promise(resolve => {
@@ -329,29 +303,44 @@ class VrpcAgent extends EventEmitter {
329
303
  }
330
304
 
331
305
  _mqttPublish (topic, message, options) {
332
- this._client.publish(topic, message, { qos: this._qos, ...options }, (err) => {
333
- if (err) {
334
- this._log.warn(`Could not publish MQTT message because: ${err.message}`)
306
+ this._client.publish(
307
+ topic,
308
+ message,
309
+ { qos: this._qos, ...options },
310
+ err => {
311
+ if (err) {
312
+ this._log.warn(
313
+ `Could not publish MQTT message because: ${err.message}`
314
+ )
315
+ }
335
316
  }
336
- })
317
+ )
337
318
  }
338
319
 
339
320
  _mqttSubscribe (topic, options) {
340
- this._client.subscribe(topic, { qos: this._qos, ...options }, (err, granted) => {
341
- if (err) {
342
- this._log.warn(`Could not subscribe to topic '${topic}', because: ${err.message}`)
343
- } else {
344
- if (granted.length === 0) {
345
- this._log.debug(`Already subscribed to topic '${topic}'`)
321
+ this._client.subscribe(
322
+ topic,
323
+ { qos: this._qos, ...options },
324
+ (err, granted) => {
325
+ if (err) {
326
+ this._log.warn(
327
+ `Could not subscribe to topic '${topic}', because: ${err.message}`
328
+ )
329
+ } else {
330
+ if (granted.length === 0) {
331
+ this._log.debug(`Already subscribed to topic '${topic}'`)
332
+ }
346
333
  }
347
334
  }
348
- })
335
+ )
349
336
  }
350
337
 
351
338
  _mqttUnsubscribe (topic, options) {
352
- this._client.unsubscribe(topic, options, (err) => {
339
+ this._client.unsubscribe(topic, options, err => {
353
340
  if (err) {
354
- this._log.warn(`Could not unsubscribe from topic: ${topic} because: ${err.message}`)
341
+ this._log.warn(
342
+ `Could not unsubscribe from topic: ${topic} because: ${err.message}`
343
+ )
355
344
  }
356
345
  })
357
346
  }
@@ -377,7 +366,7 @@ class VrpcAgent extends EventEmitter {
377
366
  }
378
367
 
379
368
  async _ensureConnected () {
380
- return new Promise((resolve) => {
369
+ return new Promise(resolve => {
381
370
  if (this._client.connected) {
382
371
  resolve()
383
372
  } else {
@@ -386,11 +375,11 @@ class VrpcAgent extends EventEmitter {
386
375
  })
387
376
  }
388
377
 
389
- _handleVrpcCallback (jsonString, jsonObject) {
390
- const { sender } = jsonObject
378
+ _handleVrpcCallback (json) {
379
+ const { s } = json
391
380
  try {
392
- this._log.debug(`Forwarding callback to: ${sender} with payload:`, jsonObject)
393
- this._mqttPublish(sender, jsonString)
381
+ this._log.debug(`Forwarding callback to: ${s} with payload:`, json)
382
+ this._mqttPublish(s, VrpcAgent._stringifySafely(json))
394
383
  } catch (err) {
395
384
  this._log.warn(
396
385
  err,
@@ -399,10 +388,29 @@ class VrpcAgent extends EventEmitter {
399
388
  }
400
389
  }
401
390
 
391
+ static _stringifySafely (json) {
392
+ let str
393
+ const { c, f } = json
394
+ try {
395
+ str = JSON.stringify(json)
396
+ } catch (err) {
397
+ this._log.debug(
398
+ `Failed serialization of return value for: ${c}::${f}, because: ${err.message}`
399
+ )
400
+ json.r = '__vrpc::not-serializable__'
401
+ str = JSON.stringify(json)
402
+ }
403
+ return str
404
+ }
405
+
402
406
  _handleConnect () {
403
407
  this._log.info('[OK]')
404
408
  if (this._isReconnect) {
405
409
  this._publishAgentInfoMessage()
410
+ const classes = this._getClasses()
411
+ classes.forEach(className => {
412
+ this._publishClassInfoMessage(className)
413
+ })
406
414
  this.emit('connect')
407
415
  this._isReconnect = false
408
416
  return
@@ -424,6 +432,13 @@ class VrpcAgent extends EventEmitter {
424
432
  classes.forEach(className => {
425
433
  this._publishClassInfoMessage(className)
426
434
  })
435
+ // Register all pre-existing instances
436
+ for (const [
437
+ instanceId,
438
+ { className }
439
+ ] of VrpcAdapter._instances.entries()) {
440
+ this._subscribeToMethodsOfNewInstance(className, instanceId)
441
+ }
427
442
  this.emit('connect')
428
443
  }
429
444
 
@@ -441,7 +456,8 @@ class VrpcAgent extends EventEmitter {
441
456
  instances: this._getInstances(className),
442
457
  memberFunctions: this._getMemberFunctions(className),
443
458
  staticFunctions: this._getStaticFunctions(className),
444
- meta: this._getMetaData(className)
459
+ meta: this._getMetaData(className),
460
+ v: VRPC_PROTOCOL_VERSION
445
461
  }
446
462
  try {
447
463
  this._mqttPublish(
@@ -479,7 +495,7 @@ class VrpcAgent extends EventEmitter {
479
495
  const json = JSON.parse(data.toString())
480
496
  this._log.debug(`Message arrived with topic: ${topic} and payload:`, json)
481
497
  const tokens = topic.split('/')
482
- const [,, className, instance, method] = tokens
498
+ const [, , className, instance, method] = tokens
483
499
 
484
500
  // Special case: clientInfo message
485
501
  if (tokens.length === 4 && tokens[3] === '__clientInfo__') {
@@ -494,142 +510,136 @@ class VrpcAgent extends EventEmitter {
494
510
  }
495
511
 
496
512
  // Prepare RPC json
497
- json.context = instance === '__static__' ? className : instance
498
- json.method = method
513
+ // SECURITY: take as much as possible params from topic structure as this
514
+ // can be authorized by the broker. Content can't be checked...
515
+ json.c = instance === '__static__' ? className : instance
516
+ json.f = method
499
517
 
500
518
  // Mutates json and adds return value
501
519
  VrpcAdapter._call(json)
502
520
 
503
521
  // Intersecting life-cycle functions
504
- let publishClassInfo = false
505
522
  switch (method) {
506
- case '__create__': {
523
+ case '__createIsolated__': {
507
524
  // TODO handle instantiation errors
508
- const instanceId = json.data.r
525
+ const instanceId = json.r
509
526
  // TODO await this
510
527
  this._subscribeToMethodsOfNewInstance(className, instanceId)
511
- this._registerUnnamedInstance(instanceId, json.sender)
528
+ this._registerIsolatedInstance(instanceId, json.s)
512
529
  break
513
530
  }
514
- case '__createNamed__': {
531
+ case '__createShared__': {
515
532
  // TODO handle instantiation errors
516
- const instanceId = json.data.r
517
- if (!this._hasNamedInstance(instanceId)) {
518
- publishClassInfo = true
533
+ const instanceId = json.r
534
+ if (!this._hasSharedInstance(instanceId)) {
519
535
  this._subscribeToMethodsOfNewInstance(className, instanceId)
536
+ this._publishClassInfoMessage(className)
520
537
  }
521
- this._registerNamedInstance(instanceId, json.sender)
522
- break
523
- }
524
- case '__getNamed__': {
525
- const { data: { _1, e }, sender } = json
526
- if (!e) this._registerNamedInstance(_1, sender)
538
+ this._registerSharedInstance(instanceId, json.s)
527
539
  break
528
540
  }
529
541
  case '__delete__': {
530
- const { data: { _1 }, sender } = json
531
542
  this._unsubscribeMethodsOfDeletedInstance(className, instance)
532
- const wasNamed = this._unregisterInstance(_1, sender)
533
- if (wasNamed) publishClassInfo = true
543
+ const wasShared = this._unregisterInstance(json.a[0], json.s)
544
+ if (wasShared) this._publishClassInfoMessage(className)
534
545
  break
535
546
  }
536
547
  }
537
- let jsonString
538
- try {
539
- jsonString = JSON.stringify(json)
540
- } catch (err) {
541
- this._log.debug(`Failed serialization of return value for: ${json.context}::${json.method}, because: ${err.message}`)
542
- json.data.r = '__vrpc::not-serializable__'
543
- jsonString = JSON.stringify(json)
544
- }
545
- if (publishClassInfo && method === '__delete__') {
546
- this._publishClassInfoMessage(className)
547
- }
548
- this._mqttPublish(json.sender, jsonString)
549
- if (publishClassInfo && method === '__createNamed__') {
550
- this._publishClassInfoMessage(className)
551
- }
548
+ this._mqttPublish(json.s, VrpcAgent._stringifySafely(json))
552
549
  } catch (err) {
553
- this._log.error(err, `Problem while handling incoming message: ${err.message}`)
550
+ this._log.error(
551
+ err,
552
+ `Problem while handling incoming message: ${err.message}`
553
+ )
554
554
  }
555
555
  }
556
556
 
557
557
  _handleClientInfoMessage (topic, json) {
558
558
  // Client went offline
559
- const clientId = topic.slice(0, -9)
559
+ const clientId = topic.slice(0, -15) // /__clientInfo__ = 15
560
560
  if (json.status === 'offline') {
561
- const entry = this._unnamedInstances.get(clientId)
562
- if (entry) { // anonymous
561
+ const entry = this._isolatedInstances.get(clientId)
562
+ if (entry) {
563
+ // anonymous
563
564
  entry.forEach(instanceId => {
564
- const json = { data: { _1: instanceId }, method: '__delete__' }
565
+ const json = { f: '__delete__', a: [instanceId], r: null }
566
+ // TODO Do not mutate!
565
567
  VrpcAdapter._call(json)
566
- const { data: { r } } = json
567
- if (r) this._log.debug(`Deleted unnamed instance: ${instanceId}`)
568
+ if (json.r) {
569
+ this._log.debug(`Auto-deleted isolated instance: ${instanceId}`)
570
+ }
568
571
  })
569
572
  }
570
573
  VrpcAdapter._unregisterEventListeners(clientId)
571
574
  this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
575
+ this.emit('clientGone', clientId)
572
576
  }
573
577
  }
574
578
 
575
- _registerUnnamedInstance (instanceId, clientId) {
576
- const entry = this._unnamedInstances.get(clientId)
577
- if (entry) { // already seen
579
+ _registerIsolatedInstance (instanceId, clientId) {
580
+ const entry = this._isolatedInstances.get(clientId)
581
+ if (entry) {
582
+ // already seen
578
583
  entry.add(instanceId)
579
- } else { // new instance
580
- this._unnamedInstances.set(clientId, new Set([instanceId]))
581
- if (!this._namedInstances.has(clientId)) {
584
+ } else {
585
+ // new instance
586
+ this._isolatedInstances.set(clientId, new Set([instanceId]))
587
+ if (!this._sharedInstances.has(clientId)) {
582
588
  this._mqttSubscribe(`${clientId}/__clientInfo__`)
583
589
  }
584
590
  this._log.info(`Tracking lifetime of client: ${clientId}`)
585
591
  }
586
592
  }
587
593
 
588
- _registerNamedInstance (instanceId, clientId) {
589
- const entry = this._namedInstances.get(clientId)
590
- if (entry) { // already seen
594
+ _registerSharedInstance (instanceId, clientId) {
595
+ const entry = this._sharedInstances.get(clientId)
596
+ if (entry) {
597
+ // already seen
591
598
  entry.add(instanceId)
592
- } else { // new instance
593
- this._namedInstances.set(clientId, new Set([instanceId]))
594
- if (!this._unnamedInstances.has(clientId)) {
599
+ } else {
600
+ // new instance
601
+ this._sharedInstances.set(clientId, new Set([instanceId]))
602
+ if (!this._isolatedInstances.has(clientId)) {
595
603
  this._mqttSubscribe(`${clientId}/__clientInfo__`)
596
604
  }
597
605
  this._log.debug(`Tracking lifetime of client: ${clientId}`)
598
606
  }
599
607
  }
600
608
 
601
- _hasNamedInstance (instanceId) {
602
- for (const [, instances] of this._namedInstances) {
609
+ _hasSharedInstance (instanceId) {
610
+ for (const [, instances] of this._sharedInstances) {
603
611
  if (instances.has(instanceId)) return true
604
612
  }
605
613
  return false
606
614
  }
607
615
 
608
616
  _unregisterInstance (instanceId, clientId) {
609
- const entryUnnamed = this._unnamedInstances.get(clientId)
610
- if (entryUnnamed && entryUnnamed.has(instanceId)) {
611
- entryUnnamed.delete(instanceId)
612
- if (entryUnnamed.length === 0) {
613
- this._unnamedInstances.delete(clientId)
617
+ const entryIsolated = this._isolatedInstances.get(clientId)
618
+ if (entryIsolated && entryIsolated.has(instanceId)) {
619
+ entryIsolated.delete(instanceId)
620
+ if (entryIsolated.length === 0) {
621
+ this._isolatedInstances.delete(clientId)
614
622
  this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
615
623
  this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
616
624
  }
617
625
  return false
618
626
  }
619
627
  let found = false
620
- this._namedInstances.forEach(async (v) => {
628
+ this._sharedInstances.forEach(async v => {
621
629
  if (v.has(instanceId)) {
622
630
  found = true
623
631
  v.delete(instanceId)
624
632
  if (v.length === 0) {
625
- this._namedInstances.delete(clientId)
633
+ this._sharedInstances.delete(clientId)
626
634
  this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
627
635
  this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
628
636
  }
629
637
  }
630
638
  })
631
639
  if (!found) {
632
- this._log.warn(`Failed un-registering not registered instance: ${instanceId}`)
640
+ this._log.warn(
641
+ `Failed un-registering not registered instance: ${instanceId}`
642
+ )
633
643
  return false
634
644
  }
635
645
  return true
@@ -678,7 +688,7 @@ class VrpcAgent extends EventEmitter {
678
688
  * @event VrpcAgent#connect
679
689
  * @type {Object}
680
690
  * @property {Boolean} sessionPresent - A session from a previous connection is already present
681
- */
691
+ */
682
692
 
683
693
  /**
684
694
  * Event 'reconnect'
@@ -686,7 +696,7 @@ class VrpcAgent extends EventEmitter {
686
696
  * Emitted when a reconnect starts.
687
697
  *
688
698
  * @event VrpcAgent#reconnect
689
- */
699
+ */
690
700
 
691
701
  /**
692
702
  * Event 'close'
@@ -729,4 +739,14 @@ class VrpcAgent extends EventEmitter {
729
739
  * @event VrpcAgent#end
730
740
  */
731
741
 
742
+ /**
743
+ * Event 'clientGone'
744
+ *
745
+ * Emitted when a VRPC client exited that before produced state (e.g. created
746
+ * an anonymous or named instance).
747
+ *
748
+ * @event VrpcAgent#clientGone
749
+ * @type {String} clientId
750
+ */
751
+
732
752
  module.exports = VrpcAgent