vrpc 3.14.0 → 3.15.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 +2 -2
- 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 +552 -294
- package/vrpc/VrpcAgent.js +230 -153
- package/vrpc/VrpcBrowserAdapter.js +417 -285
- package/vrpc/VrpcBrowserAgent.js +239 -128
- package/vrpc/VrpcClient.js +1108 -213
- 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,36 @@ 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
|
+
// labels callbacks carry towards the agent's list of registrations
|
|
184
|
+
this._subscriberLabels = new WeakMap()
|
|
171
185
|
if (log === 'console') {
|
|
172
186
|
this._log = console
|
|
173
187
|
this._log.debug = () => {}
|
|
174
188
|
} else {
|
|
175
189
|
this._log = log
|
|
176
190
|
}
|
|
191
|
+
// refused subscriptions are retried until granted
|
|
192
|
+
this._subscribeRetrier = new SubscribeRetrier({
|
|
193
|
+
subscribe: (topics, options) => this._mqttSubscribe(topics, options).catch(() => {}),
|
|
194
|
+
log: this._log
|
|
195
|
+
})
|
|
177
196
|
}
|
|
178
197
|
|
|
179
198
|
/**
|
|
@@ -289,6 +308,9 @@ class VrpcClient extends EventEmitter {
|
|
|
289
308
|
// Clear the cache to ensure a fresh state upon reconnection
|
|
290
309
|
this._agents = {}
|
|
291
310
|
this._proxies = {}
|
|
311
|
+
// the agents delete this connection's isolated instances once its
|
|
312
|
+
// will is out
|
|
313
|
+
this._ownIsolated.clear()
|
|
292
314
|
this.emit('offline')
|
|
293
315
|
})
|
|
294
316
|
|
|
@@ -306,6 +328,14 @@ class VrpcClient extends EventEmitter {
|
|
|
306
328
|
})
|
|
307
329
|
|
|
308
330
|
this._client.on('connect', async () => {
|
|
331
|
+
// the subscriptions this client holds are declared again, to every
|
|
332
|
+
// agent announcing protocol 4, once the inbox stands (an agent that
|
|
333
|
+
// saw this connection go has withdrawn them; one that did not keeps
|
|
334
|
+
// them, declaring again is a no-op there)
|
|
335
|
+
this._inboxReady = false
|
|
336
|
+
for (const sub of Object.values(this._cachedSubscriptions)) {
|
|
337
|
+
this._redeclare.add(sub.agent)
|
|
338
|
+
}
|
|
309
339
|
// forward the good news
|
|
310
340
|
this.emit('connect')
|
|
311
341
|
// everything is subscribed afresh below: pending retries are moot
|
|
@@ -325,6 +355,13 @@ class VrpcClient extends EventEmitter {
|
|
|
325
355
|
}
|
|
326
356
|
// Single wildcard catches BOTH RPC responses (length 3) AND events (length 4)
|
|
327
357
|
await this._mqttSubscribe(`${this._vrpcConnectionId}/#`)
|
|
358
|
+
this._inboxReady = true
|
|
359
|
+
for (const agent of Array.from(this._redeclare)) {
|
|
360
|
+
this._redeclareTo(agent)
|
|
361
|
+
}
|
|
362
|
+
for (const agent of Array.from(this._pendingWithdrawals.keys())) {
|
|
363
|
+
this._flushWithdrawals(agent)
|
|
364
|
+
}
|
|
328
365
|
|
|
329
366
|
// Re-subscribe to all cached event topics
|
|
330
367
|
this._log.debug('Restoring event subscriptions after reconnect...')
|
|
@@ -351,13 +388,20 @@ class VrpcClient extends EventEmitter {
|
|
|
351
388
|
}
|
|
352
389
|
// AgentInfo message
|
|
353
390
|
if (klass === '__agentInfo__') {
|
|
354
|
-
const { status, hostname, version } = JSON.parse(
|
|
391
|
+
const { status, hostname, version, v } = JSON.parse(
|
|
392
|
+
message.toString()
|
|
393
|
+
)
|
|
355
394
|
this._createIfNotExist(agent)
|
|
356
395
|
this._agents[agent].status = status
|
|
357
396
|
this._agents[agent].hostname = hostname
|
|
358
397
|
this._agents[agent].version = version
|
|
398
|
+
// the agent's protocol version (undefined before 3)
|
|
399
|
+
this._agents[agent].v = v
|
|
359
400
|
if (status === 'offline') {
|
|
360
|
-
this.
|
|
401
|
+
this._loseAgent(agent, v)
|
|
402
|
+
} else {
|
|
403
|
+
this._redeclareTo(agent)
|
|
404
|
+
this._flushWithdrawals(agent)
|
|
361
405
|
}
|
|
362
406
|
this.emit('agent', { domain, agent, status, hostname, version })
|
|
363
407
|
// ClassInfo message
|
|
@@ -401,8 +445,8 @@ class VrpcClient extends EventEmitter {
|
|
|
401
445
|
})
|
|
402
446
|
// RPC message
|
|
403
447
|
} else {
|
|
404
|
-
const { i, a, e, r } = JSON.parse(message.toString())
|
|
405
|
-
this._eventEmitter.emit(i, { a, e, r })
|
|
448
|
+
const { i, a, e, r, u, end } = JSON.parse(message.toString())
|
|
449
|
+
this._eventEmitter.emit(i, { a, e, r, u, end })
|
|
406
450
|
}
|
|
407
451
|
} catch (err) {
|
|
408
452
|
this._log.error(
|
|
@@ -554,6 +598,14 @@ class VrpcClient extends EventEmitter {
|
|
|
554
598
|
v: VRPC_PROTOCOL_VERSION
|
|
555
599
|
}
|
|
556
600
|
const proxy = await this._getProxy(agent, className, json)
|
|
601
|
+
// no class info lists an isolated instance: its creator remembers it
|
|
602
|
+
if (isIsolated) {
|
|
603
|
+
this._ownIsolated.set(`${agent}/${className}/${instance}`, {
|
|
604
|
+
agent,
|
|
605
|
+
className,
|
|
606
|
+
instance
|
|
607
|
+
})
|
|
608
|
+
}
|
|
557
609
|
if (instance && cacheProxy) {
|
|
558
610
|
this._proxies[`${agent}/${className}/${instance}`] = {
|
|
559
611
|
agent,
|
|
@@ -618,6 +670,9 @@ class VrpcClient extends EventEmitter {
|
|
|
618
670
|
this._mqttPublish(topic, JSON.stringify(json))
|
|
619
671
|
delete this._proxies[`${agent}/${className}/${instance}`]
|
|
620
672
|
const deleted = await this._handleAgentAnswer(json, agent)
|
|
673
|
+
if (deleted === true) {
|
|
674
|
+
this._ownIsolated.delete(`${agent}/${className}/${instance}`)
|
|
675
|
+
}
|
|
621
676
|
// the answer and the class info that no longer lists the instance
|
|
622
677
|
// travel on different topics, and a broker need not keep their order:
|
|
623
678
|
// what this client deleted is gone for it at once
|
|
@@ -675,6 +730,7 @@ class VrpcClient extends EventEmitter {
|
|
|
675
730
|
v: VRPC_PROTOCOL_VERSION
|
|
676
731
|
}
|
|
677
732
|
const topic = `${this._domain}/${agent}/${className}/__static__/${functionName}`
|
|
733
|
+
this._declare(json, topic)
|
|
678
734
|
await this._waitUntilClassIsOnline(agent, className)
|
|
679
735
|
this._mqttPublish(topic, JSON.stringify(json))
|
|
680
736
|
return this._handleAgentAnswer(json, agent)
|
|
@@ -686,29 +742,61 @@ class VrpcClient extends EventEmitter {
|
|
|
686
742
|
* registrations. In this case the instanceId of the emitter is injected as
|
|
687
743
|
* first argument of any event callback.
|
|
688
744
|
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
745
|
+
* `instances` selects the instances: a name pattern (`line-*`, where `*`
|
|
746
|
+
* matches any run of characters) or a list of names and patterns. The
|
|
747
|
+
* agent calls the selected ones only and answers for those - one request,
|
|
748
|
+
* however many instances. An agent that does not select instances
|
|
749
|
+
* (before protocol 4) gets one call per selected instance instead; a
|
|
750
|
+
* subscription cannot be split like that and is refused there.
|
|
751
|
+
*
|
|
752
|
+
* `agent` may name several agents as well: a pattern (`edge-*`, `*` by
|
|
753
|
+
* default) asks every online agent that offers the class and matches, a
|
|
754
|
+
* list asks each agent listed. They are asked all at once, and every
|
|
755
|
+
* entry names its agent; an agent that fails as a whole answers an error
|
|
756
|
+
* for each of its instances the client knows - or one entry with the
|
|
757
|
+
* error alone when it knows none - and the other agents' answers stand.
|
|
693
758
|
*
|
|
694
759
|
* @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
|
-
*
|
|
760
|
+
* @param {String} options.className Name of the class
|
|
761
|
+
* @param {String} options.functionName Name of the member function
|
|
762
|
+
* @param {Array} [options.args] Positional arguments of the call
|
|
763
|
+
* @param {String|Array.<String>} [options.agent] Agent name, pattern or
|
|
764
|
+
* list of names. If not provided class default is used
|
|
765
|
+
* @param {String|Array.<String>} [options.instances] The instances to call:
|
|
766
|
+
* a name pattern, or a list of names and patterns (all if not provided)
|
|
767
|
+
* @returns {Promise<Object[]>} An array of objects `{ id, val }` or
|
|
768
|
+
* `{ id, err }` carrying the instance id and its return value or error,
|
|
769
|
+
* and `agent` when the call addressed several agents
|
|
702
770
|
*/
|
|
703
771
|
async callAll ({
|
|
704
772
|
className,
|
|
705
773
|
functionName,
|
|
706
774
|
args = [],
|
|
707
|
-
agent = this._agent
|
|
775
|
+
agent = this._agent,
|
|
776
|
+
instances
|
|
708
777
|
} = {}) {
|
|
709
778
|
if (!this._isConnected()) {
|
|
710
779
|
throw new Error('Client is not connected')
|
|
711
780
|
}
|
|
781
|
+
if (instances !== undefined && !NamePattern.isSelection(instances)) {
|
|
782
|
+
throw new Error(
|
|
783
|
+
`Invalid instances: ${JSON.stringify(instances)} (a name pattern, or a list of names and patterns)`
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
// a selection of everything is no selection
|
|
787
|
+
const selection =
|
|
788
|
+
instances === undefined || NamePattern.selectsAll(instances)
|
|
789
|
+
? undefined
|
|
790
|
+
: instances
|
|
791
|
+
if (Array.isArray(agent) || agent.includes('*')) {
|
|
792
|
+
return this._callAllAgents({
|
|
793
|
+
className,
|
|
794
|
+
functionName,
|
|
795
|
+
args,
|
|
796
|
+
agents: agent,
|
|
797
|
+
selection
|
|
798
|
+
})
|
|
799
|
+
}
|
|
712
800
|
const json = {
|
|
713
801
|
c: className,
|
|
714
802
|
f: functionName,
|
|
@@ -717,40 +805,418 @@ class VrpcClient extends EventEmitter {
|
|
|
717
805
|
s: this._vrpcConnectionId,
|
|
718
806
|
v: VRPC_PROTOCOL_VERSION
|
|
719
807
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const wrapped = await this._wrapArguments({
|
|
808
|
+
let special = '__callAll__'
|
|
809
|
+
if (selection !== undefined) {
|
|
810
|
+
await this._waitUntilClassIsOnline(agent, className)
|
|
811
|
+
if (!this._selectsInstances(agent, className)) {
|
|
812
|
+
return this._callSelected({
|
|
726
813
|
agent,
|
|
727
814
|
className,
|
|
728
815
|
functionName,
|
|
729
|
-
args
|
|
816
|
+
args,
|
|
817
|
+
selection
|
|
730
818
|
})
|
|
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
819
|
}
|
|
738
|
-
|
|
820
|
+
special = '__callMatching__'
|
|
739
821
|
}
|
|
740
|
-
const topic = `${this._domain}/${agent}/${className}/__static__
|
|
822
|
+
const topic = `${this._domain}/${agent}/${className}/__static__/${special}`
|
|
741
823
|
const wrapped = await this._wrapArguments({
|
|
742
824
|
agent,
|
|
743
825
|
className,
|
|
744
826
|
functionName,
|
|
745
|
-
args
|
|
827
|
+
args,
|
|
828
|
+
isCallAll: true,
|
|
829
|
+
selection
|
|
746
830
|
})
|
|
747
831
|
if (!wrapped) return // using cache
|
|
748
|
-
json.a =
|
|
832
|
+
json.a =
|
|
833
|
+
selection === undefined
|
|
834
|
+
? [functionName, ...wrapped]
|
|
835
|
+
: [selection, functionName, ...wrapped]
|
|
836
|
+
this._declare(json, topic)
|
|
837
|
+
await this._waitUntilClassIsOnline(agent, className)
|
|
838
|
+
this._mqttPublish(topic, JSON.stringify(json))
|
|
839
|
+
return this._handleAgentAnswer(json, agent)
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* callAll on several agents: those a list names, or every online agent
|
|
844
|
+
* that offers the class and whose name a pattern matches - all at once,
|
|
845
|
+
* every entry naming its agent
|
|
846
|
+
*/
|
|
847
|
+
async _callAllAgents ({ className, functionName, args, agents, selection }) {
|
|
848
|
+
const names = Array.isArray(agents)
|
|
849
|
+
? agents
|
|
850
|
+
: this.getAvailableAgents().filter(
|
|
851
|
+
name =>
|
|
852
|
+
this._agents[name].classes[className] &&
|
|
853
|
+
NamePattern.matches(agents, name)
|
|
854
|
+
)
|
|
855
|
+
const answers = await Promise.all(
|
|
856
|
+
names.map(name =>
|
|
857
|
+
this.callAll({
|
|
858
|
+
className,
|
|
859
|
+
functionName,
|
|
860
|
+
args,
|
|
861
|
+
agent: name,
|
|
862
|
+
instances: selection
|
|
863
|
+
}).then(
|
|
864
|
+
result => (result || []).map(entry => ({ agent: name, ...entry })),
|
|
865
|
+
err => {
|
|
866
|
+
const error = { message: err.message }
|
|
867
|
+
const known = this.getAvailableInstances({ className, agent: name })
|
|
868
|
+
.filter(id => selection === undefined || NamePattern.selects(selection, id))
|
|
869
|
+
return known.length > 0
|
|
870
|
+
? known.map(id => ({ agent: name, id, err: error }))
|
|
871
|
+
: [{ agent: name, err: error }]
|
|
872
|
+
}
|
|
873
|
+
)
|
|
874
|
+
)
|
|
875
|
+
)
|
|
876
|
+
return answers.flat()
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// whether an agent selects instances itself: its class info lists
|
|
880
|
+
// __callMatching__ (agents of protocol 4)
|
|
881
|
+
_selectsInstances (agent, className) {
|
|
882
|
+
const info = this._agents[agent] && this._agents[agent].classes[className]
|
|
883
|
+
return (
|
|
884
|
+
!!info &&
|
|
885
|
+
info.staticFunctions.some(
|
|
886
|
+
f => this._stripSignature(f) === '__callMatching__'
|
|
887
|
+
)
|
|
888
|
+
)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* A selection towards an agent that does not select instances: one call
|
|
893
|
+
* per selected instance, answered as callAll answers. A subscription is
|
|
894
|
+
* refused: its events, one per instance, would not name their instance.
|
|
895
|
+
*/
|
|
896
|
+
async _callSelected ({ agent, className, functionName, args, selection }) {
|
|
897
|
+
if (args.some(x => this._isFunction(x) || this._isEmitter(x))) {
|
|
898
|
+
throw new Error(
|
|
899
|
+
`Agent ${agent} does not select instances for callbacks and events (its class info lists no __callMatching__): update it, or call without instances`
|
|
900
|
+
)
|
|
901
|
+
}
|
|
902
|
+
const selected = this.getAvailableInstances({ className, agent }).filter(
|
|
903
|
+
id => NamePattern.selects(selection, id)
|
|
904
|
+
)
|
|
905
|
+
return Promise.all(
|
|
906
|
+
selected.map(async id => {
|
|
907
|
+
try {
|
|
908
|
+
const proxy = await this.getInstance(id, { agent, className })
|
|
909
|
+
return { id, val: await proxy[functionName](...args) }
|
|
910
|
+
} catch (err) {
|
|
911
|
+
return { id, err: { message: err.message } }
|
|
912
|
+
}
|
|
913
|
+
})
|
|
914
|
+
)
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Withdraws subscriptions.
|
|
919
|
+
*
|
|
920
|
+
* A subscription is what an event function call (or `on`) declared with a
|
|
921
|
+
* callback: one per target, function, arguments and callback, named by
|
|
922
|
+
* its subscription id (see getSubscriptions). Withdrawing it stops its events at once, and
|
|
923
|
+
* every agent holding it is told by id - however it was declared (a proxy,
|
|
924
|
+
* callStatic, callAll on one agent or on all of them). The agent releases
|
|
925
|
+
* the registration when the last subscription leaves.
|
|
926
|
+
*
|
|
927
|
+
* With `instance`, only what is held on that instance is withdrawn: a
|
|
928
|
+
* subscription declared there ends, and a callAll subscription - one id
|
|
929
|
+
* on every instance of its class - stops at that instance alone while
|
|
930
|
+
* the others keep delivering. The client remembers such a withdrawal and
|
|
931
|
+
* applies it again after its own reconnect, until the subscription is
|
|
932
|
+
* declared anew (a declaration names what is wanted now).
|
|
933
|
+
*
|
|
934
|
+
* Agents before protocol 4 cannot withdraw by id: an `on` subscription is
|
|
935
|
+
* removed there as before (removeListener), an `on<Name>` one through its
|
|
936
|
+
* `off<Name>` twin when the class has one; anything else ends locally
|
|
937
|
+
* (a callAll subscription withdrawn at one instance stops delivering its
|
|
938
|
+
* events there).
|
|
939
|
+
*
|
|
940
|
+
* @param {String|Array.<String>|Function|Object} target A subscription id
|
|
941
|
+
* (or several), a callback (all its subscriptions), or an
|
|
942
|
+
* `{ emitter, event }` contract (all of its subscriptions)
|
|
943
|
+
* @param {Object} [options]
|
|
944
|
+
* @param {String} [options.instance] Withdraw at this instance only
|
|
945
|
+
* @returns {Promise<Boolean>} true if anything was withdrawn
|
|
946
|
+
*/
|
|
947
|
+
async unsubscribe (target, { instance } = {}) {
|
|
948
|
+
const all = Object.values(this._cachedSubscriptions)
|
|
949
|
+
let subs
|
|
950
|
+
if (typeof target === 'string' || Array.isArray(target)) {
|
|
951
|
+
const ids = Array.isArray(target) ? target : [target]
|
|
952
|
+
subs = ids
|
|
953
|
+
.map(id =>
|
|
954
|
+
this._cachedSubscriptions[id.startsWith('__e__') ? id.slice(5) : id]
|
|
955
|
+
)
|
|
956
|
+
.filter(Boolean)
|
|
957
|
+
} else if (this._isFunction(target)) {
|
|
958
|
+
subs = all.filter(sub => sub.callback === target)
|
|
959
|
+
} else if (this._isEmitter(target)) {
|
|
960
|
+
const callback = this._emitterCallback(target, { readOnly: true })
|
|
961
|
+
subs = callback ? all.filter(sub => sub.callback === callback) : []
|
|
962
|
+
} else {
|
|
963
|
+
throw new Error(
|
|
964
|
+
'unsubscribe expects a subscription id, a callback or an { emitter, event } contract'
|
|
965
|
+
)
|
|
966
|
+
}
|
|
967
|
+
// at one instance: a callAll subscription stops there, one declared on
|
|
968
|
+
// that instance ends, any other is not held there
|
|
969
|
+
const atInstance = []
|
|
970
|
+
if (instance !== undefined && instance !== null) {
|
|
971
|
+
atInstance.push(...subs.filter(sub => sub.callAll))
|
|
972
|
+
subs = subs.filter(sub => !sub.callAll && sub.instance === instance)
|
|
973
|
+
}
|
|
974
|
+
if (subs.length === 0 && atInstance.length === 0) return false
|
|
975
|
+
// no event reaches the consumer from here on
|
|
976
|
+
subs.forEach(sub => this._dropSubscription(sub))
|
|
977
|
+
atInstance.forEach(sub => sub.withdrawn.add(instance))
|
|
978
|
+
const byAgent = new Map()
|
|
979
|
+
const entries = (agent, sub, context) => {
|
|
980
|
+
if (!byAgent.has(agent)) byAgent.set(agent, [])
|
|
981
|
+
byAgent.get(agent).push({ sub, context })
|
|
982
|
+
}
|
|
983
|
+
for (const sub of subs) entries(sub.agent, sub)
|
|
984
|
+
for (const sub of atInstance) entries(sub.agent, sub, instance)
|
|
985
|
+
const sends = []
|
|
986
|
+
for (const [agent, agentEntries] of byAgent) {
|
|
987
|
+
sends.push(this._withdrawAt(agent, agentEntries))
|
|
988
|
+
}
|
|
989
|
+
await Promise.allSettled(sends)
|
|
990
|
+
return true
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Withdraws subscriptions at their agent: at once when the agent is online
|
|
995
|
+
* and this client connected, else as soon as both are again - an agent
|
|
996
|
+
* that only lost its connection still holds them.
|
|
997
|
+
*
|
|
998
|
+
* @param {String} agent
|
|
999
|
+
* @param {Array.<Object>} entries `{ sub, context }`, context (an
|
|
1000
|
+
* instance) for a withdrawal at one instance only
|
|
1001
|
+
*/
|
|
1002
|
+
_withdrawAt (agent, entries) {
|
|
1003
|
+
const info = this._agents[agent]
|
|
1004
|
+
if (!this._isConnected() || !info || info.status !== 'online') {
|
|
1005
|
+
const pending = this._pendingWithdrawals.get(agent) || []
|
|
1006
|
+
pending.push(...entries)
|
|
1007
|
+
this._pendingWithdrawals.set(agent, pending)
|
|
1008
|
+
return Promise.resolve()
|
|
1009
|
+
}
|
|
1010
|
+
if (info.v >= 4) return this._sendUnsubscribe(agent, entries)
|
|
1011
|
+
// an older agent cannot withdraw at one instance: local only
|
|
1012
|
+
return Promise.allSettled(
|
|
1013
|
+
entries
|
|
1014
|
+
.filter(({ context }) => context === undefined)
|
|
1015
|
+
.map(({ sub }) => this._sendLegacyRemoval(sub))
|
|
1016
|
+
)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// sends the withdrawals an agent missed, once it and this client are online
|
|
1020
|
+
_flushWithdrawals (agent) {
|
|
1021
|
+
const pending = this._pendingWithdrawals.get(agent)
|
|
1022
|
+
const info = this._agents[agent]
|
|
1023
|
+
if (!pending || !this._isConnected() || !info || info.status !== 'online') {
|
|
1024
|
+
return
|
|
1025
|
+
}
|
|
1026
|
+
this._pendingWithdrawals.delete(agent)
|
|
1027
|
+
this._withdrawAt(agent, pending)
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* An agent went offline: a loss, not an end. Its subscriptions are lost
|
|
1032
|
+
* until it serves again; an agent of protocol 4 gets them declared again
|
|
1033
|
+
* then (the loss heals - one that kept them changes nothing). Towards an
|
|
1034
|
+
* older agent a declaration again would register the handler twice: its
|
|
1035
|
+
* subscriptions are dropped, as they always were.
|
|
1036
|
+
*/
|
|
1037
|
+
_loseAgent (agent, v) {
|
|
1038
|
+
if (!(v >= 4)) {
|
|
1039
|
+
this._redeclare.delete(agent)
|
|
1040
|
+
this._clearCachedSubscriptions({ lostAgent: agent })
|
|
1041
|
+
return
|
|
1042
|
+
}
|
|
1043
|
+
this._redeclare.add(agent)
|
|
1044
|
+
for (const sub of Object.values(this._cachedSubscriptions)) {
|
|
1045
|
+
if (sub.agent !== agent || sub.lost) continue
|
|
1046
|
+
sub.lost = true
|
|
1047
|
+
sub.confirmed = false
|
|
1048
|
+
this.emit('lost', sub.id, 'agentOffline', this._subscriptionInfo(sub))
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Lists the subscriptions this client holds.
|
|
1054
|
+
*
|
|
1055
|
+
* @param {Object} [filter]
|
|
1056
|
+
* @param {Function|Object} [filter.callback] Only the subscriptions of this
|
|
1057
|
+
* callback, or of this `{ emitter, event }` contract
|
|
1058
|
+
* @returns {Array.<Object>} `{ id, agent, className, instance,
|
|
1059
|
+
* functionName, event, callAll, confirmed, lost }` per subscription;
|
|
1060
|
+
* `confirmed` once a protocol-4 agent answered that it holds it, `lost`
|
|
1061
|
+
* while its agent is offline (it is declared again once the agent is back)
|
|
1062
|
+
*/
|
|
1063
|
+
getSubscriptions ({ callback } = {}) {
|
|
1064
|
+
if (callback && this._isEmitter(callback)) {
|
|
1065
|
+
callback = this._emitterCallback(callback, { readOnly: true })
|
|
1066
|
+
if (!callback) return []
|
|
1067
|
+
}
|
|
1068
|
+
return Object.values(this._cachedSubscriptions)
|
|
1069
|
+
.filter(sub => !callback || sub.callback === callback)
|
|
1070
|
+
.map(sub => ({
|
|
1071
|
+
id: sub.id,
|
|
1072
|
+
...this._subscriptionInfo(sub),
|
|
1073
|
+
confirmed: sub.confirmed,
|
|
1074
|
+
lost: sub.lost
|
|
1075
|
+
}))
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Gives the subscriptions of a callback a label, which agents list with
|
|
1080
|
+
* their registrations (`__registrations__`), e.g. who listens.
|
|
1081
|
+
*
|
|
1082
|
+
* @param {Function|Object} callback The callback, or an `{ emitter, event }`
|
|
1083
|
+
* contract (labelled before or after it subscribes)
|
|
1084
|
+
* @param {String} label The label (sent with every declaration)
|
|
1085
|
+
*/
|
|
1086
|
+
setSubscriberLabel (callback, label) {
|
|
1087
|
+
if (this._isEmitter(callback)) callback = this._emitterCallback(callback)
|
|
1088
|
+
this._subscriberLabels.set(callback, label)
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Lists the registrations of an instance (or, without an instance, the
|
|
1093
|
+
* static ones of a class): function, arguments, subscriptions with their
|
|
1094
|
+
* labels, events and events per second. Needs a protocol-4 agent.
|
|
1095
|
+
*
|
|
1096
|
+
* @param {Object} options
|
|
1097
|
+
* @param {String} options.className Class name
|
|
1098
|
+
* @param {String} [options.instance] Instance name
|
|
1099
|
+
* @param {String} [options.agent] Agent name. If not provided class default is used
|
|
1100
|
+
* @returns {Promise<Array.<Object>>} The registrations
|
|
1101
|
+
*/
|
|
1102
|
+
async getRegistrations ({ className, instance, agent = this._agent } = {}) {
|
|
1103
|
+
return this._callProtocol({ className, instance, agent }, '__registrations__', [])
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Ends a registration for all its subscriptions; each subscriber learns
|
|
1108
|
+
* that it ended and by whom ('ended' event). Needs a protocol-4 agent.
|
|
1109
|
+
*
|
|
1110
|
+
* @param {Object} options
|
|
1111
|
+
* @param {String} options.className Class name
|
|
1112
|
+
* @param {String} [options.instance] Instance name (static registration without)
|
|
1113
|
+
* @param {String} [options.agent] Agent name. If not provided class default is used
|
|
1114
|
+
* @param {String} options.functionName The event function
|
|
1115
|
+
* @param {Array} [options.args] Its arguments without the handlers, as listed
|
|
1116
|
+
* @param {String} [options.by] Who ends it, told to every subscriber
|
|
1117
|
+
* @returns {Promise<Boolean>} Whether such a registration existed
|
|
1118
|
+
*/
|
|
1119
|
+
async endRegistration ({
|
|
1120
|
+
className,
|
|
1121
|
+
instance,
|
|
1122
|
+
agent = this._agent,
|
|
1123
|
+
functionName,
|
|
1124
|
+
args = [],
|
|
1125
|
+
by = null
|
|
1126
|
+
} = {}) {
|
|
1127
|
+
return this._callProtocol({ className, instance, agent }, '__end__', [
|
|
1128
|
+
functionName,
|
|
1129
|
+
args,
|
|
1130
|
+
by
|
|
1131
|
+
])
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async _callProtocol ({ className, instance, agent }, f, a) {
|
|
1135
|
+
if (!this._isConnected()) {
|
|
1136
|
+
throw new Error('Client is not connected')
|
|
1137
|
+
}
|
|
1138
|
+
if (instance && (!agent || agent === '*' || !className)) {
|
|
1139
|
+
const data = await this._getInstanceData(instance, { agent, className })
|
|
1140
|
+
agent = data.agent
|
|
1141
|
+
className = data.className
|
|
1142
|
+
}
|
|
1143
|
+
if (!agent || agent === '*') throw new Error('Agent must be specified')
|
|
749
1144
|
await this._waitUntilClassIsOnline(agent, className)
|
|
1145
|
+
if (!(this._agents[agent].v >= 4)) {
|
|
1146
|
+
throw new Error(`Agent "${agent}" does not speak protocol 4`)
|
|
1147
|
+
}
|
|
1148
|
+
const json = {
|
|
1149
|
+
c: instance || className,
|
|
1150
|
+
f,
|
|
1151
|
+
a,
|
|
1152
|
+
i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
|
|
1153
|
+
s: this._vrpcConnectionId,
|
|
1154
|
+
v: VRPC_PROTOCOL_VERSION
|
|
1155
|
+
}
|
|
1156
|
+
const topic = `${this._domain}/${agent}/${className}/${instance || '__static__'}/${f}`
|
|
750
1157
|
this._mqttPublish(topic, JSON.stringify(json))
|
|
751
1158
|
return this._handleAgentAnswer(json, agent)
|
|
752
1159
|
}
|
|
753
1160
|
|
|
1161
|
+
// withdraws by id on a protocol-4 agent, wherever it holds them (or, with
|
|
1162
|
+
// a context, at that instance only)
|
|
1163
|
+
async _sendUnsubscribe (agent, entries) {
|
|
1164
|
+
const className = entries[0].sub.className
|
|
1165
|
+
const json = {
|
|
1166
|
+
c: className,
|
|
1167
|
+
f: '__unsubscribe__',
|
|
1168
|
+
a: entries.map(({ sub, context }) =>
|
|
1169
|
+
context === undefined ? sub.id : { id: sub.id, context }
|
|
1170
|
+
),
|
|
1171
|
+
i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
|
|
1172
|
+
s: this._vrpcConnectionId,
|
|
1173
|
+
v: VRPC_PROTOCOL_VERSION
|
|
1174
|
+
}
|
|
1175
|
+
const topic = `${this._domain}/${agent}/${className}/__static__/__unsubscribe__`
|
|
1176
|
+
this._mqttPublish(topic, JSON.stringify(json))
|
|
1177
|
+
try {
|
|
1178
|
+
await this._handleAgentAnswer(json, agent)
|
|
1179
|
+
} catch (err) {
|
|
1180
|
+
this._log.debug(`Withdrawing subscriptions failed: ${err.message}`)
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// an agent before protocol 4: removeListener, or the class's off twin
|
|
1185
|
+
async _sendLegacyRemoval (sub) {
|
|
1186
|
+
if (sub.callAll || !sub.instance) return
|
|
1187
|
+
let f
|
|
1188
|
+
let a
|
|
1189
|
+
if (sub.functionName === 'on' || sub.functionName === 'addListener') {
|
|
1190
|
+
f = 'removeListener'
|
|
1191
|
+
a = [sub.event, sub.id]
|
|
1192
|
+
} else {
|
|
1193
|
+
const twin = sub.functionName.replace(/^on(?=[A-Z])/, 'off')
|
|
1194
|
+
const info =
|
|
1195
|
+
this._agents[sub.agent] &&
|
|
1196
|
+
this._agents[sub.agent].classes[sub.className]
|
|
1197
|
+
const functions = info ? info.memberFunctions.map(this._stripSignature) : []
|
|
1198
|
+
if (twin === sub.functionName || !functions.includes(twin)) return
|
|
1199
|
+
if (!sub.declaration) return
|
|
1200
|
+
f = twin
|
|
1201
|
+
a = sub.declaration.a
|
|
1202
|
+
}
|
|
1203
|
+
const json = {
|
|
1204
|
+
c: sub.instance,
|
|
1205
|
+
f,
|
|
1206
|
+
a,
|
|
1207
|
+
i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
|
|
1208
|
+
s: this._vrpcConnectionId,
|
|
1209
|
+
v: VRPC_PROTOCOL_VERSION
|
|
1210
|
+
}
|
|
1211
|
+
const topic = `${this._domain}/${sub.agent}/${sub.className}/${sub.instance}/${f}`
|
|
1212
|
+
this._mqttPublish(topic, JSON.stringify(json))
|
|
1213
|
+
try {
|
|
1214
|
+
await this._handleAgentAnswer(json, sub.agent)
|
|
1215
|
+
} catch (err) {
|
|
1216
|
+
this._log.debug(`Removing a subscription failed: ${err.message}`)
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
754
1220
|
/**
|
|
755
1221
|
* Retrieves all information about the currently available components.
|
|
756
1222
|
*
|
|
@@ -906,8 +1372,10 @@ class VrpcClient extends EventEmitter {
|
|
|
906
1372
|
this._mqttPublish(agentTopic, null, { retain: true })
|
|
907
1373
|
const classes = this.getAvailableClasses({ agent, mustBeOnline: false })
|
|
908
1374
|
for (const className of classes) {
|
|
909
|
-
const
|
|
910
|
-
|
|
1375
|
+
for (const info of ['__classInfo__', '__classInfoConcise__']) {
|
|
1376
|
+
const classTopic = `${this._domain}/${agent}/${className}/${info}`
|
|
1377
|
+
this._mqttPublish(classTopic, null, { retain: true })
|
|
1378
|
+
}
|
|
911
1379
|
}
|
|
912
1380
|
// Synchronize local cache
|
|
913
1381
|
delete this._agents[agent]
|
|
@@ -934,6 +1402,11 @@ class VrpcClient extends EventEmitter {
|
|
|
934
1402
|
)
|
|
935
1403
|
this._eventEmitter.removeAllListeners()
|
|
936
1404
|
this.removeAllListeners()
|
|
1405
|
+
// the agents withdraw what this connection held: nothing to declare
|
|
1406
|
+
// again, nothing left to withdraw
|
|
1407
|
+
this._cachedSubscriptions = {}
|
|
1408
|
+
this._redeclare.clear()
|
|
1409
|
+
this._pendingWithdrawals.clear()
|
|
937
1410
|
const client = this._client
|
|
938
1411
|
this._client = null // Prevent race conditions during shutdown
|
|
939
1412
|
return new Promise(resolve =>
|
|
@@ -966,13 +1439,6 @@ class VrpcClient extends EventEmitter {
|
|
|
966
1439
|
return this._client && this._client.connected
|
|
967
1440
|
}
|
|
968
1441
|
|
|
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
1442
|
|
|
977
1443
|
_generateUserName () {
|
|
978
1444
|
return `${this._domain}:client@${os.hostname()}-${os.platform()}-js`
|
|
@@ -1045,31 +1511,36 @@ class VrpcClient extends EventEmitter {
|
|
|
1045
1511
|
client.subscribe(
|
|
1046
1512
|
topic,
|
|
1047
1513
|
{ qos: this._qos, ...options },
|
|
1048
|
-
(
|
|
1049
|
-
|
|
1514
|
+
(subscribeError, subs) => {
|
|
1515
|
+
const { refused, granted, error } = SubscribeRetrier.outcome(
|
|
1516
|
+
subscribeError,
|
|
1517
|
+
subs
|
|
1518
|
+
)
|
|
1519
|
+
if (error) {
|
|
1050
1520
|
this._log.warn(
|
|
1051
|
-
`Could not subscribe to topic(s): '${topic}', because: ${
|
|
1521
|
+
`Could not subscribe to topic(s): '${topic}', because: ${error.message}`
|
|
1052
1522
|
)
|
|
1053
1523
|
if (
|
|
1054
|
-
|
|
1524
|
+
error.message === 'client disconnecting' &&
|
|
1055
1525
|
this._isRevivable(client)
|
|
1056
1526
|
) {
|
|
1057
1527
|
// This most certainly relates to a bug in MQTT.js (#1284)
|
|
1058
|
-
const msg = `Failed to subscribe, because: ${
|
|
1528
|
+
const msg = `Failed to subscribe, because: ${error.message}`
|
|
1059
1529
|
this._log.error(msg)
|
|
1060
|
-
this.emit('error',
|
|
1530
|
+
this.emit('error', error)
|
|
1061
1531
|
this._log.info('Forcing reconnect now...')
|
|
1062
1532
|
client.disconnecting = false
|
|
1063
1533
|
client.reconnect()
|
|
1064
1534
|
reject(new Error(msg))
|
|
1065
|
-
} else
|
|
1066
|
-
|
|
1535
|
+
} else {
|
|
1536
|
+
// the connection broke under the subscribe: the next connect
|
|
1537
|
+
// subscribes everything afresh
|
|
1538
|
+
reject(error)
|
|
1067
1539
|
}
|
|
1068
1540
|
} else {
|
|
1541
|
+
let err
|
|
1069
1542
|
const topicArray = Array.isArray(topic) ? topic : [topic]
|
|
1070
|
-
const erroneousGranted =
|
|
1071
|
-
.filter(x => x.qos === 128)
|
|
1072
|
-
.map(x => x.topic)
|
|
1543
|
+
const erroneousGranted = refused
|
|
1073
1544
|
if (erroneousGranted.length > 0) {
|
|
1074
1545
|
err = new Error(
|
|
1075
1546
|
`Could not subscribe all ${topicArray.length} topic(s) but got error qos=128 on following ${erroneousGranted.length} topic(s): ${erroneousGranted}`
|
|
@@ -1080,9 +1551,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1080
1551
|
this.emit('error', err)
|
|
1081
1552
|
this._subscribeRetrier.schedule(erroneousGranted, options)
|
|
1082
1553
|
}
|
|
1083
|
-
this._subscribeRetrier.granted(
|
|
1084
|
-
granted.filter(x => x.qos !== 128).map(x => x.topic)
|
|
1085
|
-
)
|
|
1554
|
+
this._subscribeRetrier.granted(granted.map(x => x.topic))
|
|
1086
1555
|
const reducedQos = granted.filter(x => x.qos < this._qos)
|
|
1087
1556
|
if (reducedQos.length > 0) {
|
|
1088
1557
|
err = new Error(
|
|
@@ -1099,7 +1568,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1099
1568
|
this._log.warn(err.message)
|
|
1100
1569
|
this.emit('error', err)
|
|
1101
1570
|
}
|
|
1102
|
-
if (granted.length === 0) {
|
|
1571
|
+
if (granted.length === 0 && refused.length === 0) {
|
|
1103
1572
|
this._log.debug(`Already subscribed to: ${topic}`)
|
|
1104
1573
|
}
|
|
1105
1574
|
resolve()
|
|
@@ -1160,15 +1629,19 @@ class VrpcClient extends EventEmitter {
|
|
|
1160
1629
|
if (this._hasClassOnline(agent, className)) return
|
|
1161
1630
|
return new Promise((resolve, reject) => {
|
|
1162
1631
|
let timer = null
|
|
1632
|
+
// the class info and the agent info travel on topics of their own and
|
|
1633
|
+
// may come in either order: each of them can complete the picture
|
|
1163
1634
|
const checkClass = () => {
|
|
1164
1635
|
if (this._hasClassOnline(agent, className)) {
|
|
1165
1636
|
if (timer) clearTimeout(timer)
|
|
1166
1637
|
this.removeListener('class', checkClass)
|
|
1638
|
+
this.removeListener('agent', checkClass)
|
|
1167
1639
|
resolve()
|
|
1168
1640
|
}
|
|
1169
1641
|
}
|
|
1170
1642
|
timer = setTimeout(() => {
|
|
1171
1643
|
this.removeListener('class', checkClass)
|
|
1644
|
+
this.removeListener('agent', checkClass)
|
|
1172
1645
|
reject(
|
|
1173
1646
|
new Error(
|
|
1174
1647
|
`Connection to class "${className}" on agent "${agent}" and domain "${this._domain}" timed out (> ${this._timeout} ms)`
|
|
@@ -1176,6 +1649,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1176
1649
|
)
|
|
1177
1650
|
}, this._timeout)
|
|
1178
1651
|
this.on('class', checkClass)
|
|
1652
|
+
this.on('agent', checkClass)
|
|
1179
1653
|
})
|
|
1180
1654
|
}
|
|
1181
1655
|
|
|
@@ -1214,23 +1688,18 @@ class VrpcClient extends EventEmitter {
|
|
|
1214
1688
|
throw new Error('VRPC does not support removing all listeners')
|
|
1215
1689
|
}
|
|
1216
1690
|
|
|
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
|
-
})
|
|
1691
|
+
// this client's subscriptions of that event on this instance end;
|
|
1692
|
+
// a protocol-4 agent withdraws exactly those, never touching the
|
|
1693
|
+
// emitter's other listeners
|
|
1694
|
+
Object.values(this._cachedSubscriptions)
|
|
1695
|
+
.filter(
|
|
1696
|
+
sub =>
|
|
1697
|
+
sub.agent === agent &&
|
|
1698
|
+
sub.className === className &&
|
|
1699
|
+
sub.instance === instance &&
|
|
1700
|
+
sub.event === eventName
|
|
1701
|
+
)
|
|
1702
|
+
.forEach(sub => this._dropSubscription(sub))
|
|
1234
1703
|
try {
|
|
1235
1704
|
const json = {
|
|
1236
1705
|
c: instance,
|
|
@@ -1276,6 +1745,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1276
1745
|
s: this._vrpcConnectionId,
|
|
1277
1746
|
v: VRPC_PROTOCOL_VERSION
|
|
1278
1747
|
}
|
|
1748
|
+
this._declare(json, `${targetTopic}/${functionName}`)
|
|
1279
1749
|
this._mqttPublish(
|
|
1280
1750
|
`${targetTopic}/${functionName}`,
|
|
1281
1751
|
JSON.stringify(json)
|
|
@@ -1311,6 +1781,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1311
1781
|
s: this._vrpcConnectionId,
|
|
1312
1782
|
v: VRPC_PROTOCOL_VERSION
|
|
1313
1783
|
}
|
|
1784
|
+
this._declare(json, `${targetTopic}/${functionName}`)
|
|
1314
1785
|
this._mqttPublish(
|
|
1315
1786
|
`${targetTopic}/${functionName}`,
|
|
1316
1787
|
JSON.stringify(json)
|
|
@@ -1322,22 +1793,37 @@ class VrpcClient extends EventEmitter {
|
|
|
1322
1793
|
)
|
|
1323
1794
|
}
|
|
1324
1795
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1796
|
+
// withdraws this client's subscriptions of a function on this instance
|
|
1797
|
+
proxy.vrpcOff = async functionName => {
|
|
1798
|
+
const ids = Object.values(this._cachedSubscriptions)
|
|
1799
|
+
.filter(
|
|
1800
|
+
sub =>
|
|
1801
|
+
sub.agent === agent &&
|
|
1802
|
+
sub.className === className &&
|
|
1803
|
+
sub.instance === instance &&
|
|
1804
|
+
sub.functionName === functionName
|
|
1805
|
+
)
|
|
1806
|
+
.map(sub => sub.id)
|
|
1807
|
+
if (ids.length === 0) return false
|
|
1808
|
+
return this.unsubscribe(ids)
|
|
1329
1809
|
}
|
|
1330
1810
|
}
|
|
1331
1811
|
return proxy
|
|
1332
1812
|
}
|
|
1333
1813
|
|
|
1334
|
-
async _handleAgentAnswer ({ i, c, f }, agent) {
|
|
1814
|
+
async _handleAgentAnswer ({ i, c, f, a }, agent) {
|
|
1335
1815
|
return new Promise((resolve, reject) => {
|
|
1336
1816
|
const msg = `Function call "${c}::${f}()" on agent "${agent}" timed out (> ${this._timeout} ms)`
|
|
1337
1817
|
const timer = setTimeout(() => {
|
|
1338
1818
|
this._eventEmitter.removeAllListeners(i)
|
|
1339
1819
|
reject(new Error(msg))
|
|
1340
1820
|
}, this._timeout)
|
|
1821
|
+
// a protocol-4 agent that answers a declaring call without an error
|
|
1822
|
+
// holds every subscription the call carried
|
|
1823
|
+
const confirm = () => {
|
|
1824
|
+
const info = this._agents[agent]
|
|
1825
|
+
if (info && info.v >= 4) this._confirmSubscriptions(a)
|
|
1826
|
+
}
|
|
1341
1827
|
this._eventEmitter.once(i, data => {
|
|
1342
1828
|
clearTimeout(timer)
|
|
1343
1829
|
if (data.e) {
|
|
@@ -1356,10 +1842,12 @@ class VrpcClient extends EventEmitter {
|
|
|
1356
1842
|
new Error(`[vrpc ${agent}-${c}-${f}]: ${message}`, { cause })
|
|
1357
1843
|
)
|
|
1358
1844
|
} else {
|
|
1845
|
+
confirm()
|
|
1359
1846
|
resolve(promiseData.r)
|
|
1360
1847
|
}
|
|
1361
1848
|
})
|
|
1362
1849
|
} else {
|
|
1850
|
+
confirm()
|
|
1363
1851
|
resolve(ret)
|
|
1364
1852
|
}
|
|
1365
1853
|
}
|
|
@@ -1396,51 +1884,114 @@ class VrpcClient extends EventEmitter {
|
|
|
1396
1884
|
})
|
|
1397
1885
|
}
|
|
1398
1886
|
|
|
1887
|
+
/**
|
|
1888
|
+
* Whether a function is an event function: declared `@returns
|
|
1889
|
+
* {Registration}` (when the class info carries meta data), else named `on`
|
|
1890
|
+
* followed by a capital letter. The prefixes `notify`, `monitor` and
|
|
1891
|
+
* `signal` still count (deprecated, gone in 4.0).
|
|
1892
|
+
*/
|
|
1893
|
+
_isEventFunction (agent, className, functionName) {
|
|
1894
|
+
const info =
|
|
1895
|
+
this._agents[agent] &&
|
|
1896
|
+
this._agents[agent].classes &&
|
|
1897
|
+
this._agents[agent].classes[className]
|
|
1898
|
+
const meta = info && info.meta && info.meta[functionName]
|
|
1899
|
+
const type = meta && meta.ret && meta.ret.type
|
|
1900
|
+
if (typeof type === 'string' && DECLARED_REGISTRATION.test(type.trim())) {
|
|
1901
|
+
return true
|
|
1902
|
+
}
|
|
1903
|
+
if (/^on[A-Z]/.test(functionName)) return true
|
|
1904
|
+
return /^(notify|monitor|signal)/.test(functionName)
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1399
1907
|
async _wrapArguments ({
|
|
1400
1908
|
agent,
|
|
1401
1909
|
className,
|
|
1402
1910
|
instance,
|
|
1403
1911
|
proxyId,
|
|
1404
1912
|
functionName,
|
|
1405
|
-
args
|
|
1913
|
+
args,
|
|
1914
|
+
isCallAll = false,
|
|
1915
|
+
selection
|
|
1406
1916
|
}) {
|
|
1407
1917
|
const wrapped = []
|
|
1408
1918
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1919
|
+
const isEventFunction = this._isEventFunction(
|
|
1920
|
+
agent,
|
|
1921
|
+
className,
|
|
1922
|
+
functionName
|
|
1923
|
+
)
|
|
1924
|
+
// continuous removers (legacy): withdraw the callback's subscription
|
|
1411
1925
|
const isContinuousRemover = /^(off|remove|unmonitor|unsignal)/.test(
|
|
1412
1926
|
functionName
|
|
1413
1927
|
)
|
|
1928
|
+
const isEmitterRemover =
|
|
1929
|
+
(functionName === 'off' || functionName === 'removeListener') &&
|
|
1930
|
+
typeof args[0] === 'string'
|
|
1414
1931
|
|
|
1415
1932
|
for (const [i, x] of args.entries()) {
|
|
1416
|
-
|
|
1417
|
-
|
|
1933
|
+
const isEmitterContract = this._isEmitter(x)
|
|
1934
|
+
if (this._isFunction(x) || isEmitterContract) {
|
|
1935
|
+
const removes =
|
|
1936
|
+
!isEventFunction &&
|
|
1937
|
+
((isEmitterRemover && i === 1) || isContinuousRemover)
|
|
1938
|
+
const callback = isEmitterContract
|
|
1939
|
+
? this._emitterCallback(x, { readOnly: removes })
|
|
1940
|
+
: x
|
|
1418
1941
|
if (
|
|
1419
|
-
|
|
1420
|
-
(
|
|
1421
|
-
|
|
1942
|
+
!removes &&
|
|
1943
|
+
(isEmitterContract ||
|
|
1944
|
+
functionName === 'vrpcOn' ||
|
|
1945
|
+
((functionName === 'on' || functionName === 'addListener') &&
|
|
1946
|
+
i === 1 &&
|
|
1947
|
+
typeof args[0] === 'string') ||
|
|
1948
|
+
isEventFunction)
|
|
1422
1949
|
) {
|
|
1423
1950
|
const event = typeof args[0] === 'string' ? args[0] : functionName
|
|
1951
|
+
// vrpcOn names the event function first
|
|
1952
|
+
const isVrpcOn = functionName === 'vrpcOn'
|
|
1424
1953
|
const id = await this._onRemoteEvent({
|
|
1425
1954
|
agent,
|
|
1426
1955
|
className,
|
|
1427
1956
|
instance,
|
|
1428
1957
|
event,
|
|
1429
|
-
callback
|
|
1958
|
+
callback,
|
|
1959
|
+
functionName: isVrpcOn ? args[0] : functionName,
|
|
1960
|
+
args: isVrpcOn ? args.slice(1) : args,
|
|
1961
|
+
position: isVrpcOn ? i - 1 : i,
|
|
1962
|
+
isCallAll,
|
|
1963
|
+
selection
|
|
1430
1964
|
})
|
|
1431
1965
|
wrapped.push(id)
|
|
1432
1966
|
continue
|
|
1433
|
-
} else if (
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1967
|
+
} else if (removes) {
|
|
1968
|
+
const subs = callback
|
|
1969
|
+
? this._offRemoteEvent({
|
|
1970
|
+
agent,
|
|
1971
|
+
className,
|
|
1972
|
+
instance,
|
|
1973
|
+
isCallAll,
|
|
1974
|
+
selection,
|
|
1975
|
+
functionName,
|
|
1976
|
+
args,
|
|
1977
|
+
position: i,
|
|
1978
|
+
callback
|
|
1979
|
+
})
|
|
1980
|
+
: []
|
|
1981
|
+
if (subs.length > 0) {
|
|
1982
|
+
const info = this._agents[agent]
|
|
1983
|
+
if (info && info.v >= 4) {
|
|
1984
|
+
// protocol 4 withdraws by id and never calls the class's
|
|
1985
|
+
// remover: handled here
|
|
1986
|
+
await this._withdrawAt(agent, subs.map(sub => ({ sub })))
|
|
1987
|
+
return null
|
|
1988
|
+
}
|
|
1989
|
+
// an agent before protocol 4 knows only the remover call, which
|
|
1990
|
+
// carries the id (further subscriptions end locally)
|
|
1991
|
+
wrapped.push(subs[0].id)
|
|
1992
|
+
continue
|
|
1993
|
+
}
|
|
1994
|
+
if (functionName === 'off' || functionName === 'removeListener') {
|
|
1444
1995
|
// Nothing of ours to remove - the subscription was already
|
|
1445
1996
|
// dropped (the instance or its agent went away and the cache
|
|
1446
1997
|
// was cleared, or the listener was never registered). Sending
|
|
@@ -1448,43 +1999,28 @@ class VrpcClient extends EventEmitter {
|
|
|
1448
1999
|
// its EventEmitter rejects. Handled locally: nothing to do.
|
|
1449
2000
|
return null
|
|
1450
2001
|
}
|
|
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)
|
|
2002
|
+
// no subscription of this callback: an { emitter, event }
|
|
2003
|
+
// contract stays empty, a function is a plain callback after all
|
|
2004
|
+
// (removeItem(id, done))
|
|
2005
|
+
if (isEmitterContract) {
|
|
2006
|
+
wrapped.push(null)
|
|
2007
|
+
continue
|
|
1475
2008
|
}
|
|
1476
|
-
} else {
|
|
1477
|
-
this._emitterListener.set(emitter, new Map([[event, callback]]))
|
|
1478
2009
|
}
|
|
1479
|
-
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
event,
|
|
1485
|
-
callback
|
|
1486
|
-
})
|
|
2010
|
+
// Regular function callback (can be static or member function)
|
|
2011
|
+
const remoteId = proxyId || `${agent}-${className}`
|
|
2012
|
+
const id = `__f__${remoteId}-${functionName}-${i}-${
|
|
2013
|
+
this._invokeId++ % Number.MAX_SAFE_INTEGER
|
|
2014
|
+
}`
|
|
1487
2015
|
wrapped.push(id)
|
|
2016
|
+
// a callback the agent could not encode is told, never called (CALL-10)
|
|
2017
|
+
this._eventEmitter.once(id, ({ a, e }) => {
|
|
2018
|
+
if (e) {
|
|
2019
|
+
this.emit('undelivered', id, e, { agent, className, instance, functionName })
|
|
2020
|
+
return
|
|
2021
|
+
}
|
|
2022
|
+
callback.apply(null, a)
|
|
2023
|
+
})
|
|
1488
2024
|
continue
|
|
1489
2025
|
}
|
|
1490
2026
|
// default behavior is to not touch
|
|
@@ -1493,98 +2029,378 @@ class VrpcClient extends EventEmitter {
|
|
|
1493
2029
|
return wrapped
|
|
1494
2030
|
}
|
|
1495
2031
|
|
|
1496
|
-
|
|
1497
|
-
|
|
2032
|
+
// the one callback standing for an { emitter, event } contract
|
|
2033
|
+
_emitterCallback ({ emitter, event }, { readOnly = false } = {}) {
|
|
2034
|
+
let eventMap = this._emitterListener.get(emitter)
|
|
2035
|
+
if (eventMap && eventMap.has(event)) return eventMap.get(event)
|
|
2036
|
+
if (readOnly) return null
|
|
2037
|
+
const callback = (...args) => emitter.emit(event, ...args)
|
|
2038
|
+
if (!eventMap) {
|
|
2039
|
+
eventMap = new Map()
|
|
2040
|
+
this._emitterListener.set(emitter, eventMap)
|
|
2041
|
+
}
|
|
2042
|
+
eventMap.set(event, callback)
|
|
2043
|
+
return callback
|
|
2044
|
+
}
|
|
1498
2045
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
2046
|
+
/**
|
|
2047
|
+
* What a subscription is to the agent: its function (`addListener` is
|
|
2048
|
+
* `on`) and arguments compared by value, the callback's own position
|
|
2049
|
+
* named and other callbacks marked. Two subscriptions of one callback
|
|
2050
|
+
* differ in it whenever the agent keeps them apart, so their ids differ:
|
|
2051
|
+
* an id names one registration per instance.
|
|
2052
|
+
*/
|
|
2053
|
+
_subscriptionKey (functionName, args, position) {
|
|
2054
|
+
const func = functionName === 'addListener' ? 'on' : functionName
|
|
2055
|
+
const marked = args.map((x, i) => {
|
|
2056
|
+
if (i === position) return '\u0000vrpc-self'
|
|
2057
|
+
if (this._isFunction(x) || this._isEmitter(x)) return '\u0000vrpc-callback'
|
|
2058
|
+
return x
|
|
2059
|
+
})
|
|
2060
|
+
return `${func}\u0000${VrpcClient._canonical(marked)}`
|
|
2061
|
+
}
|
|
1504
2062
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
const
|
|
2063
|
+
// arguments compared by value: equal JSON once object keys are sorted
|
|
2064
|
+
static _canonical (value) {
|
|
2065
|
+
const sortKeys = x => {
|
|
2066
|
+
if (Array.isArray(x)) return x.map(sortKeys)
|
|
2067
|
+
if (x && typeof x === 'object' && x.constructor === Object) {
|
|
2068
|
+
const sorted = {}
|
|
2069
|
+
for (const key of Object.keys(x).sort()) sorted[key] = sortKeys(x[key])
|
|
2070
|
+
return sorted
|
|
2071
|
+
}
|
|
2072
|
+
return x
|
|
2073
|
+
}
|
|
2074
|
+
return JSON.stringify(sortKeys(value))
|
|
2075
|
+
}
|
|
1508
2076
|
|
|
1509
|
-
|
|
2077
|
+
// the event function a remover undoes: off/removeListener -> on,
|
|
2078
|
+
// offValue -> onValue, unmonitorX -> monitorX
|
|
2079
|
+
static _twinOf (functionName) {
|
|
2080
|
+
if (functionName === 'off' || functionName === 'removeListener') return 'on'
|
|
2081
|
+
return functionName.replace(/^(off|un)(?=[A-Za-z])/, m =>
|
|
2082
|
+
m === 'off' ? 'on' : ''
|
|
2083
|
+
)
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// what a subscription is, but its callback: target, the instances a
|
|
2087
|
+
// callAll selects, and subscription key
|
|
2088
|
+
static _identity ({ agent, className, instance, isCallAll, selection, key }) {
|
|
2089
|
+
const target = [agent, className, instance || null, !!isCallAll, key]
|
|
2090
|
+
return JSON.stringify(selection === undefined ? target : [...target, selection])
|
|
2091
|
+
}
|
|
1510
2092
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
2093
|
+
/**
|
|
2094
|
+
* The topic a subscription's events arrive on; its id is `__e__<topic>`.
|
|
2095
|
+
* One per identity (target, function, arguments and callback), counted,
|
|
2096
|
+
* not hashed: two identities never share one. An identity keeps its
|
|
2097
|
+
* topic while its callback lives, so a subscription declared again after
|
|
2098
|
+
* a loss has the id an agent may still hold (which then changes
|
|
2099
|
+
* nothing); a withdrawn one gets a fresh id when declared anew (see
|
|
2100
|
+
* _dropSubscription), so a late withdrawal never meets the new one.
|
|
2101
|
+
*/
|
|
2102
|
+
_subscriptionTopic (callback, identity, { readOnly = false } = {}) {
|
|
2103
|
+
let topics = this._subscriptionTopics.get(callback)
|
|
2104
|
+
if (!topics) {
|
|
2105
|
+
if (readOnly) return null
|
|
2106
|
+
topics = new Map()
|
|
2107
|
+
this._subscriptionTopics.set(callback, topics)
|
|
1514
2108
|
}
|
|
2109
|
+
let topic = topics.get(identity)
|
|
2110
|
+
if (!topic && !readOnly) {
|
|
2111
|
+
// routed by the client's single event inbox wildcard
|
|
2112
|
+
topic = `${this._vrpcConnectionId}/s${(this._subscriptionCount++).toString(36)}`
|
|
2113
|
+
topics.set(identity, topic)
|
|
2114
|
+
}
|
|
2115
|
+
return topic || null
|
|
2116
|
+
}
|
|
1515
2117
|
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
2118
|
+
/**
|
|
2119
|
+
* A subscription: one per target, function, arguments and callback, held
|
|
2120
|
+
* once however often it is declared (the agent keeps it as a set member,
|
|
2121
|
+
* too).
|
|
2122
|
+
*/
|
|
2123
|
+
async _onRemoteEvent ({
|
|
2124
|
+
agent,
|
|
2125
|
+
className,
|
|
2126
|
+
instance,
|
|
2127
|
+
event,
|
|
2128
|
+
callback,
|
|
2129
|
+
functionName,
|
|
2130
|
+
args,
|
|
2131
|
+
position,
|
|
2132
|
+
isCallAll = false,
|
|
2133
|
+
selection
|
|
2134
|
+
}) {
|
|
2135
|
+
const identity = VrpcClient._identity({
|
|
2136
|
+
agent,
|
|
2137
|
+
className,
|
|
2138
|
+
instance,
|
|
2139
|
+
isCallAll,
|
|
2140
|
+
selection,
|
|
2141
|
+
key: this._subscriptionKey(functionName, args, position)
|
|
2142
|
+
})
|
|
2143
|
+
const topic = this._subscriptionTopic(callback, identity)
|
|
2144
|
+
const id = `__e__${topic}`
|
|
2145
|
+
if (this._cachedSubscriptions[topic]) return id
|
|
2146
|
+
const sub = {
|
|
2147
|
+
id,
|
|
2148
|
+
identity,
|
|
2149
|
+
agent,
|
|
2150
|
+
className,
|
|
2151
|
+
instance,
|
|
2152
|
+
event,
|
|
2153
|
+
functionName,
|
|
2154
|
+
callAll: isCallAll,
|
|
2155
|
+
// the instances a callAll selects (absent: all)
|
|
2156
|
+
selection,
|
|
2157
|
+
callback,
|
|
2158
|
+
handler: null,
|
|
2159
|
+
declaration: null,
|
|
2160
|
+
confirmed: false,
|
|
2161
|
+
// its agent went offline: declared again once it is back
|
|
2162
|
+
lost: false,
|
|
2163
|
+
// callAll: the instances it was withdrawn at (unsubscribe's instance)
|
|
2164
|
+
withdrawn: new Set()
|
|
2165
|
+
}
|
|
2166
|
+
// an ended notice carries `end` and never reaches the callback, nor
|
|
2167
|
+
// does an event the agent could not encode (`e`, EV-22); a callAll
|
|
2168
|
+
// event names its instance first
|
|
2169
|
+
sub.handler = ({ a, end, e }) => {
|
|
2170
|
+
if (end) {
|
|
2171
|
+
this._handleEnded(topic, end)
|
|
2172
|
+
return
|
|
1524
2173
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
2174
|
+
if (e) {
|
|
2175
|
+
this.emit('undelivered', id, e, this._subscriptionInfo(sub))
|
|
2176
|
+
return
|
|
2177
|
+
}
|
|
2178
|
+
if (sub.withdrawn.size > 0 && Array.isArray(a) && sub.withdrawn.has(a[0])) {
|
|
2179
|
+
return
|
|
2180
|
+
}
|
|
2181
|
+
callback.apply(null, a)
|
|
1527
2182
|
}
|
|
1528
|
-
|
|
2183
|
+
this._eventEmitter.on(id, sub.handler)
|
|
2184
|
+
this._cachedSubscriptions[topic] = sub
|
|
1529
2185
|
return id
|
|
1530
2186
|
}
|
|
1531
2187
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
2188
|
+
/**
|
|
2189
|
+
* The subscriptions a remover call names, withdrawn locally: the one its
|
|
2190
|
+
* arguments name (offValue(1, cb) undoes onValue(1, cb)), else all of the
|
|
2191
|
+
* callback's subscriptions of that function on the target (offValue(cb)
|
|
2192
|
+
* after onValue(1, cb) and onValue(2, cb)), or of any function when the
|
|
2193
|
+
* remover names none the client knows.
|
|
2194
|
+
*/
|
|
2195
|
+
_offRemoteEvent ({
|
|
2196
|
+
agent,
|
|
2197
|
+
className,
|
|
2198
|
+
instance,
|
|
2199
|
+
isCallAll,
|
|
2200
|
+
selection,
|
|
2201
|
+
functionName,
|
|
2202
|
+
args,
|
|
2203
|
+
position,
|
|
2204
|
+
callback
|
|
2205
|
+
}) {
|
|
2206
|
+
const twin = VrpcClient._twinOf(functionName)
|
|
2207
|
+
const exact = this._subscriptionTopic(
|
|
2208
|
+
callback,
|
|
2209
|
+
VrpcClient._identity({
|
|
2210
|
+
agent,
|
|
2211
|
+
className,
|
|
2212
|
+
instance,
|
|
2213
|
+
isCallAll,
|
|
2214
|
+
selection,
|
|
2215
|
+
key: this._subscriptionKey(twin, args, position)
|
|
2216
|
+
}),
|
|
2217
|
+
{ readOnly: true }
|
|
2218
|
+
)
|
|
2219
|
+
let subs = exact && this._cachedSubscriptions[exact]
|
|
2220
|
+
? [this._cachedSubscriptions[exact]]
|
|
2221
|
+
: []
|
|
2222
|
+
if (subs.length === 0) {
|
|
2223
|
+
const ofCallback = Object.values(this._cachedSubscriptions).filter(
|
|
2224
|
+
sub =>
|
|
2225
|
+
sub.callback === callback &&
|
|
2226
|
+
sub.agent === agent &&
|
|
2227
|
+
sub.className === className &&
|
|
2228
|
+
sub.instance === instance &&
|
|
2229
|
+
sub.callAll === !!isCallAll &&
|
|
2230
|
+
JSON.stringify(sub.selection) === JSON.stringify(selection)
|
|
2231
|
+
)
|
|
2232
|
+
const ofTwin = ofCallback.filter(
|
|
2233
|
+
sub =>
|
|
2234
|
+
(sub.functionName === 'addListener' ? 'on' : sub.functionName) ===
|
|
2235
|
+
twin
|
|
2236
|
+
)
|
|
2237
|
+
subs = ofTwin.length > 0 ? ofTwin : ofCallback
|
|
2238
|
+
}
|
|
2239
|
+
subs.forEach(sub => this._dropSubscription(sub))
|
|
2240
|
+
return subs
|
|
2241
|
+
}
|
|
1548
2242
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
2243
|
+
/**
|
|
2244
|
+
* Ends a subscription locally: no event reaches its callback any more. A
|
|
2245
|
+
* withdrawn or ended subscription also gives up its id (declared anew, it
|
|
2246
|
+
* gets a fresh one); a lost one keeps it (`forget: false`), so declaring
|
|
2247
|
+
* it again matches what an agent that went away and came back still
|
|
2248
|
+
* holds.
|
|
2249
|
+
*/
|
|
2250
|
+
_dropSubscription (sub, { forget = true } = {}) {
|
|
2251
|
+
this._eventEmitter.removeListener(sub.id, sub.handler)
|
|
2252
|
+
const topic = sub.id.slice(5)
|
|
2253
|
+
if (this._cachedSubscriptions[topic] === sub) {
|
|
2254
|
+
delete this._cachedSubscriptions[topic]
|
|
2255
|
+
}
|
|
2256
|
+
if (forget) {
|
|
2257
|
+
const topics = this._subscriptionTopics.get(sub.callback)
|
|
2258
|
+
if (topics && topics.get(sub.identity) === topic) {
|
|
2259
|
+
topics.delete(sub.identity)
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
1552
2263
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
2264
|
+
/**
|
|
2265
|
+
* Remembers the call that declared a subscription (to declare it again
|
|
2266
|
+
* after a reconnect) and sends the subscriber labels along.
|
|
2267
|
+
*/
|
|
2268
|
+
_declare (json, topic) {
|
|
2269
|
+
const labels = {}
|
|
2270
|
+
let labelled = false
|
|
2271
|
+
for (const arg of json.a) {
|
|
2272
|
+
if (typeof arg !== 'string' || !arg.startsWith('__e__')) continue
|
|
2273
|
+
const sub = this._cachedSubscriptions[arg.slice(5)]
|
|
2274
|
+
if (!sub) continue
|
|
2275
|
+
sub.declaration = { topic, c: json.c, f: json.f, a: json.a }
|
|
2276
|
+
// a declaration names what is wanted now: earlier withdrawals at
|
|
2277
|
+
// single instances are over
|
|
2278
|
+
sub.withdrawn.clear()
|
|
2279
|
+
const label = this._subscriberLabels.get(sub.callback)
|
|
2280
|
+
if (label !== undefined) {
|
|
2281
|
+
labels[arg] = label
|
|
2282
|
+
labelled = true
|
|
1557
2283
|
}
|
|
1558
|
-
}
|
|
1559
|
-
|
|
2284
|
+
}
|
|
2285
|
+
if (labelled) {
|
|
2286
|
+
json.l = labels
|
|
2287
|
+
for (const id of Object.keys(labels)) {
|
|
2288
|
+
const sub = this._cachedSubscriptions[id.slice(5)]
|
|
2289
|
+
if (sub && sub.declaration) sub.declaration.l = labels
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
1560
2292
|
}
|
|
1561
2293
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
2294
|
+
// marks the subscriptions among a declaring call's arguments as held
|
|
2295
|
+
_confirmSubscriptions (args) {
|
|
2296
|
+
if (!Array.isArray(args)) return
|
|
2297
|
+
for (const arg of args) {
|
|
2298
|
+
const sub =
|
|
2299
|
+
typeof arg === 'string' &&
|
|
2300
|
+
arg.startsWith('__e__') &&
|
|
2301
|
+
this._cachedSubscriptions[arg.slice(5)]
|
|
2302
|
+
if (sub) sub.confirmed = true
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
// declares every subscription held towards an agent again (protocol 4)
|
|
2307
|
+
_redeclareTo (agent) {
|
|
2308
|
+
if (!this._inboxReady || !this._redeclare.has(agent)) return
|
|
2309
|
+
const info = this._agents[agent]
|
|
2310
|
+
if (!info || info.status !== 'online') return
|
|
2311
|
+
this._redeclare.delete(agent)
|
|
2312
|
+
if (!(info.v >= 4)) return // an older agent learns nothing new
|
|
2313
|
+
const declarations = new Map()
|
|
2314
|
+
for (const sub of Object.values(this._cachedSubscriptions)) {
|
|
2315
|
+
if (sub.agent !== agent || !sub.declaration) continue
|
|
2316
|
+
const { topic, a } = sub.declaration
|
|
2317
|
+
const key = `${topic}\u0000${JSON.stringify(a)}`
|
|
2318
|
+
if (!declarations.has(key)) {
|
|
2319
|
+
declarations.set(key, { declaration: sub.declaration, subs: [] })
|
|
1575
2320
|
}
|
|
1576
|
-
|
|
2321
|
+
declarations.get(key).subs.push(sub)
|
|
2322
|
+
}
|
|
2323
|
+
for (const { declaration, subs } of declarations.values()) {
|
|
2324
|
+
const { topic, c, f, a, l } = declaration
|
|
2325
|
+
const json = {
|
|
2326
|
+
c,
|
|
2327
|
+
f,
|
|
2328
|
+
a,
|
|
2329
|
+
i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
|
|
2330
|
+
s: this._vrpcConnectionId,
|
|
2331
|
+
v: VRPC_PROTOCOL_VERSION
|
|
2332
|
+
}
|
|
2333
|
+
if (l) json.l = l
|
|
2334
|
+
this._mqttPublish(topic, JSON.stringify(json))
|
|
2335
|
+
this._handleAgentAnswer(json, agent).then(
|
|
2336
|
+
() => {
|
|
2337
|
+
const entries = []
|
|
2338
|
+
for (const sub of subs) {
|
|
2339
|
+
if (this._cachedSubscriptions[sub.id.slice(5)] !== sub) continue
|
|
2340
|
+
// a lost subscription is back
|
|
2341
|
+
if (sub.lost) {
|
|
2342
|
+
sub.lost = false
|
|
2343
|
+
this.emit('healed', sub.id, this._subscriptionInfo(sub))
|
|
2344
|
+
}
|
|
2345
|
+
// what was withdrawn at single instances stays withdrawn there
|
|
2346
|
+
for (const context of sub.withdrawn) entries.push({ sub, context })
|
|
2347
|
+
}
|
|
2348
|
+
if (entries.length > 0) return this._sendUnsubscribe(agent, entries)
|
|
2349
|
+
},
|
|
2350
|
+
err => {
|
|
2351
|
+
for (const sub of subs) {
|
|
2352
|
+
if (this._cachedSubscriptions[sub.id.slice(5)] !== sub) continue
|
|
2353
|
+
this._dropSubscription(sub, { forget: false })
|
|
2354
|
+
this.emit('lost', sub.id, err.message, this._subscriptionInfo(sub))
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
)
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
1577
2360
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
2361
|
+
_subscriptionInfo (sub) {
|
|
2362
|
+
return {
|
|
2363
|
+
agent: sub.agent,
|
|
2364
|
+
className: sub.className,
|
|
2365
|
+
instance: sub.instance,
|
|
2366
|
+
functionName: sub.functionName,
|
|
2367
|
+
event: sub.event,
|
|
2368
|
+
callAll: sub.callAll,
|
|
2369
|
+
...(sub.selection === undefined ? {} : { instances: sub.selection })
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
1582
2372
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
2373
|
+
// the agent ended a registration this subscription was part of
|
|
2374
|
+
_handleEnded (topic, end) {
|
|
2375
|
+
const sub = this._cachedSubscriptions[topic]
|
|
2376
|
+
if (!sub) return
|
|
2377
|
+
const info = {
|
|
2378
|
+
...this._subscriptionInfo(sub),
|
|
2379
|
+
context: end.context,
|
|
2380
|
+
failed: !!end.failed
|
|
2381
|
+
}
|
|
2382
|
+
// a callAll subscription lives on at the other instances
|
|
2383
|
+
if (!sub.callAll) this._dropSubscription(sub)
|
|
2384
|
+
this.emit('ended', sub.id, end.reason, end.by, info)
|
|
2385
|
+
}
|
|
1586
2386
|
|
|
1587
|
-
|
|
2387
|
+
_clearCachedSubscriptions ({ lostAgent, lostInstance, agent, className }) {
|
|
2388
|
+
const obsolete = Object.values(this._cachedSubscriptions).filter(sub => {
|
|
2389
|
+
if (lostAgent !== undefined) return sub.agent === lostAgent
|
|
2390
|
+
// Instance ids are unique per agent only: a twin of the same id on
|
|
2391
|
+
// another agent (a deployed app next to its builder) keeps its
|
|
2392
|
+
// subscriptions when this one goes
|
|
2393
|
+
return (
|
|
2394
|
+
sub.instance === lostInstance &&
|
|
2395
|
+
sub.agent === agent &&
|
|
2396
|
+
sub.className === className
|
|
2397
|
+
)
|
|
2398
|
+
})
|
|
2399
|
+
const reason = lostAgent !== undefined ? 'agentOffline' : 'instanceGone'
|
|
2400
|
+
obsolete.forEach(sub => {
|
|
2401
|
+
this._log.debug(`Clearing subscription for obsolete topic: ${sub.id}`)
|
|
2402
|
+
this._dropSubscription(sub, { forget: false })
|
|
2403
|
+
this.emit('lost', sub.id, reason, this._subscriptionInfo(sub))
|
|
1588
2404
|
})
|
|
1589
2405
|
}
|
|
1590
2406
|
|
|
@@ -1615,6 +2431,8 @@ class VrpcClient extends EventEmitter {
|
|
|
1615
2431
|
}
|
|
1616
2432
|
|
|
1617
2433
|
async _getInstanceData (instance, options) {
|
|
2434
|
+
const own = this._findOwnIsolated(instance, options)
|
|
2435
|
+
if (own) return own
|
|
1618
2436
|
const instanceData = this._getInstanceFromCache(instance, options)
|
|
1619
2437
|
if (!instanceData) {
|
|
1620
2438
|
if (options.noWait) {
|
|
@@ -1625,6 +2443,17 @@ class VrpcClient extends EventEmitter {
|
|
|
1625
2443
|
return instanceData
|
|
1626
2444
|
}
|
|
1627
2445
|
|
|
2446
|
+
// an isolated instance this connection created, when it is the one match
|
|
2447
|
+
_findOwnIsolated (instance, { agent, className } = {}) {
|
|
2448
|
+
const hits = Array.from(this._ownIsolated.values()).filter(
|
|
2449
|
+
x =>
|
|
2450
|
+
x.instance === instance &&
|
|
2451
|
+
(!agent || agent === '*' || x.agent === agent) &&
|
|
2452
|
+
(!className || x.className === className)
|
|
2453
|
+
)
|
|
2454
|
+
return hits.length === 1 ? { agent: hits[0].agent, className: hits[0].className } : null
|
|
2455
|
+
}
|
|
2456
|
+
|
|
1628
2457
|
_cachedProxy (instance, { agent, className } = {}) {
|
|
1629
2458
|
// Instance ids are unique per agent only: a cached proxy is served
|
|
1630
2459
|
// solely when it is the one match for the given agent and class - twins
|
|
@@ -1740,6 +2569,72 @@ class VrpcClient extends EventEmitter {
|
|
|
1740
2569
|
* @param {String} info.className - Class name
|
|
1741
2570
|
*/
|
|
1742
2571
|
|
|
2572
|
+
/**
|
|
2573
|
+
* Event 'ended'
|
|
2574
|
+
*
|
|
2575
|
+
* Emitted when a registration a subscription held ended for a reason the
|
|
2576
|
+
* subscriber did not cause: its source ended it, or a member did
|
|
2577
|
+
* (`endRegistration`). The subscription is gone; it never reaches the
|
|
2578
|
+
* callback. A callAll subscription lives on at its other instances.
|
|
2579
|
+
*
|
|
2580
|
+
* @event VrpcClient#ended
|
|
2581
|
+
*
|
|
2582
|
+
* @param {String} id - The subscription id
|
|
2583
|
+
* @param {String} reason - Why it ended (an error message when it failed)
|
|
2584
|
+
* @param {String|null} by - Who ended it (`'source'`, or what the member said)
|
|
2585
|
+
* @param {Object} info - The subscription (agent, className, instance,
|
|
2586
|
+
* functionName, event, callAll), `context` (the instance or class) and
|
|
2587
|
+
* `failed`
|
|
2588
|
+
*/
|
|
2589
|
+
|
|
2590
|
+
/**
|
|
2591
|
+
* Event 'undelivered'
|
|
2592
|
+
*
|
|
2593
|
+
* Emitted instead of an event, or a one-shot callback, whose arguments the
|
|
2594
|
+
* agent could not encode as JSON (a BigInt, say). The callback is not
|
|
2595
|
+
* called for it; a subscription stays and receives the next event. Agents
|
|
2596
|
+
* before protocol 4 drop such events without telling.
|
|
2597
|
+
*
|
|
2598
|
+
* @event VrpcClient#undelivered
|
|
2599
|
+
*
|
|
2600
|
+
* @param {String} id - The subscription id, or the one-shot callback's id
|
|
2601
|
+
* @param {Object} error - What the agent says, `{ message }`
|
|
2602
|
+
* @param {Object} info - The subscription (agent, className, instance,
|
|
2603
|
+
* functionName, event, callAll), or for a one-shot callback its agent,
|
|
2604
|
+
* className, instance and functionName
|
|
2605
|
+
*/
|
|
2606
|
+
|
|
2607
|
+
/**
|
|
2608
|
+
* Event 'lost'
|
|
2609
|
+
*
|
|
2610
|
+
* Emitted when a subscription is lost: its agent went offline
|
|
2611
|
+
* (`'agentOffline'`), its instance went away (`'instanceGone'`), or a
|
|
2612
|
+
* declaration again failed (the error's message). A subscription on an agent
|
|
2613
|
+
* of protocol 4 that went offline is declared again once the agent is back
|
|
2614
|
+
* ('healed'); every other lost subscription is gone, and declaring it again
|
|
2615
|
+
* is the consumer's.
|
|
2616
|
+
*
|
|
2617
|
+
* @event VrpcClient#lost
|
|
2618
|
+
*
|
|
2619
|
+
* @param {String} id - The subscription id
|
|
2620
|
+
* @param {String} reason - `'agentOffline'`, `'instanceGone'` or an error message
|
|
2621
|
+
* @param {Object} info - The subscription (agent, className, instance,
|
|
2622
|
+
* functionName, event, callAll)
|
|
2623
|
+
*/
|
|
2624
|
+
|
|
2625
|
+
/**
|
|
2626
|
+
* Event 'healed'
|
|
2627
|
+
*
|
|
2628
|
+
* Emitted when a subscription lost with its agent is back: the agent serves
|
|
2629
|
+
* again and holds it.
|
|
2630
|
+
*
|
|
2631
|
+
* @event VrpcClient#healed
|
|
2632
|
+
*
|
|
2633
|
+
* @param {String} id - The subscription id
|
|
2634
|
+
* @param {Object} info - The subscription (agent, className, instance,
|
|
2635
|
+
* functionName, event, callAll)
|
|
2636
|
+
*/
|
|
2637
|
+
|
|
1743
2638
|
/**
|
|
1744
2639
|
* Event 'connect'
|
|
1745
2640
|
*
|