vrpc 3.0.2 → 3.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vrpc",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "description": "Variadic Remote Procedure Calls",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -390,7 +390,7 @@ class VrpcAdapter {
390
390
  }
391
391
 
392
392
  static _call (json) {
393
- const before = Object.keys(VrpcAdapter._listeners).length
393
+ this._mustTrackClient = false // reset client tracking flag
394
394
  switch (json.f) {
395
395
  case '__createIsolated__':
396
396
  VrpcAdapter._handleCreateIsolated(json)
@@ -408,10 +408,9 @@ class VrpcAdapter {
408
408
  // this call may have the side-effect of taking a listener on board
409
409
  VrpcAdapter._handleCall(json)
410
410
  }
411
- const after = Object.keys(VrpcAdapter._listeners).length
412
411
  // if we took a listener on board we must tell our caller such that
413
412
  // things can be cleaned up later.
414
- return after > before
413
+ return this._mustTrackClient
415
414
  }
416
415
 
417
416
  static _handleCreateIsolated (json) {
@@ -500,7 +499,7 @@ class VrpcAdapter {
500
499
  }
501
500
 
502
501
  static sanitizeEventListenerReturnValues (ret) {
503
- return typeof ret === 'object' &&
502
+ return ret && typeof ret === 'object' &&
504
503
  ret._events !== undefined &&
505
504
  ret._eventsCount !== undefined
506
505
  ? true
@@ -593,13 +592,7 @@ class VrpcAdapter {
593
592
  if (VrpcAdapter._isFunction(entry.removeAllListeners)) {
594
593
  entry.removeAllListeners()
595
594
  }
596
- for (const [key, { clients, instance }] of Object.entries(
597
- VrpcAdapter._listeners
598
- )) {
599
- if (instance === instanceId) {
600
- VrpcAdapter._listeners[key] = { clients }
601
- }
602
- }
595
+ delete VrpcAdapter._listeners[instanceId]
603
596
  VrpcAdapter._instances.delete(instanceId)
604
597
  return true
605
598
  }
@@ -607,18 +600,21 @@ class VrpcAdapter {
607
600
  }
608
601
 
609
602
  static _unregisterClient (clientId) {
610
- for (const [key, value] of Object.entries(VrpcAdapter._listeners)) {
611
- const { instances, event, listener, clients } = value
612
- clients.delete(clientId)
613
- if (clients.size === 0) {
614
- if (event) {
615
- instances.forEach(instance => {
616
- VrpcAdapter.getInstance(instance).removeListener(event, listener)
617
- })
603
+ Object.entries(VrpcAdapter._listeners).forEach(([ik, iv]) => {
604
+ Object.entries(iv).forEach(([ek, ev]) => {
605
+ const { clients, listener, event } = ev
606
+ if (clients.has(clientId)) {
607
+ clients.delete(clientId)
608
+ if (clients.size === 0) {
609
+ const instance = VrpcAdapter.getInstance(ik)
610
+ if (instance && instance.removeListener) {
611
+ instance.removeListener(event, listener)
612
+ }
613
+ delete VrpcAdapter._listeners[ik][ek]
614
+ }
618
615
  }
619
- delete VrpcAdapter._listeners[key]
620
- }
621
- }
616
+ })
617
+ })
622
618
  }
623
619
 
624
620
  static _validate (schema, params) {
@@ -630,102 +626,162 @@ class VrpcAdapter {
630
626
  }
631
627
 
632
628
  static _unwrapArguments (json, instanceId) {
633
- const sender = json.s
634
- let context = json.c
635
- let func = json.f
636
- let args = json.a
637
- // things are different if we came from callAll (instanceId !== undefined)
638
- if (instanceId) {
639
- context = instanceId
640
- func = args[0]
641
- args = args.slice(1)
642
- }
643
- const unwrapped = instanceId ? [func] : []
629
+ const clientId = json.s
630
+ // if we came from a "callAll" we will have an instanceId defined
631
+ // things are different for callAll as we have to dispatch the instanceIds
632
+ const isCallAll = instanceId !== undefined
633
+ const context = isCallAll ? instanceId : json.c
634
+ const args = isCallAll ? json.a.slice(1) : json.a
635
+ const func = isCallAll ? json.a[0] : json.f
636
+ const unwrapped = isCallAll ? [func] : []
644
637
  for (const arg of args) {
645
- // find those args that actually need to be functions
646
- if (typeof arg === 'string' && arg.slice(0, 5) === '__f__') {
647
- const callback = this._generateCallback(arg, sender, instanceId)
648
- unwrapped.push(callback)
649
- } else if (typeof arg === 'string' && arg.slice(0, 5) === '__e__') {
650
- const listener = this._generateListener(arg, sender, instanceId)
651
- unwrapped.push(listener)
652
- // start tracking clients that are registering event listeners
653
- if (!VrpcAdapter._listeners[arg]) {
654
- VrpcAdapter._listeners[arg] = {
655
- listener,
656
- clients: new Set([sender]),
657
- instances: new Set(),
658
- event: null
638
+ if (typeof arg === 'string') {
639
+ if (arg.startsWith('__f__')) {
640
+ // function callback
641
+ unwrapped.push(
642
+ this._generateCallback({
643
+ clientId,
644
+ isCallAll,
645
+ instanceId: context,
646
+ eventId: arg
647
+ })
648
+ )
649
+ } else if (arg.startsWith('__e__')) {
650
+ // event registration
651
+ if (func === 'on' || func === 'addListener') {
652
+ this._mustTrackClient = true
653
+ const listener = VrpcAdapter._registerListener({
654
+ clientId,
655
+ isCallAll,
656
+ instanceId: context,
657
+ eventId: arg,
658
+ event: args[0]
659
+ })
660
+ if (
661
+ VrpcAdapter.getInstance(context)
662
+ .listeners(arg)
663
+ .includes(listener)
664
+ ) {
665
+ return null // skip call as listener is already registered
666
+ }
667
+ unwrapped.push(listener)
668
+ // event un-registration
669
+ } else if (func === 'off' || func === 'removeListener') {
670
+ const listener = VrpcAdapter._unregisterListener({
671
+ clientId,
672
+ instanceId: context,
673
+ eventId: arg
674
+ })
675
+ if (!listener) {
676
+ return null // skip call as others are still interested
677
+ }
678
+ unwrapped.push(listener)
679
+ } else {
680
+ this._mustTrackClient = true
681
+ const listener = VrpcAdapter._registerListener({
682
+ clientId,
683
+ isCallAll,
684
+ instanceId: context,
685
+ eventId: arg,
686
+ event: null
687
+ })
688
+ unwrapped.push(listener)
659
689
  }
660
690
  } else {
661
- VrpcAdapter._listeners[arg].clients.add(sender)
662
- }
663
- // see whether the requested listener is an EventEmitter registration
664
- if (func === 'on' || func === 'addListener') {
665
- // if this event is already registered on the given instance
666
- // avoid a second registration
667
- if (
668
- VrpcAdapter._listeners[arg].event &&
669
- VrpcAdapter._listeners[arg].instances.has(context)
670
- ) {
671
- return null // means that this call won't be forwarded
672
- }
673
- VrpcAdapter._listeners[arg].event = args[0]
674
- VrpcAdapter._listeners[arg].instances.add(context)
675
- }
676
- if (func === 'off' || func === 'removeListener') {
677
- const {
678
- clients,
679
- instances,
680
- listener,
681
- event
682
- } = VrpcAdapter._listeners[arg]
683
- clients.delete(sender)
684
- instances.delete(context)
685
- if (clients.size === 0) {
686
- delete VrpcAdapter._listeners[arg]
687
- VrpcAdapter.getInstance(context).off(event, listener)
688
- }
689
- return null // means that this call won't be forwarded
691
+ unwrapped.push(arg)
690
692
  }
691
693
  } else {
692
- // default behavior is to not touch
694
+ // regular argument
693
695
  unwrapped.push(arg)
694
696
  }
695
697
  }
696
698
  return unwrapped
697
699
  }
698
700
 
699
- static _removeAllListeners (event, sender, context) {
700
- for (const [key, value] of Object.entries(VrpcAdapter._listeners)) {
701
- if (value.event === event) {
702
- value.clients.delete(sender)
703
- if (value.clients.size === 0) {
704
- delete VrpcAdapter._listeners[key]
705
- VrpcAdapter.getInstance(context).off(event, value.listener)
701
+ static _generateCallback ({ eventId, clientId, instanceId, isCallAll }) {
702
+ return (...innerArgs) => {
703
+ // "callAll" event notifications expect the instanceId as first argument
704
+ const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
705
+ VrpcAdapter._callback({ a, s: clientId, i: eventId })
706
+ }
707
+ }
708
+
709
+ static _registerListener ({
710
+ eventId,
711
+ clientId,
712
+ instanceId,
713
+ event,
714
+ isCallAll
715
+ }) {
716
+ if (!VrpcAdapter._listeners[instanceId]) {
717
+ VrpcAdapter._listeners[instanceId] = {
718
+ [eventId]: {
719
+ event,
720
+ clients: new Set([clientId]),
721
+ listener: VrpcAdapter._generateListener({
722
+ eventId,
723
+ instanceId,
724
+ isCallAll
725
+ })
706
726
  }
707
727
  }
728
+ } else if (!VrpcAdapter._listeners[instanceId][eventId]) {
729
+ VrpcAdapter._listeners[instanceId][eventId] = {
730
+ event,
731
+ clients: new Set([clientId]),
732
+ listener: VrpcAdapter._generateListener({
733
+ eventId,
734
+ instanceId,
735
+ isCallAll
736
+ })
737
+ }
738
+ } else {
739
+ VrpcAdapter._listeners[instanceId][eventId].clients.add(clientId)
708
740
  }
741
+ return VrpcAdapter._listeners[instanceId][eventId].listener
709
742
  }
710
743
 
711
- static _generateCallback (id, sender, instanceId) {
744
+ static _generateListener ({ eventId, instanceId, isCallAll }) {
712
745
  return (...innerArgs) => {
713
- // callAll expects the instanceId as first argument
714
- const a = instanceId ? [instanceId, ...innerArgs] : [...innerArgs]
715
- VrpcAdapter._callback({ a, s: sender, i: id })
746
+ const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
747
+ VrpcAdapter._callback({ a, i: eventId })
716
748
  }
717
749
  }
718
750
 
719
- static _generateListener (id, sender, instanceId) {
720
- return (...innerArgs) => {
721
- // do not even emit for already offline clients
722
- if (!VrpcAdapter._listeners[id]) return
723
- // callAll expects the instanceId as first argument
724
- const a = instanceId ? [instanceId, ...innerArgs] : [...innerArgs]
725
- VrpcAdapter._callback({ a, s: sender, i: id })
751
+ static _unregisterListener ({ eventId, clientId, instanceId }) {
752
+ if (
753
+ VrpcAdapter._listeners[instanceId] &&
754
+ VrpcAdapter._listeners[instanceId][eventId]
755
+ ) {
756
+ const { listener, clients } = VrpcAdapter._listeners[instanceId][eventId]
757
+ clients.delete(clientId)
758
+ if (clients.size === 0) {
759
+ delete VrpcAdapter._listeners[instanceId][eventId]
760
+ if (Object.keys(VrpcAdapter._listeners[instanceId]).length === 0) {
761
+ delete VrpcAdapter._listeners[instanceId]
762
+ }
763
+ return listener
764
+ }
726
765
  }
727
766
  }
728
767
 
768
+ static _removeAllListeners (event, clientId, instanceId) {
769
+ const eventIds = VrpcAdapter._listeners[instanceId]
770
+ if (!eventIds) return
771
+ Object.entries(eventIds).forEach(([ek, ev]) => {
772
+ if (ev.event === event && ev.clients.has(clientId)) {
773
+ ev.clients.delete(clientId)
774
+ if (ev.clients.size === 0) {
775
+ VrpcAdapter.getInstance(instanceId).removeListener(
776
+ ev.event,
777
+ ev.listener
778
+ )
779
+ delete VrpcAdapter._listeners[instanceId][ek]
780
+ }
781
+ }
782
+ })
783
+ }
784
+
729
785
  static _handlePromise (json, promise) {
730
786
  try {
731
787
  const cid = VrpcAdapter._correlationId++ % Number.MAX_SAFE_INTEGER
package/vrpc/VrpcAgent.js CHANGED
@@ -92,15 +92,16 @@ class VrpcAgent extends EventEmitter {
92
92
  *
93
93
  * @constructor
94
94
  * @param {Object} obj
95
- * @param {String} [obj.username] MQTT username (if no token is used)
95
+ * @param {String} [obj.username] MQTT username
96
96
  * @param {String} [obj.password] MQTT password (if no token is provided)
97
- * @param {String} [obj.token] Access token as generated by: https://app.vrpc.io (only optional when using default domain and broker)
97
+ * @param {String} [obj.token] Access token
98
98
  * @param {String} [obj.domain='vrpc'] The domain under which the agent-provided code is reachable
99
99
  * @param {String} [obj.agent='<user>-<pathId>@<hostname>-<platform>-js'] This agent's name
100
100
  * @param {String} [obj.broker='mqtts://vrpc.io:8883'] Broker url in form: `<scheme>://<host>:<port>`
101
101
  * @param {Object} [obj.log=console] Log object (must support debug, info, warn, and error level)
102
102
  * @param {String} [obj.bestEffort=false] If true, message will be sent with best effort, i.e. no caching if offline
103
103
  * @param {String} [obj.version=''] The (user-defined) version of this agent
104
+ * @param {String} [obj.mqttClientId='<generated()>'] Explicitly set the mqtt client id.
104
105
  *
105
106
  * @example
106
107
  * const agent = new Agent({
@@ -117,7 +118,8 @@ class VrpcAgent extends EventEmitter {
117
118
  broker = 'mqtts://vrpc.io:8883',
118
119
  log = 'console',
119
120
  bestEffort = false,
120
- version = ''
121
+ version = '',
122
+ mqttClientId = null
121
123
  } = {}) {
122
124
  super()
123
125
  this._validateDomain(domain)
@@ -130,7 +132,13 @@ class VrpcAgent extends EventEmitter {
130
132
  this._broker = broker
131
133
  this._qos = bestEffort ? 0 : 1
132
134
  this._version = version
133
- this._isReconnect = false
135
+ this._mqttClientId =
136
+ mqttClientId ||
137
+ `va3${crypto
138
+ .createHash('md5')
139
+ .update(this._domain + this._agent)
140
+ .digest('hex')
141
+ .substring(0, 20)}`
134
142
  if (log === 'console') {
135
143
  this._log = console
136
144
  this._log.debug = () => {}
@@ -165,22 +173,21 @@ class VrpcAgent extends EventEmitter {
165
173
  * rejects
166
174
  */
167
175
  async serve () {
168
- const md5 = crypto
169
- .createHash('md5')
170
- .update(this._domain + this._agent)
171
- .digest('hex')
172
- .substr(0, 20)
173
176
  let username = this._username
174
177
  let password = this._password
175
178
  if (this._token) {
179
+ username = `${this._domain}/${this._agent}`
176
180
  password = this._token
181
+ } else if (!this._password) {
182
+ username = `${this._domain}/${this._agent}`
183
+ password = this._generateToken()
177
184
  }
178
185
  this._options = {
179
186
  username,
180
187
  password,
181
188
  keepalive: 30,
182
189
  connectTimeout: 10 * 1000,
183
- clientId: `va3${md5}`,
190
+ clientId: this._mqttClientId,
184
191
  rejectUnauthorized: false,
185
192
  will: {
186
193
  topic: `${this._baseTopic}/__agentInfo__`,
@@ -193,12 +200,7 @@ class VrpcAgent extends EventEmitter {
193
200
  this._log.info(`Agent : ${this._agent}`)
194
201
  this._log.info(`Broker : ${this._broker}`)
195
202
  this._log.info('Connecting to MQTT server...')
196
- const wasEnded = await this._clearPersistedSession()
197
- if (wasEnded) return
198
- this._client = mqtt.connect(this._broker, {
199
- ...this._options,
200
- clean: false
201
- })
203
+ this._client = mqtt.connect(this._broker, this._options)
202
204
  this._client.on('connect', this._handleConnect.bind(this))
203
205
  this._client.on('reconnect', this._handleReconnect.bind(this))
204
206
  this._client.on('error', this._handleError.bind(this))
@@ -237,7 +239,6 @@ class VrpcAgent extends EventEmitter {
237
239
  }
238
240
  }
239
241
  await new Promise(resolve => this._client.end(false, {}, resolve))
240
- await this._clearPersistedSession()
241
242
  } catch (err) {
242
243
  this._log.error(err, `Problem during disconnecting agent: ${err.message}`)
243
244
  }
@@ -249,10 +250,31 @@ class VrpcAgent extends EventEmitter {
249
250
  .createHash('md5')
250
251
  .update(path.resolve())
251
252
  .digest('hex')
252
- .substr(0, 4)
253
+ .substring(0, 4)
253
254
  return `${username}-${pathId}@${os.hostname()}-${os.platform()}-js`
254
255
  }
255
256
 
257
+ _generateToken () {
258
+ const uid =
259
+ this._domain +
260
+ this._agent +
261
+ os.userInfo().username +
262
+ path.resolve() +
263
+ os.arch() +
264
+ os.homedir() +
265
+ os.hostname() +
266
+ os.platform() +
267
+ os.release() +
268
+ os.totalmem() +
269
+ os.type() +
270
+ JSON.stringify(os.networkInterfaces()) +
271
+ JSON.stringify(os.cpus().map(({ model }) => model))
272
+ return crypto
273
+ .createHash('md5')
274
+ .update(uid)
275
+ .digest('hex')
276
+ }
277
+
256
278
  _validateDomain (domain) {
257
279
  if (!domain) throw new Error('The domain must be specified')
258
280
  if (domain.match(/[+/#*]/)) {
@@ -280,27 +302,6 @@ class VrpcAgent extends EventEmitter {
280
302
  })
281
303
  }
282
304
 
283
- /**
284
- * @return {Boolean} true if `agent.end()` was explicitly called
285
- */
286
- async _clearPersistedSession () {
287
- // Clear potentially existing persisted sessions
288
- const client = mqtt.connect(this._broker, { ...this._options, clean: true })
289
- client.on('error', err => this.emit('error', err))
290
- client.on('offline', () => this.emit('offline'))
291
- client.on('reconnect', () => this.emit('reconnect'))
292
- return new Promise(resolve => {
293
- client.on('connect', () => {
294
- client.end(false, {}, () => resolve(false))
295
- })
296
- // Will be triggered if the user called 'agent.end()'
297
- this.once('end', () => {
298
- client.end()
299
- resolve(true)
300
- })
301
- })
302
- }
303
-
304
305
  _mqttPublish (topic, message, options) {
305
306
  this._client.publish(
306
307
  topic,
@@ -323,9 +324,38 @@ class VrpcAgent extends EventEmitter {
323
324
  (err, granted) => {
324
325
  if (err) {
325
326
  this._log.warn(
326
- `Could not subscribe to topic '${topic}', because: ${err.message}`
327
+ `Could not subscribe to topic(s) '${topic}', because: ${err.message}`
327
328
  )
328
329
  } else {
330
+ const topicArray = Array.isArray(topic) ? topic : [topic]
331
+ const erroneousGranted = granted
332
+ .filter(x => x.qos === 128)
333
+ .map(x => x.topic)
334
+ if (erroneousGranted.length > 0) {
335
+ err = new Error(
336
+ `Could not subscribe all ${topicArray.length} topic(s) but got error qos=128 on following ${erroneousGranted.length} topic(s): ${erroneousGranted}`
337
+ )
338
+ err.code = 'SUBSCRIBE_FAILED'
339
+ err.subscribeOptions = options
340
+ this._log.error(err)
341
+ this.emit('error', err)
342
+ }
343
+ const reducedQos = granted.filter(x => x.qos < this._qos)
344
+ if (reducedQos.length > 0) {
345
+ err = new Error(
346
+ `Could not subscribe all ${
347
+ topicArray.length
348
+ } topic(s) at desired qos=${
349
+ this._qos
350
+ } but got reduced qos on following ${
351
+ reducedQos.length
352
+ } topic(s): ${JSON.stringify(reducedQos)}`
353
+ )
354
+ err.code = 'SUBSCRIBE_REDUCED_QOS'
355
+ err.subscribeOptions = options
356
+ this._log.warn(err)
357
+ this.emit('error', err)
358
+ }
329
359
  if (granted.length === 0) {
330
360
  this._log.debug(`Already subscribed to topic '${topic}'`)
331
361
  }
@@ -370,6 +400,8 @@ class VrpcAgent extends EventEmitter {
370
400
  resolve()
371
401
  } else {
372
402
  this._client.once('connect', resolve)
403
+ // Will be triggered if the user called 'agent.end()'
404
+ this.once('end', resolve)
373
405
  }
374
406
  })
375
407
  }
@@ -408,16 +440,6 @@ class VrpcAgent extends EventEmitter {
408
440
 
409
441
  _handleConnect () {
410
442
  this._log.info('[OK]')
411
- if (this._isReconnect) {
412
- this._publishAgentInfoMessage()
413
- const classes = this._getClasses()
414
- classes.forEach(className => {
415
- this._publishClassInfoMessage(className)
416
- })
417
- this.emit('connect')
418
- this._isReconnect = false
419
- return
420
- }
421
443
  try {
422
444
  const topics = this._generateTopics()
423
445
  if (topics.length > 0) this._mqttSubscribe(topics)
@@ -665,7 +687,6 @@ class VrpcAgent extends EventEmitter {
665
687
  }
666
688
 
667
689
  _handleReconnect () {
668
- this._isReconnect = true
669
690
  this._log.warn(`Reconnecting to ${this._broker}`)
670
691
  this.emit('reconnect')
671
692
  }