vrpc 3.1.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser/vrpc.js +1 -1
- package/browser/vrpc.js.LICENSE.txt +2 -0
- package/package.json +4 -3
- package/vrpc/VrpcAdapter.js +59 -41
- package/vrpc/VrpcAgent.js +64 -18
- package/vrpc/VrpcBrowserAdapter.js +885 -0
- package/vrpc/VrpcBrowserAgent.js +675 -0
- package/vrpc/VrpcClient.js +104 -43
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
const os = require('os')
|
|
2
|
+
const mqtt = require('mqtt')
|
|
3
|
+
const crypto = require('crypto')
|
|
4
|
+
const VrpcAdapter = require('./VrpcBrowserAdapter')
|
|
5
|
+
const EventEmitter = require('events')
|
|
6
|
+
const { nanoid } = require('nanoid')
|
|
7
|
+
|
|
8
|
+
const VRPC_PROTOCOL_VERSION = 3
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Agent capable of making existing code available to remote control by clients.
|
|
12
|
+
*
|
|
13
|
+
* @extends EventEmitter
|
|
14
|
+
*/
|
|
15
|
+
class VrpcAgent extends EventEmitter {
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Constructs an agent instance
|
|
19
|
+
*
|
|
20
|
+
* @constructor
|
|
21
|
+
* @param {Object} obj
|
|
22
|
+
* @param {String} [obj.username] MQTT username
|
|
23
|
+
* @param {String} [obj.password] MQTT password (if no token is provided)
|
|
24
|
+
* @param {String} [obj.token] Access token
|
|
25
|
+
* @param {String} [obj.domain='vrpc'] The domain under which the agent-provided code is reachable
|
|
26
|
+
* @param {String} [obj.agent='<user>-<pathId>@<hostname>-<platform>-js'] This agent's name
|
|
27
|
+
* @param {String} [obj.broker='mqtts://vrpc.io:8883'] Broker url in form: `<scheme>://<host>:<port>`
|
|
28
|
+
* @param {Object} [obj.log=console] Log object (must support debug, info, warn, and error level)
|
|
29
|
+
* @param {String} [obj.bestEffort=false] If true, message will be sent with best effort, i.e. no caching if offline
|
|
30
|
+
* @param {String} [obj.version=''] The (user-defined) version of this agent
|
|
31
|
+
* @param {String} [obj.mqttClientId='<generated()>'] Explicitly set the mqtt client id.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const agent = new Agent({
|
|
35
|
+
* domain: 'vrpc'
|
|
36
|
+
* agent: 'myAgent'
|
|
37
|
+
* })
|
|
38
|
+
*/
|
|
39
|
+
constructor ({
|
|
40
|
+
username,
|
|
41
|
+
password,
|
|
42
|
+
token,
|
|
43
|
+
domain = 'vrpc',
|
|
44
|
+
agent = nanoid(8),
|
|
45
|
+
broker = 'mqtts://vrpc.io:8883',
|
|
46
|
+
log = 'console',
|
|
47
|
+
bestEffort = false,
|
|
48
|
+
version = '',
|
|
49
|
+
mqttClientId = null
|
|
50
|
+
} = {}) {
|
|
51
|
+
super()
|
|
52
|
+
this._validateDomain(domain)
|
|
53
|
+
this._validateAgent(agent)
|
|
54
|
+
this._username = username
|
|
55
|
+
this._password = password
|
|
56
|
+
this._token = token
|
|
57
|
+
this._agent = agent
|
|
58
|
+
this._domain = domain
|
|
59
|
+
this._broker = broker
|
|
60
|
+
this._qos = bestEffort ? 0 : 1
|
|
61
|
+
this._version = version
|
|
62
|
+
this._mqttClientId =
|
|
63
|
+
mqttClientId ||
|
|
64
|
+
`va3${crypto
|
|
65
|
+
.createHash('md5')
|
|
66
|
+
.update(this._domain + this._agent)
|
|
67
|
+
.digest('hex')
|
|
68
|
+
.substring(0, 20)}`
|
|
69
|
+
if (log === 'console') {
|
|
70
|
+
this._log = console
|
|
71
|
+
this._log.debug = () => {}
|
|
72
|
+
} else {
|
|
73
|
+
this._log = log
|
|
74
|
+
}
|
|
75
|
+
this._baseTopic = `${this._domain}/${this._agent}`
|
|
76
|
+
VrpcAdapter.onCallback(this._handleVrpcCallback.bind(this))
|
|
77
|
+
// maps clientId to instanceId
|
|
78
|
+
this._isolatedInstances = new Map()
|
|
79
|
+
this._sharedInstances = new Map()
|
|
80
|
+
|
|
81
|
+
// Handle the internal error event in case the user forgot to implement it
|
|
82
|
+
this.on('error', err => {
|
|
83
|
+
this._log.debug(`Encountered an error: ${err.message}`)
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Starts the agent
|
|
89
|
+
*
|
|
90
|
+
* The returned promise will only resolve once the agent is connected to the
|
|
91
|
+
* broker. If the connection can't be established it will try connecting
|
|
92
|
+
* forever. You may want to listen to the 'offline' (initial connect attempt
|
|
93
|
+
* failed) or 'reconnect' (any further fail) event and call `agent.end()` to
|
|
94
|
+
* stop trying to connect and resolve the returned promise.
|
|
95
|
+
*
|
|
96
|
+
* If the connection could not be established because of authorization
|
|
97
|
+
* failure, the 'error' event will be emitted.
|
|
98
|
+
*
|
|
99
|
+
* @return {Promise} Resolves once connected or explicitly ended, never
|
|
100
|
+
* rejects
|
|
101
|
+
*/
|
|
102
|
+
async serve () {
|
|
103
|
+
let username = this._username
|
|
104
|
+
let password = this._password
|
|
105
|
+
if (this._token) {
|
|
106
|
+
username = `${this._domain}/${this._agent}`
|
|
107
|
+
password = this._token
|
|
108
|
+
} else if (!this._password) {
|
|
109
|
+
username = `${this._domain}/${this._agent}`
|
|
110
|
+
password = nanoid()
|
|
111
|
+
}
|
|
112
|
+
this._options = {
|
|
113
|
+
username,
|
|
114
|
+
password,
|
|
115
|
+
keepalive: 30,
|
|
116
|
+
connectTimeout: 10 * 1000,
|
|
117
|
+
clientId: this._mqttClientId,
|
|
118
|
+
rejectUnauthorized: false,
|
|
119
|
+
will: {
|
|
120
|
+
topic: `${this._baseTopic}/__agentInfo__`,
|
|
121
|
+
payload: this._createAgentInfoPayload({ status: 'offline' }),
|
|
122
|
+
qos: this._qos,
|
|
123
|
+
retain: true
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
this._log.info(`Domain : ${this._domain}`)
|
|
127
|
+
this._log.info(`Agent : ${this._agent}`)
|
|
128
|
+
this._log.info(`Broker : ${this._broker}`)
|
|
129
|
+
this._log.info('Connecting to MQTT server...')
|
|
130
|
+
this._client = mqtt.connect(this._broker, this._options)
|
|
131
|
+
this._client.on('connect', this._handleConnect.bind(this))
|
|
132
|
+
this._client.on('reconnect', this._handleReconnect.bind(this))
|
|
133
|
+
this._client.on('error', this._handleError.bind(this))
|
|
134
|
+
this._client.on('message', this._handleMessage.bind(this))
|
|
135
|
+
this._client.on('close', this._handleClose.bind(this))
|
|
136
|
+
this._client.on('offline', this._handleOffline.bind(this))
|
|
137
|
+
this._client.on('end', this._handleEnd.bind(this))
|
|
138
|
+
return this._ensureConnected()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Stops the agent
|
|
143
|
+
*
|
|
144
|
+
* @param {Object} [obj]
|
|
145
|
+
* @param {Boolean} [unregister=false] If true, fully un-registers agent from broker
|
|
146
|
+
* @returns {Promise} Resolves when disconnected and ended
|
|
147
|
+
*/
|
|
148
|
+
async end ({ unregister = false } = {}) {
|
|
149
|
+
try {
|
|
150
|
+
if (!this._client || !this._client.connected) {
|
|
151
|
+
this.emit('end')
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
const agentTopic = `${this._baseTopic}/__agentInfo__`
|
|
155
|
+
this._mqttPublish(
|
|
156
|
+
agentTopic,
|
|
157
|
+
this._createAgentInfoPayload({ status: 'offline' }),
|
|
158
|
+
{ retain: true }
|
|
159
|
+
)
|
|
160
|
+
if (unregister) {
|
|
161
|
+
this._mqttPublish(agentTopic, null, { retain: true })
|
|
162
|
+
const classes = this._getClasses()
|
|
163
|
+
for (const className of classes) {
|
|
164
|
+
const infoTopic = `${this._baseTopic}/${className}/__classInfo__`
|
|
165
|
+
this._mqttPublish(infoTopic, null, { retain: true })
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
await new Promise(resolve => this._client.end(false, {}, resolve))
|
|
169
|
+
} catch (err) {
|
|
170
|
+
this._log.error(err, `Problem during disconnecting agent: ${err.message}`)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_validateDomain (domain) {
|
|
175
|
+
if (!domain) throw new Error('The domain must be specified')
|
|
176
|
+
if (domain.match(/[+/#*]/)) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
'The domain must NOT contain any of those characters: "+", "/", "#", "*"'
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_validateAgent (agent) {
|
|
184
|
+
if (!agent) throw new Error('The agent must be specified')
|
|
185
|
+
if (agent.match(/[+/#*]/)) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
'The agent must NOT contain any of those characters: "+", "/", "#", "*"'
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
_createAgentInfoPayload ({ status }) {
|
|
193
|
+
return JSON.stringify({
|
|
194
|
+
status,
|
|
195
|
+
hostname: os.hostname(),
|
|
196
|
+
version: this._version,
|
|
197
|
+
v: VRPC_PROTOCOL_VERSION
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
_mqttPublish (topic, message, options) {
|
|
202
|
+
this._client.publish(
|
|
203
|
+
topic,
|
|
204
|
+
message,
|
|
205
|
+
{ qos: this._qos, ...options },
|
|
206
|
+
err => {
|
|
207
|
+
if (err) {
|
|
208
|
+
this._log.warn(
|
|
209
|
+
`Could not publish MQTT message because: ${err.message}`
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_mqttSubscribe (topic, options) {
|
|
217
|
+
this._client.subscribe(
|
|
218
|
+
topic,
|
|
219
|
+
{ qos: this._qos, ...options },
|
|
220
|
+
(err, granted) => {
|
|
221
|
+
if (err) {
|
|
222
|
+
this._log.warn(
|
|
223
|
+
`Could not subscribe to topic(s) '${topic}', because: ${err.message}`
|
|
224
|
+
)
|
|
225
|
+
} else {
|
|
226
|
+
const topicArray = Array.isArray(topic) ? topic : [topic]
|
|
227
|
+
const erroneousGranted = granted
|
|
228
|
+
.filter(x => x.qos === 128)
|
|
229
|
+
.map(x => x.topic)
|
|
230
|
+
if (erroneousGranted.length > 0) {
|
|
231
|
+
err = new Error(
|
|
232
|
+
`Could not subscribe all ${topicArray.length} topic(s) but got error qos=128 on following ${erroneousGranted.length} topic(s): ${erroneousGranted}`
|
|
233
|
+
)
|
|
234
|
+
err.code = 'SUBSCRIBE_FAILED'
|
|
235
|
+
err.subscribeOptions = options
|
|
236
|
+
this._log.error(err)
|
|
237
|
+
this.emit('error', err)
|
|
238
|
+
}
|
|
239
|
+
const reducedQos = granted.filter(x => x.qos < this._qos)
|
|
240
|
+
if (reducedQos.length > 0) {
|
|
241
|
+
err = new Error(
|
|
242
|
+
`Could not subscribe all ${
|
|
243
|
+
topicArray.length
|
|
244
|
+
} topic(s) at desired qos=${
|
|
245
|
+
this._qos
|
|
246
|
+
} but got reduced qos on following ${
|
|
247
|
+
reducedQos.length
|
|
248
|
+
} topic(s): ${JSON.stringify(reducedQos)}`
|
|
249
|
+
)
|
|
250
|
+
err.code = 'SUBSCRIBE_REDUCED_QOS'
|
|
251
|
+
err.subscribeOptions = options
|
|
252
|
+
this._log.warn(err)
|
|
253
|
+
this.emit('error', err)
|
|
254
|
+
}
|
|
255
|
+
if (granted.length === 0) {
|
|
256
|
+
this._log.debug(`Already subscribed to topic '${topic}'`)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
_mqttUnsubscribe (topic, options) {
|
|
264
|
+
this._client.unsubscribe(topic, options, err => {
|
|
265
|
+
if (err) {
|
|
266
|
+
this._log.warn(
|
|
267
|
+
`Could not unsubscribe from topic: ${topic} because: ${err.message}`
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
_getClasses () {
|
|
274
|
+
return VrpcAdapter._getClassesArray()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
_getInstances (className) {
|
|
278
|
+
return VrpcAdapter._getInstancesArray(className)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
_getMemberFunctions (className) {
|
|
282
|
+
return VrpcAdapter._getMemberFunctionsArray(className)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
_getStaticFunctions (className) {
|
|
286
|
+
return VrpcAdapter._getStaticFunctionsArray(className)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
_getMetaData (className) {
|
|
290
|
+
return VrpcAdapter._getMetaData(className)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async _ensureConnected () {
|
|
294
|
+
return new Promise(resolve => {
|
|
295
|
+
if (this._client.connected) {
|
|
296
|
+
resolve()
|
|
297
|
+
} else {
|
|
298
|
+
this._client.once('connect', resolve)
|
|
299
|
+
// Will be triggered if the user called 'agent.end()'
|
|
300
|
+
this.once('end', resolve)
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
_handleVrpcCallback (json) {
|
|
306
|
+
const { s, i, r, e, a } = json
|
|
307
|
+
const topic = i.startsWith('__e__') ? i.slice(5) : s
|
|
308
|
+
try {
|
|
309
|
+
this._log.debug(`Forwarding callback to: ${topic} with payload:`, json)
|
|
310
|
+
this._mqttPublish(
|
|
311
|
+
topic,
|
|
312
|
+
VrpcAgent._stringifySafely({ a, r, e, i, v: VRPC_PROTOCOL_VERSION })
|
|
313
|
+
)
|
|
314
|
+
} catch (err) {
|
|
315
|
+
this._log.warn(
|
|
316
|
+
err,
|
|
317
|
+
`Problem publishing vrpc callback to ${topic} because of: ${err.message}`
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
static _stringifySafely (json) {
|
|
323
|
+
let str
|
|
324
|
+
const { c, f } = json
|
|
325
|
+
try {
|
|
326
|
+
str = JSON.stringify(json)
|
|
327
|
+
} catch (err) {
|
|
328
|
+
this._log.debug(
|
|
329
|
+
`Failed serialization of return value for: ${c}::${f}, because: ${err.message}`
|
|
330
|
+
)
|
|
331
|
+
json.r = '__vrpc::not-serializable__'
|
|
332
|
+
str = JSON.stringify(json)
|
|
333
|
+
}
|
|
334
|
+
return str
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
_handleConnect () {
|
|
338
|
+
this._log.info('[OK]')
|
|
339
|
+
try {
|
|
340
|
+
const topics = this._generateTopics()
|
|
341
|
+
if (topics.length > 0) this._mqttSubscribe(topics)
|
|
342
|
+
} catch (err) {
|
|
343
|
+
this._log.error(
|
|
344
|
+
err,
|
|
345
|
+
`Problem during initial topic subscription: ${err.message}`
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
// Publish agent online
|
|
349
|
+
this._publishAgentInfoMessage()
|
|
350
|
+
|
|
351
|
+
// Publish class information
|
|
352
|
+
const classes = this._getClasses()
|
|
353
|
+
classes.forEach(className => {
|
|
354
|
+
this._publishClassInfoMessage(className)
|
|
355
|
+
})
|
|
356
|
+
// Register all pre-existing instances
|
|
357
|
+
for (const [
|
|
358
|
+
instanceId,
|
|
359
|
+
{ className }
|
|
360
|
+
] of VrpcAdapter._instances.entries()) {
|
|
361
|
+
this._subscribeToMethodsOfNewInstance(className, instanceId)
|
|
362
|
+
}
|
|
363
|
+
this.emit('connect')
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
_publishAgentInfoMessage () {
|
|
367
|
+
this._mqttPublish(
|
|
368
|
+
`${this._baseTopic}/__agentInfo__`,
|
|
369
|
+
this._createAgentInfoPayload({ status: 'online' }),
|
|
370
|
+
{ retain: true }
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
_publishClassInfoMessage (className) {
|
|
375
|
+
const json = {
|
|
376
|
+
className: className,
|
|
377
|
+
instances: this._getInstances(className),
|
|
378
|
+
memberFunctions: this._getMemberFunctions(className),
|
|
379
|
+
staticFunctions: this._getStaticFunctions(className),
|
|
380
|
+
meta: this._getMetaData(className),
|
|
381
|
+
v: VRPC_PROTOCOL_VERSION
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
this._mqttPublish(
|
|
385
|
+
`${this._baseTopic}/${className}/__classInfo__`,
|
|
386
|
+
JSON.stringify(json),
|
|
387
|
+
{ retain: true }
|
|
388
|
+
)
|
|
389
|
+
} catch (err) {
|
|
390
|
+
this._log.error(
|
|
391
|
+
err,
|
|
392
|
+
`Problem during publishing class info: ${err.message}`
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
_generateTopics () {
|
|
398
|
+
const topics = []
|
|
399
|
+
const classes = this._getClasses()
|
|
400
|
+
if (classes.length > 0) {
|
|
401
|
+
this._log.info(`Registering classes: ${classes.join(', ')}`)
|
|
402
|
+
} else {
|
|
403
|
+
this._log.warn('No classes are registered')
|
|
404
|
+
}
|
|
405
|
+
classes.forEach(className => {
|
|
406
|
+
const staticFunctions = this._getStaticFunctions(className)
|
|
407
|
+
staticFunctions.forEach(func => {
|
|
408
|
+
topics.push(`${this._baseTopic}/${className}/__static__/${func}`)
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
return topics
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
_handleMessage (topic, data) {
|
|
415
|
+
try {
|
|
416
|
+
const json = JSON.parse(data.toString())
|
|
417
|
+
this._log.debug(`Message arrived with topic: ${topic} and payload:`, json)
|
|
418
|
+
const tokens = topic.split('/')
|
|
419
|
+
const [, , className, instance, method] = tokens
|
|
420
|
+
|
|
421
|
+
// Special case: clientInfo message
|
|
422
|
+
if (tokens.length === 4 && tokens[3] === '__clientInfo__') {
|
|
423
|
+
this._handleClientInfoMessage(topic, json)
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Anything else must follow RPC protocol
|
|
428
|
+
if (tokens.length !== 5) {
|
|
429
|
+
this._log.warn(`Ignoring message with invalid topic: ${topic}`)
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Prepare RPC json
|
|
434
|
+
// SECURITY: take as much as possible params from topic structure as this
|
|
435
|
+
// can be authorized by the broker. Content can't be checked...
|
|
436
|
+
json.c = instance === '__static__' ? className : instance
|
|
437
|
+
json.f = method
|
|
438
|
+
|
|
439
|
+
// Mutates json and adds return value
|
|
440
|
+
const mustTrack = VrpcAdapter._call(json)
|
|
441
|
+
|
|
442
|
+
if (mustTrack) {
|
|
443
|
+
this._mqttSubscribe(`${json.s}/__clientInfo__`)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Intersecting life-cycle functions
|
|
447
|
+
switch (method) {
|
|
448
|
+
case '__createIsolated__': {
|
|
449
|
+
// TODO handle instantiation errors
|
|
450
|
+
const instanceId = json.r
|
|
451
|
+
// TODO await this
|
|
452
|
+
this._subscribeToMethodsOfNewInstance(className, instanceId)
|
|
453
|
+
this._registerIsolatedInstance(instanceId, json.s)
|
|
454
|
+
break
|
|
455
|
+
}
|
|
456
|
+
case '__createShared__': {
|
|
457
|
+
// TODO handle instantiation errors
|
|
458
|
+
const instanceId = json.r
|
|
459
|
+
if (!this._hasSharedInstance(instanceId)) {
|
|
460
|
+
this._subscribeToMethodsOfNewInstance(className, instanceId)
|
|
461
|
+
this._publishClassInfoMessage(className)
|
|
462
|
+
}
|
|
463
|
+
this._registerSharedInstance(instanceId, json.s)
|
|
464
|
+
break
|
|
465
|
+
}
|
|
466
|
+
case '__delete__': {
|
|
467
|
+
this._unsubscribeMethodsOfDeletedInstance(className, instance)
|
|
468
|
+
const wasShared = this._unregisterInstance(json.a[0], json.s)
|
|
469
|
+
if (wasShared) this._publishClassInfoMessage(className)
|
|
470
|
+
break
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const { a, r, e, i, v } = json
|
|
474
|
+
const res = e ? { a, r, e, i, v } : { a, r, i, v }
|
|
475
|
+
this._mqttPublish(json.s, VrpcAgent._stringifySafely(res))
|
|
476
|
+
} catch (err) {
|
|
477
|
+
this._log.error(
|
|
478
|
+
err,
|
|
479
|
+
`Problem while handling incoming message: ${err.message}`
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
_handleClientInfoMessage (topic, json) {
|
|
485
|
+
// Client went offline
|
|
486
|
+
const clientId = topic.slice(0, -15) // /__clientInfo__ = 15
|
|
487
|
+
if (json.status === 'offline') {
|
|
488
|
+
VrpcAdapter._unregisterClient(clientId)
|
|
489
|
+
const entry = this._isolatedInstances.get(clientId)
|
|
490
|
+
if (entry) {
|
|
491
|
+
entry.forEach(instanceId => {
|
|
492
|
+
const json = { f: '__delete__', a: [instanceId], r: null }
|
|
493
|
+
VrpcAdapter._call(json)
|
|
494
|
+
if (json.r) {
|
|
495
|
+
this._log.debug(`Auto-deleted isolated instance: ${instanceId}`)
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
}
|
|
499
|
+
this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
|
|
500
|
+
this.emit('clientGone', clientId)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
_registerIsolatedInstance (instanceId, clientId) {
|
|
505
|
+
const entry = this._isolatedInstances.get(clientId)
|
|
506
|
+
if (entry) {
|
|
507
|
+
// already seen
|
|
508
|
+
entry.add(instanceId)
|
|
509
|
+
} else {
|
|
510
|
+
// new instance
|
|
511
|
+
this._isolatedInstances.set(clientId, new Set([instanceId]))
|
|
512
|
+
if (!this._sharedInstances.has(clientId)) {
|
|
513
|
+
this._mqttSubscribe(`${clientId}/__clientInfo__`)
|
|
514
|
+
}
|
|
515
|
+
this._log.info(`Tracking lifetime of client: ${clientId}`)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
_registerSharedInstance (instanceId, clientId) {
|
|
520
|
+
const entry = this._sharedInstances.get(clientId)
|
|
521
|
+
if (entry) {
|
|
522
|
+
// already seen
|
|
523
|
+
entry.add(instanceId)
|
|
524
|
+
} else {
|
|
525
|
+
// new instance
|
|
526
|
+
this._sharedInstances.set(clientId, new Set([instanceId]))
|
|
527
|
+
if (!this._isolatedInstances.has(clientId)) {
|
|
528
|
+
this._mqttSubscribe(`${clientId}/__clientInfo__`)
|
|
529
|
+
}
|
|
530
|
+
this._log.debug(`Tracking lifetime of client: ${clientId}`)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
_hasSharedInstance (instanceId) {
|
|
535
|
+
for (const [, instances] of this._sharedInstances) {
|
|
536
|
+
if (instances.has(instanceId)) return true
|
|
537
|
+
}
|
|
538
|
+
return false
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
_unregisterInstance (instanceId, clientId) {
|
|
542
|
+
const entryIsolated = this._isolatedInstances.get(clientId)
|
|
543
|
+
if (entryIsolated && entryIsolated.has(instanceId)) {
|
|
544
|
+
entryIsolated.delete(instanceId)
|
|
545
|
+
if (entryIsolated.length === 0) {
|
|
546
|
+
this._isolatedInstances.delete(clientId)
|
|
547
|
+
this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
|
|
548
|
+
this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
|
|
549
|
+
}
|
|
550
|
+
return false
|
|
551
|
+
}
|
|
552
|
+
let found = false
|
|
553
|
+
this._sharedInstances.forEach(async v => {
|
|
554
|
+
if (v.has(instanceId)) {
|
|
555
|
+
found = true
|
|
556
|
+
v.delete(instanceId)
|
|
557
|
+
if (v.length === 0) {
|
|
558
|
+
this._sharedInstances.delete(clientId)
|
|
559
|
+
this._mqttUnsubscribe(`${clientId}/__clientInfo__`)
|
|
560
|
+
this._log.debug(`Stopped tracking lifetime of client: ${clientId}`)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
})
|
|
564
|
+
if (!found) {
|
|
565
|
+
this._log.warn(
|
|
566
|
+
`Failed un-registering not registered instance: ${instanceId}`
|
|
567
|
+
)
|
|
568
|
+
return false
|
|
569
|
+
}
|
|
570
|
+
return true
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
_subscribeToMethodsOfNewInstance (className, instance) {
|
|
574
|
+
const topic = `${this._baseTopic}/${className}/${instance}/+`
|
|
575
|
+
this._mqttSubscribe(topic)
|
|
576
|
+
this._log.debug(`Subscribed to new topic after instantiation: ${topic}`)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
_unsubscribeMethodsOfDeletedInstance (className, instance) {
|
|
580
|
+
const topic = `${this._baseTopic}/${className}/${instance}/+`
|
|
581
|
+
this._mqttUnsubscribe(topic)
|
|
582
|
+
this._log.debug(`Unsubscribed from topic after deletion: ${topic}`)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
_handleReconnect () {
|
|
586
|
+
this._log.warn(`Reconnecting to ${this._broker}`)
|
|
587
|
+
this.emit('reconnect')
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
_handleError (err) {
|
|
591
|
+
this.emit('error', err)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
_handleClose () {
|
|
595
|
+
this.emit('close')
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
_handleOffline () {
|
|
599
|
+
this.emit('offline')
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
_handleEnd () {
|
|
603
|
+
this.emit('end')
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Event 'connect'
|
|
609
|
+
*
|
|
610
|
+
* Emitted on successful (re)connection (i.e. connack rc=0).
|
|
611
|
+
*
|
|
612
|
+
* @event VrpcAgent#connect
|
|
613
|
+
* @type {Object}
|
|
614
|
+
* @property {Boolean} sessionPresent - A session from a previous connection is already present
|
|
615
|
+
*/
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Event 'reconnect'
|
|
619
|
+
*
|
|
620
|
+
* Emitted when a reconnect starts.
|
|
621
|
+
*
|
|
622
|
+
* @event VrpcAgent#reconnect
|
|
623
|
+
*/
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Event 'close'
|
|
627
|
+
*
|
|
628
|
+
* Emitted after a disconnection.
|
|
629
|
+
*
|
|
630
|
+
* @event VrpcAgent#close
|
|
631
|
+
*/
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Event 'offline'
|
|
635
|
+
*
|
|
636
|
+
* Emitted when the client goes offline.
|
|
637
|
+
*
|
|
638
|
+
* @event VrpcAgent#offline
|
|
639
|
+
*/
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Event 'error'
|
|
643
|
+
*
|
|
644
|
+
* Emitted when the client cannot connect (i.e. connack rc != 0) or when a
|
|
645
|
+
* parsing error occurs. The following TLS errors will be emitted as an error
|
|
646
|
+
* event:
|
|
647
|
+
*
|
|
648
|
+
* - ECONNREFUSED
|
|
649
|
+
* - ECONNRESET
|
|
650
|
+
* - EADDRINUSE
|
|
651
|
+
* - ENOTFOUND
|
|
652
|
+
*
|
|
653
|
+
* @event VrpcAgent#error
|
|
654
|
+
* @type {Object} Error
|
|
655
|
+
*/
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Event 'end'
|
|
659
|
+
*
|
|
660
|
+
* Emitted when mqtt.Client#end() is called. If a callback was passed to
|
|
661
|
+
* mqtt.Client#end(), this event is emitted once the callback returns.
|
|
662
|
+
*
|
|
663
|
+
* @event VrpcAgent#end
|
|
664
|
+
*/
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Event 'clientGone'
|
|
668
|
+
*
|
|
669
|
+
* Emitted when a tracked VRPC client exited.
|
|
670
|
+
*
|
|
671
|
+
* @event VrpcAgent#clientGone
|
|
672
|
+
* @type {String} clientId
|
|
673
|
+
*/
|
|
674
|
+
|
|
675
|
+
module.exports = VrpcAgent
|