vrpc 3.13.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 +5 -5
- package/index.js +7 -7
- package/package.json +25 -15
- package/vrpc/ReconnectBackoff.js +79 -0
- package/vrpc/Registrations.js +850 -0
- package/vrpc/SubscribeRetrier.js +33 -0
- package/vrpc/VrpcAdapter.js +552 -294
- package/vrpc/VrpcAgent.js +242 -154
- package/vrpc/VrpcBrowserAdapter.js +417 -285
- package/vrpc/VrpcBrowserAgent.js +239 -128
- package/vrpc/VrpcClient.js +1221 -233
- 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,13 +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
|
+
const ReconnectBackoff = require('./ReconnectBackoff')
|
|
10
11
|
|
|
11
|
-
const VRPC_PROTOCOL_VERSION =
|
|
12
|
+
const VRPC_PROTOCOL_VERSION = 4
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Agent capable of making existing code available to remote control by clients.
|
|
@@ -69,8 +70,9 @@ class VrpcAgent extends EventEmitter {
|
|
|
69
70
|
default: password
|
|
70
71
|
})
|
|
71
72
|
parser.add_argument('--bestEffort', {
|
|
72
|
-
help: '
|
|
73
|
-
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
|
|
74
76
|
})
|
|
75
77
|
parser.add_argument('-V', '-v', '--userVersion', {
|
|
76
78
|
help: 'User defined agent version. May be checked on the remote side for compatibility checks.',
|
|
@@ -81,7 +83,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
81
83
|
parser.add_argument('--version', {
|
|
82
84
|
help: 'Returns the VRPC version this client was built with.',
|
|
83
85
|
action: 'version',
|
|
84
|
-
version: '
|
|
86
|
+
version: `v${require('../package.json').version}`
|
|
85
87
|
})
|
|
86
88
|
const args = parser.parse_args()
|
|
87
89
|
return new VrpcAgent(args)
|
|
@@ -96,10 +98,12 @@ class VrpcAgent extends EventEmitter {
|
|
|
96
98
|
* @param {String} [obj.password] MQTT password (if no token is provided)
|
|
97
99
|
* @param {String} [obj.token] Access token
|
|
98
100
|
* @param {String} [obj.domain='vrpc'] The domain under which the agent-provided code is reachable
|
|
99
|
-
* @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
|
|
100
104
|
* @param {String} [obj.broker='mqtts://broker.hivemq.com:8883'] Broker url in form: `<scheme>://<host>:<port>`
|
|
101
105
|
* @param {Object} [obj.log=console] Log object (must support debug, info, warn, and error level)
|
|
102
|
-
* @param {
|
|
106
|
+
* @param {Boolean} [obj.bestEffort=true] If true, messages are sent at QoS 0 (best effort), else at QoS 1
|
|
103
107
|
* @param {String} [obj.version=''] The (user-defined) version of this agent
|
|
104
108
|
* @param {String} [obj.mqttClientId='<generated()>'] Explicitly set the mqtt client id.
|
|
105
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.
|
|
@@ -152,9 +156,24 @@ class VrpcAgent extends EventEmitter {
|
|
|
152
156
|
`Dispose of deleted instance ${className}::${instance} failed: ${error.message}`
|
|
153
157
|
)
|
|
154
158
|
VrpcAdapter.on('disposeFailed', this._handleDisposeFailed)
|
|
155
|
-
//
|
|
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)
|
|
156
174
|
this._isolatedInstances = new Map()
|
|
157
|
-
|
|
175
|
+
// connection ids whose presence is watched (see _watch)
|
|
176
|
+
this._watched = new Set()
|
|
158
177
|
// refused subscriptions (SUBACK qos=128) are retried until granted
|
|
159
178
|
this._subscribeRetrier = new SubscribeRetrier({
|
|
160
179
|
subscribe: (topics, options) => this._mqttSubscribe(topics, options),
|
|
@@ -221,6 +240,8 @@ class VrpcAgent extends EventEmitter {
|
|
|
221
240
|
this._log.info(`Broker : ${this._broker}`)
|
|
222
241
|
this._log.info('Connecting to MQTT server...')
|
|
223
242
|
this._client = mqtt.connect(this._broker, this._options)
|
|
243
|
+
// a refused agent knocks less and less often, not once a second
|
|
244
|
+
this._backoff = new ReconnectBackoff(this._client)
|
|
224
245
|
this._client.on('connect', this._handleConnect.bind(this))
|
|
225
246
|
this._client.on('reconnect', this._handleReconnect.bind(this))
|
|
226
247
|
this._client.on('error', this._handleError.bind(this))
|
|
@@ -240,10 +261,20 @@ class VrpcAgent extends EventEmitter {
|
|
|
240
261
|
*/
|
|
241
262
|
async end ({ unregister = false } = {}) {
|
|
242
263
|
VrpcAdapter.off('disposeFailed', this._handleDisposeFailed)
|
|
264
|
+
VrpcAdapter.off('releaseFailed', this._handleReleaseFailed)
|
|
265
|
+
VrpcAdapter.off('greetFailed', this._handleGreetFailed)
|
|
243
266
|
this._subscribeRetrier.cancel()
|
|
244
267
|
try {
|
|
245
268
|
if (!this._client || !this._client.connected) {
|
|
246
|
-
|
|
269
|
+
// a client that is still trying (refused, broker away) stops
|
|
270
|
+
// trying: end() is how a refusal becomes final
|
|
271
|
+
// (its 'end' is forwarded as the agent's)
|
|
272
|
+
if (this._client) {
|
|
273
|
+
const client = this._client
|
|
274
|
+
await new Promise(resolve => client.end(true, {}, resolve))
|
|
275
|
+
} else {
|
|
276
|
+
this.emit('end')
|
|
277
|
+
}
|
|
247
278
|
return
|
|
248
279
|
}
|
|
249
280
|
const agentTopic = `${this._baseTopic}/__agentInfo__`
|
|
@@ -253,11 +284,16 @@ class VrpcAgent extends EventEmitter {
|
|
|
253
284
|
{ retain: true }
|
|
254
285
|
)
|
|
255
286
|
if (unregister) {
|
|
287
|
+
// empty retained messages clear what the broker keeps of this agent
|
|
256
288
|
this._mqttPublish(agentTopic, null, { retain: true })
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
}
|
|
261
297
|
}
|
|
262
298
|
}
|
|
263
299
|
await new Promise(resolve => this._client.end(false, {}, resolve))
|
|
@@ -282,12 +318,13 @@ class VrpcAgent extends EventEmitter {
|
|
|
282
318
|
* @returns {Object} The real instance (not a proxy!)
|
|
283
319
|
*/
|
|
284
320
|
create ({ className, instance = nanoid(8), args = [], isIsolated = false }) {
|
|
321
|
+
const existed = VrpcAdapter._instances.has(instance)
|
|
285
322
|
const obj = VrpcAdapter.create({ className, instance, args, isIsolated })
|
|
286
323
|
// An agent that is not served yet only collects: once the connection
|
|
287
324
|
// stands, the connect handler subscribes every adapter instance and
|
|
288
325
|
// publishes every class info, so a caller may build its full state
|
|
289
326
|
// before going online and "online" then means "complete"
|
|
290
|
-
if (this._client && !
|
|
327
|
+
if (this._client && !existed) {
|
|
291
328
|
this._subscribeToMethodsOfNewInstance(className, instance)
|
|
292
329
|
this._publishClassInfoMessage(className)
|
|
293
330
|
this._publishClassInfoConciseMessage(className)
|
|
@@ -325,10 +362,22 @@ class VrpcAgent extends EventEmitter {
|
|
|
325
362
|
return options
|
|
326
363
|
}
|
|
327
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
|
+
*/
|
|
328
372
|
static _generateAgentName () {
|
|
329
373
|
const { username } = os.userInfo()
|
|
330
|
-
const
|
|
331
|
-
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)
|
|
332
381
|
}
|
|
333
382
|
|
|
334
383
|
_generateToken () {
|
|
@@ -401,16 +450,19 @@ class VrpcAgent extends EventEmitter {
|
|
|
401
450
|
this._client.subscribe(
|
|
402
451
|
topic,
|
|
403
452
|
{ qos: this._qos, ...options },
|
|
404
|
-
(
|
|
405
|
-
|
|
453
|
+
(subscribeError, subs) => {
|
|
454
|
+
const { refused, granted, error } = SubscribeRetrier.outcome(
|
|
455
|
+
subscribeError,
|
|
456
|
+
subs
|
|
457
|
+
)
|
|
458
|
+
if (error) {
|
|
406
459
|
this._log.warn(
|
|
407
|
-
`Could not subscribe to topic(s) '${topic}', because: ${
|
|
460
|
+
`Could not subscribe to topic(s) '${topic}', because: ${error.message}`
|
|
408
461
|
)
|
|
409
462
|
} else {
|
|
463
|
+
let err
|
|
410
464
|
const topicArray = Array.isArray(topic) ? topic : [topic]
|
|
411
|
-
const erroneousGranted =
|
|
412
|
-
.filter(x => x.qos === 128)
|
|
413
|
-
.map(x => x.topic)
|
|
465
|
+
const erroneousGranted = refused
|
|
414
466
|
if (erroneousGranted.length > 0) {
|
|
415
467
|
err = new Error(
|
|
416
468
|
`Could not subscribe all ${topicArray.length} topic(s) but got error qos=128 on following ${erroneousGranted.length} topic(s): ${erroneousGranted}`
|
|
@@ -421,9 +473,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
421
473
|
this.emit('error', err)
|
|
422
474
|
this._subscribeRetrier.schedule(erroneousGranted, options)
|
|
423
475
|
}
|
|
424
|
-
this._subscribeRetrier.granted(
|
|
425
|
-
granted.filter(x => x.qos !== 128).map(x => x.topic)
|
|
426
|
-
)
|
|
476
|
+
this._subscribeRetrier.granted(granted.map(x => x.topic))
|
|
427
477
|
const reducedQos = granted.filter(x => x.qos < this._qos)
|
|
428
478
|
if (reducedQos.length > 0) {
|
|
429
479
|
err = new Error(
|
|
@@ -440,7 +490,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
440
490
|
this._log.warn(err)
|
|
441
491
|
this.emit('error', err)
|
|
442
492
|
}
|
|
443
|
-
if (granted.length === 0) {
|
|
493
|
+
if (granted.length === 0 && refused.length === 0) {
|
|
444
494
|
this._log.debug(`Already subscribed to topic '${topic}'`)
|
|
445
495
|
}
|
|
446
496
|
}
|
|
@@ -497,15 +547,43 @@ class VrpcAgent extends EventEmitter {
|
|
|
497
547
|
})
|
|
498
548
|
}
|
|
499
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
|
+
*/
|
|
500
561
|
_handleVrpcCallback (json) {
|
|
501
|
-
const { s, i, r, e, a } = json
|
|
562
|
+
const { s, i, r, e, a, end, receiverVersion } = json
|
|
502
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
|
|
503
570
|
try {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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}`
|
|
508
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)
|
|
509
587
|
} catch (err) {
|
|
510
588
|
this._log.warn(
|
|
511
589
|
err,
|
|
@@ -514,21 +592,25 @@ class VrpcAgent extends EventEmitter {
|
|
|
514
592
|
}
|
|
515
593
|
}
|
|
516
594
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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) {
|
|
520
601
|
try {
|
|
521
|
-
|
|
602
|
+
return jsonStringifySafe(message)
|
|
522
603
|
} catch (err) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
+
})
|
|
528
610
|
}
|
|
529
|
-
return str
|
|
530
611
|
}
|
|
531
612
|
|
|
613
|
+
|
|
532
614
|
/**
|
|
533
615
|
* Serve first, announce second. The request topics - the statics and
|
|
534
616
|
* the methods of every instance that exists already - are subscribed
|
|
@@ -653,19 +735,30 @@ class VrpcAgent extends EventEmitter {
|
|
|
653
735
|
topics.push(`${this._baseTopic}/${className}/__static__/${func}`)
|
|
654
736
|
})
|
|
655
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
|
+
}
|
|
656
743
|
return topics
|
|
657
744
|
}
|
|
658
745
|
|
|
659
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
|
+
}
|
|
660
754
|
try {
|
|
661
|
-
const json = JSON.parse(data.toString())
|
|
662
755
|
this._log.debug(`Message arrived with topic: ${topic} and payload:`, json)
|
|
663
756
|
const tokens = topic.split('/')
|
|
664
757
|
const [, , className, instance, method] = tokens
|
|
665
758
|
|
|
666
759
|
// Special case: clientInfo message
|
|
667
760
|
if (tokens.length === 4 && tokens[3] === '__clientInfo__') {
|
|
668
|
-
this._handleClientInfoMessage(topic, json)
|
|
761
|
+
if (VrpcAgent._isObject(json)) this._handleClientInfoMessage(topic, json)
|
|
669
762
|
return
|
|
670
763
|
}
|
|
671
764
|
|
|
@@ -675,65 +768,56 @@ class VrpcAgent extends EventEmitter {
|
|
|
675
768
|
return
|
|
676
769
|
}
|
|
677
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
|
+
|
|
678
778
|
// Prepare RPC json
|
|
679
779
|
// SECURITY: take as much as possible params from topic structure as this
|
|
680
780
|
// can be authorized by the broker. Content can't be checked...
|
|
681
781
|
json.c = instance === '__static__' ? className : instance
|
|
682
782
|
json.f = method
|
|
683
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
|
+
|
|
684
788
|
// Mutates json and adds return value
|
|
685
789
|
const mustTrack = VrpcAdapter._call(json)
|
|
790
|
+
if (mustTrack) this._watch(json.s)
|
|
686
791
|
|
|
687
|
-
if (
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
if (json.e) {
|
|
695
|
-
// a constructor that threw leaves nothing behind: no instance,
|
|
696
|
-
// no subscription, no lifetime bookkeeping
|
|
697
|
-
this._log.warn(
|
|
698
|
-
`Instantiation of ${className} failed: ${json.e.message}`
|
|
699
|
-
)
|
|
700
|
-
break
|
|
701
|
-
}
|
|
702
|
-
const instanceId = json.r
|
|
703
|
-
// TODO await this
|
|
704
|
-
this._subscribeToMethodsOfNewInstance(className, instanceId)
|
|
705
|
-
this._registerIsolatedInstance(instanceId, json.s)
|
|
706
|
-
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
|
+
)
|
|
707
799
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
)
|
|
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)
|
|
713
817
|
break
|
|
714
|
-
}
|
|
715
|
-
const instanceId = json.r
|
|
716
|
-
if (!this._hasSharedInstance(instanceId)) {
|
|
717
|
-
this._subscribeToMethodsOfNewInstance(className, instanceId)
|
|
718
|
-
this._publishClassInfoMessage(className)
|
|
719
|
-
this._publishClassInfoConciseMessage(className)
|
|
720
|
-
}
|
|
721
|
-
this._registerSharedInstance(instanceId, json.s)
|
|
722
|
-
break
|
|
723
|
-
}
|
|
724
|
-
case '__delete__': {
|
|
725
|
-
this._unsubscribeMethodsOfDeletedInstance(className, instance)
|
|
726
|
-
const wasShared = this._unregisterInstance(json.a[0], json.s)
|
|
727
|
-
if (wasShared) {
|
|
728
|
-
this._publishClassInfoMessage(className)
|
|
729
|
-
this._publishClassInfoConciseMessage(className)
|
|
730
|
-
}
|
|
731
|
-
break
|
|
732
818
|
}
|
|
733
819
|
}
|
|
734
|
-
|
|
735
|
-
const res = e ? { a, r, e, i, v } : { a, r, i, v }
|
|
736
|
-
this._mqttPublish(json.s, VrpcAgent._stringifySafely(res))
|
|
820
|
+
this._mqttPublish(json.s, VrpcAgent._stringifySafely(VrpcAgent._answer(json)))
|
|
737
821
|
} catch (err) {
|
|
738
822
|
this._log.error(
|
|
739
823
|
err,
|
|
@@ -742,6 +826,50 @@ class VrpcAgent extends EventEmitter {
|
|
|
742
826
|
}
|
|
743
827
|
}
|
|
744
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
|
+
|
|
745
873
|
_handleClientInfoMessage (topic, json) {
|
|
746
874
|
// A client connection went offline. The topic carries the connection
|
|
747
875
|
// id (unique per VrpcClient instance) - the key of all bookkeeping -
|
|
@@ -750,6 +878,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
750
878
|
if (json.status === 'offline') {
|
|
751
879
|
VrpcAdapter._unregisterClient(connectionId)
|
|
752
880
|
const entry = this._isolatedInstances.get(connectionId)
|
|
881
|
+
this._isolatedInstances.delete(connectionId)
|
|
753
882
|
if (entry) {
|
|
754
883
|
entry.forEach(instanceId => {
|
|
755
884
|
const known = VrpcAdapter._instances.get(instanceId)
|
|
@@ -757,7 +886,6 @@ class VrpcAgent extends EventEmitter {
|
|
|
757
886
|
c: known && known.className,
|
|
758
887
|
f: '__delete__',
|
|
759
888
|
a: [instanceId],
|
|
760
|
-
r: null,
|
|
761
889
|
s: VrpcAdapter.LOCAL_SENDER
|
|
762
890
|
}
|
|
763
891
|
VrpcAdapter._call(json)
|
|
@@ -768,6 +896,9 @@ class VrpcAgent extends EventEmitter {
|
|
|
768
896
|
}
|
|
769
897
|
})
|
|
770
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)
|
|
771
902
|
this._mqttUnsubscribe(`${connectionId}/__clientInfo__`)
|
|
772
903
|
// clientId: the identity-derived id shared by all connections of one
|
|
773
904
|
// identity (undefined for clients < 3.8.0)
|
|
@@ -775,73 +906,30 @@ class VrpcAgent extends EventEmitter {
|
|
|
775
906
|
}
|
|
776
907
|
}
|
|
777
908
|
|
|
778
|
-
_registerIsolatedInstance (instanceId,
|
|
779
|
-
const entry = this._isolatedInstances.get(
|
|
780
|
-
if (entry)
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
} else {
|
|
784
|
-
// new instance
|
|
785
|
-
this._isolatedInstances.set(clientId, new Set([instanceId]))
|
|
786
|
-
if (!this._sharedInstances.has(clientId)) {
|
|
787
|
-
this._mqttSubscribe(`${clientId}/__clientInfo__`)
|
|
788
|
-
}
|
|
789
|
-
this._log.info(`Tracking lifetime of client: ${clientId}`)
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
_registerSharedInstance (instanceId, clientId) {
|
|
794
|
-
const entry = this._sharedInstances.get(clientId)
|
|
795
|
-
if (entry) {
|
|
796
|
-
// already seen
|
|
797
|
-
entry.add(instanceId)
|
|
798
|
-
} else {
|
|
799
|
-
// new instance
|
|
800
|
-
this._sharedInstances.set(clientId, new Set([instanceId]))
|
|
801
|
-
if (!this._isolatedInstances.has(clientId)) {
|
|
802
|
-
this._mqttSubscribe(`${clientId}/__clientInfo__`)
|
|
803
|
-
}
|
|
804
|
-
this._log.debug(`Tracking lifetime of client: ${clientId}`)
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
_hasSharedInstance (instanceId) {
|
|
809
|
-
for (const [, instances] of this._sharedInstances) {
|
|
810
|
-
if (instances.has(instanceId)) return true
|
|
811
|
-
}
|
|
812
|
-
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)
|
|
813
914
|
}
|
|
814
915
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
this._sharedInstances.forEach(async v => {
|
|
828
|
-
if (v.has(instanceId)) {
|
|
829
|
-
found = true
|
|
830
|
-
v.delete(instanceId)
|
|
831
|
-
if (v.size === 0) {
|
|
832
|
-
this._sharedInstances.delete(clientId)
|
|
833
|
-
this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
|
|
834
|
-
this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
|
|
835
|
-
}
|
|
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)
|
|
836
928
|
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
this.
|
|
840
|
-
`Unregistering non-tracked (possibly by agent created) instance: ${instanceId}`
|
|
841
|
-
)
|
|
842
|
-
return true
|
|
929
|
+
} else {
|
|
930
|
+
this._publishClassInfoMessage(className)
|
|
931
|
+
this._publishClassInfoConciseMessage(className)
|
|
843
932
|
}
|
|
844
|
-
return true
|
|
845
933
|
}
|
|
846
934
|
|
|
847
935
|
/**
|