vrpc 3.14.0 → 3.16.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/LICENSE +1 -1
- package/README.md +100 -77
- package/browser/vrpc.js +1 -7
- package/index.js +7 -7
- package/package.json +23 -15
- package/vrpc/Registrations.js +850 -0
- package/vrpc/SubscribeRetrier.js +33 -0
- package/vrpc/VrpcAdapter.js +556 -294
- package/vrpc/VrpcAgent.js +230 -153
- package/vrpc/VrpcBrowserAdapter.js +435 -286
- package/vrpc/VrpcBrowserAgent.js +239 -128
- package/vrpc/VrpcClient.js +1190 -224
- package/vrpc/VrpcNative.js +10 -5
- package/vrpc/VrpcPersistor.js +1 -1
- package/vrpc/adapter.hpp +1 -1
- package/vrpc/addon.cpp +1 -1
- package/vrpc/comment-parser/parser.js +23 -0
- package/vrpc/namePattern.js +86 -0
- package/vrpc/vrpc-agent-js +3 -11
package/vrpc/VrpcClient.js
CHANGED
|
@@ -14,7 +14,7 @@ procedure calls (RPC).
|
|
|
14
14
|
Author: Dr. Burkhard C. Heisen (https://github.com/heisenware/vrpc)
|
|
15
15
|
|
|
16
16
|
Licensed under the MIT License <http://opensource.org/licenses/MIT>.
|
|
17
|
-
Copyright (c) 2018
|
|
17
|
+
Copyright (c) 2018 Dr. Burkhard C. Heisen <burkhard.heisen@heisenware.com>.
|
|
18
18
|
|
|
19
19
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
20
20
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -41,8 +41,12 @@ const mqtt = require('mqtt')
|
|
|
41
41
|
const EventEmitter = require('events')
|
|
42
42
|
const SubscribeRetrier = require('./SubscribeRetrier')
|
|
43
43
|
const ReconnectBackoff = require('./ReconnectBackoff')
|
|
44
|
+
const NamePattern = require('./namePattern')
|
|
44
45
|
|
|
45
|
-
const VRPC_PROTOCOL_VERSION =
|
|
46
|
+
const VRPC_PROTOCOL_VERSION = 4
|
|
47
|
+
|
|
48
|
+
// a function declared `@returns {Registration}` (or a promise of one)
|
|
49
|
+
const DECLARED_REGISTRATION = /^(Promise\.?<\s*)?Registration\s*>?$/
|
|
46
50
|
|
|
47
51
|
// mqtt clients this client ended on purpose: the 'client disconnecting'
|
|
48
52
|
// workaround must never revive one of them
|
|
@@ -159,21 +163,39 @@ class VrpcClient extends EventEmitter {
|
|
|
159
163
|
this._client = null
|
|
160
164
|
this._cachedSubscriptions = {}
|
|
161
165
|
this._proxies = {}
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
this.
|
|
166
|
+
// `<agent>/<class>/<instance>` -> the isolated instances this connection
|
|
167
|
+
// created (no class info lists them)
|
|
168
|
+
this._ownIsolated = new Map()
|
|
169
|
+
// callback -> Map(subscription identity -> topic), see _subscriptionTopic
|
|
170
|
+
this._subscriptionTopics = new WeakMap()
|
|
171
|
+
this._subscriptionCount = 0
|
|
168
172
|
this._emitterListener = new WeakMap()
|
|
169
173
|
this._pendingSubscriptions = new Map()
|
|
170
174
|
this._pendingUnsubscriptions = new Map()
|
|
175
|
+
// agents this client declares its subscriptions to again once they
|
|
176
|
+
// announce themselves: after this client's own reconnect, and after the
|
|
177
|
+
// agent's loss (the loss heals)
|
|
178
|
+
this._redeclare = new Set()
|
|
179
|
+
// agent -> withdrawals that could not be sent yet ({ sub, context })
|
|
180
|
+
this._pendingWithdrawals = new Map()
|
|
181
|
+
// the client's own inbox is subscribed (answers can arrive)
|
|
182
|
+
this._inboxReady = false
|
|
183
|
+
// the answers this client waits for ({ agent, second, lose, forget }),
|
|
184
|
+
// lost when the path an answer would travel breaks (see _loseAnswers)
|
|
185
|
+
this._waiting = new Set()
|
|
186
|
+
// labels callbacks carry towards the agent's list of registrations
|
|
187
|
+
this._subscriberLabels = new WeakMap()
|
|
171
188
|
if (log === 'console') {
|
|
172
189
|
this._log = console
|
|
173
190
|
this._log.debug = () => {}
|
|
174
191
|
} else {
|
|
175
192
|
this._log = log
|
|
176
193
|
}
|
|
194
|
+
// refused subscriptions are retried until granted
|
|
195
|
+
this._subscribeRetrier = new SubscribeRetrier({
|
|
196
|
+
subscribe: (topics, options) => this._mqttSubscribe(topics, options).catch(() => {}),
|
|
197
|
+
log: this._log
|
|
198
|
+
})
|
|
177
199
|
}
|
|
178
200
|
|
|
179
201
|
/**
|
|
@@ -289,6 +311,9 @@ class VrpcClient extends EventEmitter {
|
|
|
289
311
|
// Clear the cache to ensure a fresh state upon reconnection
|
|
290
312
|
this._agents = {}
|
|
291
313
|
this._proxies = {}
|
|
314
|
+
// the agents delete this connection's isolated instances once its
|
|
315
|
+
// will is out
|
|
316
|
+
this._ownIsolated.clear()
|
|
292
317
|
this.emit('offline')
|
|
293
318
|
})
|
|
294
319
|
|
|
@@ -301,11 +326,25 @@ class VrpcClient extends EventEmitter {
|
|
|
301
326
|
this.emit('error', err)
|
|
302
327
|
})
|
|
303
328
|
|
|
329
|
+
// a clean session keeps nothing for this connection while it is gone:
|
|
330
|
+
// an answer published meanwhile never arrives
|
|
331
|
+
this._client.on('close', () => {
|
|
332
|
+
this._loseAnswers(() => true, 'this client lost its connection')
|
|
333
|
+
})
|
|
334
|
+
|
|
304
335
|
this._client.stream.on('error', err => {
|
|
305
336
|
this.emit('error', err)
|
|
306
337
|
})
|
|
307
338
|
|
|
308
339
|
this._client.on('connect', async () => {
|
|
340
|
+
// the subscriptions this client holds are declared again, to every
|
|
341
|
+
// agent announcing protocol 4, once the inbox stands (an agent that
|
|
342
|
+
// saw this connection go has withdrawn them; one that did not keeps
|
|
343
|
+
// them, declaring again is a no-op there)
|
|
344
|
+
this._inboxReady = false
|
|
345
|
+
for (const sub of Object.values(this._cachedSubscriptions)) {
|
|
346
|
+
this._redeclare.add(sub.agent)
|
|
347
|
+
}
|
|
309
348
|
// forward the good news
|
|
310
349
|
this.emit('connect')
|
|
311
350
|
// everything is subscribed afresh below: pending retries are moot
|
|
@@ -325,6 +364,13 @@ class VrpcClient extends EventEmitter {
|
|
|
325
364
|
}
|
|
326
365
|
// Single wildcard catches BOTH RPC responses (length 3) AND events (length 4)
|
|
327
366
|
await this._mqttSubscribe(`${this._vrpcConnectionId}/#`)
|
|
367
|
+
this._inboxReady = true
|
|
368
|
+
for (const agent of Array.from(this._redeclare)) {
|
|
369
|
+
this._redeclareTo(agent)
|
|
370
|
+
}
|
|
371
|
+
for (const agent of Array.from(this._pendingWithdrawals.keys())) {
|
|
372
|
+
this._flushWithdrawals(agent)
|
|
373
|
+
}
|
|
328
374
|
|
|
329
375
|
// Re-subscribe to all cached event topics
|
|
330
376
|
this._log.debug('Restoring event subscriptions after reconnect...')
|
|
@@ -340,7 +386,7 @@ class VrpcClient extends EventEmitter {
|
|
|
340
386
|
}
|
|
341
387
|
})
|
|
342
388
|
|
|
343
|
-
this._client.on('message', (topic, message) => {
|
|
389
|
+
this._client.on('message', (topic, message, packet) => {
|
|
344
390
|
if (message.length === 0) return
|
|
345
391
|
try {
|
|
346
392
|
const tokens = topic.split('/')
|
|
@@ -351,13 +397,33 @@ class VrpcClient extends EventEmitter {
|
|
|
351
397
|
}
|
|
352
398
|
// AgentInfo message
|
|
353
399
|
if (klass === '__agentInfo__') {
|
|
354
|
-
const { status, hostname, version } = JSON.parse(
|
|
400
|
+
const { status, hostname, version, v } = JSON.parse(
|
|
401
|
+
message.toString()
|
|
402
|
+
)
|
|
355
403
|
this._createIfNotExist(agent)
|
|
356
404
|
this._agents[agent].status = status
|
|
357
405
|
this._agents[agent].hostname = hostname
|
|
358
406
|
this._agents[agent].version = version
|
|
407
|
+
// the agent's protocol version (undefined before 3)
|
|
408
|
+
this._agents[agent].v = v
|
|
359
409
|
if (status === 'offline') {
|
|
360
|
-
this.
|
|
410
|
+
this._loseAnswers(
|
|
411
|
+
w => w.second && w.agent === agent,
|
|
412
|
+
'the agent went offline'
|
|
413
|
+
)
|
|
414
|
+
this._loseAgent(agent, v)
|
|
415
|
+
} else {
|
|
416
|
+
// an agent announces itself from a new connection only (a
|
|
417
|
+
// retained announcement is old news a subscription brings):
|
|
418
|
+
// the promises its former connection was to answer are gone
|
|
419
|
+
if (!packet.retain) {
|
|
420
|
+
this._loseAnswers(
|
|
421
|
+
w => w.second && w.agent === agent,
|
|
422
|
+
'the agent connected anew'
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
this._redeclareTo(agent)
|
|
426
|
+
this._flushWithdrawals(agent)
|
|
361
427
|
}
|
|
362
428
|
this.emit('agent', { domain, agent, status, hostname, version })
|
|
363
429
|
// ClassInfo message
|
|
@@ -401,8 +467,8 @@ class VrpcClient extends EventEmitter {
|
|
|
401
467
|
})
|
|
402
468
|
// RPC message
|
|
403
469
|
} else {
|
|
404
|
-
const { i, a, e, r } = JSON.parse(message.toString())
|
|
405
|
-
this._eventEmitter.emit(i, { a, e, r })
|
|
470
|
+
const { i, a, e, r, u, end } = JSON.parse(message.toString())
|
|
471
|
+
this._eventEmitter.emit(i, { a, e, r, u, end })
|
|
406
472
|
}
|
|
407
473
|
} catch (err) {
|
|
408
474
|
this._log.error(
|
|
@@ -554,6 +620,14 @@ class VrpcClient extends EventEmitter {
|
|
|
554
620
|
v: VRPC_PROTOCOL_VERSION
|
|
555
621
|
}
|
|
556
622
|
const proxy = await this._getProxy(agent, className, json)
|
|
623
|
+
// no class info lists an isolated instance: its creator remembers it
|
|
624
|
+
if (isIsolated) {
|
|
625
|
+
this._ownIsolated.set(`${agent}/${className}/${instance}`, {
|
|
626
|
+
agent,
|
|
627
|
+
className,
|
|
628
|
+
instance
|
|
629
|
+
})
|
|
630
|
+
}
|
|
557
631
|
if (instance && cacheProxy) {
|
|
558
632
|
this._proxies[`${agent}/${className}/${instance}`] = {
|
|
559
633
|
agent,
|
|
@@ -618,6 +692,9 @@ class VrpcClient extends EventEmitter {
|
|
|
618
692
|
this._mqttPublish(topic, JSON.stringify(json))
|
|
619
693
|
delete this._proxies[`${agent}/${className}/${instance}`]
|
|
620
694
|
const deleted = await this._handleAgentAnswer(json, agent)
|
|
695
|
+
if (deleted === true) {
|
|
696
|
+
this._ownIsolated.delete(`${agent}/${className}/${instance}`)
|
|
697
|
+
}
|
|
621
698
|
// the answer and the class info that no longer lists the instance
|
|
622
699
|
// travel on different topics, and a broker need not keep their order:
|
|
623
700
|
// what this client deleted is gone for it at once
|
|
@@ -675,6 +752,7 @@ class VrpcClient extends EventEmitter {
|
|
|
675
752
|
v: VRPC_PROTOCOL_VERSION
|
|
676
753
|
}
|
|
677
754
|
const topic = `${this._domain}/${agent}/${className}/__static__/${functionName}`
|
|
755
|
+
this._declare(json, topic)
|
|
678
756
|
await this._waitUntilClassIsOnline(agent, className)
|
|
679
757
|
this._mqttPublish(topic, JSON.stringify(json))
|
|
680
758
|
return this._handleAgentAnswer(json, agent)
|
|
@@ -686,29 +764,61 @@ class VrpcClient extends EventEmitter {
|
|
|
686
764
|
* registrations. In this case the instanceId of the emitter is injected as
|
|
687
765
|
* first argument of any event callback.
|
|
688
766
|
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
767
|
+
* `instances` selects the instances: a name pattern (`line-*`, where `*`
|
|
768
|
+
* matches any run of characters) or a list of names and patterns. The
|
|
769
|
+
* agent calls the selected ones only and answers for those - one request,
|
|
770
|
+
* however many instances. An agent that does not select instances
|
|
771
|
+
* (before protocol 4) gets one call per selected instance instead; a
|
|
772
|
+
* subscription cannot be split like that and is refused there.
|
|
773
|
+
*
|
|
774
|
+
* `agent` may name several agents as well: a pattern (`edge-*`, `*` by
|
|
775
|
+
* default) asks every online agent that offers the class and matches, a
|
|
776
|
+
* list asks each agent listed. They are asked all at once, and every
|
|
777
|
+
* entry names its agent; an agent that fails as a whole answers an error
|
|
778
|
+
* for each of its instances the client knows - or one entry with the
|
|
779
|
+
* error alone when it knows none - and the other agents' answers stand.
|
|
693
780
|
*
|
|
694
781
|
* @param {Object} options
|
|
695
|
-
* @param {String} options.className Name of the
|
|
696
|
-
* @param {
|
|
697
|
-
* call
|
|
698
|
-
* @param {String} [options.agent] Agent name
|
|
699
|
-
* is used
|
|
700
|
-
* @
|
|
701
|
-
*
|
|
782
|
+
* @param {String} options.className Name of the class
|
|
783
|
+
* @param {String} options.functionName Name of the member function
|
|
784
|
+
* @param {Array} [options.args] Positional arguments of the call
|
|
785
|
+
* @param {String|Array.<String>} [options.agent] Agent name, pattern or
|
|
786
|
+
* list of names. If not provided class default is used
|
|
787
|
+
* @param {String|Array.<String>} [options.instances] The instances to call:
|
|
788
|
+
* a name pattern, or a list of names and patterns (all if not provided)
|
|
789
|
+
* @returns {Promise<Object[]>} An array of objects `{ id, val }` or
|
|
790
|
+
* `{ id, err }` carrying the instance id and its return value or error,
|
|
791
|
+
* and `agent` when the call addressed several agents
|
|
702
792
|
*/
|
|
703
793
|
async callAll ({
|
|
704
794
|
className,
|
|
705
795
|
functionName,
|
|
706
796
|
args = [],
|
|
707
|
-
agent = this._agent
|
|
797
|
+
agent = this._agent,
|
|
798
|
+
instances
|
|
708
799
|
} = {}) {
|
|
709
800
|
if (!this._isConnected()) {
|
|
710
801
|
throw new Error('Client is not connected')
|
|
711
802
|
}
|
|
803
|
+
if (instances !== undefined && !NamePattern.isSelection(instances)) {
|
|
804
|
+
throw new Error(
|
|
805
|
+
`Invalid instances: ${JSON.stringify(instances)} (a name pattern, or a list of names and patterns)`
|
|
806
|
+
)
|
|
807
|
+
}
|
|
808
|
+
// a selection of everything is no selection
|
|
809
|
+
const selection =
|
|
810
|
+
instances === undefined || NamePattern.selectsAll(instances)
|
|
811
|
+
? undefined
|
|
812
|
+
: instances
|
|
813
|
+
if (Array.isArray(agent) || agent.includes('*')) {
|
|
814
|
+
return this._callAllAgents({
|
|
815
|
+
className,
|
|
816
|
+
functionName,
|
|
817
|
+
args,
|
|
818
|
+
agents: agent,
|
|
819
|
+
selection
|
|
820
|
+
})
|
|
821
|
+
}
|
|
712
822
|
const json = {
|
|
713
823
|
c: className,
|
|
714
824
|
f: functionName,
|
|
@@ -717,40 +827,418 @@ class VrpcClient extends EventEmitter {
|
|
|
717
827
|
s: this._vrpcConnectionId,
|
|
718
828
|
v: VRPC_PROTOCOL_VERSION
|
|
719
829
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const wrapped = await this._wrapArguments({
|
|
830
|
+
let special = '__callAll__'
|
|
831
|
+
if (selection !== undefined) {
|
|
832
|
+
await this._waitUntilClassIsOnline(agent, className)
|
|
833
|
+
if (!this._selectsInstances(agent, className)) {
|
|
834
|
+
return this._callSelected({
|
|
726
835
|
agent,
|
|
727
836
|
className,
|
|
728
837
|
functionName,
|
|
729
|
-
args
|
|
838
|
+
args,
|
|
839
|
+
selection
|
|
730
840
|
})
|
|
731
|
-
if (!wrapped) continue // using cache
|
|
732
|
-
json.a = [functionName, ...wrapped]
|
|
733
|
-
await this._waitUntilClassIsOnline(agent, className)
|
|
734
|
-
this._mqttPublish(topic, JSON.stringify(json))
|
|
735
|
-
const tmp = await this._handleAgentAnswer(json, agent)
|
|
736
|
-
result.push(...tmp)
|
|
737
841
|
}
|
|
738
|
-
|
|
842
|
+
special = '__callMatching__'
|
|
739
843
|
}
|
|
740
|
-
const topic = `${this._domain}/${agent}/${className}/__static__
|
|
844
|
+
const topic = `${this._domain}/${agent}/${className}/__static__/${special}`
|
|
741
845
|
const wrapped = await this._wrapArguments({
|
|
742
846
|
agent,
|
|
743
847
|
className,
|
|
744
848
|
functionName,
|
|
745
|
-
args
|
|
849
|
+
args,
|
|
850
|
+
isCallAll: true,
|
|
851
|
+
selection
|
|
746
852
|
})
|
|
747
853
|
if (!wrapped) return // using cache
|
|
748
|
-
json.a =
|
|
854
|
+
json.a =
|
|
855
|
+
selection === undefined
|
|
856
|
+
? [functionName, ...wrapped]
|
|
857
|
+
: [selection, functionName, ...wrapped]
|
|
858
|
+
this._declare(json, topic)
|
|
859
|
+
await this._waitUntilClassIsOnline(agent, className)
|
|
860
|
+
this._mqttPublish(topic, JSON.stringify(json))
|
|
861
|
+
return this._handleAgentAnswer(json, agent)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* callAll on several agents: those a list names, or every online agent
|
|
866
|
+
* that offers the class and whose name a pattern matches - all at once,
|
|
867
|
+
* every entry naming its agent
|
|
868
|
+
*/
|
|
869
|
+
async _callAllAgents ({ className, functionName, args, agents, selection }) {
|
|
870
|
+
const names = Array.isArray(agents)
|
|
871
|
+
? agents
|
|
872
|
+
: this.getAvailableAgents().filter(
|
|
873
|
+
name =>
|
|
874
|
+
this._agents[name].classes[className] &&
|
|
875
|
+
NamePattern.matches(agents, name)
|
|
876
|
+
)
|
|
877
|
+
const answers = await Promise.all(
|
|
878
|
+
names.map(name =>
|
|
879
|
+
this.callAll({
|
|
880
|
+
className,
|
|
881
|
+
functionName,
|
|
882
|
+
args,
|
|
883
|
+
agent: name,
|
|
884
|
+
instances: selection
|
|
885
|
+
}).then(
|
|
886
|
+
result => (result || []).map(entry => ({ agent: name, ...entry })),
|
|
887
|
+
err => {
|
|
888
|
+
const error = { message: err.message }
|
|
889
|
+
const known = this.getAvailableInstances({ className, agent: name })
|
|
890
|
+
.filter(id => selection === undefined || NamePattern.selects(selection, id))
|
|
891
|
+
return known.length > 0
|
|
892
|
+
? known.map(id => ({ agent: name, id, err: error }))
|
|
893
|
+
: [{ agent: name, err: error }]
|
|
894
|
+
}
|
|
895
|
+
)
|
|
896
|
+
)
|
|
897
|
+
)
|
|
898
|
+
return answers.flat()
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// whether an agent selects instances itself: its class info lists
|
|
902
|
+
// __callMatching__ (agents of protocol 4)
|
|
903
|
+
_selectsInstances (agent, className) {
|
|
904
|
+
const info = this._agents[agent] && this._agents[agent].classes[className]
|
|
905
|
+
return (
|
|
906
|
+
!!info &&
|
|
907
|
+
info.staticFunctions.some(
|
|
908
|
+
f => this._stripSignature(f) === '__callMatching__'
|
|
909
|
+
)
|
|
910
|
+
)
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* A selection towards an agent that does not select instances: one call
|
|
915
|
+
* per selected instance, answered as callAll answers. A subscription is
|
|
916
|
+
* refused: its events, one per instance, would not name their instance.
|
|
917
|
+
*/
|
|
918
|
+
async _callSelected ({ agent, className, functionName, args, selection }) {
|
|
919
|
+
if (args.some(x => this._isFunction(x) || this._isEmitter(x))) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
`Agent ${agent} does not select instances for callbacks and events (its class info lists no __callMatching__): update it, or call without instances`
|
|
922
|
+
)
|
|
923
|
+
}
|
|
924
|
+
const selected = this.getAvailableInstances({ className, agent }).filter(
|
|
925
|
+
id => NamePattern.selects(selection, id)
|
|
926
|
+
)
|
|
927
|
+
return Promise.all(
|
|
928
|
+
selected.map(async id => {
|
|
929
|
+
try {
|
|
930
|
+
const proxy = await this.getInstance(id, { agent, className })
|
|
931
|
+
return { id, val: await proxy[functionName](...args) }
|
|
932
|
+
} catch (err) {
|
|
933
|
+
return { id, err: { message: err.message } }
|
|
934
|
+
}
|
|
935
|
+
})
|
|
936
|
+
)
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Withdraws subscriptions.
|
|
941
|
+
*
|
|
942
|
+
* A subscription is what an event function call (or `on`) declared with a
|
|
943
|
+
* callback: one per target, function, arguments and callback, named by
|
|
944
|
+
* its subscription id (see getSubscriptions). Withdrawing it stops its events at once, and
|
|
945
|
+
* every agent holding it is told by id - however it was declared (a proxy,
|
|
946
|
+
* callStatic, callAll on one agent or on all of them). The agent releases
|
|
947
|
+
* the registration when the last subscription leaves.
|
|
948
|
+
*
|
|
949
|
+
* With `instance`, only what is held on that instance is withdrawn: a
|
|
950
|
+
* subscription declared there ends, and a callAll subscription - one id
|
|
951
|
+
* on every instance of its class - stops at that instance alone while
|
|
952
|
+
* the others keep delivering. The client remembers such a withdrawal and
|
|
953
|
+
* applies it again after its own reconnect, until the subscription is
|
|
954
|
+
* declared anew (a declaration names what is wanted now).
|
|
955
|
+
*
|
|
956
|
+
* Agents before protocol 4 cannot withdraw by id: an `on` subscription is
|
|
957
|
+
* removed there as before (removeListener), an `on<Name>` one through its
|
|
958
|
+
* `off<Name>` twin when the class has one; anything else ends locally
|
|
959
|
+
* (a callAll subscription withdrawn at one instance stops delivering its
|
|
960
|
+
* events there).
|
|
961
|
+
*
|
|
962
|
+
* @param {String|Array.<String>|Function|Object} target A subscription id
|
|
963
|
+
* (or several), a callback (all its subscriptions), or an
|
|
964
|
+
* `{ emitter, event }` contract (all of its subscriptions)
|
|
965
|
+
* @param {Object} [options]
|
|
966
|
+
* @param {String} [options.instance] Withdraw at this instance only
|
|
967
|
+
* @returns {Promise<Boolean>} true if anything was withdrawn
|
|
968
|
+
*/
|
|
969
|
+
async unsubscribe (target, { instance } = {}) {
|
|
970
|
+
const all = Object.values(this._cachedSubscriptions)
|
|
971
|
+
let subs
|
|
972
|
+
if (typeof target === 'string' || Array.isArray(target)) {
|
|
973
|
+
const ids = Array.isArray(target) ? target : [target]
|
|
974
|
+
subs = ids
|
|
975
|
+
.map(id =>
|
|
976
|
+
this._cachedSubscriptions[id.startsWith('__e__') ? id.slice(5) : id]
|
|
977
|
+
)
|
|
978
|
+
.filter(Boolean)
|
|
979
|
+
} else if (this._isFunction(target)) {
|
|
980
|
+
subs = all.filter(sub => sub.callback === target)
|
|
981
|
+
} else if (this._isEmitter(target)) {
|
|
982
|
+
const callback = this._emitterCallback(target, { readOnly: true })
|
|
983
|
+
subs = callback ? all.filter(sub => sub.callback === callback) : []
|
|
984
|
+
} else {
|
|
985
|
+
throw new Error(
|
|
986
|
+
'unsubscribe expects a subscription id, a callback or an { emitter, event } contract'
|
|
987
|
+
)
|
|
988
|
+
}
|
|
989
|
+
// at one instance: a callAll subscription stops there, one declared on
|
|
990
|
+
// that instance ends, any other is not held there
|
|
991
|
+
const atInstance = []
|
|
992
|
+
if (instance !== undefined && instance !== null) {
|
|
993
|
+
atInstance.push(...subs.filter(sub => sub.callAll))
|
|
994
|
+
subs = subs.filter(sub => !sub.callAll && sub.instance === instance)
|
|
995
|
+
}
|
|
996
|
+
if (subs.length === 0 && atInstance.length === 0) return false
|
|
997
|
+
// no event reaches the consumer from here on
|
|
998
|
+
subs.forEach(sub => this._dropSubscription(sub))
|
|
999
|
+
atInstance.forEach(sub => sub.withdrawn.add(instance))
|
|
1000
|
+
const byAgent = new Map()
|
|
1001
|
+
const entries = (agent, sub, context) => {
|
|
1002
|
+
if (!byAgent.has(agent)) byAgent.set(agent, [])
|
|
1003
|
+
byAgent.get(agent).push({ sub, context })
|
|
1004
|
+
}
|
|
1005
|
+
for (const sub of subs) entries(sub.agent, sub)
|
|
1006
|
+
for (const sub of atInstance) entries(sub.agent, sub, instance)
|
|
1007
|
+
const sends = []
|
|
1008
|
+
for (const [agent, agentEntries] of byAgent) {
|
|
1009
|
+
sends.push(this._withdrawAt(agent, agentEntries))
|
|
1010
|
+
}
|
|
1011
|
+
await Promise.allSettled(sends)
|
|
1012
|
+
return true
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Withdraws subscriptions at their agent: at once when the agent is online
|
|
1017
|
+
* and this client connected, else as soon as both are again - an agent
|
|
1018
|
+
* that only lost its connection still holds them.
|
|
1019
|
+
*
|
|
1020
|
+
* @param {String} agent
|
|
1021
|
+
* @param {Array.<Object>} entries `{ sub, context }`, context (an
|
|
1022
|
+
* instance) for a withdrawal at one instance only
|
|
1023
|
+
*/
|
|
1024
|
+
_withdrawAt (agent, entries) {
|
|
1025
|
+
const info = this._agents[agent]
|
|
1026
|
+
if (!this._isConnected() || !info || info.status !== 'online') {
|
|
1027
|
+
const pending = this._pendingWithdrawals.get(agent) || []
|
|
1028
|
+
pending.push(...entries)
|
|
1029
|
+
this._pendingWithdrawals.set(agent, pending)
|
|
1030
|
+
return Promise.resolve()
|
|
1031
|
+
}
|
|
1032
|
+
if (info.v >= 4) return this._sendUnsubscribe(agent, entries)
|
|
1033
|
+
// an older agent cannot withdraw at one instance: local only
|
|
1034
|
+
return Promise.allSettled(
|
|
1035
|
+
entries
|
|
1036
|
+
.filter(({ context }) => context === undefined)
|
|
1037
|
+
.map(({ sub }) => this._sendLegacyRemoval(sub))
|
|
1038
|
+
)
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// sends the withdrawals an agent missed, once it and this client are online
|
|
1042
|
+
_flushWithdrawals (agent) {
|
|
1043
|
+
const pending = this._pendingWithdrawals.get(agent)
|
|
1044
|
+
const info = this._agents[agent]
|
|
1045
|
+
if (!pending || !this._isConnected() || !info || info.status !== 'online') {
|
|
1046
|
+
return
|
|
1047
|
+
}
|
|
1048
|
+
this._pendingWithdrawals.delete(agent)
|
|
1049
|
+
this._withdrawAt(agent, pending)
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* An agent went offline: a loss, not an end. Its subscriptions are lost
|
|
1054
|
+
* until it serves again; an agent of protocol 4 gets them declared again
|
|
1055
|
+
* then (the loss heals - one that kept them changes nothing). Towards an
|
|
1056
|
+
* older agent a declaration again would register the handler twice: its
|
|
1057
|
+
* subscriptions are dropped, as they always were.
|
|
1058
|
+
*/
|
|
1059
|
+
_loseAgent (agent, v) {
|
|
1060
|
+
if (!(v >= 4)) {
|
|
1061
|
+
this._redeclare.delete(agent)
|
|
1062
|
+
this._clearCachedSubscriptions({ lostAgent: agent })
|
|
1063
|
+
return
|
|
1064
|
+
}
|
|
1065
|
+
this._redeclare.add(agent)
|
|
1066
|
+
for (const sub of Object.values(this._cachedSubscriptions)) {
|
|
1067
|
+
if (sub.agent !== agent || sub.lost) continue
|
|
1068
|
+
sub.lost = true
|
|
1069
|
+
sub.confirmed = false
|
|
1070
|
+
this.emit('lost', sub.id, 'agentOffline', this._subscriptionInfo(sub))
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Lists the subscriptions this client holds.
|
|
1076
|
+
*
|
|
1077
|
+
* @param {Object} [filter]
|
|
1078
|
+
* @param {Function|Object} [filter.callback] Only the subscriptions of this
|
|
1079
|
+
* callback, or of this `{ emitter, event }` contract
|
|
1080
|
+
* @returns {Array.<Object>} `{ id, agent, className, instance,
|
|
1081
|
+
* functionName, event, callAll, confirmed, lost }` per subscription;
|
|
1082
|
+
* `confirmed` once a protocol-4 agent answered that it holds it, `lost`
|
|
1083
|
+
* while its agent is offline (it is declared again once the agent is back)
|
|
1084
|
+
*/
|
|
1085
|
+
getSubscriptions ({ callback } = {}) {
|
|
1086
|
+
if (callback && this._isEmitter(callback)) {
|
|
1087
|
+
callback = this._emitterCallback(callback, { readOnly: true })
|
|
1088
|
+
if (!callback) return []
|
|
1089
|
+
}
|
|
1090
|
+
return Object.values(this._cachedSubscriptions)
|
|
1091
|
+
.filter(sub => !callback || sub.callback === callback)
|
|
1092
|
+
.map(sub => ({
|
|
1093
|
+
id: sub.id,
|
|
1094
|
+
...this._subscriptionInfo(sub),
|
|
1095
|
+
confirmed: sub.confirmed,
|
|
1096
|
+
lost: sub.lost
|
|
1097
|
+
}))
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Gives the subscriptions of a callback a label, which agents list with
|
|
1102
|
+
* their registrations (`__registrations__`), e.g. who listens.
|
|
1103
|
+
*
|
|
1104
|
+
* @param {Function|Object} callback The callback, or an `{ emitter, event }`
|
|
1105
|
+
* contract (labelled before or after it subscribes)
|
|
1106
|
+
* @param {String} label The label (sent with every declaration)
|
|
1107
|
+
*/
|
|
1108
|
+
setSubscriberLabel (callback, label) {
|
|
1109
|
+
if (this._isEmitter(callback)) callback = this._emitterCallback(callback)
|
|
1110
|
+
this._subscriberLabels.set(callback, label)
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Lists the registrations of an instance (or, without an instance, the
|
|
1115
|
+
* static ones of a class): function, arguments, subscriptions with their
|
|
1116
|
+
* labels, events and events per second. Needs a protocol-4 agent.
|
|
1117
|
+
*
|
|
1118
|
+
* @param {Object} options
|
|
1119
|
+
* @param {String} options.className Class name
|
|
1120
|
+
* @param {String} [options.instance] Instance name
|
|
1121
|
+
* @param {String} [options.agent] Agent name. If not provided class default is used
|
|
1122
|
+
* @returns {Promise<Array.<Object>>} The registrations
|
|
1123
|
+
*/
|
|
1124
|
+
async getRegistrations ({ className, instance, agent = this._agent } = {}) {
|
|
1125
|
+
return this._callProtocol({ className, instance, agent }, '__registrations__', [])
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Ends a registration for all its subscriptions; each subscriber learns
|
|
1130
|
+
* that it ended and by whom ('ended' event). Needs a protocol-4 agent.
|
|
1131
|
+
*
|
|
1132
|
+
* @param {Object} options
|
|
1133
|
+
* @param {String} options.className Class name
|
|
1134
|
+
* @param {String} [options.instance] Instance name (static registration without)
|
|
1135
|
+
* @param {String} [options.agent] Agent name. If not provided class default is used
|
|
1136
|
+
* @param {String} options.functionName The event function
|
|
1137
|
+
* @param {Array} [options.args] Its arguments without the handlers, as listed
|
|
1138
|
+
* @param {String} [options.by] Who ends it, told to every subscriber
|
|
1139
|
+
* @returns {Promise<Boolean>} Whether such a registration existed
|
|
1140
|
+
*/
|
|
1141
|
+
async endRegistration ({
|
|
1142
|
+
className,
|
|
1143
|
+
instance,
|
|
1144
|
+
agent = this._agent,
|
|
1145
|
+
functionName,
|
|
1146
|
+
args = [],
|
|
1147
|
+
by = null
|
|
1148
|
+
} = {}) {
|
|
1149
|
+
return this._callProtocol({ className, instance, agent }, '__end__', [
|
|
1150
|
+
functionName,
|
|
1151
|
+
args,
|
|
1152
|
+
by
|
|
1153
|
+
])
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
async _callProtocol ({ className, instance, agent }, f, a) {
|
|
1157
|
+
if (!this._isConnected()) {
|
|
1158
|
+
throw new Error('Client is not connected')
|
|
1159
|
+
}
|
|
1160
|
+
if (instance && (!agent || agent === '*' || !className)) {
|
|
1161
|
+
const data = await this._getInstanceData(instance, { agent, className })
|
|
1162
|
+
agent = data.agent
|
|
1163
|
+
className = data.className
|
|
1164
|
+
}
|
|
1165
|
+
if (!agent || agent === '*') throw new Error('Agent must be specified')
|
|
749
1166
|
await this._waitUntilClassIsOnline(agent, className)
|
|
1167
|
+
if (!(this._agents[agent].v >= 4)) {
|
|
1168
|
+
throw new Error(`Agent "${agent}" does not speak protocol 4`)
|
|
1169
|
+
}
|
|
1170
|
+
const json = {
|
|
1171
|
+
c: instance || className,
|
|
1172
|
+
f,
|
|
1173
|
+
a,
|
|
1174
|
+
i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
|
|
1175
|
+
s: this._vrpcConnectionId,
|
|
1176
|
+
v: VRPC_PROTOCOL_VERSION
|
|
1177
|
+
}
|
|
1178
|
+
const topic = `${this._domain}/${agent}/${className}/${instance || '__static__'}/${f}`
|
|
750
1179
|
this._mqttPublish(topic, JSON.stringify(json))
|
|
751
1180
|
return this._handleAgentAnswer(json, agent)
|
|
752
1181
|
}
|
|
753
1182
|
|
|
1183
|
+
// withdraws by id on a protocol-4 agent, wherever it holds them (or, with
|
|
1184
|
+
// a context, at that instance only)
|
|
1185
|
+
async _sendUnsubscribe (agent, entries) {
|
|
1186
|
+
const className = entries[0].sub.className
|
|
1187
|
+
const json = {
|
|
1188
|
+
c: className,
|
|
1189
|
+
f: '__unsubscribe__',
|
|
1190
|
+
a: entries.map(({ sub, context }) =>
|
|
1191
|
+
context === undefined ? sub.id : { id: sub.id, context }
|
|
1192
|
+
),
|
|
1193
|
+
i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
|
|
1194
|
+
s: this._vrpcConnectionId,
|
|
1195
|
+
v: VRPC_PROTOCOL_VERSION
|
|
1196
|
+
}
|
|
1197
|
+
const topic = `${this._domain}/${agent}/${className}/__static__/__unsubscribe__`
|
|
1198
|
+
this._mqttPublish(topic, JSON.stringify(json))
|
|
1199
|
+
try {
|
|
1200
|
+
await this._handleAgentAnswer(json, agent)
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
this._log.debug(`Withdrawing subscriptions failed: ${err.message}`)
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// an agent before protocol 4: removeListener, or the class's off twin
|
|
1207
|
+
async _sendLegacyRemoval (sub) {
|
|
1208
|
+
if (sub.callAll || !sub.instance) return
|
|
1209
|
+
let f
|
|
1210
|
+
let a
|
|
1211
|
+
if (sub.functionName === 'on' || sub.functionName === 'addListener') {
|
|
1212
|
+
f = 'removeListener'
|
|
1213
|
+
a = [sub.event, sub.id]
|
|
1214
|
+
} else {
|
|
1215
|
+
const twin = sub.functionName.replace(/^on(?=[A-Z])/, 'off')
|
|
1216
|
+
const info =
|
|
1217
|
+
this._agents[sub.agent] &&
|
|
1218
|
+
this._agents[sub.agent].classes[sub.className]
|
|
1219
|
+
const functions = info ? info.memberFunctions.map(this._stripSignature) : []
|
|
1220
|
+
if (twin === sub.functionName || !functions.includes(twin)) return
|
|
1221
|
+
if (!sub.declaration) return
|
|
1222
|
+
f = twin
|
|
1223
|
+
a = sub.declaration.a
|
|
1224
|
+
}
|
|
1225
|
+
const json = {
|
|
1226
|
+
c: sub.instance,
|
|
1227
|
+
f,
|
|
1228
|
+
a,
|
|
1229
|
+
i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
|
|
1230
|
+
s: this._vrpcConnectionId,
|
|
1231
|
+
v: VRPC_PROTOCOL_VERSION
|
|
1232
|
+
}
|
|
1233
|
+
const topic = `${this._domain}/${sub.agent}/${sub.className}/${sub.instance}/${f}`
|
|
1234
|
+
this._mqttPublish(topic, JSON.stringify(json))
|
|
1235
|
+
try {
|
|
1236
|
+
await this._handleAgentAnswer(json, sub.agent)
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
this._log.debug(`Removing a subscription failed: ${err.message}`)
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
754
1242
|
/**
|
|
755
1243
|
* Retrieves all information about the currently available components.
|
|
756
1244
|
*
|
|
@@ -906,8 +1394,10 @@ class VrpcClient extends EventEmitter {
|
|
|
906
1394
|
this._mqttPublish(agentTopic, null, { retain: true })
|
|
907
1395
|
const classes = this.getAvailableClasses({ agent, mustBeOnline: false })
|
|
908
1396
|
for (const className of classes) {
|
|
909
|
-
const
|
|
910
|
-
|
|
1397
|
+
for (const info of ['__classInfo__', '__classInfoConcise__']) {
|
|
1398
|
+
const classTopic = `${this._domain}/${agent}/${className}/${info}`
|
|
1399
|
+
this._mqttPublish(classTopic, null, { retain: true })
|
|
1400
|
+
}
|
|
911
1401
|
}
|
|
912
1402
|
// Synchronize local cache
|
|
913
1403
|
delete this._agents[agent]
|
|
@@ -932,8 +1422,14 @@ class VrpcClient extends EventEmitter {
|
|
|
932
1422
|
v: VRPC_PROTOCOL_VERSION
|
|
933
1423
|
})
|
|
934
1424
|
)
|
|
1425
|
+
this._loseAnswers(() => true, 'the client ended')
|
|
935
1426
|
this._eventEmitter.removeAllListeners()
|
|
936
1427
|
this.removeAllListeners()
|
|
1428
|
+
// the agents withdraw what this connection held: nothing to declare
|
|
1429
|
+
// again, nothing left to withdraw
|
|
1430
|
+
this._cachedSubscriptions = {}
|
|
1431
|
+
this._redeclare.clear()
|
|
1432
|
+
this._pendingWithdrawals.clear()
|
|
937
1433
|
const client = this._client
|
|
938
1434
|
this._client = null // Prevent race conditions during shutdown
|
|
939
1435
|
return new Promise(resolve =>
|
|
@@ -966,13 +1462,6 @@ class VrpcClient extends EventEmitter {
|
|
|
966
1462
|
return this._client && this._client.connected
|
|
967
1463
|
}
|
|
968
1464
|
|
|
969
|
-
_getCallbackId (callback, { readOnly = false } = {}) {
|
|
970
|
-
if (!this._callbackIds.has(callback)) {
|
|
971
|
-
if (readOnly) return null
|
|
972
|
-
this._callbackIds.set(callback, nanoid(8))
|
|
973
|
-
}
|
|
974
|
-
return this._callbackIds.get(callback)
|
|
975
|
-
}
|
|
976
1465
|
|
|
977
1466
|
_generateUserName () {
|
|
978
1467
|
return `${this._domain}:client@${os.hostname()}-${os.platform()}-js`
|
|
@@ -1045,31 +1534,36 @@ class VrpcClient extends EventEmitter {
|
|
|
1045
1534
|
client.subscribe(
|
|
1046
1535
|
topic,
|
|
1047
1536
|
{ qos: this._qos, ...options },
|
|
1048
|
-
(
|
|
1049
|
-
|
|
1537
|
+
(subscribeError, subs) => {
|
|
1538
|
+
const { refused, granted, error } = SubscribeRetrier.outcome(
|
|
1539
|
+
subscribeError,
|
|
1540
|
+
subs
|
|
1541
|
+
)
|
|
1542
|
+
if (error) {
|
|
1050
1543
|
this._log.warn(
|
|
1051
|
-
`Could not subscribe to topic(s): '${topic}', because: ${
|
|
1544
|
+
`Could not subscribe to topic(s): '${topic}', because: ${error.message}`
|
|
1052
1545
|
)
|
|
1053
1546
|
if (
|
|
1054
|
-
|
|
1547
|
+
error.message === 'client disconnecting' &&
|
|
1055
1548
|
this._isRevivable(client)
|
|
1056
1549
|
) {
|
|
1057
1550
|
// This most certainly relates to a bug in MQTT.js (#1284)
|
|
1058
|
-
const msg = `Failed to subscribe, because: ${
|
|
1551
|
+
const msg = `Failed to subscribe, because: ${error.message}`
|
|
1059
1552
|
this._log.error(msg)
|
|
1060
|
-
this.emit('error',
|
|
1553
|
+
this.emit('error', error)
|
|
1061
1554
|
this._log.info('Forcing reconnect now...')
|
|
1062
1555
|
client.disconnecting = false
|
|
1063
1556
|
client.reconnect()
|
|
1064
1557
|
reject(new Error(msg))
|
|
1065
|
-
} else
|
|
1066
|
-
|
|
1558
|
+
} else {
|
|
1559
|
+
// the connection broke under the subscribe: the next connect
|
|
1560
|
+
// subscribes everything afresh
|
|
1561
|
+
reject(error)
|
|
1067
1562
|
}
|
|
1068
1563
|
} else {
|
|
1564
|
+
let err
|
|
1069
1565
|
const topicArray = Array.isArray(topic) ? topic : [topic]
|
|
1070
|
-
const erroneousGranted =
|
|
1071
|
-
.filter(x => x.qos === 128)
|
|
1072
|
-
.map(x => x.topic)
|
|
1566
|
+
const erroneousGranted = refused
|
|
1073
1567
|
if (erroneousGranted.length > 0) {
|
|
1074
1568
|
err = new Error(
|
|
1075
1569
|
`Could not subscribe all ${topicArray.length} topic(s) but got error qos=128 on following ${erroneousGranted.length} topic(s): ${erroneousGranted}`
|
|
@@ -1080,9 +1574,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1080
1574
|
this.emit('error', err)
|
|
1081
1575
|
this._subscribeRetrier.schedule(erroneousGranted, options)
|
|
1082
1576
|
}
|
|
1083
|
-
this._subscribeRetrier.granted(
|
|
1084
|
-
granted.filter(x => x.qos !== 128).map(x => x.topic)
|
|
1085
|
-
)
|
|
1577
|
+
this._subscribeRetrier.granted(granted.map(x => x.topic))
|
|
1086
1578
|
const reducedQos = granted.filter(x => x.qos < this._qos)
|
|
1087
1579
|
if (reducedQos.length > 0) {
|
|
1088
1580
|
err = new Error(
|
|
@@ -1099,7 +1591,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1099
1591
|
this._log.warn(err.message)
|
|
1100
1592
|
this.emit('error', err)
|
|
1101
1593
|
}
|
|
1102
|
-
if (granted.length === 0) {
|
|
1594
|
+
if (granted.length === 0 && refused.length === 0) {
|
|
1103
1595
|
this._log.debug(`Already subscribed to: ${topic}`)
|
|
1104
1596
|
}
|
|
1105
1597
|
resolve()
|
|
@@ -1135,12 +1627,16 @@ class VrpcClient extends EventEmitter {
|
|
|
1135
1627
|
await this._waitUntilClassIsOnline(agent, className)
|
|
1136
1628
|
this._mqttPublish(topic, JSON.stringify(json))
|
|
1137
1629
|
return new Promise((resolve, reject) => {
|
|
1138
|
-
const
|
|
1630
|
+
const call = `Proxy creation for class "${className}" on agent "${agent}" and domain "${this._domain}"`
|
|
1139
1631
|
const timer = setTimeout(() => {
|
|
1140
|
-
|
|
1141
|
-
reject(new Error(
|
|
1632
|
+
forget()
|
|
1633
|
+
reject(new Error(`${call} timed out (> ${this._timeout} ms)`))
|
|
1142
1634
|
}, this._timeout)
|
|
1143
|
-
|
|
1635
|
+
const lose = reason => {
|
|
1636
|
+
clearTimeout(timer)
|
|
1637
|
+
reject(new Error(`${call} lost its answer: ${reason}`))
|
|
1638
|
+
}
|
|
1639
|
+
const forget = this._awaitAnswer(i, { agent, lose }, ({ e, r }) => {
|
|
1144
1640
|
clearTimeout(timer)
|
|
1145
1641
|
if (e) {
|
|
1146
1642
|
// the agent answers a throwing constructor with { message, cause }
|
|
@@ -1160,15 +1656,19 @@ class VrpcClient extends EventEmitter {
|
|
|
1160
1656
|
if (this._hasClassOnline(agent, className)) return
|
|
1161
1657
|
return new Promise((resolve, reject) => {
|
|
1162
1658
|
let timer = null
|
|
1659
|
+
// the class info and the agent info travel on topics of their own and
|
|
1660
|
+
// may come in either order: each of them can complete the picture
|
|
1163
1661
|
const checkClass = () => {
|
|
1164
1662
|
if (this._hasClassOnline(agent, className)) {
|
|
1165
1663
|
if (timer) clearTimeout(timer)
|
|
1166
1664
|
this.removeListener('class', checkClass)
|
|
1665
|
+
this.removeListener('agent', checkClass)
|
|
1167
1666
|
resolve()
|
|
1168
1667
|
}
|
|
1169
1668
|
}
|
|
1170
1669
|
timer = setTimeout(() => {
|
|
1171
1670
|
this.removeListener('class', checkClass)
|
|
1671
|
+
this.removeListener('agent', checkClass)
|
|
1172
1672
|
reject(
|
|
1173
1673
|
new Error(
|
|
1174
1674
|
`Connection to class "${className}" on agent "${agent}" and domain "${this._domain}" timed out (> ${this._timeout} ms)`
|
|
@@ -1176,6 +1676,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1176
1676
|
)
|
|
1177
1677
|
}, this._timeout)
|
|
1178
1678
|
this.on('class', checkClass)
|
|
1679
|
+
this.on('agent', checkClass)
|
|
1179
1680
|
})
|
|
1180
1681
|
}
|
|
1181
1682
|
|
|
@@ -1214,23 +1715,18 @@ class VrpcClient extends EventEmitter {
|
|
|
1214
1715
|
throw new Error('VRPC does not support removing all listeners')
|
|
1215
1716
|
}
|
|
1216
1717
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
this._eventEmitter.removeListener(id, handler)
|
|
1230
|
-
})
|
|
1231
|
-
|
|
1232
|
-
delete this._cachedSubscriptions[topic]
|
|
1233
|
-
})
|
|
1718
|
+
// this client's subscriptions of that event on this instance end;
|
|
1719
|
+
// a protocol-4 agent withdraws exactly those, never touching the
|
|
1720
|
+
// emitter's other listeners
|
|
1721
|
+
Object.values(this._cachedSubscriptions)
|
|
1722
|
+
.filter(
|
|
1723
|
+
sub =>
|
|
1724
|
+
sub.agent === agent &&
|
|
1725
|
+
sub.className === className &&
|
|
1726
|
+
sub.instance === instance &&
|
|
1727
|
+
sub.event === eventName
|
|
1728
|
+
)
|
|
1729
|
+
.forEach(sub => this._dropSubscription(sub))
|
|
1234
1730
|
try {
|
|
1235
1731
|
const json = {
|
|
1236
1732
|
c: instance,
|
|
@@ -1276,6 +1772,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1276
1772
|
s: this._vrpcConnectionId,
|
|
1277
1773
|
v: VRPC_PROTOCOL_VERSION
|
|
1278
1774
|
}
|
|
1775
|
+
this._declare(json, `${targetTopic}/${functionName}`)
|
|
1279
1776
|
this._mqttPublish(
|
|
1280
1777
|
`${targetTopic}/${functionName}`,
|
|
1281
1778
|
JSON.stringify(json)
|
|
@@ -1311,6 +1808,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1311
1808
|
s: this._vrpcConnectionId,
|
|
1312
1809
|
v: VRPC_PROTOCOL_VERSION
|
|
1313
1810
|
}
|
|
1811
|
+
this._declare(json, `${targetTopic}/${functionName}`)
|
|
1314
1812
|
this._mqttPublish(
|
|
1315
1813
|
`${targetTopic}/${functionName}`,
|
|
1316
1814
|
JSON.stringify(json)
|
|
@@ -1322,32 +1820,53 @@ class VrpcClient extends EventEmitter {
|
|
|
1322
1820
|
)
|
|
1323
1821
|
}
|
|
1324
1822
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1823
|
+
// withdraws this client's subscriptions of a function on this instance
|
|
1824
|
+
proxy.vrpcOff = async functionName => {
|
|
1825
|
+
const ids = Object.values(this._cachedSubscriptions)
|
|
1826
|
+
.filter(
|
|
1827
|
+
sub =>
|
|
1828
|
+
sub.agent === agent &&
|
|
1829
|
+
sub.className === className &&
|
|
1830
|
+
sub.instance === instance &&
|
|
1831
|
+
sub.functionName === functionName
|
|
1832
|
+
)
|
|
1833
|
+
.map(sub => sub.id)
|
|
1834
|
+
if (ids.length === 0) return false
|
|
1835
|
+
return this.unsubscribe(ids)
|
|
1329
1836
|
}
|
|
1330
1837
|
}
|
|
1331
1838
|
return proxy
|
|
1332
1839
|
}
|
|
1333
1840
|
|
|
1334
|
-
async _handleAgentAnswer ({ i, c, f }, agent) {
|
|
1841
|
+
async _handleAgentAnswer ({ i, c, f, a }, agent) {
|
|
1335
1842
|
return new Promise((resolve, reject) => {
|
|
1336
|
-
const
|
|
1843
|
+
const call = `Function call "${c}::${f}()" on agent "${agent}"`
|
|
1337
1844
|
const timer = setTimeout(() => {
|
|
1338
|
-
|
|
1339
|
-
reject(new Error(
|
|
1845
|
+
forget()
|
|
1846
|
+
reject(new Error(`${call} timed out (> ${this._timeout} ms)`))
|
|
1340
1847
|
}, this._timeout)
|
|
1341
|
-
|
|
1848
|
+
const lose = reason => {
|
|
1849
|
+
clearTimeout(timer)
|
|
1850
|
+
reject(new Error(`${call} lost its answer: ${reason}`))
|
|
1851
|
+
}
|
|
1852
|
+
// a protocol-4 agent that answers a declaring call without an error
|
|
1853
|
+
// holds every subscription the call carried
|
|
1854
|
+
const confirm = () => {
|
|
1855
|
+
const info = this._agents[agent]
|
|
1856
|
+
if (info && info.v >= 4) this._confirmSubscriptions(a)
|
|
1857
|
+
}
|
|
1858
|
+
const forget = this._awaitAnswer(i, { agent, lose }, data => {
|
|
1342
1859
|
clearTimeout(timer)
|
|
1343
1860
|
if (data.e) {
|
|
1344
1861
|
const { message, cause } = VrpcClient._prepareError(data.e)
|
|
1345
1862
|
reject(new Error(`[vrpc ${agent}-${c}-${f}]: ${message}`, { cause }))
|
|
1346
1863
|
} else {
|
|
1347
1864
|
const ret = data.r
|
|
1348
|
-
// Handle functions returning a promise
|
|
1865
|
+
// Handle functions returning a promise: the result follows when
|
|
1866
|
+
// it settles, however long that takes - unless the path it would
|
|
1867
|
+
// travel breaks first
|
|
1349
1868
|
if (typeof ret === 'string' && ret.substring(0, 5) === '__p__') {
|
|
1350
|
-
this.
|
|
1869
|
+
this._awaitAnswer(ret, { agent, lose, second: true }, promiseData => {
|
|
1351
1870
|
if (promiseData.e) {
|
|
1352
1871
|
const { message, cause } = VrpcClient._prepareError(
|
|
1353
1872
|
promiseData.e
|
|
@@ -1356,10 +1875,12 @@ class VrpcClient extends EventEmitter {
|
|
|
1356
1875
|
new Error(`[vrpc ${agent}-${c}-${f}]: ${message}`, { cause })
|
|
1357
1876
|
)
|
|
1358
1877
|
} else {
|
|
1878
|
+
confirm()
|
|
1359
1879
|
resolve(promiseData.r)
|
|
1360
1880
|
}
|
|
1361
1881
|
})
|
|
1362
1882
|
} else {
|
|
1883
|
+
confirm()
|
|
1363
1884
|
resolve(ret)
|
|
1364
1885
|
}
|
|
1365
1886
|
}
|
|
@@ -1367,6 +1888,44 @@ class VrpcClient extends EventEmitter {
|
|
|
1367
1888
|
})
|
|
1368
1889
|
}
|
|
1369
1890
|
|
|
1891
|
+
/**
|
|
1892
|
+
* Waits for the answer with id `id`.
|
|
1893
|
+
*
|
|
1894
|
+
* @param {String} id
|
|
1895
|
+
* @param {Object} waiter
|
|
1896
|
+
* @param {String} waiter.agent the agent that answers
|
|
1897
|
+
* @param {Function} waiter.lose called with a reason when the answer can
|
|
1898
|
+
* no longer arrive
|
|
1899
|
+
* @param {Boolean} [waiter.second] a promise's result, the second answer
|
|
1900
|
+
* to its call
|
|
1901
|
+
* @param {Function} onAnswer called with the answer
|
|
1902
|
+
* @returns {Function} stops waiting, without a call to `lose`
|
|
1903
|
+
*/
|
|
1904
|
+
_awaitAnswer (id, { agent, lose, second = false }, onAnswer) {
|
|
1905
|
+
const waiter = { agent, lose, second }
|
|
1906
|
+
const listener = data => {
|
|
1907
|
+
this._waiting.delete(waiter)
|
|
1908
|
+
onAnswer(data)
|
|
1909
|
+
}
|
|
1910
|
+
waiter.forget = () => {
|
|
1911
|
+
this._waiting.delete(waiter)
|
|
1912
|
+
this._eventEmitter.removeListener(id, listener)
|
|
1913
|
+
}
|
|
1914
|
+
this._waiting.add(waiter)
|
|
1915
|
+
this._eventEmitter.once(id, listener)
|
|
1916
|
+
return waiter.forget
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// the answers that can no longer arrive: their calls reject at once, and
|
|
1920
|
+
// a late answer finds no listener
|
|
1921
|
+
_loseAnswers (selects, reason) {
|
|
1922
|
+
for (const waiter of Array.from(this._waiting)) {
|
|
1923
|
+
if (!selects(waiter)) continue
|
|
1924
|
+
waiter.forget()
|
|
1925
|
+
waiter.lose(reason)
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1370
1929
|
static _prepareError (rpcError) {
|
|
1371
1930
|
if (typeof rpcError === 'object') {
|
|
1372
1931
|
return { message: rpcError.message, cause: rpcError.cause }
|
|
@@ -1396,51 +1955,114 @@ class VrpcClient extends EventEmitter {
|
|
|
1396
1955
|
})
|
|
1397
1956
|
}
|
|
1398
1957
|
|
|
1958
|
+
/**
|
|
1959
|
+
* Whether a function is an event function: declared `@returns
|
|
1960
|
+
* {Registration}` (when the class info carries meta data), else named `on`
|
|
1961
|
+
* followed by a capital letter. The prefixes `notify`, `monitor` and
|
|
1962
|
+
* `signal` still count (deprecated, gone in 4.0).
|
|
1963
|
+
*/
|
|
1964
|
+
_isEventFunction (agent, className, functionName) {
|
|
1965
|
+
const info =
|
|
1966
|
+
this._agents[agent] &&
|
|
1967
|
+
this._agents[agent].classes &&
|
|
1968
|
+
this._agents[agent].classes[className]
|
|
1969
|
+
const meta = info && info.meta && info.meta[functionName]
|
|
1970
|
+
const type = meta && meta.ret && meta.ret.type
|
|
1971
|
+
if (typeof type === 'string' && DECLARED_REGISTRATION.test(type.trim())) {
|
|
1972
|
+
return true
|
|
1973
|
+
}
|
|
1974
|
+
if (/^on[A-Z]/.test(functionName)) return true
|
|
1975
|
+
return /^(notify|monitor|signal)/.test(functionName)
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1399
1978
|
async _wrapArguments ({
|
|
1400
1979
|
agent,
|
|
1401
1980
|
className,
|
|
1402
1981
|
instance,
|
|
1403
1982
|
proxyId,
|
|
1404
1983
|
functionName,
|
|
1405
|
-
args
|
|
1984
|
+
args,
|
|
1985
|
+
isCallAll = false,
|
|
1986
|
+
selection
|
|
1406
1987
|
}) {
|
|
1407
1988
|
const wrapped = []
|
|
1408
1989
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1990
|
+
const isEventFunction = this._isEventFunction(
|
|
1991
|
+
agent,
|
|
1992
|
+
className,
|
|
1993
|
+
functionName
|
|
1994
|
+
)
|
|
1995
|
+
// continuous removers (legacy): withdraw the callback's subscription
|
|
1411
1996
|
const isContinuousRemover = /^(off|remove|unmonitor|unsignal)/.test(
|
|
1412
1997
|
functionName
|
|
1413
1998
|
)
|
|
1999
|
+
const isEmitterRemover =
|
|
2000
|
+
(functionName === 'off' || functionName === 'removeListener') &&
|
|
2001
|
+
typeof args[0] === 'string'
|
|
1414
2002
|
|
|
1415
2003
|
for (const [i, x] of args.entries()) {
|
|
1416
|
-
|
|
1417
|
-
|
|
2004
|
+
const isEmitterContract = this._isEmitter(x)
|
|
2005
|
+
if (this._isFunction(x) || isEmitterContract) {
|
|
2006
|
+
const removes =
|
|
2007
|
+
!isEventFunction &&
|
|
2008
|
+
((isEmitterRemover && i === 1) || isContinuousRemover)
|
|
2009
|
+
const callback = isEmitterContract
|
|
2010
|
+
? this._emitterCallback(x, { readOnly: removes })
|
|
2011
|
+
: x
|
|
1418
2012
|
if (
|
|
1419
|
-
|
|
1420
|
-
(
|
|
1421
|
-
|
|
2013
|
+
!removes &&
|
|
2014
|
+
(isEmitterContract ||
|
|
2015
|
+
functionName === 'vrpcOn' ||
|
|
2016
|
+
((functionName === 'on' || functionName === 'addListener') &&
|
|
2017
|
+
i === 1 &&
|
|
2018
|
+
typeof args[0] === 'string') ||
|
|
2019
|
+
isEventFunction)
|
|
1422
2020
|
) {
|
|
1423
2021
|
const event = typeof args[0] === 'string' ? args[0] : functionName
|
|
2022
|
+
// vrpcOn names the event function first
|
|
2023
|
+
const isVrpcOn = functionName === 'vrpcOn'
|
|
1424
2024
|
const id = await this._onRemoteEvent({
|
|
1425
2025
|
agent,
|
|
1426
2026
|
className,
|
|
1427
2027
|
instance,
|
|
1428
2028
|
event,
|
|
1429
|
-
callback
|
|
2029
|
+
callback,
|
|
2030
|
+
functionName: isVrpcOn ? args[0] : functionName,
|
|
2031
|
+
args: isVrpcOn ? args.slice(1) : args,
|
|
2032
|
+
position: isVrpcOn ? i - 1 : i,
|
|
2033
|
+
isCallAll,
|
|
2034
|
+
selection
|
|
1430
2035
|
})
|
|
1431
2036
|
wrapped.push(id)
|
|
1432
2037
|
continue
|
|
1433
|
-
} else if (
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
2038
|
+
} else if (removes) {
|
|
2039
|
+
const subs = callback
|
|
2040
|
+
? this._offRemoteEvent({
|
|
2041
|
+
agent,
|
|
2042
|
+
className,
|
|
2043
|
+
instance,
|
|
2044
|
+
isCallAll,
|
|
2045
|
+
selection,
|
|
2046
|
+
functionName,
|
|
2047
|
+
args,
|
|
2048
|
+
position: i,
|
|
2049
|
+
callback
|
|
2050
|
+
})
|
|
2051
|
+
: []
|
|
2052
|
+
if (subs.length > 0) {
|
|
2053
|
+
const info = this._agents[agent]
|
|
2054
|
+
if (info && info.v >= 4) {
|
|
2055
|
+
// protocol 4 withdraws by id and never calls the class's
|
|
2056
|
+
// remover: handled here
|
|
2057
|
+
await this._withdrawAt(agent, subs.map(sub => ({ sub })))
|
|
2058
|
+
return null
|
|
2059
|
+
}
|
|
2060
|
+
// an agent before protocol 4 knows only the remover call, which
|
|
2061
|
+
// carries the id (further subscriptions end locally)
|
|
2062
|
+
wrapped.push(subs[0].id)
|
|
2063
|
+
continue
|
|
2064
|
+
}
|
|
2065
|
+
if (functionName === 'off' || functionName === 'removeListener') {
|
|
1444
2066
|
// Nothing of ours to remove - the subscription was already
|
|
1445
2067
|
// dropped (the instance or its agent went away and the cache
|
|
1446
2068
|
// was cleared, or the listener was never registered). Sending
|
|
@@ -1448,43 +2070,28 @@ class VrpcClient extends EventEmitter {
|
|
|
1448
2070
|
// its EventEmitter rejects. Handled locally: nothing to do.
|
|
1449
2071
|
return null
|
|
1450
2072
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
this._invokeId++ % Number.MAX_SAFE_INTEGER
|
|
1458
|
-
}`
|
|
1459
|
-
wrapped.push(id)
|
|
1460
|
-
this._eventEmitter.once(id, ({ a }) => x.apply(null, a))
|
|
1461
|
-
continue
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
// special case of an EventEmitter provided as argument
|
|
1466
|
-
if (this._isEmitter(x)) {
|
|
1467
|
-
const { emitter, event } = x
|
|
1468
|
-
let callback = (...args) => emitter.emit(event, ...args)
|
|
1469
|
-
if (this._emitterListener.has(emitter)) {
|
|
1470
|
-
const eventMap = this._emitterListener.get(emitter)
|
|
1471
|
-
if (eventMap.has(event)) {
|
|
1472
|
-
callback = eventMap.get(event)
|
|
1473
|
-
} else {
|
|
1474
|
-
eventMap.set(event, callback)
|
|
2073
|
+
// no subscription of this callback: an { emitter, event }
|
|
2074
|
+
// contract stays empty, a function is a plain callback after all
|
|
2075
|
+
// (removeItem(id, done))
|
|
2076
|
+
if (isEmitterContract) {
|
|
2077
|
+
wrapped.push(null)
|
|
2078
|
+
continue
|
|
1475
2079
|
}
|
|
1476
|
-
} else {
|
|
1477
|
-
this._emitterListener.set(emitter, new Map([[event, callback]]))
|
|
1478
2080
|
}
|
|
1479
|
-
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
event,
|
|
1485
|
-
callback
|
|
1486
|
-
})
|
|
2081
|
+
// Regular function callback (can be static or member function)
|
|
2082
|
+
const remoteId = proxyId || `${agent}-${className}`
|
|
2083
|
+
const id = `__f__${remoteId}-${functionName}-${i}-${
|
|
2084
|
+
this._invokeId++ % Number.MAX_SAFE_INTEGER
|
|
2085
|
+
}`
|
|
1487
2086
|
wrapped.push(id)
|
|
2087
|
+
// a callback the agent could not encode is told, never called (CALL-10)
|
|
2088
|
+
this._eventEmitter.once(id, ({ a, e }) => {
|
|
2089
|
+
if (e) {
|
|
2090
|
+
this.emit('undelivered', id, e, { agent, className, instance, functionName })
|
|
2091
|
+
return
|
|
2092
|
+
}
|
|
2093
|
+
callback.apply(null, a)
|
|
2094
|
+
})
|
|
1488
2095
|
continue
|
|
1489
2096
|
}
|
|
1490
2097
|
// default behavior is to not touch
|
|
@@ -1493,98 +2100,378 @@ class VrpcClient extends EventEmitter {
|
|
|
1493
2100
|
return wrapped
|
|
1494
2101
|
}
|
|
1495
2102
|
|
|
1496
|
-
|
|
1497
|
-
|
|
2103
|
+
// the one callback standing for an { emitter, event } contract
|
|
2104
|
+
_emitterCallback ({ emitter, event }, { readOnly = false } = {}) {
|
|
2105
|
+
let eventMap = this._emitterListener.get(emitter)
|
|
2106
|
+
if (eventMap && eventMap.has(event)) return eventMap.get(event)
|
|
2107
|
+
if (readOnly) return null
|
|
2108
|
+
const callback = (...args) => emitter.emit(event, ...args)
|
|
2109
|
+
if (!eventMap) {
|
|
2110
|
+
eventMap = new Map()
|
|
2111
|
+
this._emitterListener.set(emitter, eventMap)
|
|
2112
|
+
}
|
|
2113
|
+
eventMap.set(event, callback)
|
|
2114
|
+
return callback
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
/**
|
|
2118
|
+
* What a subscription is to the agent: its function (`addListener` is
|
|
2119
|
+
* `on`) and arguments compared by value, the callback's own position
|
|
2120
|
+
* named and other callbacks marked. Two subscriptions of one callback
|
|
2121
|
+
* differ in it whenever the agent keeps them apart, so their ids differ:
|
|
2122
|
+
* an id names one registration per instance.
|
|
2123
|
+
*/
|
|
2124
|
+
_subscriptionKey (functionName, args, position) {
|
|
2125
|
+
const func = functionName === 'addListener' ? 'on' : functionName
|
|
2126
|
+
const marked = args.map((x, i) => {
|
|
2127
|
+
if (i === position) return '\u0000vrpc-self'
|
|
2128
|
+
if (this._isFunction(x) || this._isEmitter(x)) return '\u0000vrpc-callback'
|
|
2129
|
+
return x
|
|
2130
|
+
})
|
|
2131
|
+
return `${func}\u0000${VrpcClient._canonical(marked)}`
|
|
2132
|
+
}
|
|
1498
2133
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
2134
|
+
// arguments compared by value: equal JSON once object keys are sorted
|
|
2135
|
+
static _canonical (value) {
|
|
2136
|
+
const sortKeys = x => {
|
|
2137
|
+
if (Array.isArray(x)) return x.map(sortKeys)
|
|
2138
|
+
if (x && typeof x === 'object' && x.constructor === Object) {
|
|
2139
|
+
const sorted = {}
|
|
2140
|
+
for (const key of Object.keys(x).sort()) sorted[key] = sortKeys(x[key])
|
|
2141
|
+
return sorted
|
|
2142
|
+
}
|
|
2143
|
+
return x
|
|
2144
|
+
}
|
|
2145
|
+
return JSON.stringify(sortKeys(value))
|
|
2146
|
+
}
|
|
1504
2147
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
2148
|
+
// the event function a remover undoes: off/removeListener -> on,
|
|
2149
|
+
// offValue -> onValue, unmonitorX -> monitorX
|
|
2150
|
+
static _twinOf (functionName) {
|
|
2151
|
+
if (functionName === 'off' || functionName === 'removeListener') return 'on'
|
|
2152
|
+
return functionName.replace(/^(off|un)(?=[A-Za-z])/, m =>
|
|
2153
|
+
m === 'off' ? 'on' : ''
|
|
2154
|
+
)
|
|
2155
|
+
}
|
|
1508
2156
|
|
|
1509
|
-
|
|
2157
|
+
// what a subscription is, but its callback: target, the instances a
|
|
2158
|
+
// callAll selects, and subscription key
|
|
2159
|
+
static _identity ({ agent, className, instance, isCallAll, selection, key }) {
|
|
2160
|
+
const target = [agent, className, instance || null, !!isCallAll, key]
|
|
2161
|
+
return JSON.stringify(selection === undefined ? target : [...target, selection])
|
|
2162
|
+
}
|
|
1510
2163
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
2164
|
+
/**
|
|
2165
|
+
* The topic a subscription's events arrive on; its id is `__e__<topic>`.
|
|
2166
|
+
* One per identity (target, function, arguments and callback), counted,
|
|
2167
|
+
* not hashed: two identities never share one. An identity keeps its
|
|
2168
|
+
* topic while its callback lives, so a subscription declared again after
|
|
2169
|
+
* a loss has the id an agent may still hold (which then changes
|
|
2170
|
+
* nothing); a withdrawn one gets a fresh id when declared anew (see
|
|
2171
|
+
* _dropSubscription), so a late withdrawal never meets the new one.
|
|
2172
|
+
*/
|
|
2173
|
+
_subscriptionTopic (callback, identity, { readOnly = false } = {}) {
|
|
2174
|
+
let topics = this._subscriptionTopics.get(callback)
|
|
2175
|
+
if (!topics) {
|
|
2176
|
+
if (readOnly) return null
|
|
2177
|
+
topics = new Map()
|
|
2178
|
+
this._subscriptionTopics.set(callback, topics)
|
|
2179
|
+
}
|
|
2180
|
+
let topic = topics.get(identity)
|
|
2181
|
+
if (!topic && !readOnly) {
|
|
2182
|
+
// routed by the client's single event inbox wildcard
|
|
2183
|
+
topic = `${this._vrpcConnectionId}/s${(this._subscriptionCount++).toString(36)}`
|
|
2184
|
+
topics.set(identity, topic)
|
|
1514
2185
|
}
|
|
2186
|
+
return topic || null
|
|
2187
|
+
}
|
|
1515
2188
|
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
2189
|
+
/**
|
|
2190
|
+
* A subscription: one per target, function, arguments and callback, held
|
|
2191
|
+
* once however often it is declared (the agent keeps it as a set member,
|
|
2192
|
+
* too).
|
|
2193
|
+
*/
|
|
2194
|
+
async _onRemoteEvent ({
|
|
2195
|
+
agent,
|
|
2196
|
+
className,
|
|
2197
|
+
instance,
|
|
2198
|
+
event,
|
|
2199
|
+
callback,
|
|
2200
|
+
functionName,
|
|
2201
|
+
args,
|
|
2202
|
+
position,
|
|
2203
|
+
isCallAll = false,
|
|
2204
|
+
selection
|
|
2205
|
+
}) {
|
|
2206
|
+
const identity = VrpcClient._identity({
|
|
2207
|
+
agent,
|
|
2208
|
+
className,
|
|
2209
|
+
instance,
|
|
2210
|
+
isCallAll,
|
|
2211
|
+
selection,
|
|
2212
|
+
key: this._subscriptionKey(functionName, args, position)
|
|
2213
|
+
})
|
|
2214
|
+
const topic = this._subscriptionTopic(callback, identity)
|
|
2215
|
+
const id = `__e__${topic}`
|
|
2216
|
+
if (this._cachedSubscriptions[topic]) return id
|
|
2217
|
+
const sub = {
|
|
2218
|
+
id,
|
|
2219
|
+
identity,
|
|
2220
|
+
agent,
|
|
2221
|
+
className,
|
|
2222
|
+
instance,
|
|
2223
|
+
event,
|
|
2224
|
+
functionName,
|
|
2225
|
+
callAll: isCallAll,
|
|
2226
|
+
// the instances a callAll selects (absent: all)
|
|
2227
|
+
selection,
|
|
2228
|
+
callback,
|
|
2229
|
+
handler: null,
|
|
2230
|
+
declaration: null,
|
|
2231
|
+
confirmed: false,
|
|
2232
|
+
// its agent went offline: declared again once it is back
|
|
2233
|
+
lost: false,
|
|
2234
|
+
// callAll: the instances it was withdrawn at (unsubscribe's instance)
|
|
2235
|
+
withdrawn: new Set()
|
|
2236
|
+
}
|
|
2237
|
+
// an ended notice carries `end` and never reaches the callback, nor
|
|
2238
|
+
// does an event the agent could not encode (`e`, EV-22); a callAll
|
|
2239
|
+
// event names its instance first
|
|
2240
|
+
sub.handler = ({ a, end, e }) => {
|
|
2241
|
+
if (end) {
|
|
2242
|
+
this._handleEnded(topic, end)
|
|
2243
|
+
return
|
|
1524
2244
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
2245
|
+
if (e) {
|
|
2246
|
+
this.emit('undelivered', id, e, this._subscriptionInfo(sub))
|
|
2247
|
+
return
|
|
2248
|
+
}
|
|
2249
|
+
if (sub.withdrawn.size > 0 && Array.isArray(a) && sub.withdrawn.has(a[0])) {
|
|
2250
|
+
return
|
|
2251
|
+
}
|
|
2252
|
+
callback.apply(null, a)
|
|
1527
2253
|
}
|
|
1528
|
-
|
|
2254
|
+
this._eventEmitter.on(id, sub.handler)
|
|
2255
|
+
this._cachedSubscriptions[topic] = sub
|
|
1529
2256
|
return id
|
|
1530
2257
|
}
|
|
1531
2258
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
2259
|
+
/**
|
|
2260
|
+
* The subscriptions a remover call names, withdrawn locally: the one its
|
|
2261
|
+
* arguments name (offValue(1, cb) undoes onValue(1, cb)), else all of the
|
|
2262
|
+
* callback's subscriptions of that function on the target (offValue(cb)
|
|
2263
|
+
* after onValue(1, cb) and onValue(2, cb)), or of any function when the
|
|
2264
|
+
* remover names none the client knows.
|
|
2265
|
+
*/
|
|
2266
|
+
_offRemoteEvent ({
|
|
2267
|
+
agent,
|
|
2268
|
+
className,
|
|
2269
|
+
instance,
|
|
2270
|
+
isCallAll,
|
|
2271
|
+
selection,
|
|
2272
|
+
functionName,
|
|
2273
|
+
args,
|
|
2274
|
+
position,
|
|
2275
|
+
callback
|
|
2276
|
+
}) {
|
|
2277
|
+
const twin = VrpcClient._twinOf(functionName)
|
|
2278
|
+
const exact = this._subscriptionTopic(
|
|
2279
|
+
callback,
|
|
2280
|
+
VrpcClient._identity({
|
|
2281
|
+
agent,
|
|
2282
|
+
className,
|
|
2283
|
+
instance,
|
|
2284
|
+
isCallAll,
|
|
2285
|
+
selection,
|
|
2286
|
+
key: this._subscriptionKey(twin, args, position)
|
|
2287
|
+
}),
|
|
2288
|
+
{ readOnly: true }
|
|
2289
|
+
)
|
|
2290
|
+
let subs = exact && this._cachedSubscriptions[exact]
|
|
2291
|
+
? [this._cachedSubscriptions[exact]]
|
|
2292
|
+
: []
|
|
2293
|
+
if (subs.length === 0) {
|
|
2294
|
+
const ofCallback = Object.values(this._cachedSubscriptions).filter(
|
|
2295
|
+
sub =>
|
|
2296
|
+
sub.callback === callback &&
|
|
2297
|
+
sub.agent === agent &&
|
|
2298
|
+
sub.className === className &&
|
|
2299
|
+
sub.instance === instance &&
|
|
2300
|
+
sub.callAll === !!isCallAll &&
|
|
2301
|
+
JSON.stringify(sub.selection) === JSON.stringify(selection)
|
|
2302
|
+
)
|
|
2303
|
+
const ofTwin = ofCallback.filter(
|
|
2304
|
+
sub =>
|
|
2305
|
+
(sub.functionName === 'addListener' ? 'on' : sub.functionName) ===
|
|
2306
|
+
twin
|
|
2307
|
+
)
|
|
2308
|
+
subs = ofTwin.length > 0 ? ofTwin : ofCallback
|
|
2309
|
+
}
|
|
2310
|
+
subs.forEach(sub => this._dropSubscription(sub))
|
|
2311
|
+
return subs
|
|
2312
|
+
}
|
|
1548
2313
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
2314
|
+
/**
|
|
2315
|
+
* Ends a subscription locally: no event reaches its callback any more. A
|
|
2316
|
+
* withdrawn or ended subscription also gives up its id (declared anew, it
|
|
2317
|
+
* gets a fresh one); a lost one keeps it (`forget: false`), so declaring
|
|
2318
|
+
* it again matches what an agent that went away and came back still
|
|
2319
|
+
* holds.
|
|
2320
|
+
*/
|
|
2321
|
+
_dropSubscription (sub, { forget = true } = {}) {
|
|
2322
|
+
this._eventEmitter.removeListener(sub.id, sub.handler)
|
|
2323
|
+
const topic = sub.id.slice(5)
|
|
2324
|
+
if (this._cachedSubscriptions[topic] === sub) {
|
|
2325
|
+
delete this._cachedSubscriptions[topic]
|
|
2326
|
+
}
|
|
2327
|
+
if (forget) {
|
|
2328
|
+
const topics = this._subscriptionTopics.get(sub.callback)
|
|
2329
|
+
if (topics && topics.get(sub.identity) === topic) {
|
|
2330
|
+
topics.delete(sub.identity)
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
1552
2334
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
2335
|
+
/**
|
|
2336
|
+
* Remembers the call that declared a subscription (to declare it again
|
|
2337
|
+
* after a reconnect) and sends the subscriber labels along.
|
|
2338
|
+
*/
|
|
2339
|
+
_declare (json, topic) {
|
|
2340
|
+
const labels = {}
|
|
2341
|
+
let labelled = false
|
|
2342
|
+
for (const arg of json.a) {
|
|
2343
|
+
if (typeof arg !== 'string' || !arg.startsWith('__e__')) continue
|
|
2344
|
+
const sub = this._cachedSubscriptions[arg.slice(5)]
|
|
2345
|
+
if (!sub) continue
|
|
2346
|
+
sub.declaration = { topic, c: json.c, f: json.f, a: json.a }
|
|
2347
|
+
// a declaration names what is wanted now: earlier withdrawals at
|
|
2348
|
+
// single instances are over
|
|
2349
|
+
sub.withdrawn.clear()
|
|
2350
|
+
const label = this._subscriberLabels.get(sub.callback)
|
|
2351
|
+
if (label !== undefined) {
|
|
2352
|
+
labels[arg] = label
|
|
2353
|
+
labelled = true
|
|
1557
2354
|
}
|
|
1558
|
-
}
|
|
1559
|
-
|
|
2355
|
+
}
|
|
2356
|
+
if (labelled) {
|
|
2357
|
+
json.l = labels
|
|
2358
|
+
for (const id of Object.keys(labels)) {
|
|
2359
|
+
const sub = this._cachedSubscriptions[id.slice(5)]
|
|
2360
|
+
if (sub && sub.declaration) sub.declaration.l = labels
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
1560
2363
|
}
|
|
1561
2364
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
2365
|
+
// marks the subscriptions among a declaring call's arguments as held
|
|
2366
|
+
_confirmSubscriptions (args) {
|
|
2367
|
+
if (!Array.isArray(args)) return
|
|
2368
|
+
for (const arg of args) {
|
|
2369
|
+
const sub =
|
|
2370
|
+
typeof arg === 'string' &&
|
|
2371
|
+
arg.startsWith('__e__') &&
|
|
2372
|
+
this._cachedSubscriptions[arg.slice(5)]
|
|
2373
|
+
if (sub) sub.confirmed = true
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
// declares every subscription held towards an agent again (protocol 4)
|
|
2378
|
+
_redeclareTo (agent) {
|
|
2379
|
+
if (!this._inboxReady || !this._redeclare.has(agent)) return
|
|
2380
|
+
const info = this._agents[agent]
|
|
2381
|
+
if (!info || info.status !== 'online') return
|
|
2382
|
+
this._redeclare.delete(agent)
|
|
2383
|
+
if (!(info.v >= 4)) return // an older agent learns nothing new
|
|
2384
|
+
const declarations = new Map()
|
|
2385
|
+
for (const sub of Object.values(this._cachedSubscriptions)) {
|
|
2386
|
+
if (sub.agent !== agent || !sub.declaration) continue
|
|
2387
|
+
const { topic, a } = sub.declaration
|
|
2388
|
+
const key = `${topic}\u0000${JSON.stringify(a)}`
|
|
2389
|
+
if (!declarations.has(key)) {
|
|
2390
|
+
declarations.set(key, { declaration: sub.declaration, subs: [] })
|
|
1575
2391
|
}
|
|
1576
|
-
|
|
2392
|
+
declarations.get(key).subs.push(sub)
|
|
2393
|
+
}
|
|
2394
|
+
for (const { declaration, subs } of declarations.values()) {
|
|
2395
|
+
const { topic, c, f, a, l } = declaration
|
|
2396
|
+
const json = {
|
|
2397
|
+
c,
|
|
2398
|
+
f,
|
|
2399
|
+
a,
|
|
2400
|
+
i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
|
|
2401
|
+
s: this._vrpcConnectionId,
|
|
2402
|
+
v: VRPC_PROTOCOL_VERSION
|
|
2403
|
+
}
|
|
2404
|
+
if (l) json.l = l
|
|
2405
|
+
this._mqttPublish(topic, JSON.stringify(json))
|
|
2406
|
+
this._handleAgentAnswer(json, agent).then(
|
|
2407
|
+
() => {
|
|
2408
|
+
const entries = []
|
|
2409
|
+
for (const sub of subs) {
|
|
2410
|
+
if (this._cachedSubscriptions[sub.id.slice(5)] !== sub) continue
|
|
2411
|
+
// a lost subscription is back
|
|
2412
|
+
if (sub.lost) {
|
|
2413
|
+
sub.lost = false
|
|
2414
|
+
this.emit('healed', sub.id, this._subscriptionInfo(sub))
|
|
2415
|
+
}
|
|
2416
|
+
// what was withdrawn at single instances stays withdrawn there
|
|
2417
|
+
for (const context of sub.withdrawn) entries.push({ sub, context })
|
|
2418
|
+
}
|
|
2419
|
+
if (entries.length > 0) return this._sendUnsubscribe(agent, entries)
|
|
2420
|
+
},
|
|
2421
|
+
err => {
|
|
2422
|
+
for (const sub of subs) {
|
|
2423
|
+
if (this._cachedSubscriptions[sub.id.slice(5)] !== sub) continue
|
|
2424
|
+
this._dropSubscription(sub, { forget: false })
|
|
2425
|
+
this.emit('lost', sub.id, err.message, this._subscriptionInfo(sub))
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
)
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
1577
2431
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
2432
|
+
_subscriptionInfo (sub) {
|
|
2433
|
+
return {
|
|
2434
|
+
agent: sub.agent,
|
|
2435
|
+
className: sub.className,
|
|
2436
|
+
instance: sub.instance,
|
|
2437
|
+
functionName: sub.functionName,
|
|
2438
|
+
event: sub.event,
|
|
2439
|
+
callAll: sub.callAll,
|
|
2440
|
+
...(sub.selection === undefined ? {} : { instances: sub.selection })
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
1582
2443
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
2444
|
+
// the agent ended a registration this subscription was part of
|
|
2445
|
+
_handleEnded (topic, end) {
|
|
2446
|
+
const sub = this._cachedSubscriptions[topic]
|
|
2447
|
+
if (!sub) return
|
|
2448
|
+
const info = {
|
|
2449
|
+
...this._subscriptionInfo(sub),
|
|
2450
|
+
context: end.context,
|
|
2451
|
+
failed: !!end.failed
|
|
2452
|
+
}
|
|
2453
|
+
// a callAll subscription lives on at the other instances
|
|
2454
|
+
if (!sub.callAll) this._dropSubscription(sub)
|
|
2455
|
+
this.emit('ended', sub.id, end.reason, end.by, info)
|
|
2456
|
+
}
|
|
1586
2457
|
|
|
1587
|
-
|
|
2458
|
+
_clearCachedSubscriptions ({ lostAgent, lostInstance, agent, className }) {
|
|
2459
|
+
const obsolete = Object.values(this._cachedSubscriptions).filter(sub => {
|
|
2460
|
+
if (lostAgent !== undefined) return sub.agent === lostAgent
|
|
2461
|
+
// Instance ids are unique per agent only: a twin of the same id on
|
|
2462
|
+
// another agent (a deployed app next to its builder) keeps its
|
|
2463
|
+
// subscriptions when this one goes
|
|
2464
|
+
return (
|
|
2465
|
+
sub.instance === lostInstance &&
|
|
2466
|
+
sub.agent === agent &&
|
|
2467
|
+
sub.className === className
|
|
2468
|
+
)
|
|
2469
|
+
})
|
|
2470
|
+
const reason = lostAgent !== undefined ? 'agentOffline' : 'instanceGone'
|
|
2471
|
+
obsolete.forEach(sub => {
|
|
2472
|
+
this._log.debug(`Clearing subscription for obsolete topic: ${sub.id}`)
|
|
2473
|
+
this._dropSubscription(sub, { forget: false })
|
|
2474
|
+
this.emit('lost', sub.id, reason, this._subscriptionInfo(sub))
|
|
1588
2475
|
})
|
|
1589
2476
|
}
|
|
1590
2477
|
|
|
@@ -1615,6 +2502,8 @@ class VrpcClient extends EventEmitter {
|
|
|
1615
2502
|
}
|
|
1616
2503
|
|
|
1617
2504
|
async _getInstanceData (instance, options) {
|
|
2505
|
+
const own = this._findOwnIsolated(instance, options)
|
|
2506
|
+
if (own) return own
|
|
1618
2507
|
const instanceData = this._getInstanceFromCache(instance, options)
|
|
1619
2508
|
if (!instanceData) {
|
|
1620
2509
|
if (options.noWait) {
|
|
@@ -1625,6 +2514,17 @@ class VrpcClient extends EventEmitter {
|
|
|
1625
2514
|
return instanceData
|
|
1626
2515
|
}
|
|
1627
2516
|
|
|
2517
|
+
// an isolated instance this connection created, when it is the one match
|
|
2518
|
+
_findOwnIsolated (instance, { agent, className } = {}) {
|
|
2519
|
+
const hits = Array.from(this._ownIsolated.values()).filter(
|
|
2520
|
+
x =>
|
|
2521
|
+
x.instance === instance &&
|
|
2522
|
+
(!agent || agent === '*' || x.agent === agent) &&
|
|
2523
|
+
(!className || x.className === className)
|
|
2524
|
+
)
|
|
2525
|
+
return hits.length === 1 ? { agent: hits[0].agent, className: hits[0].className } : null
|
|
2526
|
+
}
|
|
2527
|
+
|
|
1628
2528
|
_cachedProxy (instance, { agent, className } = {}) {
|
|
1629
2529
|
// Instance ids are unique per agent only: a cached proxy is served
|
|
1630
2530
|
// solely when it is the one match for the given agent and class - twins
|
|
@@ -1740,6 +2640,72 @@ class VrpcClient extends EventEmitter {
|
|
|
1740
2640
|
* @param {String} info.className - Class name
|
|
1741
2641
|
*/
|
|
1742
2642
|
|
|
2643
|
+
/**
|
|
2644
|
+
* Event 'ended'
|
|
2645
|
+
*
|
|
2646
|
+
* Emitted when a registration a subscription held ended for a reason the
|
|
2647
|
+
* subscriber did not cause: its source ended it, or a member did
|
|
2648
|
+
* (`endRegistration`). The subscription is gone; it never reaches the
|
|
2649
|
+
* callback. A callAll subscription lives on at its other instances.
|
|
2650
|
+
*
|
|
2651
|
+
* @event VrpcClient#ended
|
|
2652
|
+
*
|
|
2653
|
+
* @param {String} id - The subscription id
|
|
2654
|
+
* @param {String} reason - Why it ended (an error message when it failed)
|
|
2655
|
+
* @param {String|null} by - Who ended it (`'source'`, or what the member said)
|
|
2656
|
+
* @param {Object} info - The subscription (agent, className, instance,
|
|
2657
|
+
* functionName, event, callAll), `context` (the instance or class) and
|
|
2658
|
+
* `failed`
|
|
2659
|
+
*/
|
|
2660
|
+
|
|
2661
|
+
/**
|
|
2662
|
+
* Event 'undelivered'
|
|
2663
|
+
*
|
|
2664
|
+
* Emitted instead of an event, or a one-shot callback, whose arguments the
|
|
2665
|
+
* agent could not encode as JSON (a BigInt, say). The callback is not
|
|
2666
|
+
* called for it; a subscription stays and receives the next event. Agents
|
|
2667
|
+
* before protocol 4 drop such events without telling.
|
|
2668
|
+
*
|
|
2669
|
+
* @event VrpcClient#undelivered
|
|
2670
|
+
*
|
|
2671
|
+
* @param {String} id - The subscription id, or the one-shot callback's id
|
|
2672
|
+
* @param {Object} error - What the agent says, `{ message }`
|
|
2673
|
+
* @param {Object} info - The subscription (agent, className, instance,
|
|
2674
|
+
* functionName, event, callAll), or for a one-shot callback its agent,
|
|
2675
|
+
* className, instance and functionName
|
|
2676
|
+
*/
|
|
2677
|
+
|
|
2678
|
+
/**
|
|
2679
|
+
* Event 'lost'
|
|
2680
|
+
*
|
|
2681
|
+
* Emitted when a subscription is lost: its agent went offline
|
|
2682
|
+
* (`'agentOffline'`), its instance went away (`'instanceGone'`), or a
|
|
2683
|
+
* declaration again failed (the error's message). A subscription on an agent
|
|
2684
|
+
* of protocol 4 that went offline is declared again once the agent is back
|
|
2685
|
+
* ('healed'); every other lost subscription is gone, and declaring it again
|
|
2686
|
+
* is the consumer's.
|
|
2687
|
+
*
|
|
2688
|
+
* @event VrpcClient#lost
|
|
2689
|
+
*
|
|
2690
|
+
* @param {String} id - The subscription id
|
|
2691
|
+
* @param {String} reason - `'agentOffline'`, `'instanceGone'` or an error message
|
|
2692
|
+
* @param {Object} info - The subscription (agent, className, instance,
|
|
2693
|
+
* functionName, event, callAll)
|
|
2694
|
+
*/
|
|
2695
|
+
|
|
2696
|
+
/**
|
|
2697
|
+
* Event 'healed'
|
|
2698
|
+
*
|
|
2699
|
+
* Emitted when a subscription lost with its agent is back: the agent serves
|
|
2700
|
+
* again and holds it.
|
|
2701
|
+
*
|
|
2702
|
+
* @event VrpcClient#healed
|
|
2703
|
+
*
|
|
2704
|
+
* @param {String} id - The subscription id
|
|
2705
|
+
* @param {Object} info - The subscription (agent, className, instance,
|
|
2706
|
+
* functionName, event, callAll)
|
|
2707
|
+
*/
|
|
2708
|
+
|
|
1743
2709
|
/**
|
|
1744
2710
|
* Event 'connect'
|
|
1745
2711
|
*
|