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/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 = 3
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: 'Calls function on best-effort. Improves performance but may fail under unstable network connections.',
74
- 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
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: 'v2.3.2'
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>-<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
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 {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
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
- // 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)
157
174
  this._isolatedInstances = new Map()
158
- this._sharedInstances = new Map()
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 classes = this._getClasses()
269
- for (const className of classes) {
270
- const infoTopic = `${this._baseTopic}/${className}/__classInfo__`
271
- 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
+ }
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 && !this._hasSharedInstance(instance)) {
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 pathId = VrpcAgent._createHash(path.resolve, 4)
342
- 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)
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
- (err, granted) => {
416
- if (err) {
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: ${err.message}`
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 = granted
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
- this._log.debug(`Forwarding callback to: ${topic} with payload:`, json)
516
- this._mqttPublish(
517
- topic,
518
- 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}`
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
- static _stringifySafely (json) {
529
- let str
530
- 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) {
531
601
  try {
532
- str = jsonStringifySafe(json)
602
+ return jsonStringifySafe(message)
533
603
  } catch (err) {
534
- console.error(
535
- `Failed serialization of return value for: ${c}::${f}, because: ${err.message}`
536
- )
537
- json.r = '__vrpc::not-serializable__'
538
- 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
+ })
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 (mustTrack) {
699
- this._mqttSubscribe(`${json.s}/__clientInfo__`)
700
- }
701
-
702
- // Intersecting life-cycle functions
703
- switch (method) {
704
- case '__createIsolated__': {
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
- case '__createShared__': {
720
- if (json.e) {
721
- this._log.warn(
722
- `Instantiation of ${className} failed: ${json.e.message}`
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
- const { a, r, e, i, v } = json
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, clientId) {
790
- const entry = this._isolatedInstances.get(clientId)
791
- if (entry) {
792
- // already seen
793
- entry.add(instanceId)
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
- _unregisterInstance (instanceId, clientId) {
827
- const entryIsolated = this._isolatedInstances.get(clientId)
828
- if (entryIsolated && entryIsolated.has(instanceId)) {
829
- entryIsolated.delete(instanceId)
830
- if (entryIsolated.size === 0) {
831
- this._isolatedInstances.delete(clientId)
832
- this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
833
- this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
834
- }
835
- return false
836
- }
837
- let found = false
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
- if (!found) {
850
- this._log.info(
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
  /**