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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vrpc",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0",
|
|
4
4
|
"description": "Variadic Remote Procedure Calls",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"build:addon": "ln -sf binding.gyp.no-auto binding.gyp && node-gyp rebuild && rm binding.gyp",
|
|
13
13
|
"build:browser": "./node_modules/.bin/webpack --stats-error-details",
|
|
14
14
|
"build:doc": "./node_modules/.bin/jsdoc2md -f vrpc/VrpcAdapter.js vrpc/VrpcAgent.js vrpc/VrpcClient.js vrpc/VrpcNative.js --param-list-format=list --separators > docs/api.md",
|
|
15
|
-
"test": "npm run test:adapter && npm run test:agent && npm run test:client && npm run test:persistor && npm run test:lifecycle && npm run test:production",
|
|
15
|
+
"test": "npm run test:adapter && npm run test:agent && npm run test:client && npm run test:persistor && npm run test:lifecycle && npm run test:legacy-runtime && npm run test:production",
|
|
16
16
|
"test:adapter": "./node_modules/.bin/mocha tests/adapter/*.js --exit",
|
|
17
17
|
"test:native": "./node_modules/.bin/mocha tests/native/*.js --exit",
|
|
18
18
|
"test:agent": "tests/agent/test.sh",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"test:performance": "tests/performance/test.sh",
|
|
21
21
|
"test:persistor": "./node_modules/.bin/mocha tests/persistor/*.js --timeout 30000 --exit",
|
|
22
22
|
"test:lifecycle": "./node_modules/.bin/mocha tests/lifecycle/*.js --timeout 30000 --exit",
|
|
23
|
+
"test:legacy-runtime": "tests/legacy-runtime/test.sh",
|
|
23
24
|
"test:production": "./node_modules/.bin/mocha tests/production/lifeCycleTest.js --timeout 30000 --exit && tests/production/test.sh"
|
|
24
25
|
},
|
|
25
26
|
"bin": {
|
package/vrpc/VrpcAdapter.js
CHANGED
|
@@ -43,6 +43,17 @@ const commentParser = require('./comment-parser')
|
|
|
43
43
|
const EventEmitter = require('events')
|
|
44
44
|
const { nanoid } = require('nanoid')
|
|
45
45
|
|
|
46
|
+
// Node before 18.18 / 20.4 has no dispose symbols, and a class's
|
|
47
|
+
// [Symbol.asyncDispose]() would become a method named 'undefined'. Node's
|
|
48
|
+
// own stand-ins live in the symbol registry, so a class file and the adapter
|
|
49
|
+
// meet on one symbol whichever of them installs it first.
|
|
50
|
+
if (typeof Symbol.dispose !== 'symbol') {
|
|
51
|
+
Symbol.dispose = Symbol.for('nodejs.dispose')
|
|
52
|
+
}
|
|
53
|
+
if (typeof Symbol.asyncDispose !== 'symbol') {
|
|
54
|
+
Symbol.asyncDispose = Symbol.for('nodejs.asyncDispose')
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
/**
|
|
47
58
|
* Generates an adapter layer for existing code and enables further VRPC-based
|
|
48
59
|
* communication.
|
|
@@ -197,7 +208,10 @@ class VrpcAdapter {
|
|
|
197
208
|
}
|
|
198
209
|
|
|
199
210
|
/**
|
|
200
|
-
* Deletes an instance
|
|
211
|
+
* Deletes an instance: its name is free at once, the adapter takes back
|
|
212
|
+
* every listener it attached, and the object releases what it runs
|
|
213
|
+
* through its `[Symbol.asyncDispose]` or `[Symbol.dispose]` method, which
|
|
214
|
+
* runs in the background here (a remote delete answers once it settled).
|
|
201
215
|
*
|
|
202
216
|
* @param {String|Object} instance Instance (name or object itself) to be deleted
|
|
203
217
|
* @returns {Boolean} True in case of success, false otherwise
|
|
@@ -208,10 +222,7 @@ class VrpcAdapter {
|
|
|
208
222
|
}
|
|
209
223
|
if (typeof instance === 'object') {
|
|
210
224
|
for (const [k, v] of VrpcAdapter._instances) {
|
|
211
|
-
if (v.instance === instance)
|
|
212
|
-
VrpcAdapter._instances.delete(k)
|
|
213
|
-
return true
|
|
214
|
-
}
|
|
225
|
+
if (v.instance === instance) return VrpcAdapter._delete(k)
|
|
215
226
|
}
|
|
216
227
|
return false
|
|
217
228
|
}
|
|
@@ -577,11 +588,17 @@ class VrpcAdapter {
|
|
|
577
588
|
instance,
|
|
578
589
|
className: json.c
|
|
579
590
|
})
|
|
580
|
-
|
|
591
|
+
const { ended, disposed } = VrpcAdapter._end(instance)
|
|
581
592
|
VrpcAdapter._emitter.emit('delete', {
|
|
582
593
|
instance,
|
|
583
594
|
className: json.c
|
|
584
595
|
})
|
|
596
|
+
// a delete answers once the object released what it runs, so that a
|
|
597
|
+
// constructor right after it never meets a port or a session the old
|
|
598
|
+
// object still holds; the agent's own housekeeping awaits no answer
|
|
599
|
+
if (disposed && json.s !== VrpcAdapter.LOCAL_SENDER) {
|
|
600
|
+
VrpcAdapter._handlePromise(json, disposed.then(() => ended))
|
|
601
|
+
} else json.r = ended
|
|
585
602
|
} catch (err) {
|
|
586
603
|
const { message, cause } = err
|
|
587
604
|
json.e = { message, cause }
|
|
@@ -603,6 +620,15 @@ class VrpcAdapter {
|
|
|
603
620
|
json.r = true
|
|
604
621
|
return
|
|
605
622
|
}
|
|
623
|
+
// an unknown context fails before its arguments are unwrapped: a
|
|
624
|
+
// listener registered for a name that does not exist would outlive the
|
|
625
|
+
// failed call and pass for attached once the name is created
|
|
626
|
+
if (
|
|
627
|
+
!VrpcAdapter._functionRegistry.has(json.c) &&
|
|
628
|
+
!VrpcAdapter._instances.has(json.c)
|
|
629
|
+
) {
|
|
630
|
+
throw new Error(`Could not find context: ${json.c}`)
|
|
631
|
+
}
|
|
606
632
|
const unwrapped = VrpcAdapter._unwrapArguments(json)
|
|
607
633
|
// special case: removing a listener the adapter does not hold (the
|
|
608
634
|
// client's subscription outlived a restart of this agent, or it sent
|
|
@@ -671,33 +697,115 @@ class VrpcAdapter {
|
|
|
671
697
|
}
|
|
672
698
|
|
|
673
699
|
static _delete (instanceId) {
|
|
700
|
+
return VrpcAdapter._end(instanceId).ended
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Ends an instance - the one teardown behind every door (a remote delete,
|
|
705
|
+
* the auto-delete of an isolated instance whose client went, an
|
|
706
|
+
* in-process delete). Node has no destructor, so an object that runs a
|
|
707
|
+
* clock or holds a connection lives on as long as anything reaches it;
|
|
708
|
+
* ending it takes three steps:
|
|
709
|
+
* 1. its name is free at once: no call reaches the object any more;
|
|
710
|
+
* 2. the adapter takes back every listener it attached (its listeners are
|
|
711
|
+
* silent from now on anyway, see _generateListener);
|
|
712
|
+
* 3. the object releases what it runs through the ECMAScript dispose
|
|
713
|
+
* protocol, `[Symbol.asyncDispose]` or else `[Symbol.dispose]`.
|
|
714
|
+
*
|
|
715
|
+
* @returns {Object} `{ ended, disposed }`: whether the name was known, and
|
|
716
|
+
* a promise when an asynchronous dispose is under way (it never rejects)
|
|
717
|
+
*/
|
|
718
|
+
static _end (instanceId) {
|
|
674
719
|
const entry = VrpcAdapter._instances.get(instanceId)
|
|
675
|
-
if (entry) {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
720
|
+
if (!entry) return { ended: false, disposed: null }
|
|
721
|
+
VrpcAdapter._instances.delete(instanceId)
|
|
722
|
+
VrpcAdapter._detachListeners(instanceId, entry.instance)
|
|
723
|
+
return { ended: true, disposed: VrpcAdapter._dispose(instanceId, entry) }
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
static _detachListeners (instanceId, instance) {
|
|
727
|
+
const registrations = VrpcAdapter._listeners[instanceId]
|
|
728
|
+
delete VrpcAdapter._listeners[instanceId]
|
|
729
|
+
if (!registrations || !VrpcAdapter._isFunction(instance.removeListener)) {
|
|
730
|
+
return
|
|
731
|
+
}
|
|
732
|
+
for (const { event, listener, clients } of Object.values(registrations)) {
|
|
733
|
+
try {
|
|
734
|
+
VrpcAdapter._removeTimes(instance, event, listener, clients.length)
|
|
735
|
+
} catch (err) {
|
|
736
|
+
// a class whose removeListener fails keeps a silent listener
|
|
679
737
|
}
|
|
680
|
-
delete VrpcAdapter._listeners[instanceId]
|
|
681
|
-
VrpcAdapter._instances.delete(instanceId)
|
|
682
|
-
return true
|
|
683
738
|
}
|
|
684
|
-
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// a subscription repeated n times may hold n attachments of one listener
|
|
742
|
+
// (EventEmitter semantics): each removeListener takes one of them
|
|
743
|
+
static _removeTimes (instance, event, listener, times) {
|
|
744
|
+
for (let n = 0; n < Math.max(1, times); n++) {
|
|
745
|
+
instance.removeListener(event, listener)
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Runs the object's dispose method. A dispose that fails or outlasts
|
|
751
|
+
* VrpcAdapter.DISPOSE_TIMEOUT is reported (event 'disposeFailed') and
|
|
752
|
+
* never thrown: the instance is gone either way.
|
|
753
|
+
*
|
|
754
|
+
* @returns {Promise|null} Settles when an asynchronous dispose is done
|
|
755
|
+
*/
|
|
756
|
+
static _dispose (instanceId, { instance, className }) {
|
|
757
|
+
const fail = error =>
|
|
758
|
+
VrpcAdapter._emitter.emit('disposeFailed', {
|
|
759
|
+
instance: instanceId,
|
|
760
|
+
className,
|
|
761
|
+
error
|
|
762
|
+
})
|
|
763
|
+
let result
|
|
764
|
+
try {
|
|
765
|
+
if (VrpcAdapter._isFunction(instance[Symbol.asyncDispose])) {
|
|
766
|
+
result = instance[Symbol.asyncDispose]()
|
|
767
|
+
} else if (VrpcAdapter._isFunction(instance[Symbol.dispose])) {
|
|
768
|
+
result = instance[Symbol.dispose]()
|
|
769
|
+
}
|
|
770
|
+
} catch (err) {
|
|
771
|
+
fail(err)
|
|
772
|
+
return null
|
|
773
|
+
}
|
|
774
|
+
if (!VrpcAdapter._isPromise(result)) return null
|
|
775
|
+
let timer
|
|
776
|
+
const bound = new Promise(resolve => {
|
|
777
|
+
timer = setTimeout(() => {
|
|
778
|
+
fail(
|
|
779
|
+
new Error(
|
|
780
|
+
`dispose did not settle within ${VrpcAdapter.DISPOSE_TIMEOUT} ms`
|
|
781
|
+
)
|
|
782
|
+
)
|
|
783
|
+
resolve()
|
|
784
|
+
}, VrpcAdapter.DISPOSE_TIMEOUT)
|
|
785
|
+
})
|
|
786
|
+
const settled = result.then(
|
|
787
|
+
() => {},
|
|
788
|
+
err => fail(err)
|
|
789
|
+
)
|
|
790
|
+
return Promise.race([settled, bound]).then(() => clearTimeout(timer))
|
|
685
791
|
}
|
|
686
792
|
|
|
687
793
|
static _unregisterClient (clientId) {
|
|
688
794
|
Object.entries(VrpcAdapter._listeners).forEach(([ik, iv]) => {
|
|
689
795
|
Object.entries(iv).forEach(([ek, ev]) => {
|
|
690
|
-
const {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
796
|
+
const { listener, event } = ev
|
|
797
|
+
// a client is listed once per subscription: its going ends them all
|
|
798
|
+
const remaining = ev.clients.filter(x => x !== clientId)
|
|
799
|
+
const gone = ev.clients.length - remaining.length
|
|
800
|
+
if (gone === 0) return
|
|
801
|
+
ev.clients = remaining
|
|
802
|
+
if (remaining.length === 0) {
|
|
803
|
+
const entry = VrpcAdapter._instances.get(ik)
|
|
804
|
+
const instance = entry && entry.instance
|
|
805
|
+
if (instance && instance.removeListener) {
|
|
806
|
+
VrpcAdapter._removeTimes(instance, event, listener, gone)
|
|
700
807
|
}
|
|
808
|
+
delete VrpcAdapter._listeners[ik][ek]
|
|
701
809
|
}
|
|
702
810
|
})
|
|
703
811
|
})
|
|
@@ -774,8 +882,27 @@ class VrpcAdapter {
|
|
|
774
882
|
return unwrapped
|
|
775
883
|
}
|
|
776
884
|
|
|
885
|
+
/**
|
|
886
|
+
* Whether the object under `instanceId` is still the one the adapter saw
|
|
887
|
+
* when it built a listener or a callback for it: a deleted instance never
|
|
888
|
+
* speaks on the bus again (#1705), even when its class kept the listener
|
|
889
|
+
* or its clock kept running. A static context (a class name) has no
|
|
890
|
+
* object and always speaks.
|
|
891
|
+
*/
|
|
892
|
+
static _speaksFor (instanceId) {
|
|
893
|
+
const entry = VrpcAdapter._instances.get(instanceId)
|
|
894
|
+
if (!entry) return () => true
|
|
895
|
+
const { instance } = entry
|
|
896
|
+
return () => {
|
|
897
|
+
const live = VrpcAdapter._instances.get(instanceId)
|
|
898
|
+
return live !== undefined && live.instance === instance
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
777
902
|
static _generateCallback ({ eventId, clientId, instanceId, isCallAll }) {
|
|
903
|
+
const speaks = VrpcAdapter._speaksFor(instanceId)
|
|
778
904
|
return (...innerArgs) => {
|
|
905
|
+
if (!speaks()) return
|
|
779
906
|
// "callAll" event notifications expect the instanceId as first argument
|
|
780
907
|
const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
|
|
781
908
|
VrpcAdapter._callback({ a, s: clientId, i: eventId })
|
|
@@ -820,7 +947,9 @@ class VrpcAdapter {
|
|
|
820
947
|
}
|
|
821
948
|
|
|
822
949
|
static _generateListener ({ eventId, instanceId, isCallAll }) {
|
|
950
|
+
const speaks = VrpcAdapter._speaksFor(instanceId)
|
|
823
951
|
return (...innerArgs) => {
|
|
952
|
+
if (!speaks()) return
|
|
824
953
|
const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
|
|
825
954
|
VrpcAdapter._callback({ a, i: eventId })
|
|
826
955
|
}
|
|
@@ -1003,12 +1132,28 @@ class VrpcAdapter {
|
|
|
1003
1132
|
* @property {String} instance The instance name
|
|
1004
1133
|
*/
|
|
1005
1134
|
|
|
1135
|
+
/**
|
|
1136
|
+
* Event 'disposeFailed'
|
|
1137
|
+
*
|
|
1138
|
+
* Emitted when the dispose method of a deleted instance throws, rejects or
|
|
1139
|
+
* does not settle within VrpcAdapter.DISPOSE_TIMEOUT. The instance is gone
|
|
1140
|
+
* regardless.
|
|
1141
|
+
*
|
|
1142
|
+
* @event VrpcAdapter#disposeFailed
|
|
1143
|
+
* @type {Object}
|
|
1144
|
+
* @property {String} className The class name of the deleted instance
|
|
1145
|
+
* @property {String} instance The instance name
|
|
1146
|
+
* @property {Error} error What went wrong
|
|
1147
|
+
*/
|
|
1148
|
+
|
|
1006
1149
|
// Initialize static members
|
|
1007
1150
|
VrpcAdapter._functionRegistry = new Map()
|
|
1008
1151
|
VrpcAdapter._instances = new Map()
|
|
1009
1152
|
VrpcAdapter._emitter = new EventEmitter()
|
|
1010
1153
|
VrpcAdapter._correlationId = 0
|
|
1011
1154
|
VrpcAdapter._listeners = {}
|
|
1155
|
+
// the longest a delete waits for the dispose method of its instance
|
|
1156
|
+
VrpcAdapter.DISPOSE_TIMEOUT = 10000
|
|
1012
1157
|
VrpcAdapter._blackList = new Set([
|
|
1013
1158
|
'caller',
|
|
1014
1159
|
'callee',
|
package/vrpc/VrpcAgent.js
CHANGED
|
@@ -145,6 +145,13 @@ class VrpcAgent extends EventEmitter {
|
|
|
145
145
|
}
|
|
146
146
|
this._baseTopic = `${this._domain}/${this._agent}`
|
|
147
147
|
VrpcAdapter.onCallback(this._handleVrpcCallback.bind(this))
|
|
148
|
+
// a deleted instance whose dispose failed is gone anyway: say why
|
|
149
|
+
this._handleDisposeFailed = ({ instance, className, error }) =>
|
|
150
|
+
this._log.warn(
|
|
151
|
+
error,
|
|
152
|
+
`Dispose of deleted instance ${className}::${instance} failed: ${error.message}`
|
|
153
|
+
)
|
|
154
|
+
VrpcAdapter.on('disposeFailed', this._handleDisposeFailed)
|
|
148
155
|
// maps clientId to instanceId
|
|
149
156
|
this._isolatedInstances = new Map()
|
|
150
157
|
this._sharedInstances = new Map()
|
|
@@ -153,6 +160,11 @@ class VrpcAgent extends EventEmitter {
|
|
|
153
160
|
subscribe: (topics, options) => this._mqttSubscribe(topics, options),
|
|
154
161
|
log: this._log
|
|
155
162
|
})
|
|
163
|
+
// counts the connections: an announcement belongs to the connection
|
|
164
|
+
// that prepared it (see _handleConnect)
|
|
165
|
+
this._connectGeneration = 0
|
|
166
|
+
// connected, request topics subscribed and info announced
|
|
167
|
+
this._serving = false
|
|
156
168
|
|
|
157
169
|
// Handle the internal error event in case the user forgot to implement it
|
|
158
170
|
this.on('error', err => {
|
|
@@ -172,8 +184,9 @@ class VrpcAgent extends EventEmitter {
|
|
|
172
184
|
* If the connection could not be established because of authorization
|
|
173
185
|
* failure, the 'error' event will be emitted.
|
|
174
186
|
*
|
|
175
|
-
* @return {Promise} Resolves once
|
|
176
|
-
*
|
|
187
|
+
* @return {Promise} Resolves once the agent serves - connected, its
|
|
188
|
+
* request topics subscribed and its info announced - or once it was
|
|
189
|
+
* explicitly ended before that, never rejects
|
|
177
190
|
*/
|
|
178
191
|
async serve () {
|
|
179
192
|
let username = this._username
|
|
@@ -226,6 +239,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
226
239
|
* @returns {Promise} Resolves when disconnected and ended
|
|
227
240
|
*/
|
|
228
241
|
async end ({ unregister = false } = {}) {
|
|
242
|
+
VrpcAdapter.off('disposeFailed', this._handleDisposeFailed)
|
|
229
243
|
this._subscribeRetrier.cancel()
|
|
230
244
|
try {
|
|
231
245
|
if (!this._client || !this._client.connected) {
|
|
@@ -377,7 +391,13 @@ class VrpcAgent extends EventEmitter {
|
|
|
377
391
|
)
|
|
378
392
|
}
|
|
379
393
|
|
|
380
|
-
|
|
394
|
+
/**
|
|
395
|
+
* Subscribes topic(s); `onSettled` is called once the broker has
|
|
396
|
+
* answered (granted, refused or failed alike).
|
|
397
|
+
*
|
|
398
|
+
* @private
|
|
399
|
+
*/
|
|
400
|
+
_mqttSubscribe (topic, options, onSettled) {
|
|
381
401
|
this._client.subscribe(
|
|
382
402
|
topic,
|
|
383
403
|
{ qos: this._qos, ...options },
|
|
@@ -424,6 +444,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
424
444
|
this._log.debug(`Already subscribed to topic '${topic}'`)
|
|
425
445
|
}
|
|
426
446
|
}
|
|
447
|
+
if (onSettled) onSettled()
|
|
427
448
|
}
|
|
428
449
|
)
|
|
429
450
|
}
|
|
@@ -458,12 +479,18 @@ class VrpcAgent extends EventEmitter {
|
|
|
458
479
|
return VrpcAdapter._getMetaData(className)
|
|
459
480
|
}
|
|
460
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Resolves once the agent serves (its own 'connect' event: connected,
|
|
484
|
+
* request topics subscribed, info announced), or once it was ended.
|
|
485
|
+
*
|
|
486
|
+
* @private
|
|
487
|
+
*/
|
|
461
488
|
async _ensureConnected () {
|
|
462
489
|
return new Promise(resolve => {
|
|
463
|
-
if (this.
|
|
490
|
+
if (this._serving) {
|
|
464
491
|
resolve()
|
|
465
492
|
} else {
|
|
466
|
-
this.
|
|
493
|
+
this.once('connect', resolve)
|
|
467
494
|
// Will be triggered if the user called 'agent.end()'
|
|
468
495
|
this.once('end', resolve)
|
|
469
496
|
}
|
|
@@ -502,36 +529,64 @@ class VrpcAgent extends EventEmitter {
|
|
|
502
529
|
return str
|
|
503
530
|
}
|
|
504
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Serve first, announce second. The request topics - the statics and
|
|
534
|
+
* the methods of every instance that exists already - are subscribed
|
|
535
|
+
* before the agent publishes its agent and class info, and the
|
|
536
|
+
* announcement waits until the broker has answered every subscription.
|
|
537
|
+
* A client acts on the announcement at once; a request it publishes
|
|
538
|
+
* before the broker routes the topic is lost (QoS 0) and only times out
|
|
539
|
+
* on the caller's side. That was the fate of the first call into an
|
|
540
|
+
* agent that came online after the client waiting for it. A refused
|
|
541
|
+
* subscription is the retrier's business and delays nothing.
|
|
542
|
+
*
|
|
543
|
+
* @private
|
|
544
|
+
*/
|
|
505
545
|
_handleConnect () {
|
|
506
546
|
this._log.info('[OK]')
|
|
507
547
|
// everything is subscribed afresh: pending retries are moot
|
|
508
548
|
this._subscribeRetrier.cancel()
|
|
549
|
+
const generation = ++this._connectGeneration
|
|
550
|
+
const batches = []
|
|
509
551
|
try {
|
|
510
|
-
const
|
|
511
|
-
if (
|
|
552
|
+
const statics = this._generateTopics()
|
|
553
|
+
if (statics.length > 0) batches.push(statics)
|
|
512
554
|
} catch (err) {
|
|
513
555
|
this._log.error(
|
|
514
556
|
err,
|
|
515
557
|
`Problem during initial topic subscription: ${err.message}`
|
|
516
558
|
)
|
|
517
559
|
}
|
|
518
|
-
//
|
|
519
|
-
this._publishAgentInfoMessage()
|
|
520
|
-
|
|
521
|
-
// Publish class information
|
|
522
|
-
const classes = this._getClasses()
|
|
523
|
-
classes.forEach(className => {
|
|
524
|
-
this._publishClassInfoMessage(className)
|
|
525
|
-
this._publishClassInfoConciseMessage(className)
|
|
526
|
-
})
|
|
527
|
-
// Register all pre-existing instances
|
|
560
|
+
// all pre-existing instances
|
|
528
561
|
for (const [
|
|
529
562
|
instanceId,
|
|
530
563
|
{ className }
|
|
531
564
|
] of VrpcAdapter._instances.entries()) {
|
|
532
|
-
this.
|
|
565
|
+
batches.push(this._methodsTopic(className, instanceId))
|
|
566
|
+
}
|
|
567
|
+
let pending = batches.length
|
|
568
|
+
const announce = () => {
|
|
569
|
+
// a connection that ended meanwhile announces nothing: the next
|
|
570
|
+
// one prepares and announces itself
|
|
571
|
+
if (generation !== this._connectGeneration) return
|
|
572
|
+
this._publishAgentInfoMessage()
|
|
573
|
+
for (const className of this._getClasses()) {
|
|
574
|
+
this._publishClassInfoMessage(className)
|
|
575
|
+
this._publishClassInfoConciseMessage(className)
|
|
576
|
+
}
|
|
577
|
+
this._serving = true
|
|
578
|
+
this.emit('connect')
|
|
579
|
+
}
|
|
580
|
+
if (pending === 0) {
|
|
581
|
+
announce()
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
for (const topic of batches) {
|
|
585
|
+
this._mqttSubscribe(topic, undefined, () => {
|
|
586
|
+
pending -= 1
|
|
587
|
+
if (pending === 0) announce()
|
|
588
|
+
})
|
|
533
589
|
}
|
|
534
|
-
this.emit('connect')
|
|
535
590
|
}
|
|
536
591
|
|
|
537
592
|
_publishAgentInfoMessage () {
|
|
@@ -697,7 +752,9 @@ class VrpcAgent extends EventEmitter {
|
|
|
697
752
|
const entry = this._isolatedInstances.get(connectionId)
|
|
698
753
|
if (entry) {
|
|
699
754
|
entry.forEach(instanceId => {
|
|
755
|
+
const known = VrpcAdapter._instances.get(instanceId)
|
|
700
756
|
const json = {
|
|
757
|
+
c: known && known.className,
|
|
701
758
|
f: '__delete__',
|
|
702
759
|
a: [instanceId],
|
|
703
760
|
r: null,
|
|
@@ -705,6 +762,8 @@ class VrpcAgent extends EventEmitter {
|
|
|
705
762
|
}
|
|
706
763
|
VrpcAdapter._call(json)
|
|
707
764
|
if (json.r) {
|
|
765
|
+
// gone like a remote delete: no request topic outlives it
|
|
766
|
+
this._unsubscribeMethodsOfDeletedInstance(known.className, instanceId)
|
|
708
767
|
this._log.debug(`Auto-deleted isolated instance: ${instanceId}`)
|
|
709
768
|
}
|
|
710
769
|
})
|
|
@@ -785,14 +844,23 @@ class VrpcAgent extends EventEmitter {
|
|
|
785
844
|
return true
|
|
786
845
|
}
|
|
787
846
|
|
|
847
|
+
/**
|
|
848
|
+
* The request topics of one instance: every method of it.
|
|
849
|
+
*
|
|
850
|
+
* @private
|
|
851
|
+
*/
|
|
852
|
+
_methodsTopic (className, instance) {
|
|
853
|
+
return `${this._baseTopic}/${className}/${instance}/+`
|
|
854
|
+
}
|
|
855
|
+
|
|
788
856
|
_subscribeToMethodsOfNewInstance (className, instance) {
|
|
789
|
-
const topic =
|
|
857
|
+
const topic = this._methodsTopic(className, instance)
|
|
790
858
|
this._mqttSubscribe(topic)
|
|
791
859
|
this._log.debug(`Subscribed to new topic after instantiation: ${topic}`)
|
|
792
860
|
}
|
|
793
861
|
|
|
794
862
|
_unsubscribeMethodsOfDeletedInstance (className, instance) {
|
|
795
|
-
const topic =
|
|
863
|
+
const topic = this._methodsTopic(className, instance)
|
|
796
864
|
this._mqttUnsubscribe(topic)
|
|
797
865
|
this._log.debug(`Unsubscribed from topic after deletion: ${topic}`)
|
|
798
866
|
}
|
|
@@ -807,6 +875,8 @@ class VrpcAgent extends EventEmitter {
|
|
|
807
875
|
}
|
|
808
876
|
|
|
809
877
|
_handleClose () {
|
|
878
|
+
// the next connection prepares and announces itself afresh
|
|
879
|
+
this._serving = false
|
|
810
880
|
this.emit('close')
|
|
811
881
|
}
|
|
812
882
|
|