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/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 = 3
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: 'Calls function on best-effort. Improves performance but may fail under unstable network connections.',
73
- action: 'store_true'
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: 'v2.3.2'
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>-<pathId>@<hostname>-<platform>-js'] This agent's name
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 {String} [obj.bestEffort=true] If true, message will be sent with best effort, i.e. no caching if offline
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
- // maps clientId to instanceId
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
- this._sharedInstances = new Map()
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
- this.emit('end')
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 classes = this._getClasses()
258
- for (const className of classes) {
259
- const infoTopic = `${this._baseTopic}/${className}/__classInfo__`
260
- this._mqttPublish(infoTopic, null, { retain: true })
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 && !this._hasSharedInstance(instance)) {
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 pathId = VrpcAgent._createHash(path.resolve, 4)
331
- return `${username}-${pathId}@${os.hostname()}-${os.platform()}-js`
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
- (err, granted) => {
405
- if (err) {
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: ${err.message}`
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 = granted
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
- this._log.debug(`Forwarding callback to: ${topic} with payload:`, json)
505
- this._mqttPublish(
506
- topic,
507
- VrpcAgent._stringifySafely({ a, r, e, i, v: VRPC_PROTOCOL_VERSION })
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
- static _stringifySafely (json) {
518
- let str
519
- const { c, f } = json
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
- str = jsonStringifySafe(json)
602
+ return jsonStringifySafe(message)
522
603
  } catch (err) {
523
- console.error(
524
- `Failed serialization of return value for: ${c}::${f}, because: ${err.message}`
525
- )
526
- json.r = '__vrpc::not-serializable__'
527
- str = JSON.stringify(json)
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 (mustTrack) {
688
- this._mqttSubscribe(`${json.s}/__clientInfo__`)
689
- }
690
-
691
- // Intersecting life-cycle functions
692
- switch (method) {
693
- case '__createIsolated__': {
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
- case '__createShared__': {
709
- if (json.e) {
710
- this._log.warn(
711
- `Instantiation of ${className} failed: ${json.e.message}`
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
- const { a, r, e, i, v } = json
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, clientId) {
779
- const entry = this._isolatedInstances.get(clientId)
780
- if (entry) {
781
- // already seen
782
- entry.add(instanceId)
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
- _unregisterInstance (instanceId, clientId) {
816
- const entryIsolated = this._isolatedInstances.get(clientId)
817
- if (entryIsolated && entryIsolated.has(instanceId)) {
818
- entryIsolated.delete(instanceId)
819
- if (entryIsolated.size === 0) {
820
- this._isolatedInstances.delete(clientId)
821
- this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
822
- this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
823
- }
824
- return false
825
- }
826
- let found = false
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
- if (!found) {
839
- this._log.info(
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
  /**