vrpc 3.12.1 → 3.14.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 +5 -5
- package/package.json +5 -2
- package/vrpc/ReconnectBackoff.js +79 -0
- package/vrpc/VrpcAdapter.js +169 -24
- package/vrpc/VrpcAgent.js +24 -1
- package/vrpc/VrpcBrowserAdapter.js +174 -28
- package/vrpc/VrpcBrowserAgent.js +12 -0
- package/vrpc/VrpcClient.js +137 -24
- 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.14.0",
|
|
4
4
|
"description": "Variadic Remote Procedure Calls",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|
|
@@ -12,14 +12,16 @@
|
|
|
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:reconnect && 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
|
+
"test:reconnect": "./node_modules/.bin/mocha tests/reconnect/*.spec.js --exit",
|
|
18
19
|
"test:agent": "tests/agent/test.sh",
|
|
19
20
|
"test:client": "tests/client/test.sh",
|
|
20
21
|
"test:performance": "tests/performance/test.sh",
|
|
21
22
|
"test:persistor": "./node_modules/.bin/mocha tests/persistor/*.js --timeout 30000 --exit",
|
|
22
23
|
"test:lifecycle": "./node_modules/.bin/mocha tests/lifecycle/*.js --timeout 30000 --exit",
|
|
24
|
+
"test:legacy-runtime": "tests/legacy-runtime/test.sh",
|
|
23
25
|
"test:production": "./node_modules/.bin/mocha tests/production/lifeCycleTest.js --timeout 30000 --exit && tests/production/test.sh"
|
|
24
26
|
},
|
|
25
27
|
"bin": {
|
|
@@ -56,6 +58,7 @@
|
|
|
56
58
|
"fs-extra": "^11.3.1",
|
|
57
59
|
"jsdoc-to-markdown": "^7.0.1",
|
|
58
60
|
"mocha": "^9.2.2",
|
|
61
|
+
"mqtt-packet": "^9.0.2",
|
|
59
62
|
"os-browserify": "^0.3.0",
|
|
60
63
|
"sinon": "^21.0.0",
|
|
61
64
|
"stream-browserify": "^3.0.0",
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paces the reconnects of an mqtt.js client after refused CONNACKs.
|
|
3
|
+
*
|
|
4
|
+
* mqtt.js retries at a fixed `reconnectPeriod`. A connection the broker
|
|
5
|
+
* refuses - a stale credential, an authorization service that is away -
|
|
6
|
+
* would knock once per period for as long as it lives, and an
|
|
7
|
+
* authorization service may block a client id that knocks too often. So
|
|
8
|
+
* every refusal doubles the wait up to `max`, and a successful connect
|
|
9
|
+
* restores the base. The refusal itself stays visible as the client's
|
|
10
|
+
* 'error' event, so a host can renew its credentials and `hurry()`.
|
|
11
|
+
*
|
|
12
|
+
* mqtt.js reads `reconnectPeriod` only when it arms its reconnect
|
|
13
|
+
* interval, once per outage; re-arming it takes the internal
|
|
14
|
+
* `_clearReconnect` / `_setupReconnect`. Without them (a future mqtt.js)
|
|
15
|
+
* a new period applies from the next outage on.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const REFUSED = /^Connection refused/
|
|
19
|
+
|
|
20
|
+
class ReconnectBackoff {
|
|
21
|
+
/**
|
|
22
|
+
* @param {Object} client an mqtt.js client created with
|
|
23
|
+
* `reconnectOnConnackError: true`
|
|
24
|
+
* @param {Object} [options]
|
|
25
|
+
* @param {Number} [options.max=30000] longest wait in ms between attempts
|
|
26
|
+
*/
|
|
27
|
+
constructor (client, { max = 30000 } = {}) {
|
|
28
|
+
this._client = client
|
|
29
|
+
this._base = client.options.reconnectPeriod
|
|
30
|
+
this._max = Math.max(max, this._base)
|
|
31
|
+
this._next = this._base
|
|
32
|
+
client.on('error', err => {
|
|
33
|
+
if (ReconnectBackoff.isRefusal(err)) this._slowDown()
|
|
34
|
+
})
|
|
35
|
+
client.on('connect', () => this._restore())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Whether an mqtt.js error is a refused CONNACK. */
|
|
39
|
+
static isRefusal (err) {
|
|
40
|
+
return (
|
|
41
|
+
!!err && typeof err.code === 'number' && REFUSED.test(err.message || '')
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The wait in ms before the next attempt. */
|
|
46
|
+
get period () {
|
|
47
|
+
return this._client.options.reconnectPeriod
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* New credentials: the next attempt follows within the base period
|
|
52
|
+
* instead of waiting out the back-off. A live connection stays as it is.
|
|
53
|
+
*/
|
|
54
|
+
hurry () {
|
|
55
|
+
this._restore()
|
|
56
|
+
const client = this._client
|
|
57
|
+
if (client.connected || client.disconnecting || !client.reconnecting) return
|
|
58
|
+
if (typeof client._clearReconnect !== 'function') return
|
|
59
|
+
client._clearReconnect()
|
|
60
|
+
client._setupReconnect()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Runs inside the CONNACK handler's 'error' emit: the handler re-arms
|
|
64
|
+
// the reconnect interval right after (reconnectOnConnackError), with the
|
|
65
|
+
// period set here
|
|
66
|
+
_slowDown () {
|
|
67
|
+
const client = this._client
|
|
68
|
+
client.options.reconnectPeriod = this._next
|
|
69
|
+
this._next = Math.min(this._next * 2, this._max)
|
|
70
|
+
if (typeof client._clearReconnect === 'function') client._clearReconnect()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_restore () {
|
|
74
|
+
this._next = this._base
|
|
75
|
+
this._client.options.reconnectPeriod = this._base
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = ReconnectBackoff
|
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
|
@@ -7,6 +7,7 @@ const EventEmitter = require('events')
|
|
|
7
7
|
const jsonStringifySafe = require('json-stringify-safe')
|
|
8
8
|
const VrpcAdapter = require('./VrpcAdapter')
|
|
9
9
|
const SubscribeRetrier = require('./SubscribeRetrier')
|
|
10
|
+
const ReconnectBackoff = require('./ReconnectBackoff')
|
|
10
11
|
|
|
11
12
|
const VRPC_PROTOCOL_VERSION = 3
|
|
12
13
|
|
|
@@ -145,6 +146,13 @@ class VrpcAgent extends EventEmitter {
|
|
|
145
146
|
}
|
|
146
147
|
this._baseTopic = `${this._domain}/${this._agent}`
|
|
147
148
|
VrpcAdapter.onCallback(this._handleVrpcCallback.bind(this))
|
|
149
|
+
// a deleted instance whose dispose failed is gone anyway: say why
|
|
150
|
+
this._handleDisposeFailed = ({ instance, className, error }) =>
|
|
151
|
+
this._log.warn(
|
|
152
|
+
error,
|
|
153
|
+
`Dispose of deleted instance ${className}::${instance} failed: ${error.message}`
|
|
154
|
+
)
|
|
155
|
+
VrpcAdapter.on('disposeFailed', this._handleDisposeFailed)
|
|
148
156
|
// maps clientId to instanceId
|
|
149
157
|
this._isolatedInstances = new Map()
|
|
150
158
|
this._sharedInstances = new Map()
|
|
@@ -214,6 +222,8 @@ class VrpcAgent extends EventEmitter {
|
|
|
214
222
|
this._log.info(`Broker : ${this._broker}`)
|
|
215
223
|
this._log.info('Connecting to MQTT server...')
|
|
216
224
|
this._client = mqtt.connect(this._broker, this._options)
|
|
225
|
+
// a refused agent knocks less and less often, not once a second
|
|
226
|
+
this._backoff = new ReconnectBackoff(this._client)
|
|
217
227
|
this._client.on('connect', this._handleConnect.bind(this))
|
|
218
228
|
this._client.on('reconnect', this._handleReconnect.bind(this))
|
|
219
229
|
this._client.on('error', this._handleError.bind(this))
|
|
@@ -232,10 +242,19 @@ class VrpcAgent extends EventEmitter {
|
|
|
232
242
|
* @returns {Promise} Resolves when disconnected and ended
|
|
233
243
|
*/
|
|
234
244
|
async end ({ unregister = false } = {}) {
|
|
245
|
+
VrpcAdapter.off('disposeFailed', this._handleDisposeFailed)
|
|
235
246
|
this._subscribeRetrier.cancel()
|
|
236
247
|
try {
|
|
237
248
|
if (!this._client || !this._client.connected) {
|
|
238
|
-
|
|
249
|
+
// a client that is still trying (refused, broker away) stops
|
|
250
|
+
// trying: end() is how a refusal becomes final
|
|
251
|
+
// (its 'end' is forwarded as the agent's)
|
|
252
|
+
if (this._client) {
|
|
253
|
+
const client = this._client
|
|
254
|
+
await new Promise(resolve => client.end(true, {}, resolve))
|
|
255
|
+
} else {
|
|
256
|
+
this.emit('end')
|
|
257
|
+
}
|
|
239
258
|
return
|
|
240
259
|
}
|
|
241
260
|
const agentTopic = `${this._baseTopic}/__agentInfo__`
|
|
@@ -744,7 +763,9 @@ class VrpcAgent extends EventEmitter {
|
|
|
744
763
|
const entry = this._isolatedInstances.get(connectionId)
|
|
745
764
|
if (entry) {
|
|
746
765
|
entry.forEach(instanceId => {
|
|
766
|
+
const known = VrpcAdapter._instances.get(instanceId)
|
|
747
767
|
const json = {
|
|
768
|
+
c: known && known.className,
|
|
748
769
|
f: '__delete__',
|
|
749
770
|
a: [instanceId],
|
|
750
771
|
r: null,
|
|
@@ -752,6 +773,8 @@ class VrpcAgent extends EventEmitter {
|
|
|
752
773
|
}
|
|
753
774
|
VrpcAdapter._call(json)
|
|
754
775
|
if (json.r) {
|
|
776
|
+
// gone like a remote delete: no request topic outlives it
|
|
777
|
+
this._unsubscribeMethodsOfDeletedInstance(known.className, instanceId)
|
|
755
778
|
this._log.debug(`Auto-deleted isolated instance: ${instanceId}`)
|
|
756
779
|
}
|
|
757
780
|
})
|