vrpc 3.11.1 → 3.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vrpc",
3
- "version": "3.11.1",
3
+ "version": "3.12.1",
4
4
  "description": "Variadic Remote Procedure Calls",
5
5
  "main": "index.js",
6
6
  "directories": {
package/vrpc/VrpcAgent.js CHANGED
@@ -102,6 +102,7 @@ class VrpcAgent extends EventEmitter {
102
102
  * @param {String} [obj.bestEffort=true] If true, message will be sent with best effort, i.e. no caching if offline
103
103
  * @param {String} [obj.version=''] The (user-defined) version of this agent
104
104
  * @param {String} [obj.mqttClientId='<generated()>'] Explicitly set the mqtt client id.
105
+ * @param {Object} [obj.tls] TLS settings for a secure broker: `{ ca, rejectUnauthorized }`. Without it the broker's certificate is not verified (the historical behaviour); with `ca` the broker must present a chain the given certificates anchor.
105
106
  *
106
107
  * @example
107
108
  * const agent = new Agent({
@@ -119,7 +120,8 @@ class VrpcAgent extends EventEmitter {
119
120
  log = 'console',
120
121
  bestEffort = true,
121
122
  version = '',
122
- mqttClientId = null
123
+ mqttClientId = null,
124
+ tls = null
123
125
  } = {}) {
124
126
  super()
125
127
  this._validateDomain(domain)
@@ -132,6 +134,7 @@ class VrpcAgent extends EventEmitter {
132
134
  this._broker = broker
133
135
  this._qos = bestEffort ? 0 : 1
134
136
  this._version = version
137
+ this._tls = tls
135
138
  this._mqttClientId =
136
139
  mqttClientId || `va3${VrpcAgent._createHash(this._domain + this._agent)}`
137
140
  if (log === 'console') {
@@ -150,6 +153,11 @@ class VrpcAgent extends EventEmitter {
150
153
  subscribe: (topics, options) => this._mqttSubscribe(topics, options),
151
154
  log: this._log
152
155
  })
156
+ // counts the connections: an announcement belongs to the connection
157
+ // that prepared it (see _handleConnect)
158
+ this._connectGeneration = 0
159
+ // connected, request topics subscribed and info announced
160
+ this._serving = false
153
161
 
154
162
  // Handle the internal error event in case the user forgot to implement it
155
163
  this.on('error', err => {
@@ -169,8 +177,9 @@ class VrpcAgent extends EventEmitter {
169
177
  * If the connection could not be established because of authorization
170
178
  * failure, the 'error' event will be emitted.
171
179
  *
172
- * @return {Promise} Resolves once connected or explicitly ended, never
173
- * rejects
180
+ * @return {Promise} Resolves once the agent serves - connected, its
181
+ * request topics subscribed and its info announced - or once it was
182
+ * explicitly ended before that, never rejects
174
183
  */
175
184
  async serve () {
176
185
  let username = this._username
@@ -192,7 +201,7 @@ class VrpcAgent extends EventEmitter {
192
201
  // agent must come back when it does (mqtt >= 5 stops otherwise)
193
202
  reconnectOnConnackError: true,
194
203
  clientId: this._mqttClientId,
195
- rejectUnauthorized: false,
204
+ ...VrpcAgent._tlsOptions(this._tls),
196
205
  will: {
197
206
  topic: `${this._baseTopic}/__agentInfo__`,
198
207
  payload: this._createAgentInfoPayload({ status: 'offline' }),
@@ -297,6 +306,17 @@ class VrpcAgent extends EventEmitter {
297
306
  return token.slice(0, length)
298
307
  }
299
308
 
309
+ /**
310
+ * The TLS part of the MQTT options: verification off when nothing was
311
+ * given (as it always was), the given anchors otherwise.
312
+ */
313
+ static _tlsOptions (tls) {
314
+ if (!tls) return { rejectUnauthorized: false }
315
+ const options = { rejectUnauthorized: tls.rejectUnauthorized !== false }
316
+ if (tls.ca) options.ca = tls.ca
317
+ return options
318
+ }
319
+
300
320
  static _generateAgentName () {
301
321
  const { username } = os.userInfo()
302
322
  const pathId = VrpcAgent._createHash(path.resolve, 4)
@@ -363,7 +383,13 @@ class VrpcAgent extends EventEmitter {
363
383
  )
364
384
  }
365
385
 
366
- _mqttSubscribe (topic, options) {
386
+ /**
387
+ * Subscribes topic(s); `onSettled` is called once the broker has
388
+ * answered (granted, refused or failed alike).
389
+ *
390
+ * @private
391
+ */
392
+ _mqttSubscribe (topic, options, onSettled) {
367
393
  this._client.subscribe(
368
394
  topic,
369
395
  { qos: this._qos, ...options },
@@ -410,6 +436,7 @@ class VrpcAgent extends EventEmitter {
410
436
  this._log.debug(`Already subscribed to topic '${topic}'`)
411
437
  }
412
438
  }
439
+ if (onSettled) onSettled()
413
440
  }
414
441
  )
415
442
  }
@@ -444,12 +471,18 @@ class VrpcAgent extends EventEmitter {
444
471
  return VrpcAdapter._getMetaData(className)
445
472
  }
446
473
 
474
+ /**
475
+ * Resolves once the agent serves (its own 'connect' event: connected,
476
+ * request topics subscribed, info announced), or once it was ended.
477
+ *
478
+ * @private
479
+ */
447
480
  async _ensureConnected () {
448
481
  return new Promise(resolve => {
449
- if (this._client.connected) {
482
+ if (this._serving) {
450
483
  resolve()
451
484
  } else {
452
- this._client.once('connect', resolve)
485
+ this.once('connect', resolve)
453
486
  // Will be triggered if the user called 'agent.end()'
454
487
  this.once('end', resolve)
455
488
  }
@@ -488,36 +521,64 @@ class VrpcAgent extends EventEmitter {
488
521
  return str
489
522
  }
490
523
 
524
+ /**
525
+ * Serve first, announce second. The request topics - the statics and
526
+ * the methods of every instance that exists already - are subscribed
527
+ * before the agent publishes its agent and class info, and the
528
+ * announcement waits until the broker has answered every subscription.
529
+ * A client acts on the announcement at once; a request it publishes
530
+ * before the broker routes the topic is lost (QoS 0) and only times out
531
+ * on the caller's side. That was the fate of the first call into an
532
+ * agent that came online after the client waiting for it. A refused
533
+ * subscription is the retrier's business and delays nothing.
534
+ *
535
+ * @private
536
+ */
491
537
  _handleConnect () {
492
538
  this._log.info('[OK]')
493
539
  // everything is subscribed afresh: pending retries are moot
494
540
  this._subscribeRetrier.cancel()
541
+ const generation = ++this._connectGeneration
542
+ const batches = []
495
543
  try {
496
- const topics = this._generateTopics()
497
- if (topics.length > 0) this._mqttSubscribe(topics)
544
+ const statics = this._generateTopics()
545
+ if (statics.length > 0) batches.push(statics)
498
546
  } catch (err) {
499
547
  this._log.error(
500
548
  err,
501
549
  `Problem during initial topic subscription: ${err.message}`
502
550
  )
503
551
  }
504
- // Publish agent online
505
- this._publishAgentInfoMessage()
506
-
507
- // Publish class information
508
- const classes = this._getClasses()
509
- classes.forEach(className => {
510
- this._publishClassInfoMessage(className)
511
- this._publishClassInfoConciseMessage(className)
512
- })
513
- // Register all pre-existing instances
552
+ // all pre-existing instances
514
553
  for (const [
515
554
  instanceId,
516
555
  { className }
517
556
  ] of VrpcAdapter._instances.entries()) {
518
- this._subscribeToMethodsOfNewInstance(className, instanceId)
557
+ batches.push(this._methodsTopic(className, instanceId))
558
+ }
559
+ let pending = batches.length
560
+ const announce = () => {
561
+ // a connection that ended meanwhile announces nothing: the next
562
+ // one prepares and announces itself
563
+ if (generation !== this._connectGeneration) return
564
+ this._publishAgentInfoMessage()
565
+ for (const className of this._getClasses()) {
566
+ this._publishClassInfoMessage(className)
567
+ this._publishClassInfoConciseMessage(className)
568
+ }
569
+ this._serving = true
570
+ this.emit('connect')
571
+ }
572
+ if (pending === 0) {
573
+ announce()
574
+ return
575
+ }
576
+ for (const topic of batches) {
577
+ this._mqttSubscribe(topic, undefined, () => {
578
+ pending -= 1
579
+ if (pending === 0) announce()
580
+ })
519
581
  }
520
- this.emit('connect')
521
582
  }
522
583
 
523
584
  _publishAgentInfoMessage () {
@@ -771,14 +832,23 @@ class VrpcAgent extends EventEmitter {
771
832
  return true
772
833
  }
773
834
 
835
+ /**
836
+ * The request topics of one instance: every method of it.
837
+ *
838
+ * @private
839
+ */
840
+ _methodsTopic (className, instance) {
841
+ return `${this._baseTopic}/${className}/${instance}/+`
842
+ }
843
+
774
844
  _subscribeToMethodsOfNewInstance (className, instance) {
775
- const topic = `${this._baseTopic}/${className}/${instance}/+`
845
+ const topic = this._methodsTopic(className, instance)
776
846
  this._mqttSubscribe(topic)
777
847
  this._log.debug(`Subscribed to new topic after instantiation: ${topic}`)
778
848
  }
779
849
 
780
850
  _unsubscribeMethodsOfDeletedInstance (className, instance) {
781
- const topic = `${this._baseTopic}/${className}/${instance}/+`
851
+ const topic = this._methodsTopic(className, instance)
782
852
  this._mqttUnsubscribe(topic)
783
853
  this._log.debug(`Unsubscribed from topic after deletion: ${topic}`)
784
854
  }
@@ -793,6 +863,8 @@ class VrpcAgent extends EventEmitter {
793
863
  }
794
864
 
795
865
  _handleClose () {
866
+ // the next connection prepares and announces itself afresh
867
+ this._serving = false
796
868
  this.emit('close')
797
869
  }
798
870
 
@@ -71,6 +71,11 @@ class VrpcAgent extends EventEmitter {
71
71
  // maps clientId to instanceId
72
72
  this._isolatedInstances = new Map()
73
73
  this._sharedInstances = new Map()
74
+ // counts the connections: an announcement belongs to the connection
75
+ // that prepared it (see _handleConnect)
76
+ this._connectGeneration = 0
77
+ // connected, request topics subscribed and info announced
78
+ this._serving = false
74
79
 
75
80
  // Handle the internal error event in case the user forgot to implement it
76
81
  this.on('error', err => {
@@ -90,8 +95,9 @@ class VrpcAgent extends EventEmitter {
90
95
  * If the connection could not be established because of authorization
91
96
  * failure, the 'error' event will be emitted.
92
97
  *
93
- * @return {Promise} Resolves once connected or explicitly ended, never
94
- * rejects
98
+ * @return {Promise} Resolves once the agent serves - connected, its
99
+ * request topics subscribed and its info announced - or once it was
100
+ * explicitly ended before that, never rejects
95
101
  */
96
102
  async serve () {
97
103
  let username = this._username
@@ -226,7 +232,11 @@ class VrpcAgent extends EventEmitter {
226
232
  )
227
233
  }
228
234
 
229
- _mqttSubscribe (topic, options) {
235
+ /**
236
+ * Subscribes topic(s); `onSettled` is called once the broker has
237
+ * answered (granted, refused or failed alike).
238
+ */
239
+ _mqttSubscribe (topic, options, onSettled) {
230
240
  this._client.subscribe(
231
241
  topic,
232
242
  { qos: this._qos, ...options },
@@ -269,6 +279,7 @@ class VrpcAgent extends EventEmitter {
269
279
  this._log.debug(`Already subscribed to topic '${topic}'`)
270
280
  }
271
281
  }
282
+ if (onSettled) onSettled()
272
283
  }
273
284
  )
274
285
  }
@@ -303,12 +314,16 @@ class VrpcAgent extends EventEmitter {
303
314
  return VrpcAdapter._getMetaData(className)
304
315
  }
305
316
 
317
+ /**
318
+ * Resolves once the agent serves (its own 'connect' event: connected,
319
+ * request topics subscribed, info announced), or once it was ended.
320
+ */
306
321
  async _ensureConnected () {
307
322
  return new Promise(resolve => {
308
- if (this._client.connected) {
323
+ if (this._serving) {
309
324
  resolve()
310
325
  } else {
311
- this._client.once('connect', resolve)
326
+ this.once('connect', resolve)
312
327
  // Will be triggered if the user called 'agent.end()'
313
328
  this.once('end', resolve)
314
329
  }
@@ -347,33 +362,56 @@ class VrpcAgent extends EventEmitter {
347
362
  return str
348
363
  }
349
364
 
365
+ /**
366
+ * Serve first, announce second - the twin of VrpcAgent._handleConnect:
367
+ * every request topic (statics, the methods of every existing instance)
368
+ * is subscribed before the agent and class info go out, and they go out
369
+ * once the broker has answered every subscription. A client acts on the
370
+ * announcement at once; a request published before the broker routes
371
+ * the topic is lost (QoS 0) and only times out on the caller's side.
372
+ */
350
373
  _handleConnect () {
351
374
  this._log.info('[OK]')
375
+ const generation = ++this._connectGeneration
376
+ const batches = []
352
377
  try {
353
- const topics = this._generateTopics()
354
- if (topics.length > 0) this._mqttSubscribe(topics)
378
+ const statics = this._generateTopics()
379
+ if (statics.length > 0) batches.push(statics)
355
380
  } catch (err) {
356
381
  this._log.error(
357
382
  err,
358
383
  `Problem during initial topic subscription: ${err.message}`
359
384
  )
360
385
  }
361
- // Publish agent online
362
- this._publishAgentInfoMessage()
363
-
364
- // Publish class information
365
- const classes = this._getClasses()
366
- classes.forEach(className => {
367
- this._publishClassInfoMessage(className)
368
- })
369
- // Register all pre-existing instances
386
+ // all pre-existing instances
370
387
  for (const [
371
388
  instanceId,
372
389
  { className }
373
390
  ] of VrpcAdapter._instances.entries()) {
374
- this._subscribeToMethodsOfNewInstance(className, instanceId)
391
+ batches.push(this._methodsTopic(className, instanceId))
392
+ }
393
+ let pending = batches.length
394
+ const announce = () => {
395
+ // a connection that ended meanwhile announces nothing: the next
396
+ // one prepares and announces itself
397
+ if (generation !== this._connectGeneration) return
398
+ this._publishAgentInfoMessage()
399
+ for (const className of this._getClasses()) {
400
+ this._publishClassInfoMessage(className)
401
+ }
402
+ this._serving = true
403
+ this.emit('connect')
404
+ }
405
+ if (pending === 0) {
406
+ announce()
407
+ return
408
+ }
409
+ for (const topic of batches) {
410
+ this._mqttSubscribe(topic, undefined, () => {
411
+ pending -= 1
412
+ if (pending === 0) announce()
413
+ })
375
414
  }
376
- this.emit('connect')
377
415
  }
378
416
 
379
417
  _publishAgentInfoMessage () {
@@ -592,14 +630,19 @@ class VrpcAgent extends EventEmitter {
592
630
  return true
593
631
  }
594
632
 
633
+ /** The request topics of one instance: every method of it. */
634
+ _methodsTopic (className, instance) {
635
+ return `${this._baseTopic}/${className}/${instance}/+`
636
+ }
637
+
595
638
  _subscribeToMethodsOfNewInstance (className, instance) {
596
- const topic = `${this._baseTopic}/${className}/${instance}/+`
639
+ const topic = this._methodsTopic(className, instance)
597
640
  this._mqttSubscribe(topic)
598
641
  this._log.debug(`Subscribed to new topic after instantiation: ${topic}`)
599
642
  }
600
643
 
601
644
  _unsubscribeMethodsOfDeletedInstance (className, instance) {
602
- const topic = `${this._baseTopic}/${className}/${instance}/+`
645
+ const topic = this._methodsTopic(className, instance)
603
646
  this._mqttUnsubscribe(topic)
604
647
  this._log.debug(`Unsubscribed from topic after deletion: ${topic}`)
605
648
  }
@@ -614,6 +657,8 @@ class VrpcAgent extends EventEmitter {
614
657
  }
615
658
 
616
659
  _handleClose () {
660
+ // the next connection prepares and announces itself afresh
661
+ this._serving = false
617
662
  this.emit('close')
618
663
  }
619
664
 
@@ -75,6 +75,7 @@ class VrpcClient extends EventEmitter {
75
75
  * domain: 'vrpc',
76
76
  * broker: 'mqtts://broker.hivemq.com:8883'
77
77
  * })
78
+ * @param {Object} [obj.tls] TLS settings for a secure broker: `{ ca, rejectUnauthorized }`. Without it the broker's certificate is not verified (the historical behaviour); with `ca` the broker must present a chain the given certificates anchor.
78
79
  */
79
80
  constructor ({
80
81
  token,
@@ -89,7 +90,8 @@ class VrpcClient extends EventEmitter {
89
90
  mqttClientId = null,
90
91
  identity = null,
91
92
  keepalive = 30,
92
- requiresSchema = false
93
+ requiresSchema = false,
94
+ tls = null
93
95
  } = {}) {
94
96
  super()
95
97
  // domain sanity check
@@ -137,6 +139,7 @@ class VrpcClient extends EventEmitter {
137
139
 
138
140
  this._instance = nanoid(8)
139
141
  this._mqttClientId = mqttClientId || this._createMqttClientId()
142
+ this._tls = tls
140
143
  this._vrpcClientId = this._createVrpcClientId()
141
144
  // the part of the connection id nobody else can learn: a broker that
142
145
  // confines a client to topics under its own mqtt client id keeps
@@ -203,6 +206,17 @@ class VrpcClient extends EventEmitter {
203
206
  return this._vrpcConnectionId
204
207
  }
205
208
 
209
+ /**
210
+ * The TLS part of the MQTT options: verification off when nothing was
211
+ * given (as it always was), the given anchors otherwise.
212
+ */
213
+ static _tlsOptions (tls) {
214
+ if (!tls) return { rejectUnauthorized: false }
215
+ const options = { rejectUnauthorized: tls.rejectUnauthorized !== false }
216
+ if (tls.ca) options.ca = tls.ca
217
+ return options
218
+ }
219
+
206
220
  /**
207
221
  * Actually connects to the MQTT broker.
208
222
  *
@@ -229,7 +243,7 @@ class VrpcClient extends EventEmitter {
229
243
  clean: true,
230
244
  keepalive: this._keepalive,
231
245
  clientId: this._mqttClientId,
232
- rejectUnauthorized: false,
246
+ ...VrpcClient._tlsOptions(this._tls),
233
247
  connectTimeout: this._timeout,
234
248
  // a refused CONNACK is retried like any other failed attempt (see
235
249
  // VrpcAgent): whoever wants a refusal to be final ends the client