vrpc 3.14.0 → 3.15.0
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/LICENSE +1 -1
- package/README.md +100 -77
- package/browser/vrpc.js +2 -2
- package/index.js +7 -7
- package/package.json +23 -15
- package/vrpc/Registrations.js +850 -0
- package/vrpc/SubscribeRetrier.js +33 -0
- package/vrpc/VrpcAdapter.js +552 -294
- package/vrpc/VrpcAgent.js +230 -153
- package/vrpc/VrpcBrowserAdapter.js +417 -285
- package/vrpc/VrpcBrowserAgent.js +239 -128
- package/vrpc/VrpcClient.js +1108 -213
- package/vrpc/VrpcNative.js +10 -5
- package/vrpc/VrpcPersistor.js +1 -1
- package/vrpc/adapter.hpp +1 -1
- package/vrpc/addon.cpp +1 -1
- package/vrpc/comment-parser/parser.js +23 -0
- package/vrpc/namePattern.js +86 -0
- package/vrpc/vrpc-agent-js +3 -11
package/vrpc/VrpcAgent.js
CHANGED
|
@@ -2,14 +2,14 @@ const os = require('os')
|
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const mqtt = require('mqtt')
|
|
4
4
|
const { nanoid } = require('nanoid')
|
|
5
|
-
const { ArgumentParser } = require('argparse')
|
|
5
|
+
const { ArgumentParser, BooleanOptionalAction } = require('argparse')
|
|
6
6
|
const EventEmitter = require('events')
|
|
7
7
|
const jsonStringifySafe = require('json-stringify-safe')
|
|
8
8
|
const VrpcAdapter = require('./VrpcAdapter')
|
|
9
9
|
const SubscribeRetrier = require('./SubscribeRetrier')
|
|
10
10
|
const ReconnectBackoff = require('./ReconnectBackoff')
|
|
11
11
|
|
|
12
|
-
const VRPC_PROTOCOL_VERSION =
|
|
12
|
+
const VRPC_PROTOCOL_VERSION = 4
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Agent capable of making existing code available to remote control by clients.
|
|
@@ -70,8 +70,9 @@ class VrpcAgent extends EventEmitter {
|
|
|
70
70
|
default: password
|
|
71
71
|
})
|
|
72
72
|
parser.add_argument('--bestEffort', {
|
|
73
|
-
help: '
|
|
74
|
-
action:
|
|
73
|
+
help: 'Publishes and subscribes at QoS 0 (the default); --no-bestEffort uses QoS 1, which rides out short connection losses at some cost.',
|
|
74
|
+
action: BooleanOptionalAction,
|
|
75
|
+
default: true
|
|
75
76
|
})
|
|
76
77
|
parser.add_argument('-V', '-v', '--userVersion', {
|
|
77
78
|
help: 'User defined agent version. May be checked on the remote side for compatibility checks.',
|
|
@@ -82,7 +83,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
82
83
|
parser.add_argument('--version', {
|
|
83
84
|
help: 'Returns the VRPC version this client was built with.',
|
|
84
85
|
action: 'version',
|
|
85
|
-
version: '
|
|
86
|
+
version: `v${require('../package.json').version}`
|
|
86
87
|
})
|
|
87
88
|
const args = parser.parse_args()
|
|
88
89
|
return new VrpcAgent(args)
|
|
@@ -97,10 +98,12 @@ class VrpcAgent extends EventEmitter {
|
|
|
97
98
|
* @param {String} [obj.password] MQTT password (if no token is provided)
|
|
98
99
|
* @param {String} [obj.token] Access token
|
|
99
100
|
* @param {String} [obj.domain='vrpc'] The domain under which the agent-provided code is reachable
|
|
100
|
-
* @param {String} [obj.agent='<user>-<
|
|
101
|
+
* @param {String} [obj.agent='<user>-<installationId>@<hostname>-<platform>-js'] This agent's name;
|
|
102
|
+
* the default is stable for the installation (where this vrpc is installed),
|
|
103
|
+
* however and from wherever the agent is started
|
|
101
104
|
* @param {String} [obj.broker='mqtts://broker.hivemq.com:8883'] Broker url in form: `<scheme>://<host>:<port>`
|
|
102
105
|
* @param {Object} [obj.log=console] Log object (must support debug, info, warn, and error level)
|
|
103
|
-
* @param {
|
|
106
|
+
* @param {Boolean} [obj.bestEffort=true] If true, messages are sent at QoS 0 (best effort), else at QoS 1
|
|
104
107
|
* @param {String} [obj.version=''] The (user-defined) version of this agent
|
|
105
108
|
* @param {String} [obj.mqttClientId='<generated()>'] Explicitly set the mqtt client id.
|
|
106
109
|
* @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.
|
|
@@ -153,9 +156,24 @@ class VrpcAgent extends EventEmitter {
|
|
|
153
156
|
`Dispose of deleted instance ${className}::${instance} failed: ${error.message}`
|
|
154
157
|
)
|
|
155
158
|
VrpcAdapter.on('disposeFailed', this._handleDisposeFailed)
|
|
156
|
-
//
|
|
159
|
+
// a registration whose release failed is gone from the book anyway
|
|
160
|
+
this._handleReleaseFailed = ({ context, error, ...rest }) =>
|
|
161
|
+
this._log.warn(
|
|
162
|
+
error,
|
|
163
|
+
`Release of registration ${context}::${rest.function} failed: ${error && error.message}`
|
|
164
|
+
)
|
|
165
|
+
VrpcAdapter.on('releaseFailed', this._handleReleaseFailed)
|
|
166
|
+
// a greeting that failed leaves its subscription in place
|
|
167
|
+
this._handleGreetFailed = ({ context, error, ...rest }) =>
|
|
168
|
+
this._log.warn(
|
|
169
|
+
error,
|
|
170
|
+
`Greeting of a subscriber to ${context}::${rest.function} failed: ${error && error.message}`
|
|
171
|
+
)
|
|
172
|
+
VrpcAdapter.on('greetFailed', this._handleGreetFailed)
|
|
173
|
+
// connection id -> Set(isolated instance ids it owns)
|
|
157
174
|
this._isolatedInstances = new Map()
|
|
158
|
-
|
|
175
|
+
// connection ids whose presence is watched (see _watch)
|
|
176
|
+
this._watched = new Set()
|
|
159
177
|
// refused subscriptions (SUBACK qos=128) are retried until granted
|
|
160
178
|
this._subscribeRetrier = new SubscribeRetrier({
|
|
161
179
|
subscribe: (topics, options) => this._mqttSubscribe(topics, options),
|
|
@@ -243,6 +261,8 @@ class VrpcAgent extends EventEmitter {
|
|
|
243
261
|
*/
|
|
244
262
|
async end ({ unregister = false } = {}) {
|
|
245
263
|
VrpcAdapter.off('disposeFailed', this._handleDisposeFailed)
|
|
264
|
+
VrpcAdapter.off('releaseFailed', this._handleReleaseFailed)
|
|
265
|
+
VrpcAdapter.off('greetFailed', this._handleGreetFailed)
|
|
246
266
|
this._subscribeRetrier.cancel()
|
|
247
267
|
try {
|
|
248
268
|
if (!this._client || !this._client.connected) {
|
|
@@ -264,11 +284,16 @@ class VrpcAgent extends EventEmitter {
|
|
|
264
284
|
{ retain: true }
|
|
265
285
|
)
|
|
266
286
|
if (unregister) {
|
|
287
|
+
// empty retained messages clear what the broker keeps of this agent
|
|
267
288
|
this._mqttPublish(agentTopic, null, { retain: true })
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
289
|
+
for (const className of this._getClasses()) {
|
|
290
|
+
for (const info of ['__classInfo__', '__classInfoConcise__']) {
|
|
291
|
+
this._mqttPublish(
|
|
292
|
+
`${this._baseTopic}/${className}/${info}`,
|
|
293
|
+
null,
|
|
294
|
+
{ retain: true }
|
|
295
|
+
)
|
|
296
|
+
}
|
|
272
297
|
}
|
|
273
298
|
}
|
|
274
299
|
await new Promise(resolve => this._client.end(false, {}, resolve))
|
|
@@ -293,12 +318,13 @@ class VrpcAgent extends EventEmitter {
|
|
|
293
318
|
* @returns {Object} The real instance (not a proxy!)
|
|
294
319
|
*/
|
|
295
320
|
create ({ className, instance = nanoid(8), args = [], isIsolated = false }) {
|
|
321
|
+
const existed = VrpcAdapter._instances.has(instance)
|
|
296
322
|
const obj = VrpcAdapter.create({ className, instance, args, isIsolated })
|
|
297
323
|
// An agent that is not served yet only collects: once the connection
|
|
298
324
|
// stands, the connect handler subscribes every adapter instance and
|
|
299
325
|
// publishes every class info, so a caller may build its full state
|
|
300
326
|
// before going online and "online" then means "complete"
|
|
301
|
-
if (this._client && !
|
|
327
|
+
if (this._client && !existed) {
|
|
302
328
|
this._subscribeToMethodsOfNewInstance(className, instance)
|
|
303
329
|
this._publishClassInfoMessage(className)
|
|
304
330
|
this._publishClassInfoConciseMessage(className)
|
|
@@ -336,10 +362,22 @@ class VrpcAgent extends EventEmitter {
|
|
|
336
362
|
return options
|
|
337
363
|
}
|
|
338
364
|
|
|
365
|
+
/**
|
|
366
|
+
* The name of an agent nobody named:
|
|
367
|
+
* `<user>-<installationId>@<hostname>-<platform>-js`. The installation id
|
|
368
|
+
* stands for where this vrpc is installed (the app's own
|
|
369
|
+
* `node_modules/vrpc`): the same however and from wherever the agent is
|
|
370
|
+
* started, different for every installation of a user on a host.
|
|
371
|
+
*/
|
|
339
372
|
static _generateAgentName () {
|
|
340
373
|
const { username } = os.userInfo()
|
|
341
|
-
const
|
|
342
|
-
return `${username}-${
|
|
374
|
+
const installationId = VrpcAgent._installationId(path.resolve(__dirname, '..'))
|
|
375
|
+
return `${username}-${installationId}@${os.hostname()}-${os.platform()}-js`
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// four hex characters standing for an installation directory
|
|
379
|
+
static _installationId (directory) {
|
|
380
|
+
return VrpcAgent._createHash(directory, 4)
|
|
343
381
|
}
|
|
344
382
|
|
|
345
383
|
_generateToken () {
|
|
@@ -412,16 +450,19 @@ class VrpcAgent extends EventEmitter {
|
|
|
412
450
|
this._client.subscribe(
|
|
413
451
|
topic,
|
|
414
452
|
{ qos: this._qos, ...options },
|
|
415
|
-
(
|
|
416
|
-
|
|
453
|
+
(subscribeError, subs) => {
|
|
454
|
+
const { refused, granted, error } = SubscribeRetrier.outcome(
|
|
455
|
+
subscribeError,
|
|
456
|
+
subs
|
|
457
|
+
)
|
|
458
|
+
if (error) {
|
|
417
459
|
this._log.warn(
|
|
418
|
-
`Could not subscribe to topic(s) '${topic}', because: ${
|
|
460
|
+
`Could not subscribe to topic(s) '${topic}', because: ${error.message}`
|
|
419
461
|
)
|
|
420
462
|
} else {
|
|
463
|
+
let err
|
|
421
464
|
const topicArray = Array.isArray(topic) ? topic : [topic]
|
|
422
|
-
const erroneousGranted =
|
|
423
|
-
.filter(x => x.qos === 128)
|
|
424
|
-
.map(x => x.topic)
|
|
465
|
+
const erroneousGranted = refused
|
|
425
466
|
if (erroneousGranted.length > 0) {
|
|
426
467
|
err = new Error(
|
|
427
468
|
`Could not subscribe all ${topicArray.length} topic(s) but got error qos=128 on following ${erroneousGranted.length} topic(s): ${erroneousGranted}`
|
|
@@ -432,9 +473,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
432
473
|
this.emit('error', err)
|
|
433
474
|
this._subscribeRetrier.schedule(erroneousGranted, options)
|
|
434
475
|
}
|
|
435
|
-
this._subscribeRetrier.granted(
|
|
436
|
-
granted.filter(x => x.qos !== 128).map(x => x.topic)
|
|
437
|
-
)
|
|
476
|
+
this._subscribeRetrier.granted(granted.map(x => x.topic))
|
|
438
477
|
const reducedQos = granted.filter(x => x.qos < this._qos)
|
|
439
478
|
if (reducedQos.length > 0) {
|
|
440
479
|
err = new Error(
|
|
@@ -451,7 +490,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
451
490
|
this._log.warn(err)
|
|
452
491
|
this.emit('error', err)
|
|
453
492
|
}
|
|
454
|
-
if (granted.length === 0) {
|
|
493
|
+
if (granted.length === 0 && refused.length === 0) {
|
|
455
494
|
this._log.debug(`Already subscribed to topic '${topic}'`)
|
|
456
495
|
}
|
|
457
496
|
}
|
|
@@ -508,15 +547,43 @@ class VrpcAgent extends EventEmitter {
|
|
|
508
547
|
})
|
|
509
548
|
}
|
|
510
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Publishes what the adapter sends on its own: a callback or an event
|
|
552
|
+
* `{ a, i, v }`, the second answer of a promise `{ r | e, i, v }`, an
|
|
553
|
+
* ended notice `{ i, end, v }`. Events and notices go to the topic their
|
|
554
|
+
* subscription id names, everything else to the sender.
|
|
555
|
+
*
|
|
556
|
+
* An event or a callback whose arguments JSON cannot carry (a BigInt) is
|
|
557
|
+
* not dropped silently: a receiver of protocol 4 gets `{ i, e, v }`
|
|
558
|
+
* instead (EV-22, CALL-10). An older one would call its handler without
|
|
559
|
+
* arguments, so it gets nothing, and the agent logs a warning.
|
|
560
|
+
*/
|
|
511
561
|
_handleVrpcCallback (json) {
|
|
512
|
-
const { s, i, r, e, a } = json
|
|
562
|
+
const { s, i, r, e, a, end, receiverVersion } = json
|
|
513
563
|
const topic = i.startsWith('__e__') ? i.slice(5) : s
|
|
564
|
+
const v = VRPC_PROTOCOL_VERSION
|
|
565
|
+
let message
|
|
566
|
+
if (end) message = { i, end, v }
|
|
567
|
+
else if (a !== undefined) message = { a, i, v }
|
|
568
|
+
else message = VrpcAgent._answer({ r, e, i })
|
|
569
|
+
let payload
|
|
514
570
|
try {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
571
|
+
payload = VrpcAgent._stringifySafely(message)
|
|
572
|
+
} catch (err) {
|
|
573
|
+
const what = i.startsWith('__e__') ? 'event' : 'callback'
|
|
574
|
+
this._log.warn(
|
|
575
|
+
`The arguments of ${what === 'event' ? 'an' : 'a'} ${what} to ${topic} cannot be encoded as JSON: ${err.message}`
|
|
519
576
|
)
|
|
577
|
+
if (!(receiverVersion >= 4)) return
|
|
578
|
+
payload = JSON.stringify({
|
|
579
|
+
i,
|
|
580
|
+
e: { message: `The arguments of this ${what} cannot be encoded as JSON: ${err.message}` },
|
|
581
|
+
v
|
|
582
|
+
})
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
this._log.debug(`Forwarding callback to: ${topic} with payload:`, json)
|
|
586
|
+
this._mqttPublish(topic, payload)
|
|
520
587
|
} catch (err) {
|
|
521
588
|
this._log.warn(
|
|
522
589
|
err,
|
|
@@ -525,21 +592,25 @@ class VrpcAgent extends EventEmitter {
|
|
|
525
592
|
}
|
|
526
593
|
}
|
|
527
594
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
595
|
+
/**
|
|
596
|
+
* A message as JSON. Circular structures are cut (json-stringify-safe); a
|
|
597
|
+
* result JSON cannot carry at all (a BigInt) is answered as an error, and
|
|
598
|
+
* an event or a callback that cannot travel is dropped (thrown).
|
|
599
|
+
*/
|
|
600
|
+
static _stringifySafely (message) {
|
|
531
601
|
try {
|
|
532
|
-
|
|
602
|
+
return jsonStringifySafe(message)
|
|
533
603
|
} catch (err) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
604
|
+
if (message.a !== undefined || message.end) throw err
|
|
605
|
+
return JSON.stringify({
|
|
606
|
+
e: { message: `The result cannot be encoded as JSON: ${err.message}` },
|
|
607
|
+
i: message.i,
|
|
608
|
+
v: message.v
|
|
609
|
+
})
|
|
539
610
|
}
|
|
540
|
-
return str
|
|
541
611
|
}
|
|
542
612
|
|
|
613
|
+
|
|
543
614
|
/**
|
|
544
615
|
* Serve first, announce second. The request topics - the statics and
|
|
545
616
|
* the methods of every instance that exists already - are subscribed
|
|
@@ -664,19 +735,30 @@ class VrpcAgent extends EventEmitter {
|
|
|
664
735
|
topics.push(`${this._baseTopic}/${className}/__static__/${func}`)
|
|
665
736
|
})
|
|
666
737
|
})
|
|
738
|
+
// the subscription protocol (version 4) answers on the static topic of
|
|
739
|
+
// any class: withdraw by id, list and end registrations
|
|
740
|
+
for (const func of ['__unsubscribe__', '__registrations__', '__end__']) {
|
|
741
|
+
topics.push(`${this._baseTopic}/+/__static__/${func}`)
|
|
742
|
+
}
|
|
667
743
|
return topics
|
|
668
744
|
}
|
|
669
745
|
|
|
670
746
|
_handleMessage (topic, data) {
|
|
747
|
+
let json
|
|
748
|
+
try {
|
|
749
|
+
json = JSON.parse(data.toString())
|
|
750
|
+
} catch (err) {
|
|
751
|
+
this._log.warn(`Ignoring a message that is no JSON on: ${topic}`)
|
|
752
|
+
return
|
|
753
|
+
}
|
|
671
754
|
try {
|
|
672
|
-
const json = JSON.parse(data.toString())
|
|
673
755
|
this._log.debug(`Message arrived with topic: ${topic} and payload:`, json)
|
|
674
756
|
const tokens = topic.split('/')
|
|
675
757
|
const [, , className, instance, method] = tokens
|
|
676
758
|
|
|
677
759
|
// Special case: clientInfo message
|
|
678
760
|
if (tokens.length === 4 && tokens[3] === '__clientInfo__') {
|
|
679
|
-
this._handleClientInfoMessage(topic, json)
|
|
761
|
+
if (VrpcAgent._isObject(json)) this._handleClientInfoMessage(topic, json)
|
|
680
762
|
return
|
|
681
763
|
}
|
|
682
764
|
|
|
@@ -686,65 +768,56 @@ class VrpcAgent extends EventEmitter {
|
|
|
686
768
|
return
|
|
687
769
|
}
|
|
688
770
|
|
|
771
|
+
// the sender names where answers, callbacks and events go: without a
|
|
772
|
+
// connection id of this domain there is nowhere safe to answer
|
|
773
|
+
if (!VrpcAgent._isObject(json) || !this._isConnectionId(json.s)) {
|
|
774
|
+
this._log.warn(`Ignoring a request without a valid sender on: ${topic}`)
|
|
775
|
+
return
|
|
776
|
+
}
|
|
777
|
+
|
|
689
778
|
// Prepare RPC json
|
|
690
779
|
// SECURITY: take as much as possible params from topic structure as this
|
|
691
780
|
// can be authorized by the broker. Content can't be checked...
|
|
692
781
|
json.c = instance === '__static__' ? className : instance
|
|
693
782
|
json.f = method
|
|
694
783
|
|
|
784
|
+
// what a lifecycle call works on, as it was before the call
|
|
785
|
+
const named = Array.isArray(json.a) ? json.a[0] : undefined
|
|
786
|
+
const before = VrpcAdapter._instances.get(named)
|
|
787
|
+
|
|
695
788
|
// Mutates json and adds return value
|
|
696
789
|
const mustTrack = VrpcAdapter._call(json)
|
|
790
|
+
if (mustTrack) this._watch(json.s)
|
|
697
791
|
|
|
698
|
-
if (
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
if (json.e) {
|
|
706
|
-
// a constructor that threw leaves nothing behind: no instance,
|
|
707
|
-
// no subscription, no lifetime bookkeeping
|
|
708
|
-
this._log.warn(
|
|
709
|
-
`Instantiation of ${className} failed: ${json.e.message}`
|
|
710
|
-
)
|
|
711
|
-
break
|
|
712
|
-
}
|
|
713
|
-
const instanceId = json.r
|
|
714
|
-
// TODO await this
|
|
715
|
-
this._subscribeToMethodsOfNewInstance(className, instanceId)
|
|
716
|
-
this._registerIsolatedInstance(instanceId, json.s)
|
|
717
|
-
break
|
|
792
|
+
if (json.e) {
|
|
793
|
+
if (method === '__createIsolated__' || method === '__createShared__') {
|
|
794
|
+
// a constructor that threw leaves nothing behind: no instance, no
|
|
795
|
+
// subscription, no lifetime bookkeeping
|
|
796
|
+
this._log.warn(
|
|
797
|
+
`Instantiation of ${className} failed: ${json.e.message}`
|
|
798
|
+
)
|
|
718
799
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
)
|
|
800
|
+
} else {
|
|
801
|
+
switch (method) {
|
|
802
|
+
case '__createIsolated__':
|
|
803
|
+
if (!before) this._subscribeToMethodsOfNewInstance(className, json.r)
|
|
804
|
+
this._registerIsolatedInstance(json.r, json.s)
|
|
805
|
+
break
|
|
806
|
+
case '__createShared__':
|
|
807
|
+
if (!before) {
|
|
808
|
+
this._subscribeToMethodsOfNewInstance(className, json.r)
|
|
809
|
+
this._publishClassInfoMessage(className)
|
|
810
|
+
this._publishClassInfoConciseMessage(className)
|
|
811
|
+
}
|
|
812
|
+
// its creator's going is told (clientGone)
|
|
813
|
+
this._watch(json.s)
|
|
814
|
+
break
|
|
815
|
+
case '__delete__':
|
|
816
|
+
if (before) this._forgetInstance(named, before)
|
|
724
817
|
break
|
|
725
|
-
}
|
|
726
|
-
const instanceId = json.r
|
|
727
|
-
if (!this._hasSharedInstance(instanceId)) {
|
|
728
|
-
this._subscribeToMethodsOfNewInstance(className, instanceId)
|
|
729
|
-
this._publishClassInfoMessage(className)
|
|
730
|
-
this._publishClassInfoConciseMessage(className)
|
|
731
|
-
}
|
|
732
|
-
this._registerSharedInstance(instanceId, json.s)
|
|
733
|
-
break
|
|
734
|
-
}
|
|
735
|
-
case '__delete__': {
|
|
736
|
-
this._unsubscribeMethodsOfDeletedInstance(className, instance)
|
|
737
|
-
const wasShared = this._unregisterInstance(json.a[0], json.s)
|
|
738
|
-
if (wasShared) {
|
|
739
|
-
this._publishClassInfoMessage(className)
|
|
740
|
-
this._publishClassInfoConciseMessage(className)
|
|
741
|
-
}
|
|
742
|
-
break
|
|
743
818
|
}
|
|
744
819
|
}
|
|
745
|
-
|
|
746
|
-
const res = e ? { a, r, e, i, v } : { a, r, i, v }
|
|
747
|
-
this._mqttPublish(json.s, VrpcAgent._stringifySafely(res))
|
|
820
|
+
this._mqttPublish(json.s, VrpcAgent._stringifySafely(VrpcAgent._answer(json)))
|
|
748
821
|
} catch (err) {
|
|
749
822
|
this._log.error(
|
|
750
823
|
err,
|
|
@@ -753,6 +826,50 @@ class VrpcAgent extends EventEmitter {
|
|
|
753
826
|
}
|
|
754
827
|
}
|
|
755
828
|
|
|
829
|
+
/**
|
|
830
|
+
* The answer to a request: its result `r` or its error `e`, the request's
|
|
831
|
+
* correlation id `i` and this agent's protocol version - nothing of the
|
|
832
|
+
* request is echoed.
|
|
833
|
+
*/
|
|
834
|
+
static _answer ({ r, e, i }) {
|
|
835
|
+
return e
|
|
836
|
+
? { e, i, v: VRPC_PROTOCOL_VERSION }
|
|
837
|
+
: { r, i, v: VRPC_PROTOCOL_VERSION }
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
static _isObject (json) {
|
|
841
|
+
return !!json && typeof json === 'object' && !Array.isArray(json)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Whether a request's sender is a connection id of this domain:
|
|
846
|
+
* `<domain>/<mqttClientId>/<secret>`, three levels, none empty or a
|
|
847
|
+
* wildcard - and never an agent's info topic, the one other topic of
|
|
848
|
+
* three levels.
|
|
849
|
+
*/
|
|
850
|
+
_isConnectionId (s) {
|
|
851
|
+
if (typeof s !== 'string') return false
|
|
852
|
+
const levels = s.split('/')
|
|
853
|
+
return (
|
|
854
|
+
levels.length === 3 &&
|
|
855
|
+
levels[0] === this._domain &&
|
|
856
|
+
levels[2] !== '__agentInfo__' &&
|
|
857
|
+
levels.every(level => level.length > 0 && !/[+#]/.test(level))
|
|
858
|
+
)
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Watches a connection's presence while it holds anything here: isolated
|
|
863
|
+
* instances, subscriptions, shared instances it created. The watch ends
|
|
864
|
+
* when the connection goes.
|
|
865
|
+
*/
|
|
866
|
+
_watch (connectionId) {
|
|
867
|
+
if (this._watched.has(connectionId)) return
|
|
868
|
+
this._watched.add(connectionId)
|
|
869
|
+
this._mqttSubscribe(`${connectionId}/__clientInfo__`)
|
|
870
|
+
this._log.debug(`Tracking lifetime of client: ${connectionId}`)
|
|
871
|
+
}
|
|
872
|
+
|
|
756
873
|
_handleClientInfoMessage (topic, json) {
|
|
757
874
|
// A client connection went offline. The topic carries the connection
|
|
758
875
|
// id (unique per VrpcClient instance) - the key of all bookkeeping -
|
|
@@ -761,6 +878,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
761
878
|
if (json.status === 'offline') {
|
|
762
879
|
VrpcAdapter._unregisterClient(connectionId)
|
|
763
880
|
const entry = this._isolatedInstances.get(connectionId)
|
|
881
|
+
this._isolatedInstances.delete(connectionId)
|
|
764
882
|
if (entry) {
|
|
765
883
|
entry.forEach(instanceId => {
|
|
766
884
|
const known = VrpcAdapter._instances.get(instanceId)
|
|
@@ -768,7 +886,6 @@ class VrpcAgent extends EventEmitter {
|
|
|
768
886
|
c: known && known.className,
|
|
769
887
|
f: '__delete__',
|
|
770
888
|
a: [instanceId],
|
|
771
|
-
r: null,
|
|
772
889
|
s: VrpcAdapter.LOCAL_SENDER
|
|
773
890
|
}
|
|
774
891
|
VrpcAdapter._call(json)
|
|
@@ -779,6 +896,9 @@ class VrpcAgent extends EventEmitter {
|
|
|
779
896
|
}
|
|
780
897
|
})
|
|
781
898
|
}
|
|
899
|
+
// a connection id is kept across reconnects: whatever the connection
|
|
900
|
+
// holds after it comes back is watched afresh
|
|
901
|
+
this._watched.delete(connectionId)
|
|
782
902
|
this._mqttUnsubscribe(`${connectionId}/__clientInfo__`)
|
|
783
903
|
// clientId: the identity-derived id shared by all connections of one
|
|
784
904
|
// identity (undefined for clients < 3.8.0)
|
|
@@ -786,73 +906,30 @@ class VrpcAgent extends EventEmitter {
|
|
|
786
906
|
}
|
|
787
907
|
}
|
|
788
908
|
|
|
789
|
-
_registerIsolatedInstance (instanceId,
|
|
790
|
-
const entry = this._isolatedInstances.get(
|
|
791
|
-
if (entry)
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
} else {
|
|
795
|
-
// new instance
|
|
796
|
-
this._isolatedInstances.set(clientId, new Set([instanceId]))
|
|
797
|
-
if (!this._sharedInstances.has(clientId)) {
|
|
798
|
-
this._mqttSubscribe(`${clientId}/__clientInfo__`)
|
|
799
|
-
}
|
|
800
|
-
this._log.info(`Tracking lifetime of client: ${clientId}`)
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
_registerSharedInstance (instanceId, clientId) {
|
|
805
|
-
const entry = this._sharedInstances.get(clientId)
|
|
806
|
-
if (entry) {
|
|
807
|
-
// already seen
|
|
808
|
-
entry.add(instanceId)
|
|
809
|
-
} else {
|
|
810
|
-
// new instance
|
|
811
|
-
this._sharedInstances.set(clientId, new Set([instanceId]))
|
|
812
|
-
if (!this._isolatedInstances.has(clientId)) {
|
|
813
|
-
this._mqttSubscribe(`${clientId}/__clientInfo__`)
|
|
814
|
-
}
|
|
815
|
-
this._log.debug(`Tracking lifetime of client: ${clientId}`)
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
_hasSharedInstance (instanceId) {
|
|
820
|
-
for (const [, instances] of this._sharedInstances) {
|
|
821
|
-
if (instances.has(instanceId)) return true
|
|
822
|
-
}
|
|
823
|
-
return false
|
|
909
|
+
_registerIsolatedInstance (instanceId, connectionId) {
|
|
910
|
+
const entry = this._isolatedInstances.get(connectionId)
|
|
911
|
+
if (entry) entry.add(instanceId)
|
|
912
|
+
else this._isolatedInstances.set(connectionId, new Set([instanceId]))
|
|
913
|
+
this._watch(connectionId)
|
|
824
914
|
}
|
|
825
915
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
this._sharedInstances.forEach(async v => {
|
|
839
|
-
if (v.has(instanceId)) {
|
|
840
|
-
found = true
|
|
841
|
-
v.delete(instanceId)
|
|
842
|
-
if (v.size === 0) {
|
|
843
|
-
this._sharedInstances.delete(clientId)
|
|
844
|
-
this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
|
|
845
|
-
this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
|
|
846
|
-
}
|
|
916
|
+
/**
|
|
917
|
+
* A deleted instance leaves the agent: no request topic outlives it, an
|
|
918
|
+
* isolated one leaves its owner's bookkeeping, a shared one leaves its
|
|
919
|
+
* class info.
|
|
920
|
+
*/
|
|
921
|
+
_forgetInstance (instanceId, { className, isIsolated, owner }) {
|
|
922
|
+
this._unsubscribeMethodsOfDeletedInstance(className, instanceId)
|
|
923
|
+
if (isIsolated) {
|
|
924
|
+
const entry = this._isolatedInstances.get(owner)
|
|
925
|
+
if (entry) {
|
|
926
|
+
entry.delete(instanceId)
|
|
927
|
+
if (entry.size === 0) this._isolatedInstances.delete(owner)
|
|
847
928
|
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
this.
|
|
851
|
-
`Unregistering non-tracked (possibly by agent created) instance: ${instanceId}`
|
|
852
|
-
)
|
|
853
|
-
return true
|
|
929
|
+
} else {
|
|
930
|
+
this._publishClassInfoMessage(className)
|
|
931
|
+
this._publishClassInfoConciseMessage(className)
|
|
854
932
|
}
|
|
855
|
-
return true
|
|
856
933
|
}
|
|
857
934
|
|
|
858
935
|
/**
|