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/browser/vrpc.js +1 -1
- package/package.json +1 -1
- package/vrpc/VrpcAgent.js +95 -23
- package/vrpc/VrpcBrowserAgent.js +65 -20
- package/vrpc/VrpcClient.js +16 -2
package/package.json
CHANGED
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
|
|
173
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
482
|
+
if (this._serving) {
|
|
450
483
|
resolve()
|
|
451
484
|
} else {
|
|
452
|
-
this.
|
|
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
|
|
497
|
-
if (
|
|
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
|
-
//
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
|
package/vrpc/VrpcBrowserAgent.js
CHANGED
|
@@ -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
|
|
94
|
-
*
|
|
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
|
-
|
|
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.
|
|
323
|
+
if (this._serving) {
|
|
309
324
|
resolve()
|
|
310
325
|
} else {
|
|
311
|
-
this.
|
|
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
|
|
354
|
-
if (
|
|
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
|
-
//
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
|
package/vrpc/VrpcClient.js
CHANGED
|
@@ -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
|
-
|
|
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
|