vrpc 3.14.0 → 3.16.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 +1 -7
- 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 +556 -294
- package/vrpc/VrpcAgent.js +230 -153
- package/vrpc/VrpcBrowserAdapter.js +435 -286
- package/vrpc/VrpcBrowserAgent.js +239 -128
- package/vrpc/VrpcClient.js +1190 -224
- 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/VrpcBrowserAgent.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
const os = require('os')
|
|
2
2
|
const mqtt = require('mqtt')
|
|
3
3
|
const VrpcAdapter = require('./VrpcBrowserAdapter')
|
|
4
|
+
const SubscribeRetrier = require('./SubscribeRetrier')
|
|
5
|
+
const ReconnectBackoff = require('./ReconnectBackoff')
|
|
4
6
|
const EventEmitter = require('events')
|
|
5
7
|
const { nanoid } = require('nanoid')
|
|
6
8
|
|
|
7
|
-
const VRPC_PROTOCOL_VERSION =
|
|
9
|
+
const VRPC_PROTOCOL_VERSION = 4
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Agent capable of making existing code available to remote control by clients.
|
|
@@ -21,10 +23,11 @@ class VrpcAgent extends EventEmitter {
|
|
|
21
23
|
* @param {String} [obj.password] MQTT password (if no token is provided)
|
|
22
24
|
* @param {String} [obj.token] Access token
|
|
23
25
|
* @param {String} [obj.domain='vrpc'] The domain under which the agent-provided code is reachable
|
|
24
|
-
* @param {String} [obj.agent='<
|
|
26
|
+
* @param {String} [obj.agent='<random>'] This agent's name; the default is
|
|
27
|
+
* eight random characters, new on every start
|
|
25
28
|
* @param {String} [obj.broker='mqtts://broker.hivemq.com:8883'] Broker url in form: `<scheme>://<host>:<port>`
|
|
26
29
|
* @param {Object} [obj.log=console] Log object (must support debug, info, warn, and error level)
|
|
27
|
-
* @param {
|
|
30
|
+
* @param {Boolean} [obj.bestEffort=true] If true, messages are sent at QoS 0 (best effort), else at QoS 1
|
|
28
31
|
* @param {String} [obj.version=''] The (user-defined) version of this agent
|
|
29
32
|
* @param {String} [obj.mqttClientId='<generated()>'] Explicitly set the mqtt client id.
|
|
30
33
|
*
|
|
@@ -42,7 +45,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
42
45
|
agent = nanoid(8),
|
|
43
46
|
broker = 'mqtts://broker.hivemq.com:8883',
|
|
44
47
|
log = 'console',
|
|
45
|
-
bestEffort =
|
|
48
|
+
bestEffort = true,
|
|
46
49
|
version = '',
|
|
47
50
|
mqttClientId = null
|
|
48
51
|
} = {}) {
|
|
@@ -75,9 +78,29 @@ class VrpcAgent extends EventEmitter {
|
|
|
75
78
|
`Dispose of deleted instance ${className}::${instance} failed: ${error.message}`
|
|
76
79
|
)
|
|
77
80
|
VrpcAdapter.on('disposeFailed', this._handleDisposeFailed)
|
|
78
|
-
//
|
|
81
|
+
// a registration whose release failed is gone from the book anyway
|
|
82
|
+
this._handleReleaseFailed = ({ context, error, ...rest }) =>
|
|
83
|
+
this._log.warn(
|
|
84
|
+
error,
|
|
85
|
+
`Release of registration ${context}::${rest.function} failed: ${error && error.message}`
|
|
86
|
+
)
|
|
87
|
+
VrpcAdapter.on('releaseFailed', this._handleReleaseFailed)
|
|
88
|
+
// a greeting that failed leaves its subscription in place
|
|
89
|
+
this._handleGreetFailed = ({ context, error, ...rest }) =>
|
|
90
|
+
this._log.warn(
|
|
91
|
+
error,
|
|
92
|
+
`Greeting of a subscriber to ${context}::${rest.function} failed: ${error && error.message}`
|
|
93
|
+
)
|
|
94
|
+
VrpcAdapter.on('greetFailed', this._handleGreetFailed)
|
|
95
|
+
// connection id -> Set(isolated instance ids it owns)
|
|
79
96
|
this._isolatedInstances = new Map()
|
|
80
|
-
|
|
97
|
+
// connection ids whose presence is watched (see _watch)
|
|
98
|
+
this._watched = new Set()
|
|
99
|
+
// refused subscriptions are retried until granted
|
|
100
|
+
this._subscribeRetrier = new SubscribeRetrier({
|
|
101
|
+
subscribe: (topics, options) => this._mqttSubscribe(topics, options),
|
|
102
|
+
log: this._log
|
|
103
|
+
})
|
|
81
104
|
// counts the connections: an announcement belongs to the connection
|
|
82
105
|
// that prepared it (see _handleConnect)
|
|
83
106
|
this._connectGeneration = 0
|
|
@@ -121,6 +144,10 @@ class VrpcAgent extends EventEmitter {
|
|
|
121
144
|
password,
|
|
122
145
|
keepalive: 30,
|
|
123
146
|
connectTimeout: 10 * 1000,
|
|
147
|
+
// a refused CONNACK is retried like any other failed attempt: an
|
|
148
|
+
// authorization service that is briefly away refuses, too, and an
|
|
149
|
+
// agent must come back when it does (mqtt >= 5 stops otherwise)
|
|
150
|
+
reconnectOnConnackError: true,
|
|
124
151
|
clientId: this._mqttClientId,
|
|
125
152
|
rejectUnauthorized: false,
|
|
126
153
|
will: {
|
|
@@ -135,6 +162,8 @@ class VrpcAgent extends EventEmitter {
|
|
|
135
162
|
this._log.info(`Broker : ${this._broker}`)
|
|
136
163
|
this._log.info('Connecting to MQTT server...')
|
|
137
164
|
this._client = mqtt.connect(this._broker, this._options)
|
|
165
|
+
// a refused agent knocks less and less often, not once a second
|
|
166
|
+
this._backoff = new ReconnectBackoff(this._client)
|
|
138
167
|
this._client.on('connect', this._handleConnect.bind(this))
|
|
139
168
|
this._client.on('reconnect', this._handleReconnect.bind(this))
|
|
140
169
|
this._client.on('error', this._handleError.bind(this))
|
|
@@ -154,8 +183,16 @@ class VrpcAgent extends EventEmitter {
|
|
|
154
183
|
*/
|
|
155
184
|
async end ({ unregister = false } = {}) {
|
|
156
185
|
VrpcAdapter.off('disposeFailed', this._handleDisposeFailed)
|
|
186
|
+
VrpcAdapter.off('releaseFailed', this._handleReleaseFailed)
|
|
187
|
+
VrpcAdapter.off('greetFailed', this._handleGreetFailed)
|
|
188
|
+
this._subscribeRetrier.cancel()
|
|
157
189
|
try {
|
|
158
190
|
if (!this._client || !this._client.connected) {
|
|
191
|
+
// a client that is still trying stops trying: end() is final
|
|
192
|
+
if (this._client) {
|
|
193
|
+
const client = this._client
|
|
194
|
+
await new Promise(resolve => client.end(true, {}, resolve))
|
|
195
|
+
}
|
|
159
196
|
this.emit('end')
|
|
160
197
|
return
|
|
161
198
|
}
|
|
@@ -166,11 +203,16 @@ class VrpcAgent extends EventEmitter {
|
|
|
166
203
|
{ retain: true }
|
|
167
204
|
)
|
|
168
205
|
if (unregister) {
|
|
206
|
+
// empty retained messages clear what the broker keeps of this agent
|
|
169
207
|
this._mqttPublish(agentTopic, null, { retain: true })
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
208
|
+
for (const className of this._getClasses()) {
|
|
209
|
+
for (const info of ['__classInfo__', '__classInfoConcise__']) {
|
|
210
|
+
this._mqttPublish(
|
|
211
|
+
`${this._baseTopic}/${className}/${info}`,
|
|
212
|
+
null,
|
|
213
|
+
{ retain: true }
|
|
214
|
+
)
|
|
215
|
+
}
|
|
174
216
|
}
|
|
175
217
|
}
|
|
176
218
|
await new Promise(resolve => this._client.end(false, {}, resolve))
|
|
@@ -248,16 +290,19 @@ class VrpcAgent extends EventEmitter {
|
|
|
248
290
|
this._client.subscribe(
|
|
249
291
|
topic,
|
|
250
292
|
{ qos: this._qos, ...options },
|
|
251
|
-
(
|
|
252
|
-
|
|
293
|
+
(subscribeError, subs) => {
|
|
294
|
+
const { refused, granted, error } = SubscribeRetrier.outcome(
|
|
295
|
+
subscribeError,
|
|
296
|
+
subs
|
|
297
|
+
)
|
|
298
|
+
if (error) {
|
|
253
299
|
this._log.warn(
|
|
254
|
-
`Could not subscribe to topic(s) '${topic}', because: ${
|
|
300
|
+
`Could not subscribe to topic(s) '${topic}', because: ${error.message}`
|
|
255
301
|
)
|
|
256
302
|
} else {
|
|
303
|
+
let err
|
|
257
304
|
const topicArray = Array.isArray(topic) ? topic : [topic]
|
|
258
|
-
const erroneousGranted =
|
|
259
|
-
.filter(x => x.qos === 128)
|
|
260
|
-
.map(x => x.topic)
|
|
305
|
+
const erroneousGranted = refused
|
|
261
306
|
if (erroneousGranted.length > 0) {
|
|
262
307
|
err = new Error(
|
|
263
308
|
`Could not subscribe all ${topicArray.length} topic(s) but got error qos=128 on following ${erroneousGranted.length} topic(s): ${erroneousGranted}`
|
|
@@ -266,7 +311,9 @@ class VrpcAgent extends EventEmitter {
|
|
|
266
311
|
err.subscribeOptions = options
|
|
267
312
|
this._log.error(err)
|
|
268
313
|
this.emit('error', err)
|
|
314
|
+
this._subscribeRetrier.schedule(erroneousGranted, options)
|
|
269
315
|
}
|
|
316
|
+
this._subscribeRetrier.granted(granted.map(x => x.topic))
|
|
270
317
|
const reducedQos = granted.filter(x => x.qos < this._qos)
|
|
271
318
|
if (reducedQos.length > 0) {
|
|
272
319
|
err = new Error(
|
|
@@ -338,15 +385,43 @@ class VrpcAgent extends EventEmitter {
|
|
|
338
385
|
})
|
|
339
386
|
}
|
|
340
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Publishes what the adapter sends on its own: a callback or an event
|
|
390
|
+
* `{ a, i, v }`, the second answer of a promise `{ r | e, i, v }`, an
|
|
391
|
+
* ended notice `{ i, end, v }`. Events and notices go to the topic their
|
|
392
|
+
* subscription id names, everything else to the sender.
|
|
393
|
+
*
|
|
394
|
+
* An event or a callback whose arguments JSON cannot carry (a BigInt) is
|
|
395
|
+
* not dropped silently: a receiver of protocol 4 gets `{ i, e, v }`
|
|
396
|
+
* instead (EV-22, CALL-10). An older one would call its handler without
|
|
397
|
+
* arguments, so it gets nothing, and the agent logs a warning.
|
|
398
|
+
*/
|
|
341
399
|
_handleVrpcCallback (json) {
|
|
342
|
-
const { s, i, r, e, a } = json
|
|
400
|
+
const { s, i, r, e, a, end, receiverVersion } = json
|
|
343
401
|
const topic = i.startsWith('__e__') ? i.slice(5) : s
|
|
402
|
+
const v = VRPC_PROTOCOL_VERSION
|
|
403
|
+
let message
|
|
404
|
+
if (end) message = { i, end, v }
|
|
405
|
+
else if (a !== undefined) message = { a, i, v }
|
|
406
|
+
else message = VrpcAgent._answer({ r, e, i })
|
|
407
|
+
let payload
|
|
344
408
|
try {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
409
|
+
payload = VrpcAgent._stringifySafely(message)
|
|
410
|
+
} catch (err) {
|
|
411
|
+
const what = i.startsWith('__e__') ? 'event' : 'callback'
|
|
412
|
+
this._log.warn(
|
|
413
|
+
`The arguments of ${what === 'event' ? 'an' : 'a'} ${what} to ${topic} cannot be encoded as JSON: ${err.message}`
|
|
349
414
|
)
|
|
415
|
+
if (!(receiverVersion >= 4)) return
|
|
416
|
+
payload = JSON.stringify({
|
|
417
|
+
i,
|
|
418
|
+
e: { message: `The arguments of this ${what} cannot be encoded as JSON: ${err.message}` },
|
|
419
|
+
v
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
this._log.debug(`Forwarding callback to: ${topic} with payload:`, json)
|
|
424
|
+
this._mqttPublish(topic, payload)
|
|
350
425
|
} catch (err) {
|
|
351
426
|
this._log.warn(
|
|
352
427
|
err,
|
|
@@ -355,19 +430,22 @@ class VrpcAgent extends EventEmitter {
|
|
|
355
430
|
}
|
|
356
431
|
}
|
|
357
432
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
433
|
+
/**
|
|
434
|
+
* A message as JSON. A result JSON cannot carry (a circular structure, a
|
|
435
|
+
* BigInt) is answered as an error; an event or a callback that cannot
|
|
436
|
+
* travel is dropped (thrown).
|
|
437
|
+
*/
|
|
438
|
+
static _stringifySafely (message) {
|
|
361
439
|
try {
|
|
362
|
-
|
|
440
|
+
return JSON.stringify(message)
|
|
363
441
|
} catch (err) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
442
|
+
if (message.a !== undefined || message.end) throw err
|
|
443
|
+
return JSON.stringify({
|
|
444
|
+
e: { message: `The result cannot be encoded as JSON: ${err.message}` },
|
|
445
|
+
i: message.i,
|
|
446
|
+
v: message.v
|
|
447
|
+
})
|
|
369
448
|
}
|
|
370
|
-
return str
|
|
371
449
|
}
|
|
372
450
|
|
|
373
451
|
/**
|
|
@@ -380,6 +458,8 @@ class VrpcAgent extends EventEmitter {
|
|
|
380
458
|
*/
|
|
381
459
|
_handleConnect () {
|
|
382
460
|
this._log.info('[OK]')
|
|
461
|
+
// everything is subscribed afresh: pending retries are moot
|
|
462
|
+
this._subscribeRetrier.cancel()
|
|
383
463
|
const generation = ++this._connectGeneration
|
|
384
464
|
const batches = []
|
|
385
465
|
try {
|
|
@@ -430,6 +510,11 @@ class VrpcAgent extends EventEmitter {
|
|
|
430
510
|
)
|
|
431
511
|
}
|
|
432
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Publishes the class info twice, as every agent does: in full
|
|
515
|
+
* (`__classInfo__`, with meta data) and concise (`__classInfoConcise__`,
|
|
516
|
+
* without), which is what clients subscribe to by default.
|
|
517
|
+
*/
|
|
433
518
|
_publishClassInfoMessage (className) {
|
|
434
519
|
const json = {
|
|
435
520
|
className,
|
|
@@ -439,12 +524,18 @@ class VrpcAgent extends EventEmitter {
|
|
|
439
524
|
meta: this._getMetaData(className),
|
|
440
525
|
v: VRPC_PROTOCOL_VERSION
|
|
441
526
|
}
|
|
527
|
+
const { meta, ...concise } = json
|
|
442
528
|
try {
|
|
443
529
|
this._mqttPublish(
|
|
444
530
|
`${this._baseTopic}/${className}/__classInfo__`,
|
|
445
531
|
JSON.stringify(json),
|
|
446
532
|
{ retain: true }
|
|
447
533
|
)
|
|
534
|
+
this._mqttPublish(
|
|
535
|
+
`${this._baseTopic}/${className}/__classInfoConcise__`,
|
|
536
|
+
JSON.stringify(concise),
|
|
537
|
+
{ retain: true }
|
|
538
|
+
)
|
|
448
539
|
} catch (err) {
|
|
449
540
|
this._log.error(
|
|
450
541
|
err,
|
|
@@ -467,19 +558,30 @@ class VrpcAgent extends EventEmitter {
|
|
|
467
558
|
topics.push(`${this._baseTopic}/${className}/__static__/${func}`)
|
|
468
559
|
})
|
|
469
560
|
})
|
|
561
|
+
// the subscription protocol (version 4) answers on the static topic of
|
|
562
|
+
// any class: withdraw by id, list and end registrations
|
|
563
|
+
for (const func of ['__unsubscribe__', '__registrations__', '__end__']) {
|
|
564
|
+
topics.push(`${this._baseTopic}/+/__static__/${func}`)
|
|
565
|
+
}
|
|
470
566
|
return topics
|
|
471
567
|
}
|
|
472
568
|
|
|
473
569
|
_handleMessage (topic, data) {
|
|
570
|
+
let json
|
|
571
|
+
try {
|
|
572
|
+
json = JSON.parse(data.toString())
|
|
573
|
+
} catch (err) {
|
|
574
|
+
this._log.warn(`Ignoring a message that is no JSON on: ${topic}`)
|
|
575
|
+
return
|
|
576
|
+
}
|
|
474
577
|
try {
|
|
475
|
-
const json = JSON.parse(data.toString())
|
|
476
578
|
this._log.debug(`Message arrived with topic: ${topic} and payload:`, json)
|
|
477
579
|
const tokens = topic.split('/')
|
|
478
580
|
const [, , className, instance, method] = tokens
|
|
479
581
|
|
|
480
582
|
// Special case: clientInfo message
|
|
481
583
|
if (tokens.length === 4 && tokens[3] === '__clientInfo__') {
|
|
482
|
-
this._handleClientInfoMessage(topic, json)
|
|
584
|
+
if (VrpcAgent._isObject(json)) this._handleClientInfoMessage(topic, json)
|
|
483
585
|
return
|
|
484
586
|
}
|
|
485
587
|
|
|
@@ -489,49 +591,55 @@ class VrpcAgent extends EventEmitter {
|
|
|
489
591
|
return
|
|
490
592
|
}
|
|
491
593
|
|
|
594
|
+
// the sender names where answers, callbacks and events go: without a
|
|
595
|
+
// connection id of this domain there is nowhere safe to answer
|
|
596
|
+
if (!VrpcAgent._isObject(json) || !this._isConnectionId(json.s)) {
|
|
597
|
+
this._log.warn(`Ignoring a request without a valid sender on: ${topic}`)
|
|
598
|
+
return
|
|
599
|
+
}
|
|
600
|
+
|
|
492
601
|
// Prepare RPC json
|
|
493
602
|
// SECURITY: take as much as possible params from topic structure as this
|
|
494
603
|
// can be authorized by the broker. Content can't be checked...
|
|
495
604
|
json.c = instance === '__static__' ? className : instance
|
|
496
605
|
json.f = method
|
|
497
606
|
|
|
607
|
+
// what a lifecycle call works on, as it was before the call
|
|
608
|
+
const named = Array.isArray(json.a) ? json.a[0] : undefined
|
|
609
|
+
const before = VrpcAdapter._instances.get(named)
|
|
610
|
+
|
|
498
611
|
// Mutates json and adds return value
|
|
499
612
|
const mustTrack = VrpcAdapter._call(json)
|
|
613
|
+
if (mustTrack) this._watch(json.s)
|
|
500
614
|
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
// TODO handle instantiation errors
|
|
509
|
-
const instanceId = json.r
|
|
510
|
-
// TODO await this
|
|
511
|
-
this._subscribeToMethodsOfNewInstance(className, instanceId)
|
|
512
|
-
this._registerIsolatedInstance(instanceId, json.s)
|
|
513
|
-
break
|
|
514
|
-
}
|
|
515
|
-
case '__createShared__': {
|
|
516
|
-
// TODO handle instantiation errors
|
|
517
|
-
const instanceId = json.r
|
|
518
|
-
if (!this._hasSharedInstance(instanceId)) {
|
|
519
|
-
this._subscribeToMethodsOfNewInstance(className, instanceId)
|
|
520
|
-
this._publishClassInfoMessage(className)
|
|
521
|
-
}
|
|
522
|
-
this._registerSharedInstance(instanceId, json.s)
|
|
523
|
-
break
|
|
615
|
+
if (json.e) {
|
|
616
|
+
if (method === '__createIsolated__' || method === '__createShared__') {
|
|
617
|
+
// a constructor that threw leaves nothing behind: no instance, no
|
|
618
|
+
// subscription, no lifetime bookkeeping
|
|
619
|
+
this._log.warn(
|
|
620
|
+
`Instantiation of ${className} failed: ${json.e.message}`
|
|
621
|
+
)
|
|
524
622
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
623
|
+
} else {
|
|
624
|
+
switch (method) {
|
|
625
|
+
case '__createIsolated__':
|
|
626
|
+
if (!before) this._subscribeToMethodsOfNewInstance(className, json.r)
|
|
627
|
+
this._registerIsolatedInstance(json.r, json.s)
|
|
628
|
+
break
|
|
629
|
+
case '__createShared__':
|
|
630
|
+
if (!before) {
|
|
631
|
+
this._subscribeToMethodsOfNewInstance(className, json.r)
|
|
632
|
+
this._publishClassInfoMessage(className)
|
|
633
|
+
}
|
|
634
|
+
// its creator's going is told (clientGone)
|
|
635
|
+
this._watch(json.s)
|
|
636
|
+
break
|
|
637
|
+
case '__delete__':
|
|
638
|
+
if (before) this._forgetInstance(named, before)
|
|
639
|
+
break
|
|
530
640
|
}
|
|
531
641
|
}
|
|
532
|
-
|
|
533
|
-
const res = e ? { a, r, e, i, v } : { a, r, i, v }
|
|
534
|
-
this._mqttPublish(json.s, VrpcAgent._stringifySafely(res))
|
|
642
|
+
this._mqttPublish(json.s, VrpcAgent._stringifySafely(VrpcAgent._answer(json)))
|
|
535
643
|
} catch (err) {
|
|
536
644
|
this._log.error(
|
|
537
645
|
err,
|
|
@@ -540,6 +648,50 @@ class VrpcAgent extends EventEmitter {
|
|
|
540
648
|
}
|
|
541
649
|
}
|
|
542
650
|
|
|
651
|
+
/**
|
|
652
|
+
* The answer to a request: its result `r` or its error `e`, the request's
|
|
653
|
+
* correlation id `i` and this agent's protocol version - nothing of the
|
|
654
|
+
* request is echoed.
|
|
655
|
+
*/
|
|
656
|
+
static _answer ({ r, e, i }) {
|
|
657
|
+
return e
|
|
658
|
+
? { e, i, v: VRPC_PROTOCOL_VERSION }
|
|
659
|
+
: { r, i, v: VRPC_PROTOCOL_VERSION }
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
static _isObject (json) {
|
|
663
|
+
return !!json && typeof json === 'object' && !Array.isArray(json)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Whether a request's sender is a connection id of this domain:
|
|
668
|
+
* `<domain>/<mqttClientId>/<secret>`, three levels, none empty or a
|
|
669
|
+
* wildcard - and never an agent's info topic, the one other topic of
|
|
670
|
+
* three levels.
|
|
671
|
+
*/
|
|
672
|
+
_isConnectionId (s) {
|
|
673
|
+
if (typeof s !== 'string') return false
|
|
674
|
+
const levels = s.split('/')
|
|
675
|
+
return (
|
|
676
|
+
levels.length === 3 &&
|
|
677
|
+
levels[0] === this._domain &&
|
|
678
|
+
levels[2] !== '__agentInfo__' &&
|
|
679
|
+
levels.every(level => level.length > 0 && !/[+#]/.test(level))
|
|
680
|
+
)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Watches a connection's presence while it holds anything here: isolated
|
|
685
|
+
* instances, subscriptions, shared instances it created. The watch ends
|
|
686
|
+
* when the connection goes.
|
|
687
|
+
*/
|
|
688
|
+
_watch (connectionId) {
|
|
689
|
+
if (this._watched.has(connectionId)) return
|
|
690
|
+
this._watched.add(connectionId)
|
|
691
|
+
this._mqttSubscribe(`${connectionId}/__clientInfo__`)
|
|
692
|
+
this._log.debug(`Tracking lifetime of client: ${connectionId}`)
|
|
693
|
+
}
|
|
694
|
+
|
|
543
695
|
_handleClientInfoMessage (topic, json) {
|
|
544
696
|
// A client connection went offline. The topic carries the connection
|
|
545
697
|
// id (unique per VrpcClient instance) - the key of all bookkeeping -
|
|
@@ -548,6 +700,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
548
700
|
if (json.status === 'offline') {
|
|
549
701
|
VrpcAdapter._unregisterClient(connectionId)
|
|
550
702
|
const entry = this._isolatedInstances.get(connectionId)
|
|
703
|
+
this._isolatedInstances.delete(connectionId)
|
|
551
704
|
if (entry) {
|
|
552
705
|
entry.forEach(instanceId => {
|
|
553
706
|
const known = VrpcAdapter._instances.get(instanceId)
|
|
@@ -555,7 +708,6 @@ class VrpcAgent extends EventEmitter {
|
|
|
555
708
|
c: known && known.className,
|
|
556
709
|
f: '__delete__',
|
|
557
710
|
a: [instanceId],
|
|
558
|
-
r: null,
|
|
559
711
|
s: VrpcAdapter.LOCAL_SENDER
|
|
560
712
|
}
|
|
561
713
|
VrpcAdapter._call(json)
|
|
@@ -566,6 +718,9 @@ class VrpcAgent extends EventEmitter {
|
|
|
566
718
|
}
|
|
567
719
|
})
|
|
568
720
|
}
|
|
721
|
+
// a connection id is kept across reconnects: whatever the connection
|
|
722
|
+
// holds after it comes back is watched afresh
|
|
723
|
+
this._watched.delete(connectionId)
|
|
569
724
|
this._mqttUnsubscribe(`${connectionId}/__clientInfo__`)
|
|
570
725
|
// clientId: the identity-derived id shared by all connections of one
|
|
571
726
|
// identity (undefined for clients < 3.8.0)
|
|
@@ -573,73 +728,29 @@ class VrpcAgent extends EventEmitter {
|
|
|
573
728
|
}
|
|
574
729
|
}
|
|
575
730
|
|
|
576
|
-
_registerIsolatedInstance (instanceId,
|
|
577
|
-
const entry = this._isolatedInstances.get(
|
|
578
|
-
if (entry)
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
} else {
|
|
582
|
-
// new instance
|
|
583
|
-
this._isolatedInstances.set(clientId, new Set([instanceId]))
|
|
584
|
-
if (!this._sharedInstances.has(clientId)) {
|
|
585
|
-
this._mqttSubscribe(`${clientId}/__clientInfo__`)
|
|
586
|
-
}
|
|
587
|
-
this._log.info(`Tracking lifetime of client: ${clientId}`)
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
_registerSharedInstance (instanceId, clientId) {
|
|
592
|
-
const entry = this._sharedInstances.get(clientId)
|
|
593
|
-
if (entry) {
|
|
594
|
-
// already seen
|
|
595
|
-
entry.add(instanceId)
|
|
596
|
-
} else {
|
|
597
|
-
// new instance
|
|
598
|
-
this._sharedInstances.set(clientId, new Set([instanceId]))
|
|
599
|
-
if (!this._isolatedInstances.has(clientId)) {
|
|
600
|
-
this._mqttSubscribe(`${clientId}/__clientInfo__`)
|
|
601
|
-
}
|
|
602
|
-
this._log.debug(`Tracking lifetime of client: ${clientId}`)
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
_hasSharedInstance (instanceId) {
|
|
607
|
-
for (const [, instances] of this._sharedInstances) {
|
|
608
|
-
if (instances.has(instanceId)) return true
|
|
609
|
-
}
|
|
610
|
-
return false
|
|
731
|
+
_registerIsolatedInstance (instanceId, connectionId) {
|
|
732
|
+
const entry = this._isolatedInstances.get(connectionId)
|
|
733
|
+
if (entry) entry.add(instanceId)
|
|
734
|
+
else this._isolatedInstances.set(connectionId, new Set([instanceId]))
|
|
735
|
+
this._watch(connectionId)
|
|
611
736
|
}
|
|
612
737
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
this._sharedInstances.forEach(async v => {
|
|
626
|
-
if (v.has(instanceId)) {
|
|
627
|
-
found = true
|
|
628
|
-
v.delete(instanceId)
|
|
629
|
-
if (v.size === 0) {
|
|
630
|
-
this._sharedInstances.delete(clientId)
|
|
631
|
-
this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
|
|
632
|
-
this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
|
|
633
|
-
}
|
|
738
|
+
/**
|
|
739
|
+
* A deleted instance leaves the agent: no request topic outlives it, an
|
|
740
|
+
* isolated one leaves its owner's bookkeeping, a shared one leaves its
|
|
741
|
+
* class info.
|
|
742
|
+
*/
|
|
743
|
+
_forgetInstance (instanceId, { className, isIsolated, owner }) {
|
|
744
|
+
this._unsubscribeMethodsOfDeletedInstance(className, instanceId)
|
|
745
|
+
if (isIsolated) {
|
|
746
|
+
const entry = this._isolatedInstances.get(owner)
|
|
747
|
+
if (entry) {
|
|
748
|
+
entry.delete(instanceId)
|
|
749
|
+
if (entry.size === 0) this._isolatedInstances.delete(owner)
|
|
634
750
|
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
this._log.warn(
|
|
638
|
-
`Failed un-registering not registered instance: ${instanceId}`
|
|
639
|
-
)
|
|
640
|
-
return false
|
|
751
|
+
} else {
|
|
752
|
+
this._publishClassInfoMessage(className)
|
|
641
753
|
}
|
|
642
|
-
return true
|
|
643
754
|
}
|
|
644
755
|
|
|
645
756
|
/** The request topics of one instance: every method of it. */
|