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
|
@@ -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,6 +68,13 @@ 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()
|
|
@@ -146,6 +153,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
146
153
|
* @returns {Promise} Resolves when disconnected and ended
|
|
147
154
|
*/
|
|
148
155
|
async end ({ unregister = false } = {}) {
|
|
156
|
+
VrpcAdapter.off('disposeFailed', this._handleDisposeFailed)
|
|
149
157
|
try {
|
|
150
158
|
if (!this._client || !this._client.connected) {
|
|
151
159
|
this.emit('end')
|
|
@@ -542,7 +550,9 @@ class VrpcAgent extends EventEmitter {
|
|
|
542
550
|
const entry = this._isolatedInstances.get(connectionId)
|
|
543
551
|
if (entry) {
|
|
544
552
|
entry.forEach(instanceId => {
|
|
553
|
+
const known = VrpcAdapter._instances.get(instanceId)
|
|
545
554
|
const json = {
|
|
555
|
+
c: known && known.className,
|
|
546
556
|
f: '__delete__',
|
|
547
557
|
a: [instanceId],
|
|
548
558
|
r: null,
|
|
@@ -550,6 +560,8 @@ class VrpcAgent extends EventEmitter {
|
|
|
550
560
|
}
|
|
551
561
|
VrpcAdapter._call(json)
|
|
552
562
|
if (json.r) {
|
|
563
|
+
// gone like a remote delete: no request topic outlives it
|
|
564
|
+
this._unsubscribeMethodsOfDeletedInstance(known.className, instanceId)
|
|
553
565
|
this._log.debug(`Auto-deleted isolated instance: ${instanceId}`)
|
|
554
566
|
}
|
|
555
567
|
})
|
package/vrpc/VrpcClient.js
CHANGED
|
@@ -40,9 +40,14 @@ const { nanoid } = require('nanoid')
|
|
|
40
40
|
const mqtt = require('mqtt')
|
|
41
41
|
const EventEmitter = require('events')
|
|
42
42
|
const SubscribeRetrier = require('./SubscribeRetrier')
|
|
43
|
+
const ReconnectBackoff = require('./ReconnectBackoff')
|
|
43
44
|
|
|
44
45
|
const VRPC_PROTOCOL_VERSION = 3
|
|
45
46
|
|
|
47
|
+
// mqtt clients this client ended on purpose: the 'client disconnecting'
|
|
48
|
+
// workaround must never revive one of them
|
|
49
|
+
const ENDED = new WeakSet()
|
|
50
|
+
|
|
46
51
|
/**
|
|
47
52
|
* Client capable of creating proxy objects and remotely calling
|
|
48
53
|
* functions as provided through one or more (distributed) agents.
|
|
@@ -220,7 +225,19 @@ class VrpcClient extends EventEmitter {
|
|
|
220
225
|
/**
|
|
221
226
|
* Actually connects to the MQTT broker.
|
|
222
227
|
*
|
|
228
|
+
* A refused connection is retried with a growing wait (doubling up to
|
|
229
|
+
* 30 s, back to the start once connected), and every refusal is emitted
|
|
230
|
+
* as 'error', so a caller can renew its credentials (see
|
|
231
|
+
* updateCredentials).
|
|
232
|
+
*
|
|
223
233
|
* @emits connected
|
|
234
|
+
* @param {Object} [options]
|
|
235
|
+
* @param {Boolean} [options.keepTrying=false] If true, a connection that
|
|
236
|
+
* is not made within [timeout] still rejects the returned promise, but
|
|
237
|
+
* keeps trying until end() and emits 'connect' once it stands, and a
|
|
238
|
+
* further connect({ keepTrying: true }) while it is trying waits for it
|
|
239
|
+
* instead of opening a second connection. If false (the default), nothing
|
|
240
|
+
* changes: the client gives up at the timeout and must be connected again.
|
|
224
241
|
* @returns {Promise} Resolves once connected within [timeout], rejects otherwise
|
|
225
242
|
* @example
|
|
226
243
|
* try {
|
|
@@ -229,14 +246,14 @@ class VrpcClient extends EventEmitter {
|
|
|
229
246
|
* console.error(`Could not connect because: ${err.message}`)
|
|
230
247
|
* }
|
|
231
248
|
*/
|
|
232
|
-
async connect () {
|
|
249
|
+
async connect ({ keepTrying = false } = {}) {
|
|
233
250
|
if (this._client && this._client.connected) return
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (this.
|
|
237
|
-
|
|
238
|
-
password = this._token
|
|
251
|
+
// keepTrying: a connection that is still trying is waited for, never
|
|
252
|
+
// doubled (without it, connect() behaves as it always did)
|
|
253
|
+
if (keepTrying && this._client && !ENDED.has(this._client)) {
|
|
254
|
+
return this._awaitConnect(this._client, { keepTrying })
|
|
239
255
|
}
|
|
256
|
+
const { username, password } = this._credentials()
|
|
240
257
|
const options = {
|
|
241
258
|
username,
|
|
242
259
|
password,
|
|
@@ -264,6 +281,7 @@ class VrpcClient extends EventEmitter {
|
|
|
264
281
|
})
|
|
265
282
|
|
|
266
283
|
this._client = mqtt.connect(this._broker, options)
|
|
284
|
+
this._backoff = new ReconnectBackoff(this._client)
|
|
267
285
|
|
|
268
286
|
// Persistent lifecycle handlers
|
|
269
287
|
this._client.on('offline', () => {
|
|
@@ -393,25 +411,78 @@ class VrpcClient extends EventEmitter {
|
|
|
393
411
|
}
|
|
394
412
|
})
|
|
395
413
|
|
|
414
|
+
return this._awaitConnect(this._client, { keepTrying })
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
_awaitConnect (client, { keepTrying }) {
|
|
396
418
|
return new Promise((resolve, reject) => {
|
|
397
419
|
const timeout = setTimeout(() => {
|
|
398
420
|
// This will run only if the 'connect' event isn't fired in time.
|
|
421
|
+
const error = new Error(
|
|
422
|
+
`Connection trial timed out (> ${this._timeout} ms)`
|
|
423
|
+
)
|
|
424
|
+
if (keepTrying) {
|
|
425
|
+
// the client keeps trying in the background, until end()
|
|
426
|
+
client.removeListener('connect', onConnect)
|
|
427
|
+
reject(error)
|
|
428
|
+
return
|
|
429
|
+
}
|
|
399
430
|
// We explicitly end the client to stop it from retrying in the background.
|
|
400
|
-
|
|
401
|
-
reject(
|
|
402
|
-
new Error(`Connection trial timed out (> ${this._timeout} ms)`)
|
|
403
|
-
)
|
|
404
|
-
})
|
|
431
|
+
VrpcClient._endMqtt(client, true, {}, () => reject(error))
|
|
405
432
|
}, this._timeout)
|
|
406
433
|
|
|
407
|
-
|
|
434
|
+
const onConnect = () => {
|
|
408
435
|
// If we connect successfully, clear the timeout and resolve the promise.
|
|
409
436
|
clearTimeout(timeout)
|
|
410
437
|
resolve()
|
|
411
|
-
}
|
|
438
|
+
}
|
|
439
|
+
client.once('connect', onConnect)
|
|
412
440
|
})
|
|
413
441
|
}
|
|
414
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Replaces the credentials the connection logs in with
|
|
445
|
+
*
|
|
446
|
+
* The broker checks credentials when a connection is made: a live
|
|
447
|
+
* connection carries on undisturbed, and the next one - after a network
|
|
448
|
+
* loss, a broker restart - logs in with these. A connection that is
|
|
449
|
+
* being refused right now tries again with them within a second,
|
|
450
|
+
* instead of waiting out its back-off.
|
|
451
|
+
*
|
|
452
|
+
* @param {Object} credentials Only the given fields change
|
|
453
|
+
* @param {String} [credentials.username] MQTT username
|
|
454
|
+
* @param {String} [credentials.password] MQTT password (if no token is used)
|
|
455
|
+
* @param {String} [credentials.token] Access token
|
|
456
|
+
*/
|
|
457
|
+
updateCredentials ({ username, password, token } = {}) {
|
|
458
|
+
if (username !== undefined) this._username = username
|
|
459
|
+
if (password !== undefined) this._password = password
|
|
460
|
+
if (token !== undefined) this._token = token
|
|
461
|
+
if (!this._client || ENDED.has(this._client)) return
|
|
462
|
+
const credentials = this._credentials()
|
|
463
|
+
for (const key of ['username', 'password']) {
|
|
464
|
+
if (credentials[key] === undefined) delete this._client.options[key]
|
|
465
|
+
else this._client.options[key] = credentials[key]
|
|
466
|
+
}
|
|
467
|
+
this._backoff.hurry()
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// what the broker sees: a token rides as the password
|
|
471
|
+
_credentials () {
|
|
472
|
+
let username = this._username
|
|
473
|
+
let password = this._password
|
|
474
|
+
if (this._token) {
|
|
475
|
+
username = username || this._generateUserName()
|
|
476
|
+
password = this._token
|
|
477
|
+
}
|
|
478
|
+
return { username, password }
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
static _endMqtt (client, force, options, callback) {
|
|
482
|
+
ENDED.add(client)
|
|
483
|
+
client.end(force, options, callback)
|
|
484
|
+
}
|
|
485
|
+
|
|
415
486
|
/**
|
|
416
487
|
* Creates a new remote instance and provides a proxy to it.
|
|
417
488
|
*
|
|
@@ -546,7 +617,27 @@ class VrpcClient extends EventEmitter {
|
|
|
546
617
|
const topic = `${this._domain}/${agent}/${className}/__static__/__delete__`
|
|
547
618
|
this._mqttPublish(topic, JSON.stringify(json))
|
|
548
619
|
delete this._proxies[`${agent}/${className}/${instance}`]
|
|
549
|
-
|
|
620
|
+
const deleted = await this._handleAgentAnswer(json, agent)
|
|
621
|
+
// the answer and the class info that no longer lists the instance
|
|
622
|
+
// travel on different topics, and a broker need not keep their order:
|
|
623
|
+
// what this client deleted is gone for it at once
|
|
624
|
+
if (deleted === true) this._forgetInstance({ agent, className, instance })
|
|
625
|
+
return deleted
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Takes an instance off this client's view of its agent, the way a class
|
|
630
|
+
* info without it does: listing, instanceGone, cached subscriptions. A
|
|
631
|
+
* class info that arrives later finds nothing left to remove.
|
|
632
|
+
*/
|
|
633
|
+
_forgetInstance ({ agent, className, instance }) {
|
|
634
|
+
const info = this._agents[agent] && this._agents[agent].classes[className]
|
|
635
|
+
if (!info || !Array.isArray(info.instances)) return
|
|
636
|
+
if (!info.instances.includes(instance)) return
|
|
637
|
+
info.instances = info.instances.filter(x => x !== instance)
|
|
638
|
+
const domain = this._domain
|
|
639
|
+
this.emit('instanceGone', [instance], { domain, agent, className })
|
|
640
|
+
this._clearCachedSubscriptions({ lostInstance: instance, agent, className })
|
|
550
641
|
}
|
|
551
642
|
|
|
552
643
|
/**
|
|
@@ -795,7 +886,10 @@ class VrpcClient extends EventEmitter {
|
|
|
795
886
|
this._token = token
|
|
796
887
|
this._agent = agent
|
|
797
888
|
if (this._client) {
|
|
798
|
-
|
|
889
|
+
const client = this._client
|
|
890
|
+
await new Promise(resolve =>
|
|
891
|
+
VrpcClient._endMqtt(client, false, {}, resolve)
|
|
892
|
+
)
|
|
799
893
|
}
|
|
800
894
|
return this.connect()
|
|
801
895
|
}
|
|
@@ -842,7 +936,9 @@ class VrpcClient extends EventEmitter {
|
|
|
842
936
|
this.removeAllListeners()
|
|
843
937
|
const client = this._client
|
|
844
938
|
this._client = null // Prevent race conditions during shutdown
|
|
845
|
-
return new Promise(resolve =>
|
|
939
|
+
return new Promise(resolve =>
|
|
940
|
+
VrpcClient._endMqtt(client, false, {}, resolve)
|
|
941
|
+
)
|
|
846
942
|
}
|
|
847
943
|
|
|
848
944
|
// Private functions
|
|
@@ -909,8 +1005,16 @@ class VrpcClient extends EventEmitter {
|
|
|
909
1005
|
return `${this._domain}/${this._mqttClientId}/${this._connectionSecret}`
|
|
910
1006
|
}
|
|
911
1007
|
|
|
1008
|
+
// The 'client disconnecting' workaround below revives a client mqtt.js
|
|
1009
|
+
// left half-closed, never one that was ended on purpose (a connect
|
|
1010
|
+
// timeout, end(), reconnectWithToken) or has been replaced since
|
|
1011
|
+
_isRevivable (client) {
|
|
1012
|
+
return client === this._client && !ENDED.has(client)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
912
1015
|
_mqttPublish (topic, message, options) {
|
|
913
|
-
this._client
|
|
1016
|
+
const client = this._client
|
|
1017
|
+
client.publish(
|
|
914
1018
|
topic,
|
|
915
1019
|
message,
|
|
916
1020
|
{ qos: this._qos, ...options },
|
|
@@ -919,13 +1023,16 @@ class VrpcClient extends EventEmitter {
|
|
|
919
1023
|
this._log.warn(
|
|
920
1024
|
`Could not publish MQTT message because: ${err.message}`
|
|
921
1025
|
)
|
|
922
|
-
if (
|
|
1026
|
+
if (
|
|
1027
|
+
err.message === 'client disconnecting' &&
|
|
1028
|
+
this._isRevivable(client)
|
|
1029
|
+
) {
|
|
923
1030
|
// This most certainly relates to a bug in MQTT.js (#1284)
|
|
924
1031
|
this._log.error(`Failed to publish, because: ${err.message}`)
|
|
925
1032
|
this.emit('error', err)
|
|
926
1033
|
this._log.info('Forcing reconnect now...')
|
|
927
|
-
|
|
928
|
-
|
|
1034
|
+
client.disconnecting = false
|
|
1035
|
+
client.reconnect()
|
|
929
1036
|
}
|
|
930
1037
|
}
|
|
931
1038
|
}
|
|
@@ -933,8 +1040,9 @@ class VrpcClient extends EventEmitter {
|
|
|
933
1040
|
}
|
|
934
1041
|
|
|
935
1042
|
async _mqttSubscribe (topic, options) {
|
|
1043
|
+
const client = this._client
|
|
936
1044
|
return new Promise((resolve, reject) => {
|
|
937
|
-
|
|
1045
|
+
client.subscribe(
|
|
938
1046
|
topic,
|
|
939
1047
|
{ qos: this._qos, ...options },
|
|
940
1048
|
(err, granted) => {
|
|
@@ -942,15 +1050,20 @@ class VrpcClient extends EventEmitter {
|
|
|
942
1050
|
this._log.warn(
|
|
943
1051
|
`Could not subscribe to topic(s): '${topic}', because: ${err.message}`
|
|
944
1052
|
)
|
|
945
|
-
if (
|
|
1053
|
+
if (
|
|
1054
|
+
err.message === 'client disconnecting' &&
|
|
1055
|
+
this._isRevivable(client)
|
|
1056
|
+
) {
|
|
946
1057
|
// This most certainly relates to a bug in MQTT.js (#1284)
|
|
947
1058
|
const msg = `Failed to subscribe, because: ${err.message}`
|
|
948
1059
|
this._log.error(msg)
|
|
949
1060
|
this.emit('error', err)
|
|
950
1061
|
this._log.info('Forcing reconnect now...')
|
|
951
|
-
|
|
952
|
-
|
|
1062
|
+
client.disconnecting = false
|
|
1063
|
+
client.reconnect()
|
|
953
1064
|
reject(new Error(msg))
|
|
1065
|
+
} else if (err.message === 'client disconnecting') {
|
|
1066
|
+
reject(err)
|
|
954
1067
|
}
|
|
955
1068
|
} else {
|
|
956
1069
|
const topicArray = Array.isArray(topic) ? topic : [topic]
|
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*\(/)
|