vrpc 3.13.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 +5 -5
- package/index.js +7 -7
- package/package.json +25 -15
- package/vrpc/ReconnectBackoff.js +79 -0
- package/vrpc/Registrations.js +850 -0
- package/vrpc/SubscribeRetrier.js +33 -0
- package/vrpc/VrpcAdapter.js +552 -294
- package/vrpc/VrpcAgent.js +242 -154
- package/vrpc/VrpcBrowserAdapter.js +417 -285
- package/vrpc/VrpcBrowserAgent.js +239 -128
- package/vrpc/VrpcClient.js +1221 -233
- 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
|
|
@@ -40,8 +40,17 @@ const { nanoid } = require('nanoid')
|
|
|
40
40
|
const mqtt = require('mqtt')
|
|
41
41
|
const EventEmitter = require('events')
|
|
42
42
|
const SubscribeRetrier = require('./SubscribeRetrier')
|
|
43
|
+
const ReconnectBackoff = require('./ReconnectBackoff')
|
|
44
|
+
const NamePattern = require('./namePattern')
|
|
43
45
|
|
|
44
|
-
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*>?$/
|
|
50
|
+
|
|
51
|
+
// mqtt clients this client ended on purpose: the 'client disconnecting'
|
|
52
|
+
// workaround must never revive one of them
|
|
53
|
+
const ENDED = new WeakSet()
|
|
45
54
|
|
|
46
55
|
/**
|
|
47
56
|
* Client capable of creating proxy objects and remotely calling
|
|
@@ -154,21 +163,36 @@ class VrpcClient extends EventEmitter {
|
|
|
154
163
|
this._client = null
|
|
155
164
|
this._cachedSubscriptions = {}
|
|
156
165
|
this._proxies = {}
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
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
|
|
163
172
|
this._emitterListener = new WeakMap()
|
|
164
173
|
this._pendingSubscriptions = new Map()
|
|
165
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()
|
|
166
185
|
if (log === 'console') {
|
|
167
186
|
this._log = console
|
|
168
187
|
this._log.debug = () => {}
|
|
169
188
|
} else {
|
|
170
189
|
this._log = log
|
|
171
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
|
+
})
|
|
172
196
|
}
|
|
173
197
|
|
|
174
198
|
/**
|
|
@@ -220,7 +244,19 @@ class VrpcClient extends EventEmitter {
|
|
|
220
244
|
/**
|
|
221
245
|
* Actually connects to the MQTT broker.
|
|
222
246
|
*
|
|
247
|
+
* A refused connection is retried with a growing wait (doubling up to
|
|
248
|
+
* 30 s, back to the start once connected), and every refusal is emitted
|
|
249
|
+
* as 'error', so a caller can renew its credentials (see
|
|
250
|
+
* updateCredentials).
|
|
251
|
+
*
|
|
223
252
|
* @emits connected
|
|
253
|
+
* @param {Object} [options]
|
|
254
|
+
* @param {Boolean} [options.keepTrying=false] If true, a connection that
|
|
255
|
+
* is not made within [timeout] still rejects the returned promise, but
|
|
256
|
+
* keeps trying until end() and emits 'connect' once it stands, and a
|
|
257
|
+
* further connect({ keepTrying: true }) while it is trying waits for it
|
|
258
|
+
* instead of opening a second connection. If false (the default), nothing
|
|
259
|
+
* changes: the client gives up at the timeout and must be connected again.
|
|
224
260
|
* @returns {Promise} Resolves once connected within [timeout], rejects otherwise
|
|
225
261
|
* @example
|
|
226
262
|
* try {
|
|
@@ -229,14 +265,14 @@ class VrpcClient extends EventEmitter {
|
|
|
229
265
|
* console.error(`Could not connect because: ${err.message}`)
|
|
230
266
|
* }
|
|
231
267
|
*/
|
|
232
|
-
async connect () {
|
|
268
|
+
async connect ({ keepTrying = false } = {}) {
|
|
233
269
|
if (this._client && this._client.connected) return
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (this.
|
|
237
|
-
|
|
238
|
-
password = this._token
|
|
270
|
+
// keepTrying: a connection that is still trying is waited for, never
|
|
271
|
+
// doubled (without it, connect() behaves as it always did)
|
|
272
|
+
if (keepTrying && this._client && !ENDED.has(this._client)) {
|
|
273
|
+
return this._awaitConnect(this._client, { keepTrying })
|
|
239
274
|
}
|
|
275
|
+
const { username, password } = this._credentials()
|
|
240
276
|
const options = {
|
|
241
277
|
username,
|
|
242
278
|
password,
|
|
@@ -264,6 +300,7 @@ class VrpcClient extends EventEmitter {
|
|
|
264
300
|
})
|
|
265
301
|
|
|
266
302
|
this._client = mqtt.connect(this._broker, options)
|
|
303
|
+
this._backoff = new ReconnectBackoff(this._client)
|
|
267
304
|
|
|
268
305
|
// Persistent lifecycle handlers
|
|
269
306
|
this._client.on('offline', () => {
|
|
@@ -271,6 +308,9 @@ class VrpcClient extends EventEmitter {
|
|
|
271
308
|
// Clear the cache to ensure a fresh state upon reconnection
|
|
272
309
|
this._agents = {}
|
|
273
310
|
this._proxies = {}
|
|
311
|
+
// the agents delete this connection's isolated instances once its
|
|
312
|
+
// will is out
|
|
313
|
+
this._ownIsolated.clear()
|
|
274
314
|
this.emit('offline')
|
|
275
315
|
})
|
|
276
316
|
|
|
@@ -288,6 +328,14 @@ class VrpcClient extends EventEmitter {
|
|
|
288
328
|
})
|
|
289
329
|
|
|
290
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
|
+
}
|
|
291
339
|
// forward the good news
|
|
292
340
|
this.emit('connect')
|
|
293
341
|
// everything is subscribed afresh below: pending retries are moot
|
|
@@ -307,6 +355,13 @@ class VrpcClient extends EventEmitter {
|
|
|
307
355
|
}
|
|
308
356
|
// Single wildcard catches BOTH RPC responses (length 3) AND events (length 4)
|
|
309
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
|
+
}
|
|
310
365
|
|
|
311
366
|
// Re-subscribe to all cached event topics
|
|
312
367
|
this._log.debug('Restoring event subscriptions after reconnect...')
|
|
@@ -333,13 +388,20 @@ class VrpcClient extends EventEmitter {
|
|
|
333
388
|
}
|
|
334
389
|
// AgentInfo message
|
|
335
390
|
if (klass === '__agentInfo__') {
|
|
336
|
-
const { status, hostname, version } = JSON.parse(
|
|
391
|
+
const { status, hostname, version, v } = JSON.parse(
|
|
392
|
+
message.toString()
|
|
393
|
+
)
|
|
337
394
|
this._createIfNotExist(agent)
|
|
338
395
|
this._agents[agent].status = status
|
|
339
396
|
this._agents[agent].hostname = hostname
|
|
340
397
|
this._agents[agent].version = version
|
|
398
|
+
// the agent's protocol version (undefined before 3)
|
|
399
|
+
this._agents[agent].v = v
|
|
341
400
|
if (status === 'offline') {
|
|
342
|
-
this.
|
|
401
|
+
this._loseAgent(agent, v)
|
|
402
|
+
} else {
|
|
403
|
+
this._redeclareTo(agent)
|
|
404
|
+
this._flushWithdrawals(agent)
|
|
343
405
|
}
|
|
344
406
|
this.emit('agent', { domain, agent, status, hostname, version })
|
|
345
407
|
// ClassInfo message
|
|
@@ -383,8 +445,8 @@ class VrpcClient extends EventEmitter {
|
|
|
383
445
|
})
|
|
384
446
|
// RPC message
|
|
385
447
|
} else {
|
|
386
|
-
const { i, a, e, r } = JSON.parse(message.toString())
|
|
387
|
-
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 })
|
|
388
450
|
}
|
|
389
451
|
} catch (err) {
|
|
390
452
|
this._log.error(
|
|
@@ -393,25 +455,78 @@ class VrpcClient extends EventEmitter {
|
|
|
393
455
|
}
|
|
394
456
|
})
|
|
395
457
|
|
|
458
|
+
return this._awaitConnect(this._client, { keepTrying })
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
_awaitConnect (client, { keepTrying }) {
|
|
396
462
|
return new Promise((resolve, reject) => {
|
|
397
463
|
const timeout = setTimeout(() => {
|
|
398
464
|
// This will run only if the 'connect' event isn't fired in time.
|
|
465
|
+
const error = new Error(
|
|
466
|
+
`Connection trial timed out (> ${this._timeout} ms)`
|
|
467
|
+
)
|
|
468
|
+
if (keepTrying) {
|
|
469
|
+
// the client keeps trying in the background, until end()
|
|
470
|
+
client.removeListener('connect', onConnect)
|
|
471
|
+
reject(error)
|
|
472
|
+
return
|
|
473
|
+
}
|
|
399
474
|
// We explicitly end the client to stop it from retrying in the background.
|
|
400
|
-
|
|
401
|
-
reject(
|
|
402
|
-
new Error(`Connection trial timed out (> ${this._timeout} ms)`)
|
|
403
|
-
)
|
|
404
|
-
})
|
|
475
|
+
VrpcClient._endMqtt(client, true, {}, () => reject(error))
|
|
405
476
|
}, this._timeout)
|
|
406
477
|
|
|
407
|
-
|
|
478
|
+
const onConnect = () => {
|
|
408
479
|
// If we connect successfully, clear the timeout and resolve the promise.
|
|
409
480
|
clearTimeout(timeout)
|
|
410
481
|
resolve()
|
|
411
|
-
}
|
|
482
|
+
}
|
|
483
|
+
client.once('connect', onConnect)
|
|
412
484
|
})
|
|
413
485
|
}
|
|
414
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Replaces the credentials the connection logs in with
|
|
489
|
+
*
|
|
490
|
+
* The broker checks credentials when a connection is made: a live
|
|
491
|
+
* connection carries on undisturbed, and the next one - after a network
|
|
492
|
+
* loss, a broker restart - logs in with these. A connection that is
|
|
493
|
+
* being refused right now tries again with them within a second,
|
|
494
|
+
* instead of waiting out its back-off.
|
|
495
|
+
*
|
|
496
|
+
* @param {Object} credentials Only the given fields change
|
|
497
|
+
* @param {String} [credentials.username] MQTT username
|
|
498
|
+
* @param {String} [credentials.password] MQTT password (if no token is used)
|
|
499
|
+
* @param {String} [credentials.token] Access token
|
|
500
|
+
*/
|
|
501
|
+
updateCredentials ({ username, password, token } = {}) {
|
|
502
|
+
if (username !== undefined) this._username = username
|
|
503
|
+
if (password !== undefined) this._password = password
|
|
504
|
+
if (token !== undefined) this._token = token
|
|
505
|
+
if (!this._client || ENDED.has(this._client)) return
|
|
506
|
+
const credentials = this._credentials()
|
|
507
|
+
for (const key of ['username', 'password']) {
|
|
508
|
+
if (credentials[key] === undefined) delete this._client.options[key]
|
|
509
|
+
else this._client.options[key] = credentials[key]
|
|
510
|
+
}
|
|
511
|
+
this._backoff.hurry()
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// what the broker sees: a token rides as the password
|
|
515
|
+
_credentials () {
|
|
516
|
+
let username = this._username
|
|
517
|
+
let password = this._password
|
|
518
|
+
if (this._token) {
|
|
519
|
+
username = username || this._generateUserName()
|
|
520
|
+
password = this._token
|
|
521
|
+
}
|
|
522
|
+
return { username, password }
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
static _endMqtt (client, force, options, callback) {
|
|
526
|
+
ENDED.add(client)
|
|
527
|
+
client.end(force, options, callback)
|
|
528
|
+
}
|
|
529
|
+
|
|
415
530
|
/**
|
|
416
531
|
* Creates a new remote instance and provides a proxy to it.
|
|
417
532
|
*
|
|
@@ -483,6 +598,14 @@ class VrpcClient extends EventEmitter {
|
|
|
483
598
|
v: VRPC_PROTOCOL_VERSION
|
|
484
599
|
}
|
|
485
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
|
+
}
|
|
486
609
|
if (instance && cacheProxy) {
|
|
487
610
|
this._proxies[`${agent}/${className}/${instance}`] = {
|
|
488
611
|
agent,
|
|
@@ -547,6 +670,9 @@ class VrpcClient extends EventEmitter {
|
|
|
547
670
|
this._mqttPublish(topic, JSON.stringify(json))
|
|
548
671
|
delete this._proxies[`${agent}/${className}/${instance}`]
|
|
549
672
|
const deleted = await this._handleAgentAnswer(json, agent)
|
|
673
|
+
if (deleted === true) {
|
|
674
|
+
this._ownIsolated.delete(`${agent}/${className}/${instance}`)
|
|
675
|
+
}
|
|
550
676
|
// the answer and the class info that no longer lists the instance
|
|
551
677
|
// travel on different topics, and a broker need not keep their order:
|
|
552
678
|
// what this client deleted is gone for it at once
|
|
@@ -604,6 +730,7 @@ class VrpcClient extends EventEmitter {
|
|
|
604
730
|
v: VRPC_PROTOCOL_VERSION
|
|
605
731
|
}
|
|
606
732
|
const topic = `${this._domain}/${agent}/${className}/__static__/${functionName}`
|
|
733
|
+
this._declare(json, topic)
|
|
607
734
|
await this._waitUntilClassIsOnline(agent, className)
|
|
608
735
|
this._mqttPublish(topic, JSON.stringify(json))
|
|
609
736
|
return this._handleAgentAnswer(json, agent)
|
|
@@ -615,29 +742,61 @@ class VrpcClient extends EventEmitter {
|
|
|
615
742
|
* registrations. In this case the instanceId of the emitter is injected as
|
|
616
743
|
* first argument of any event callback.
|
|
617
744
|
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
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.
|
|
622
758
|
*
|
|
623
759
|
* @param {Object} options
|
|
624
|
-
* @param {String} options.className Name of the
|
|
625
|
-
* @param {
|
|
626
|
-
* call
|
|
627
|
-
* @param {String} [options.agent] Agent name
|
|
628
|
-
* is used
|
|
629
|
-
* @
|
|
630
|
-
*
|
|
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
|
|
631
770
|
*/
|
|
632
771
|
async callAll ({
|
|
633
772
|
className,
|
|
634
773
|
functionName,
|
|
635
774
|
args = [],
|
|
636
|
-
agent = this._agent
|
|
775
|
+
agent = this._agent,
|
|
776
|
+
instances
|
|
637
777
|
} = {}) {
|
|
638
778
|
if (!this._isConnected()) {
|
|
639
779
|
throw new Error('Client is not connected')
|
|
640
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
|
+
}
|
|
641
800
|
const json = {
|
|
642
801
|
c: className,
|
|
643
802
|
f: functionName,
|
|
@@ -646,40 +805,418 @@ class VrpcClient extends EventEmitter {
|
|
|
646
805
|
s: this._vrpcConnectionId,
|
|
647
806
|
v: VRPC_PROTOCOL_VERSION
|
|
648
807
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
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({
|
|
655
813
|
agent,
|
|
656
814
|
className,
|
|
657
815
|
functionName,
|
|
658
|
-
args
|
|
816
|
+
args,
|
|
817
|
+
selection
|
|
659
818
|
})
|
|
660
|
-
if (!wrapped) continue // using cache
|
|
661
|
-
json.a = [functionName, ...wrapped]
|
|
662
|
-
await this._waitUntilClassIsOnline(agent, className)
|
|
663
|
-
this._mqttPublish(topic, JSON.stringify(json))
|
|
664
|
-
const tmp = await this._handleAgentAnswer(json, agent)
|
|
665
|
-
result.push(...tmp)
|
|
666
819
|
}
|
|
667
|
-
|
|
820
|
+
special = '__callMatching__'
|
|
668
821
|
}
|
|
669
|
-
const topic = `${this._domain}/${agent}/${className}/__static__
|
|
822
|
+
const topic = `${this._domain}/${agent}/${className}/__static__/${special}`
|
|
670
823
|
const wrapped = await this._wrapArguments({
|
|
671
824
|
agent,
|
|
672
825
|
className,
|
|
673
826
|
functionName,
|
|
674
|
-
args
|
|
827
|
+
args,
|
|
828
|
+
isCallAll: true,
|
|
829
|
+
selection
|
|
675
830
|
})
|
|
676
831
|
if (!wrapped) return // using cache
|
|
677
|
-
json.a =
|
|
832
|
+
json.a =
|
|
833
|
+
selection === undefined
|
|
834
|
+
? [functionName, ...wrapped]
|
|
835
|
+
: [selection, functionName, ...wrapped]
|
|
836
|
+
this._declare(json, topic)
|
|
678
837
|
await this._waitUntilClassIsOnline(agent, className)
|
|
679
838
|
this._mqttPublish(topic, JSON.stringify(json))
|
|
680
839
|
return this._handleAgentAnswer(json, agent)
|
|
681
840
|
}
|
|
682
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')
|
|
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}`
|
|
1157
|
+
this._mqttPublish(topic, JSON.stringify(json))
|
|
1158
|
+
return this._handleAgentAnswer(json, agent)
|
|
1159
|
+
}
|
|
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
|
+
|
|
683
1220
|
/**
|
|
684
1221
|
* Retrieves all information about the currently available components.
|
|
685
1222
|
*
|
|
@@ -815,7 +1352,10 @@ class VrpcClient extends EventEmitter {
|
|
|
815
1352
|
this._token = token
|
|
816
1353
|
this._agent = agent
|
|
817
1354
|
if (this._client) {
|
|
818
|
-
|
|
1355
|
+
const client = this._client
|
|
1356
|
+
await new Promise(resolve =>
|
|
1357
|
+
VrpcClient._endMqtt(client, false, {}, resolve)
|
|
1358
|
+
)
|
|
819
1359
|
}
|
|
820
1360
|
return this.connect()
|
|
821
1361
|
}
|
|
@@ -832,8 +1372,10 @@ class VrpcClient extends EventEmitter {
|
|
|
832
1372
|
this._mqttPublish(agentTopic, null, { retain: true })
|
|
833
1373
|
const classes = this.getAvailableClasses({ agent, mustBeOnline: false })
|
|
834
1374
|
for (const className of classes) {
|
|
835
|
-
const
|
|
836
|
-
|
|
1375
|
+
for (const info of ['__classInfo__', '__classInfoConcise__']) {
|
|
1376
|
+
const classTopic = `${this._domain}/${agent}/${className}/${info}`
|
|
1377
|
+
this._mqttPublish(classTopic, null, { retain: true })
|
|
1378
|
+
}
|
|
837
1379
|
}
|
|
838
1380
|
// Synchronize local cache
|
|
839
1381
|
delete this._agents[agent]
|
|
@@ -860,9 +1402,16 @@ class VrpcClient extends EventEmitter {
|
|
|
860
1402
|
)
|
|
861
1403
|
this._eventEmitter.removeAllListeners()
|
|
862
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()
|
|
863
1410
|
const client = this._client
|
|
864
1411
|
this._client = null // Prevent race conditions during shutdown
|
|
865
|
-
return new Promise(resolve =>
|
|
1412
|
+
return new Promise(resolve =>
|
|
1413
|
+
VrpcClient._endMqtt(client, false, {}, resolve)
|
|
1414
|
+
)
|
|
866
1415
|
}
|
|
867
1416
|
|
|
868
1417
|
// Private functions
|
|
@@ -890,13 +1439,6 @@ class VrpcClient extends EventEmitter {
|
|
|
890
1439
|
return this._client && this._client.connected
|
|
891
1440
|
}
|
|
892
1441
|
|
|
893
|
-
_getCallbackId (callback, { readOnly = false } = {}) {
|
|
894
|
-
if (!this._callbackIds.has(callback)) {
|
|
895
|
-
if (readOnly) return null
|
|
896
|
-
this._callbackIds.set(callback, nanoid(8))
|
|
897
|
-
}
|
|
898
|
-
return this._callbackIds.get(callback)
|
|
899
|
-
}
|
|
900
1442
|
|
|
901
1443
|
_generateUserName () {
|
|
902
1444
|
return `${this._domain}:client@${os.hostname()}-${os.platform()}-js`
|
|
@@ -929,8 +1471,16 @@ class VrpcClient extends EventEmitter {
|
|
|
929
1471
|
return `${this._domain}/${this._mqttClientId}/${this._connectionSecret}`
|
|
930
1472
|
}
|
|
931
1473
|
|
|
1474
|
+
// The 'client disconnecting' workaround below revives a client mqtt.js
|
|
1475
|
+
// left half-closed, never one that was ended on purpose (a connect
|
|
1476
|
+
// timeout, end(), reconnectWithToken) or has been replaced since
|
|
1477
|
+
_isRevivable (client) {
|
|
1478
|
+
return client === this._client && !ENDED.has(client)
|
|
1479
|
+
}
|
|
1480
|
+
|
|
932
1481
|
_mqttPublish (topic, message, options) {
|
|
933
|
-
this._client
|
|
1482
|
+
const client = this._client
|
|
1483
|
+
client.publish(
|
|
934
1484
|
topic,
|
|
935
1485
|
message,
|
|
936
1486
|
{ qos: this._qos, ...options },
|
|
@@ -939,13 +1489,16 @@ class VrpcClient extends EventEmitter {
|
|
|
939
1489
|
this._log.warn(
|
|
940
1490
|
`Could not publish MQTT message because: ${err.message}`
|
|
941
1491
|
)
|
|
942
|
-
if (
|
|
1492
|
+
if (
|
|
1493
|
+
err.message === 'client disconnecting' &&
|
|
1494
|
+
this._isRevivable(client)
|
|
1495
|
+
) {
|
|
943
1496
|
// This most certainly relates to a bug in MQTT.js (#1284)
|
|
944
1497
|
this._log.error(`Failed to publish, because: ${err.message}`)
|
|
945
1498
|
this.emit('error', err)
|
|
946
1499
|
this._log.info('Forcing reconnect now...')
|
|
947
|
-
|
|
948
|
-
|
|
1500
|
+
client.disconnecting = false
|
|
1501
|
+
client.reconnect()
|
|
949
1502
|
}
|
|
950
1503
|
}
|
|
951
1504
|
}
|
|
@@ -953,30 +1506,41 @@ class VrpcClient extends EventEmitter {
|
|
|
953
1506
|
}
|
|
954
1507
|
|
|
955
1508
|
async _mqttSubscribe (topic, options) {
|
|
1509
|
+
const client = this._client
|
|
956
1510
|
return new Promise((resolve, reject) => {
|
|
957
|
-
|
|
1511
|
+
client.subscribe(
|
|
958
1512
|
topic,
|
|
959
1513
|
{ qos: this._qos, ...options },
|
|
960
|
-
(
|
|
961
|
-
|
|
1514
|
+
(subscribeError, subs) => {
|
|
1515
|
+
const { refused, granted, error } = SubscribeRetrier.outcome(
|
|
1516
|
+
subscribeError,
|
|
1517
|
+
subs
|
|
1518
|
+
)
|
|
1519
|
+
if (error) {
|
|
962
1520
|
this._log.warn(
|
|
963
|
-
`Could not subscribe to topic(s): '${topic}', because: ${
|
|
1521
|
+
`Could not subscribe to topic(s): '${topic}', because: ${error.message}`
|
|
964
1522
|
)
|
|
965
|
-
if (
|
|
1523
|
+
if (
|
|
1524
|
+
error.message === 'client disconnecting' &&
|
|
1525
|
+
this._isRevivable(client)
|
|
1526
|
+
) {
|
|
966
1527
|
// This most certainly relates to a bug in MQTT.js (#1284)
|
|
967
|
-
const msg = `Failed to subscribe, because: ${
|
|
1528
|
+
const msg = `Failed to subscribe, because: ${error.message}`
|
|
968
1529
|
this._log.error(msg)
|
|
969
|
-
this.emit('error',
|
|
1530
|
+
this.emit('error', error)
|
|
970
1531
|
this._log.info('Forcing reconnect now...')
|
|
971
|
-
|
|
972
|
-
|
|
1532
|
+
client.disconnecting = false
|
|
1533
|
+
client.reconnect()
|
|
973
1534
|
reject(new Error(msg))
|
|
1535
|
+
} else {
|
|
1536
|
+
// the connection broke under the subscribe: the next connect
|
|
1537
|
+
// subscribes everything afresh
|
|
1538
|
+
reject(error)
|
|
974
1539
|
}
|
|
975
1540
|
} else {
|
|
1541
|
+
let err
|
|
976
1542
|
const topicArray = Array.isArray(topic) ? topic : [topic]
|
|
977
|
-
const erroneousGranted =
|
|
978
|
-
.filter(x => x.qos === 128)
|
|
979
|
-
.map(x => x.topic)
|
|
1543
|
+
const erroneousGranted = refused
|
|
980
1544
|
if (erroneousGranted.length > 0) {
|
|
981
1545
|
err = new Error(
|
|
982
1546
|
`Could not subscribe all ${topicArray.length} topic(s) but got error qos=128 on following ${erroneousGranted.length} topic(s): ${erroneousGranted}`
|
|
@@ -987,9 +1551,7 @@ class VrpcClient extends EventEmitter {
|
|
|
987
1551
|
this.emit('error', err)
|
|
988
1552
|
this._subscribeRetrier.schedule(erroneousGranted, options)
|
|
989
1553
|
}
|
|
990
|
-
this._subscribeRetrier.granted(
|
|
991
|
-
granted.filter(x => x.qos !== 128).map(x => x.topic)
|
|
992
|
-
)
|
|
1554
|
+
this._subscribeRetrier.granted(granted.map(x => x.topic))
|
|
993
1555
|
const reducedQos = granted.filter(x => x.qos < this._qos)
|
|
994
1556
|
if (reducedQos.length > 0) {
|
|
995
1557
|
err = new Error(
|
|
@@ -1006,7 +1568,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1006
1568
|
this._log.warn(err.message)
|
|
1007
1569
|
this.emit('error', err)
|
|
1008
1570
|
}
|
|
1009
|
-
if (granted.length === 0) {
|
|
1571
|
+
if (granted.length === 0 && refused.length === 0) {
|
|
1010
1572
|
this._log.debug(`Already subscribed to: ${topic}`)
|
|
1011
1573
|
}
|
|
1012
1574
|
resolve()
|
|
@@ -1067,15 +1629,19 @@ class VrpcClient extends EventEmitter {
|
|
|
1067
1629
|
if (this._hasClassOnline(agent, className)) return
|
|
1068
1630
|
return new Promise((resolve, reject) => {
|
|
1069
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
|
|
1070
1634
|
const checkClass = () => {
|
|
1071
1635
|
if (this._hasClassOnline(agent, className)) {
|
|
1072
1636
|
if (timer) clearTimeout(timer)
|
|
1073
1637
|
this.removeListener('class', checkClass)
|
|
1638
|
+
this.removeListener('agent', checkClass)
|
|
1074
1639
|
resolve()
|
|
1075
1640
|
}
|
|
1076
1641
|
}
|
|
1077
1642
|
timer = setTimeout(() => {
|
|
1078
1643
|
this.removeListener('class', checkClass)
|
|
1644
|
+
this.removeListener('agent', checkClass)
|
|
1079
1645
|
reject(
|
|
1080
1646
|
new Error(
|
|
1081
1647
|
`Connection to class "${className}" on agent "${agent}" and domain "${this._domain}" timed out (> ${this._timeout} ms)`
|
|
@@ -1083,6 +1649,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1083
1649
|
)
|
|
1084
1650
|
}, this._timeout)
|
|
1085
1651
|
this.on('class', checkClass)
|
|
1652
|
+
this.on('agent', checkClass)
|
|
1086
1653
|
})
|
|
1087
1654
|
}
|
|
1088
1655
|
|
|
@@ -1121,23 +1688,18 @@ class VrpcClient extends EventEmitter {
|
|
|
1121
1688
|
throw new Error('VRPC does not support removing all listeners')
|
|
1122
1689
|
}
|
|
1123
1690
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
this._eventEmitter.removeListener(id, handler)
|
|
1137
|
-
})
|
|
1138
|
-
|
|
1139
|
-
delete this._cachedSubscriptions[topic]
|
|
1140
|
-
})
|
|
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))
|
|
1141
1703
|
try {
|
|
1142
1704
|
const json = {
|
|
1143
1705
|
c: instance,
|
|
@@ -1183,6 +1745,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1183
1745
|
s: this._vrpcConnectionId,
|
|
1184
1746
|
v: VRPC_PROTOCOL_VERSION
|
|
1185
1747
|
}
|
|
1748
|
+
this._declare(json, `${targetTopic}/${functionName}`)
|
|
1186
1749
|
this._mqttPublish(
|
|
1187
1750
|
`${targetTopic}/${functionName}`,
|
|
1188
1751
|
JSON.stringify(json)
|
|
@@ -1218,6 +1781,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1218
1781
|
s: this._vrpcConnectionId,
|
|
1219
1782
|
v: VRPC_PROTOCOL_VERSION
|
|
1220
1783
|
}
|
|
1784
|
+
this._declare(json, `${targetTopic}/${functionName}`)
|
|
1221
1785
|
this._mqttPublish(
|
|
1222
1786
|
`${targetTopic}/${functionName}`,
|
|
1223
1787
|
JSON.stringify(json)
|
|
@@ -1229,22 +1793,37 @@ class VrpcClient extends EventEmitter {
|
|
|
1229
1793
|
)
|
|
1230
1794
|
}
|
|
1231
1795
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
const
|
|
1235
|
-
|
|
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)
|
|
1236
1809
|
}
|
|
1237
1810
|
}
|
|
1238
1811
|
return proxy
|
|
1239
1812
|
}
|
|
1240
1813
|
|
|
1241
|
-
async _handleAgentAnswer ({ i, c, f }, agent) {
|
|
1814
|
+
async _handleAgentAnswer ({ i, c, f, a }, agent) {
|
|
1242
1815
|
return new Promise((resolve, reject) => {
|
|
1243
1816
|
const msg = `Function call "${c}::${f}()" on agent "${agent}" timed out (> ${this._timeout} ms)`
|
|
1244
1817
|
const timer = setTimeout(() => {
|
|
1245
1818
|
this._eventEmitter.removeAllListeners(i)
|
|
1246
1819
|
reject(new Error(msg))
|
|
1247
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
|
+
}
|
|
1248
1827
|
this._eventEmitter.once(i, data => {
|
|
1249
1828
|
clearTimeout(timer)
|
|
1250
1829
|
if (data.e) {
|
|
@@ -1263,10 +1842,12 @@ class VrpcClient extends EventEmitter {
|
|
|
1263
1842
|
new Error(`[vrpc ${agent}-${c}-${f}]: ${message}`, { cause })
|
|
1264
1843
|
)
|
|
1265
1844
|
} else {
|
|
1845
|
+
confirm()
|
|
1266
1846
|
resolve(promiseData.r)
|
|
1267
1847
|
}
|
|
1268
1848
|
})
|
|
1269
1849
|
} else {
|
|
1850
|
+
confirm()
|
|
1270
1851
|
resolve(ret)
|
|
1271
1852
|
}
|
|
1272
1853
|
}
|
|
@@ -1303,51 +1884,114 @@ class VrpcClient extends EventEmitter {
|
|
|
1303
1884
|
})
|
|
1304
1885
|
}
|
|
1305
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
|
+
|
|
1306
1907
|
async _wrapArguments ({
|
|
1307
1908
|
agent,
|
|
1308
1909
|
className,
|
|
1309
1910
|
instance,
|
|
1310
1911
|
proxyId,
|
|
1311
1912
|
functionName,
|
|
1312
|
-
args
|
|
1913
|
+
args,
|
|
1914
|
+
isCallAll = false,
|
|
1915
|
+
selection
|
|
1313
1916
|
}) {
|
|
1314
1917
|
const wrapped = []
|
|
1315
1918
|
|
|
1316
|
-
|
|
1317
|
-
|
|
1919
|
+
const isEventFunction = this._isEventFunction(
|
|
1920
|
+
agent,
|
|
1921
|
+
className,
|
|
1922
|
+
functionName
|
|
1923
|
+
)
|
|
1924
|
+
// continuous removers (legacy): withdraw the callback's subscription
|
|
1318
1925
|
const isContinuousRemover = /^(off|remove|unmonitor|unsignal)/.test(
|
|
1319
1926
|
functionName
|
|
1320
1927
|
)
|
|
1928
|
+
const isEmitterRemover =
|
|
1929
|
+
(functionName === 'off' || functionName === 'removeListener') &&
|
|
1930
|
+
typeof args[0] === 'string'
|
|
1321
1931
|
|
|
1322
1932
|
for (const [i, x] of args.entries()) {
|
|
1323
|
-
|
|
1324
|
-
|
|
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
|
|
1325
1941
|
if (
|
|
1326
|
-
|
|
1327
|
-
(
|
|
1328
|
-
|
|
1942
|
+
!removes &&
|
|
1943
|
+
(isEmitterContract ||
|
|
1944
|
+
functionName === 'vrpcOn' ||
|
|
1945
|
+
((functionName === 'on' || functionName === 'addListener') &&
|
|
1946
|
+
i === 1 &&
|
|
1947
|
+
typeof args[0] === 'string') ||
|
|
1948
|
+
isEventFunction)
|
|
1329
1949
|
) {
|
|
1330
1950
|
const event = typeof args[0] === 'string' ? args[0] : functionName
|
|
1951
|
+
// vrpcOn names the event function first
|
|
1952
|
+
const isVrpcOn = functionName === 'vrpcOn'
|
|
1331
1953
|
const id = await this._onRemoteEvent({
|
|
1332
1954
|
agent,
|
|
1333
1955
|
className,
|
|
1334
1956
|
instance,
|
|
1335
1957
|
event,
|
|
1336
|
-
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
|
|
1337
1964
|
})
|
|
1338
1965
|
wrapped.push(id)
|
|
1339
1966
|
continue
|
|
1340
|
-
} else if (
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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') {
|
|
1351
1995
|
// Nothing of ours to remove - the subscription was already
|
|
1352
1996
|
// dropped (the instance or its agent went away and the cache
|
|
1353
1997
|
// was cleared, or the listener was never registered). Sending
|
|
@@ -1355,43 +1999,28 @@ class VrpcClient extends EventEmitter {
|
|
|
1355
1999
|
// its EventEmitter rejects. Handled locally: nothing to do.
|
|
1356
2000
|
return null
|
|
1357
2001
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
this._invokeId++ % Number.MAX_SAFE_INTEGER
|
|
1365
|
-
}`
|
|
1366
|
-
wrapped.push(id)
|
|
1367
|
-
this._eventEmitter.once(id, ({ a }) => x.apply(null, a))
|
|
1368
|
-
continue
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
// special case of an EventEmitter provided as argument
|
|
1373
|
-
if (this._isEmitter(x)) {
|
|
1374
|
-
const { emitter, event } = x
|
|
1375
|
-
let callback = (...args) => emitter.emit(event, ...args)
|
|
1376
|
-
if (this._emitterListener.has(emitter)) {
|
|
1377
|
-
const eventMap = this._emitterListener.get(emitter)
|
|
1378
|
-
if (eventMap.has(event)) {
|
|
1379
|
-
callback = eventMap.get(event)
|
|
1380
|
-
} else {
|
|
1381
|
-
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
|
|
1382
2008
|
}
|
|
1383
|
-
} else {
|
|
1384
|
-
this._emitterListener.set(emitter, new Map([[event, callback]]))
|
|
1385
2009
|
}
|
|
1386
|
-
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
event,
|
|
1392
|
-
callback
|
|
1393
|
-
})
|
|
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
|
+
}`
|
|
1394
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
|
+
})
|
|
1395
2024
|
continue
|
|
1396
2025
|
}
|
|
1397
2026
|
// default behavior is to not touch
|
|
@@ -1400,98 +2029,378 @@ class VrpcClient extends EventEmitter {
|
|
|
1400
2029
|
return wrapped
|
|
1401
2030
|
}
|
|
1402
2031
|
|
|
1403
|
-
|
|
1404
|
-
|
|
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
|
+
}
|
|
2045
|
+
|
|
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
|
+
}
|
|
1405
2062
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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
|
+
}
|
|
1411
2076
|
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
+
}
|
|
1415
2085
|
|
|
1416
|
-
|
|
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
|
+
}
|
|
1417
2092
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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)
|
|
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)
|
|
1421
2114
|
}
|
|
2115
|
+
return topic || null
|
|
2116
|
+
}
|
|
1422
2117
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
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
|
|
1431
2173
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
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)
|
|
1434
2182
|
}
|
|
1435
|
-
|
|
2183
|
+
this._eventEmitter.on(id, sub.handler)
|
|
2184
|
+
this._cachedSubscriptions[topic] = sub
|
|
1436
2185
|
return id
|
|
1437
2186
|
}
|
|
1438
2187
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
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
|
+
}
|
|
1455
2242
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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
|
+
}
|
|
1459
2263
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
|
1464
2283
|
}
|
|
1465
|
-
}
|
|
1466
|
-
|
|
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
|
+
}
|
|
1467
2292
|
}
|
|
1468
2293
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
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: [] })
|
|
1482
2320
|
}
|
|
1483
|
-
|
|
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
|
+
}
|
|
1484
2360
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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
|
+
}
|
|
1489
2372
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
+
}
|
|
1493
2386
|
|
|
1494
|
-
|
|
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))
|
|
1495
2404
|
})
|
|
1496
2405
|
}
|
|
1497
2406
|
|
|
@@ -1522,6 +2431,8 @@ class VrpcClient extends EventEmitter {
|
|
|
1522
2431
|
}
|
|
1523
2432
|
|
|
1524
2433
|
async _getInstanceData (instance, options) {
|
|
2434
|
+
const own = this._findOwnIsolated(instance, options)
|
|
2435
|
+
if (own) return own
|
|
1525
2436
|
const instanceData = this._getInstanceFromCache(instance, options)
|
|
1526
2437
|
if (!instanceData) {
|
|
1527
2438
|
if (options.noWait) {
|
|
@@ -1532,6 +2443,17 @@ class VrpcClient extends EventEmitter {
|
|
|
1532
2443
|
return instanceData
|
|
1533
2444
|
}
|
|
1534
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
|
+
|
|
1535
2457
|
_cachedProxy (instance, { agent, className } = {}) {
|
|
1536
2458
|
// Instance ids are unique per agent only: a cached proxy is served
|
|
1537
2459
|
// solely when it is the one match for the given agent and class - twins
|
|
@@ -1647,6 +2569,72 @@ class VrpcClient extends EventEmitter {
|
|
|
1647
2569
|
* @param {String} info.className - Class name
|
|
1648
2570
|
*/
|
|
1649
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
|
+
|
|
1650
2638
|
/**
|
|
1651
2639
|
* Event 'connect'
|
|
1652
2640
|
*
|