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.
@@ -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 = 3
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='<user>-<pathId>@<hostname>-<platform>-js'] This agent's name
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 {String} [obj.bestEffort=false] If true, message will be sent with best effort, i.e. no caching if offline
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 = false,
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
- // maps clientId to instanceId
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
- this._sharedInstances = new Map()
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 classes = this._getClasses()
171
- for (const className of classes) {
172
- const infoTopic = `${this._baseTopic}/${className}/__classInfo__`
173
- this._mqttPublish(infoTopic, null, { retain: true })
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
- (err, granted) => {
252
- if (err) {
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: ${err.message}`
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 = granted
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
- this._log.debug(`Forwarding callback to: ${topic} with payload:`, json)
346
- this._mqttPublish(
347
- topic,
348
- VrpcAgent._stringifySafely({ a, r, e, i, v: VRPC_PROTOCOL_VERSION })
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
- static _stringifySafely (json) {
359
- let str
360
- const { c, f } = json
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
- str = JSON.stringify(json)
440
+ return JSON.stringify(message)
363
441
  } catch (err) {
364
- this._log.debug(
365
- `Failed serialization of return value for: ${c}::${f}, because: ${err.message}`
366
- )
367
- json.r = '__vrpc::not-serializable__'
368
- str = JSON.stringify(json)
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 (mustTrack) {
502
- this._mqttSubscribe(`${json.s}/__clientInfo__`)
503
- }
504
-
505
- // Intersecting life-cycle functions
506
- switch (method) {
507
- case '__createIsolated__': {
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
- case '__delete__': {
526
- this._unsubscribeMethodsOfDeletedInstance(className, instance)
527
- const wasShared = this._unregisterInstance(json.a[0], json.s)
528
- if (wasShared) this._publishClassInfoMessage(className)
529
- break
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
- const { a, r, e, i, v } = json
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, clientId) {
577
- const entry = this._isolatedInstances.get(clientId)
578
- if (entry) {
579
- // already seen
580
- entry.add(instanceId)
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
- _unregisterInstance (instanceId, clientId) {
614
- const entryIsolated = this._isolatedInstances.get(clientId)
615
- if (entryIsolated && entryIsolated.has(instanceId)) {
616
- entryIsolated.delete(instanceId)
617
- if (entryIsolated.size === 0) {
618
- this._isolatedInstances.delete(clientId)
619
- this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
620
- this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
621
- }
622
- return false
623
- }
624
- let found = false
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
- if (!found) {
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. */