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.
- package/browser/vrpc.js +1 -1
- package/package.json +3 -2
- package/vrpc/VrpcAdapter.js +169 -24
- package/vrpc/VrpcAgent.js +91 -21
- package/vrpc/VrpcBrowserAdapter.js +174 -28
- package/vrpc/VrpcBrowserAgent.js +77 -20
- package/vrpc/VrpcClient.js +21 -1
- package/vrpc/VrpcPersistor.js +15 -2
- package/vrpc/comment-parser/parser.js +11 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
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 {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
633
|
-
|
|
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
|
|
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',
|
package/vrpc/VrpcBrowserAgent.js
CHANGED
|
@@ -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
|
|
94
|
-
*
|
|
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
|
-
|
|
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.
|
|
331
|
+
if (this._serving) {
|
|
309
332
|
resolve()
|
|
310
333
|
} else {
|
|
311
|
-
this.
|
|
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
|
|
354
|
-
if (
|
|
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
|
-
//
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
|
package/vrpc/VrpcClient.js
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|
package/vrpc/VrpcPersistor.js
CHANGED
|
@@ -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
|
-
|
|
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*\(/)
|