vrpc 3.12.0 → 3.13.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.
@@ -39,6 +39,16 @@ const Ajv = require('ajv')
39
39
  const EventEmitter = require('events')
40
40
  const { nanoid } = require('nanoid')
41
41
 
42
+ // A runtime without dispose symbols (older browsers, Node before 18.18 /
43
+ // 20.4) gets Node's own stand-ins from the symbol registry, so a class file
44
+ // and the adapter meet on one symbol whichever of them installs it first.
45
+ if (typeof Symbol.dispose !== 'symbol') {
46
+ Symbol.dispose = Symbol.for('nodejs.dispose')
47
+ }
48
+ if (typeof Symbol.asyncDispose !== 'symbol') {
49
+ Symbol.asyncDispose = Symbol.for('nodejs.asyncDispose')
50
+ }
51
+
42
52
  const VRPC_GLOBAL = '__global__'
43
53
  const VRPC_WINDOW_NS = 'vrpc'
44
54
 
@@ -188,7 +198,10 @@ class VrpcAdapter {
188
198
  }
189
199
 
190
200
  /**
191
- * Deletes an instance
201
+ * Deletes an instance: its name is free at once, the adapter takes back
202
+ * every listener it attached, and the object releases what it runs
203
+ * through its `[Symbol.asyncDispose]` or `[Symbol.dispose]` method, which
204
+ * runs in the background here (a remote delete answers once it settled).
192
205
  *
193
206
  * @param {String|Object} instance Instance (name or object itself) to be deleted
194
207
  * @returns {Boolean} True in case of success, false otherwise
@@ -199,10 +212,7 @@ class VrpcAdapter {
199
212
  }
200
213
  if (typeof instance === 'object') {
201
214
  for (const [k, v] of VrpcAdapter._instances) {
202
- if (v.instance === instance) {
203
- VrpcAdapter._instances.delete(k)
204
- return true
205
- }
215
+ if (v.instance === instance) return VrpcAdapter._delete(k)
206
216
  }
207
217
  return false
208
218
  }
@@ -457,11 +467,17 @@ class VrpcAdapter {
457
467
  instance,
458
468
  className: json.c
459
469
  })
460
- json.r = VrpcAdapter._delete(instance)
470
+ const { ended, disposed } = VrpcAdapter._end(instance)
461
471
  VrpcAdapter._emitter.emit('delete', {
462
472
  instance,
463
473
  className: json.c
464
474
  })
475
+ // a delete answers once the object released what it runs, so that a
476
+ // constructor right after it never meets a resource the old object
477
+ // still holds; the agent's own housekeeping awaits no answer
478
+ if (disposed && json.s !== VrpcAdapter.LOCAL_SENDER) {
479
+ VrpcAdapter._handlePromise(json, disposed.then(() => ended))
480
+ } else json.r = ended
465
481
  } catch (err) {
466
482
  json.e = err.message
467
483
  }
@@ -482,6 +498,15 @@ class VrpcAdapter {
482
498
  json.r = true
483
499
  return
484
500
  }
501
+ // an unknown context fails before its arguments are unwrapped: a
502
+ // listener registered for a name that does not exist would outlive the
503
+ // failed call and pass for attached once the name is created
504
+ if (
505
+ !VrpcAdapter._functionRegistry.has(json.c) &&
506
+ !VrpcAdapter._instances.has(json.c)
507
+ ) {
508
+ throw new Error(`Could not find context: ${json.c}`)
509
+ }
485
510
  const unwrapped = VrpcAdapter._unwrapArguments(json)
486
511
  if (!unwrapped) {
487
512
  // this call must be skipped (event emitter stuff)
@@ -557,33 +582,112 @@ class VrpcAdapter {
557
582
  }
558
583
 
559
584
  static _delete (instanceId) {
585
+ return VrpcAdapter._end(instanceId).ended
586
+ }
587
+
588
+ /**
589
+ * Ends an instance - the one teardown behind every door (a remote delete,
590
+ * the auto-delete of an isolated instance whose client went, an
591
+ * in-process delete): its name is free at once, the adapter takes back
592
+ * every listener it attached (silent from now on anyway, see
593
+ * _generateListener), and the object releases what it runs through the
594
+ * ECMAScript dispose protocol, `[Symbol.asyncDispose]` or else
595
+ * `[Symbol.dispose]`.
596
+ *
597
+ * @returns {Object} `{ ended, disposed }`: whether the name was known, and
598
+ * a promise when an asynchronous dispose is under way (it never rejects)
599
+ */
600
+ static _end (instanceId) {
560
601
  const entry = VrpcAdapter._instances.get(instanceId)
561
- if (entry) {
562
- // in case the instance was an EventEmitter, free all registered listeners
563
- if (VrpcAdapter._isFunction(entry.removeAllListeners)) {
564
- entry.removeAllListeners()
602
+ if (!entry) return { ended: false, disposed: null }
603
+ VrpcAdapter._instances.delete(instanceId)
604
+ VrpcAdapter._detachListeners(instanceId, entry.instance)
605
+ return { ended: true, disposed: VrpcAdapter._dispose(instanceId, entry) }
606
+ }
607
+
608
+ static _detachListeners (instanceId, instance) {
609
+ const registrations = VrpcAdapter._listeners[instanceId]
610
+ delete VrpcAdapter._listeners[instanceId]
611
+ if (!registrations || !VrpcAdapter._isFunction(instance.removeListener)) {
612
+ return
613
+ }
614
+ for (const { event, listener, clients } of Object.values(registrations)) {
615
+ try {
616
+ VrpcAdapter._removeTimes(instance, event, listener, clients.length)
617
+ } catch (err) {
618
+ // a class whose removeListener fails keeps a silent listener
565
619
  }
566
- delete VrpcAdapter._listeners[instanceId]
567
- VrpcAdapter._instances.delete(instanceId)
568
- return true
569
620
  }
570
- return false
621
+ }
622
+
623
+ // a subscription repeated n times may hold n attachments of one listener
624
+ // (EventEmitter semantics): each removeListener takes one of them
625
+ static _removeTimes (instance, event, listener, times) {
626
+ for (let n = 0; n < Math.max(1, times); n++) {
627
+ instance.removeListener(event, listener)
628
+ }
629
+ }
630
+
631
+ /**
632
+ * Runs the object's dispose method. A dispose that fails or outlasts
633
+ * VrpcAdapter.DISPOSE_TIMEOUT is reported (event 'disposeFailed') and
634
+ * never thrown: the instance is gone either way.
635
+ *
636
+ * @returns {Promise|null} Settles when an asynchronous dispose is done
637
+ */
638
+ static _dispose (instanceId, { instance, className }) {
639
+ const fail = error =>
640
+ VrpcAdapter._emitter.emit('disposeFailed', {
641
+ instance: instanceId,
642
+ className,
643
+ error
644
+ })
645
+ let result
646
+ try {
647
+ if (VrpcAdapter._isFunction(instance[Symbol.asyncDispose])) {
648
+ result = instance[Symbol.asyncDispose]()
649
+ } else if (VrpcAdapter._isFunction(instance[Symbol.dispose])) {
650
+ result = instance[Symbol.dispose]()
651
+ }
652
+ } catch (err) {
653
+ fail(err)
654
+ return null
655
+ }
656
+ if (!VrpcAdapter._isPromise(result)) return null
657
+ let timer
658
+ const bound = new Promise(resolve => {
659
+ timer = setTimeout(() => {
660
+ fail(
661
+ new Error(
662
+ `dispose did not settle within ${VrpcAdapter.DISPOSE_TIMEOUT} ms`
663
+ )
664
+ )
665
+ resolve()
666
+ }, VrpcAdapter.DISPOSE_TIMEOUT)
667
+ })
668
+ const settled = result.then(
669
+ () => {},
670
+ err => fail(err)
671
+ )
672
+ return Promise.race([settled, bound]).then(() => clearTimeout(timer))
571
673
  }
572
674
 
573
675
  static _unregisterClient (clientId) {
574
676
  Object.entries(VrpcAdapter._listeners).forEach(([ik, iv]) => {
575
677
  Object.entries(iv).forEach(([ek, ev]) => {
576
- const { clients, listener, event } = ev
577
- const index = clients.indexOf(clientId)
578
- if (index !== -1) {
579
- clients.splice(index, 1)
580
- if (clients.length === 0) {
581
- const instance = VrpcAdapter.getInstance(ik)
582
- if (instance && instance.removeListener) {
583
- instance.removeListener(event, listener)
584
- }
585
- delete VrpcAdapter._listeners[ik][ek]
678
+ const { listener, event } = ev
679
+ // a client is listed once per subscription: its going ends them all
680
+ const remaining = ev.clients.filter(x => x !== clientId)
681
+ const gone = ev.clients.length - remaining.length
682
+ if (gone === 0) return
683
+ ev.clients = remaining
684
+ if (remaining.length === 0) {
685
+ const entry = VrpcAdapter._instances.get(ik)
686
+ const instance = entry && entry.instance
687
+ if (instance && instance.removeListener) {
688
+ VrpcAdapter._removeTimes(instance, event, listener, gone)
586
689
  }
690
+ delete VrpcAdapter._listeners[ik][ek]
587
691
  }
588
692
  })
589
693
  })
@@ -622,6 +726,7 @@ class VrpcAdapter {
622
726
  // event registration
623
727
  if (func === 'on' || func === 'addListener') {
624
728
  VrpcAdapter._mustTrackClient = true
729
+ const held = VrpcAdapter._holdsListener(context, arg)
625
730
  const listener = VrpcAdapter._registerListener({
626
731
  clientId,
627
732
  isCallAll,
@@ -629,9 +734,8 @@ class VrpcAdapter {
629
734
  eventId: arg,
630
735
  event: args[0]
631
736
  })
632
- if (!listener) {
633
- return null // skip call as listener is already registered
634
- }
737
+ // attached already: skip the call, one listener hears once
738
+ if (held) return null
635
739
  unwrapped.push(listener)
636
740
  // event un-registration
637
741
  } else if (func === 'off' || func === 'removeListener') {
@@ -666,8 +770,32 @@ class VrpcAdapter {
666
770
  return unwrapped
667
771
  }
668
772
 
773
+ /**
774
+ * Whether the object under `instanceId` is still the one the adapter saw
775
+ * when it built a listener or a callback for it: a deleted instance never
776
+ * speaks on the bus again, even when its class kept the listener or its
777
+ * clock kept running. A static context (a class name) has no object and
778
+ * always speaks.
779
+ */
780
+ static _speaksFor (instanceId) {
781
+ const entry = VrpcAdapter._instances.get(instanceId)
782
+ if (!entry) return () => true
783
+ const { instance } = entry
784
+ return () => {
785
+ const live = VrpcAdapter._instances.get(instanceId)
786
+ return live !== undefined && live.instance === instance
787
+ }
788
+ }
789
+
790
+ static _holdsListener (instanceId, eventId) {
791
+ const registrations = VrpcAdapter._listeners[instanceId]
792
+ return Boolean(registrations && registrations[eventId])
793
+ }
794
+
669
795
  static _generateCallback ({ eventId, clientId, instanceId, isCallAll }) {
796
+ const speaks = VrpcAdapter._speaksFor(instanceId)
670
797
  return (...innerArgs) => {
798
+ if (!speaks()) return
671
799
  // "callAll" event notifications expect the instanceId as first argument
672
800
  const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
673
801
  VrpcAdapter._callback({ a, s: clientId, i: eventId })
@@ -708,11 +836,13 @@ class VrpcAdapter {
708
836
  return VrpcAdapter._listeners[instanceId][eventId].listener
709
837
  }
710
838
  VrpcAdapter._listeners[instanceId][eventId].clients.push(clientId)
711
- return null
839
+ return VrpcAdapter._listeners[instanceId][eventId].listener
712
840
  }
713
841
 
714
842
  static _generateListener ({ eventId, instanceId, isCallAll }) {
843
+ const speaks = VrpcAdapter._speaksFor(instanceId)
715
844
  return (...innerArgs) => {
845
+ if (!speaks()) return
716
846
  const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
717
847
  VrpcAdapter._callback({ a, i: eventId })
718
848
  }
@@ -892,12 +1022,28 @@ class VrpcAdapter {
892
1022
  * @property {String} instance The instance name
893
1023
  */
894
1024
 
1025
+ /**
1026
+ * Event 'disposeFailed'
1027
+ *
1028
+ * Emitted when the dispose method of a deleted instance throws, rejects or
1029
+ * does not settle within VrpcAdapter.DISPOSE_TIMEOUT. The instance is gone
1030
+ * regardless.
1031
+ *
1032
+ * @event VrpcAdapter#disposeFailed
1033
+ * @type {Object}
1034
+ * @property {String} className The class name of the deleted instance
1035
+ * @property {String} instance The instance name
1036
+ * @property {Error} error What went wrong
1037
+ */
1038
+
895
1039
  // Initialize static members
896
1040
  VrpcAdapter._functionRegistry = new Map()
897
1041
  VrpcAdapter._instances = new Map()
898
1042
  VrpcAdapter._emitter = new EventEmitter()
899
1043
  VrpcAdapter._correlationId = 0
900
1044
  VrpcAdapter._listeners = {}
1045
+ // the longest a delete waits for the dispose method of its instance
1046
+ VrpcAdapter.DISPOSE_TIMEOUT = 10000
901
1047
  VrpcAdapter._blackList = new Set([
902
1048
  'caller',
903
1049
  'callee',
@@ -68,9 +68,21 @@ class VrpcAgent extends EventEmitter {
68
68
  }
69
69
  this._baseTopic = `${this._domain}/${this._agent}`
70
70
  VrpcAdapter.onCallback(this._handleVrpcCallback.bind(this))
71
+ // a deleted instance whose dispose failed is gone anyway: say why
72
+ this._handleDisposeFailed = ({ instance, className, error }) =>
73
+ this._log.warn(
74
+ error,
75
+ `Dispose of deleted instance ${className}::${instance} failed: ${error.message}`
76
+ )
77
+ VrpcAdapter.on('disposeFailed', this._handleDisposeFailed)
71
78
  // maps clientId to instanceId
72
79
  this._isolatedInstances = new Map()
73
80
  this._sharedInstances = new Map()
81
+ // counts the connections: an announcement belongs to the connection
82
+ // that prepared it (see _handleConnect)
83
+ this._connectGeneration = 0
84
+ // connected, request topics subscribed and info announced
85
+ this._serving = false
74
86
 
75
87
  // Handle the internal error event in case the user forgot to implement it
76
88
  this.on('error', err => {
@@ -90,8 +102,9 @@ class VrpcAgent extends EventEmitter {
90
102
  * If the connection could not be established because of authorization
91
103
  * failure, the 'error' event will be emitted.
92
104
  *
93
- * @return {Promise} Resolves once connected or explicitly ended, never
94
- * rejects
105
+ * @return {Promise} Resolves once the agent serves - connected, its
106
+ * request topics subscribed and its info announced - or once it was
107
+ * explicitly ended before that, never rejects
95
108
  */
96
109
  async serve () {
97
110
  let username = this._username
@@ -140,6 +153,7 @@ class VrpcAgent extends EventEmitter {
140
153
  * @returns {Promise} Resolves when disconnected and ended
141
154
  */
142
155
  async end ({ unregister = false } = {}) {
156
+ VrpcAdapter.off('disposeFailed', this._handleDisposeFailed)
143
157
  try {
144
158
  if (!this._client || !this._client.connected) {
145
159
  this.emit('end')
@@ -226,7 +240,11 @@ class VrpcAgent extends EventEmitter {
226
240
  )
227
241
  }
228
242
 
229
- _mqttSubscribe (topic, options) {
243
+ /**
244
+ * Subscribes topic(s); `onSettled` is called once the broker has
245
+ * answered (granted, refused or failed alike).
246
+ */
247
+ _mqttSubscribe (topic, options, onSettled) {
230
248
  this._client.subscribe(
231
249
  topic,
232
250
  { qos: this._qos, ...options },
@@ -269,6 +287,7 @@ class VrpcAgent extends EventEmitter {
269
287
  this._log.debug(`Already subscribed to topic '${topic}'`)
270
288
  }
271
289
  }
290
+ if (onSettled) onSettled()
272
291
  }
273
292
  )
274
293
  }
@@ -303,12 +322,16 @@ class VrpcAgent extends EventEmitter {
303
322
  return VrpcAdapter._getMetaData(className)
304
323
  }
305
324
 
325
+ /**
326
+ * Resolves once the agent serves (its own 'connect' event: connected,
327
+ * request topics subscribed, info announced), or once it was ended.
328
+ */
306
329
  async _ensureConnected () {
307
330
  return new Promise(resolve => {
308
- if (this._client.connected) {
331
+ if (this._serving) {
309
332
  resolve()
310
333
  } else {
311
- this._client.once('connect', resolve)
334
+ this.once('connect', resolve)
312
335
  // Will be triggered if the user called 'agent.end()'
313
336
  this.once('end', resolve)
314
337
  }
@@ -347,33 +370,56 @@ class VrpcAgent extends EventEmitter {
347
370
  return str
348
371
  }
349
372
 
373
+ /**
374
+ * Serve first, announce second - the twin of VrpcAgent._handleConnect:
375
+ * every request topic (statics, the methods of every existing instance)
376
+ * is subscribed before the agent and class info go out, and they go out
377
+ * once the broker has answered every subscription. A client acts on the
378
+ * announcement at once; a request published before the broker routes
379
+ * the topic is lost (QoS 0) and only times out on the caller's side.
380
+ */
350
381
  _handleConnect () {
351
382
  this._log.info('[OK]')
383
+ const generation = ++this._connectGeneration
384
+ const batches = []
352
385
  try {
353
- const topics = this._generateTopics()
354
- if (topics.length > 0) this._mqttSubscribe(topics)
386
+ const statics = this._generateTopics()
387
+ if (statics.length > 0) batches.push(statics)
355
388
  } catch (err) {
356
389
  this._log.error(
357
390
  err,
358
391
  `Problem during initial topic subscription: ${err.message}`
359
392
  )
360
393
  }
361
- // Publish agent online
362
- this._publishAgentInfoMessage()
363
-
364
- // Publish class information
365
- const classes = this._getClasses()
366
- classes.forEach(className => {
367
- this._publishClassInfoMessage(className)
368
- })
369
- // Register all pre-existing instances
394
+ // all pre-existing instances
370
395
  for (const [
371
396
  instanceId,
372
397
  { className }
373
398
  ] of VrpcAdapter._instances.entries()) {
374
- this._subscribeToMethodsOfNewInstance(className, instanceId)
399
+ batches.push(this._methodsTopic(className, instanceId))
400
+ }
401
+ let pending = batches.length
402
+ const announce = () => {
403
+ // a connection that ended meanwhile announces nothing: the next
404
+ // one prepares and announces itself
405
+ if (generation !== this._connectGeneration) return
406
+ this._publishAgentInfoMessage()
407
+ for (const className of this._getClasses()) {
408
+ this._publishClassInfoMessage(className)
409
+ }
410
+ this._serving = true
411
+ this.emit('connect')
412
+ }
413
+ if (pending === 0) {
414
+ announce()
415
+ return
416
+ }
417
+ for (const topic of batches) {
418
+ this._mqttSubscribe(topic, undefined, () => {
419
+ pending -= 1
420
+ if (pending === 0) announce()
421
+ })
375
422
  }
376
- this.emit('connect')
377
423
  }
378
424
 
379
425
  _publishAgentInfoMessage () {
@@ -504,7 +550,9 @@ class VrpcAgent extends EventEmitter {
504
550
  const entry = this._isolatedInstances.get(connectionId)
505
551
  if (entry) {
506
552
  entry.forEach(instanceId => {
553
+ const known = VrpcAdapter._instances.get(instanceId)
507
554
  const json = {
555
+ c: known && known.className,
508
556
  f: '__delete__',
509
557
  a: [instanceId],
510
558
  r: null,
@@ -512,6 +560,8 @@ class VrpcAgent extends EventEmitter {
512
560
  }
513
561
  VrpcAdapter._call(json)
514
562
  if (json.r) {
563
+ // gone like a remote delete: no request topic outlives it
564
+ this._unsubscribeMethodsOfDeletedInstance(known.className, instanceId)
515
565
  this._log.debug(`Auto-deleted isolated instance: ${instanceId}`)
516
566
  }
517
567
  })
@@ -592,14 +642,19 @@ class VrpcAgent extends EventEmitter {
592
642
  return true
593
643
  }
594
644
 
645
+ /** The request topics of one instance: every method of it. */
646
+ _methodsTopic (className, instance) {
647
+ return `${this._baseTopic}/${className}/${instance}/+`
648
+ }
649
+
595
650
  _subscribeToMethodsOfNewInstance (className, instance) {
596
- const topic = `${this._baseTopic}/${className}/${instance}/+`
651
+ const topic = this._methodsTopic(className, instance)
597
652
  this._mqttSubscribe(topic)
598
653
  this._log.debug(`Subscribed to new topic after instantiation: ${topic}`)
599
654
  }
600
655
 
601
656
  _unsubscribeMethodsOfDeletedInstance (className, instance) {
602
- const topic = `${this._baseTopic}/${className}/${instance}/+`
657
+ const topic = this._methodsTopic(className, instance)
603
658
  this._mqttUnsubscribe(topic)
604
659
  this._log.debug(`Unsubscribed from topic after deletion: ${topic}`)
605
660
  }
@@ -614,6 +669,8 @@ class VrpcAgent extends EventEmitter {
614
669
  }
615
670
 
616
671
  _handleClose () {
672
+ // the next connection prepares and announces itself afresh
673
+ this._serving = false
617
674
  this.emit('close')
618
675
  }
619
676
 
@@ -546,7 +546,27 @@ class VrpcClient extends EventEmitter {
546
546
  const topic = `${this._domain}/${agent}/${className}/__static__/__delete__`
547
547
  this._mqttPublish(topic, JSON.stringify(json))
548
548
  delete this._proxies[`${agent}/${className}/${instance}`]
549
- return this._handleAgentAnswer(json, agent)
549
+ const deleted = await this._handleAgentAnswer(json, agent)
550
+ // the answer and the class info that no longer lists the instance
551
+ // travel on different topics, and a broker need not keep their order:
552
+ // what this client deleted is gone for it at once
553
+ if (deleted === true) this._forgetInstance({ agent, className, instance })
554
+ return deleted
555
+ }
556
+
557
+ /**
558
+ * Takes an instance off this client's view of its agent, the way a class
559
+ * info without it does: listing, instanceGone, cached subscriptions. A
560
+ * class info that arrives later finds nothing left to remove.
561
+ */
562
+ _forgetInstance ({ agent, className, instance }) {
563
+ const info = this._agents[agent] && this._agents[agent].classes[className]
564
+ if (!info || !Array.isArray(info.instances)) return
565
+ if (!info.instances.includes(instance)) return
566
+ info.instances = info.instances.filter(x => x !== instance)
567
+ const domain = this._domain
568
+ this.emit('instanceGone', [instance], { domain, agent, className })
569
+ this._clearCachedSubscriptions({ lostInstance: instance, agent, className })
550
570
  }
551
571
 
552
572
  /**
@@ -98,6 +98,8 @@ class VrpcPersistor {
98
98
  .replace(/[^a-zA-Z0-9]/g, '-')}`
99
99
 
100
100
  this._storage = null
101
+ // the 'update' listener on each persisted instance, dropped on delete
102
+ this._updateListeners = new Map()
101
103
  this._isInitialized = this._init(Storage)
102
104
 
103
105
  this._log.info(
@@ -282,7 +284,11 @@ class VrpcPersistor {
282
284
  const obj = VrpcAdapter.getInstance(instance)
283
285
  // Ensure the object is an event emitter
284
286
  if (obj && typeof obj.on === 'function') {
285
- obj.on('update', data => {
287
+ const listener = data => {
288
+ // a deleted object never rewrites the record of its name
289
+ // (#1705): an in-process delete announces nothing
290
+ const live = VrpcAdapter._instances.get(instance)
291
+ if (!live || live.instance !== obj) return
286
292
  this._log.info(
287
293
  `[VrpcPersistor] Persisting update for: ${instance} (${className})`
288
294
  )
@@ -293,7 +299,9 @@ class VrpcPersistor {
293
299
  `[VrpcPersistor] Failed persisting update of ${instance} (${className}): ${err.message}`
294
300
  )
295
301
  )
296
- })
302
+ }
303
+ obj.on('update', listener)
304
+ this._updateListeners.set(instance, { obj, listener })
297
305
  }
298
306
  } catch (err) {
299
307
  this._log.warn(
@@ -304,6 +312,11 @@ class VrpcPersistor {
304
312
 
305
313
  // Persist instance deletion
306
314
  VrpcAdapter.on('delete', ({ instance, className }) => {
315
+ const update = this._updateListeners.get(instance)
316
+ if (update) {
317
+ this._updateListeners.delete(instance)
318
+ update.obj.removeListener('update', update.listener)
319
+ }
307
320
  this._log.info(
308
321
  `[VrpcPersistor] Deleting persisted instance: ${instance} (${className})`
309
322
  )
@@ -230,6 +230,17 @@ function mkextract (opts) {
230
230
  indent = startPos + MARKER_START.length
231
231
  }
232
232
 
233
+ // a member with a computed key ([Symbol.asyncDispose]) names no remote
234
+ // function: the block documents it and nothing else - without this it
235
+ // landed on the first call in the member's body
236
+ if (
237
+ findMethod &&
238
+ /^\s*(static\s+)?(async\s+)?\*?\s*((get|set)\s+)?\[[^\]]+\]\s*\(/.test(line)
239
+ ) {
240
+ result = parse_block(chunk, opts)
241
+ findMethod = false
242
+ }
243
+
233
244
  if (findMethod) {
234
245
  const ret = line.match(/\s*function\s+(\w+)\s*\(/) ||
235
246
  line.match(/\s*(\w+)\s*=?\s*\(/)