vrpc 3.0.3 → 3.1.1

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.3",
3
+ "version": "3.1.1",
4
4
  "description": "Variadic Remote Procedure Calls",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -350,9 +350,17 @@ class VrpcAdapter {
350
350
  ({ tag }) => tag === 'param' || tag === 'arg' || tag === 'argument'
351
351
  )
352
352
  if (params.length > 0) {
353
- params = params.map(({ name, optional, description, type }) => {
354
- return { name, optional, description, type }
355
- })
353
+ params = params.map(
354
+ ({
355
+ name,
356
+ optional,
357
+ description,
358
+ type,
359
+ default: defaultValue
360
+ }) => {
361
+ return { name, optional, description, type, defaultValue }
362
+ }
363
+ )
356
364
  } else {
357
365
  params = []
358
366
  }
@@ -375,7 +383,8 @@ class VrpcAdapter {
375
383
  name: 'instanceName',
376
384
  optional: false,
377
385
  description: 'Name of the instance to be created',
378
- type: 'string'
386
+ type: 'string',
387
+ defaultValue: undefined
379
388
  },
380
389
  ...params
381
390
  ]
@@ -390,7 +399,7 @@ class VrpcAdapter {
390
399
  }
391
400
 
392
401
  static _call (json) {
393
- const before = Object.keys(VrpcAdapter._listeners).length
402
+ this._mustTrackClient = false // reset client tracking flag
394
403
  switch (json.f) {
395
404
  case '__createIsolated__':
396
405
  VrpcAdapter._handleCreateIsolated(json)
@@ -408,10 +417,9 @@ class VrpcAdapter {
408
417
  // this call may have the side-effect of taking a listener on board
409
418
  VrpcAdapter._handleCall(json)
410
419
  }
411
- const after = Object.keys(VrpcAdapter._listeners).length
412
420
  // if we took a listener on board we must tell our caller such that
413
421
  // things can be cleaned up later.
414
- return after > before
422
+ return this._mustTrackClient
415
423
  }
416
424
 
417
425
  static _handleCreateIsolated (json) {
@@ -500,7 +508,8 @@ class VrpcAdapter {
500
508
  }
501
509
 
502
510
  static sanitizeEventListenerReturnValues (ret) {
503
- return typeof ret === 'object' &&
511
+ return ret &&
512
+ typeof ret === 'object' &&
504
513
  ret._events !== undefined &&
505
514
  ret._eventsCount !== undefined
506
515
  ? true
@@ -593,13 +602,7 @@ class VrpcAdapter {
593
602
  if (VrpcAdapter._isFunction(entry.removeAllListeners)) {
594
603
  entry.removeAllListeners()
595
604
  }
596
- for (const [key, { clients, instance }] of Object.entries(
597
- VrpcAdapter._listeners
598
- )) {
599
- if (instance === instanceId) {
600
- VrpcAdapter._listeners[key] = { clients }
601
- }
602
- }
605
+ delete VrpcAdapter._listeners[instanceId]
603
606
  VrpcAdapter._instances.delete(instanceId)
604
607
  return true
605
608
  }
@@ -607,18 +610,21 @@ class VrpcAdapter {
607
610
  }
608
611
 
609
612
  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
- })
613
+ Object.entries(VrpcAdapter._listeners).forEach(([ik, iv]) => {
614
+ Object.entries(iv).forEach(([ek, ev]) => {
615
+ const { clients, listener, event } = ev
616
+ if (clients.has(clientId)) {
617
+ clients.delete(clientId)
618
+ if (clients.size === 0) {
619
+ const instance = VrpcAdapter.getInstance(ik)
620
+ if (instance && instance.removeListener) {
621
+ instance.removeListener(event, listener)
622
+ }
623
+ delete VrpcAdapter._listeners[ik][ek]
624
+ }
618
625
  }
619
- delete VrpcAdapter._listeners[key]
620
- }
621
- }
626
+ })
627
+ })
622
628
  }
623
629
 
624
630
  static _validate (schema, params) {
@@ -630,102 +636,162 @@ class VrpcAdapter {
630
636
  }
631
637
 
632
638
  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] : []
639
+ const clientId = json.s
640
+ // if we came from a "callAll" we will have an instanceId defined
641
+ // things are different for callAll as we have to dispatch the instanceIds
642
+ const isCallAll = instanceId !== undefined
643
+ const context = isCallAll ? instanceId : json.c
644
+ const args = isCallAll ? json.a.slice(1) : json.a
645
+ const func = isCallAll ? json.a[0] : json.f
646
+ const unwrapped = isCallAll ? [func] : []
644
647
  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
648
+ if (typeof arg === 'string') {
649
+ if (arg.startsWith('__f__')) {
650
+ // function callback
651
+ unwrapped.push(
652
+ this._generateCallback({
653
+ clientId,
654
+ isCallAll,
655
+ instanceId: context,
656
+ eventId: arg
657
+ })
658
+ )
659
+ } else if (arg.startsWith('__e__')) {
660
+ // event registration
661
+ if (func === 'on' || func === 'addListener') {
662
+ this._mustTrackClient = true
663
+ const listener = VrpcAdapter._registerListener({
664
+ clientId,
665
+ isCallAll,
666
+ instanceId: context,
667
+ eventId: arg,
668
+ event: args[0]
669
+ })
670
+ if (
671
+ VrpcAdapter.getInstance(context)
672
+ .listeners(arg)
673
+ .includes(listener)
674
+ ) {
675
+ return null // skip call as listener is already registered
676
+ }
677
+ unwrapped.push(listener)
678
+ // event un-registration
679
+ } else if (func === 'off' || func === 'removeListener') {
680
+ const listener = VrpcAdapter._unregisterListener({
681
+ clientId,
682
+ instanceId: context,
683
+ eventId: arg
684
+ })
685
+ if (!listener) {
686
+ return null // skip call as others are still interested
687
+ }
688
+ unwrapped.push(listener)
689
+ } else {
690
+ this._mustTrackClient = true
691
+ const listener = VrpcAdapter._registerListener({
692
+ clientId,
693
+ isCallAll,
694
+ instanceId: context,
695
+ eventId: arg,
696
+ event: null
697
+ })
698
+ unwrapped.push(listener)
659
699
  }
660
700
  } 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
701
+ unwrapped.push(arg)
690
702
  }
691
703
  } else {
692
- // default behavior is to not touch
704
+ // regular argument
693
705
  unwrapped.push(arg)
694
706
  }
695
707
  }
696
708
  return unwrapped
697
709
  }
698
710
 
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)
711
+ static _generateCallback ({ eventId, clientId, instanceId, isCallAll }) {
712
+ return (...innerArgs) => {
713
+ // "callAll" event notifications expect the instanceId as first argument
714
+ const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
715
+ VrpcAdapter._callback({ a, s: clientId, i: eventId })
716
+ }
717
+ }
718
+
719
+ static _registerListener ({
720
+ eventId,
721
+ clientId,
722
+ instanceId,
723
+ event,
724
+ isCallAll
725
+ }) {
726
+ if (!VrpcAdapter._listeners[instanceId]) {
727
+ VrpcAdapter._listeners[instanceId] = {
728
+ [eventId]: {
729
+ event,
730
+ clients: new Set([clientId]),
731
+ listener: VrpcAdapter._generateListener({
732
+ eventId,
733
+ instanceId,
734
+ isCallAll
735
+ })
706
736
  }
707
737
  }
738
+ } else if (!VrpcAdapter._listeners[instanceId][eventId]) {
739
+ VrpcAdapter._listeners[instanceId][eventId] = {
740
+ event,
741
+ clients: new Set([clientId]),
742
+ listener: VrpcAdapter._generateListener({
743
+ eventId,
744
+ instanceId,
745
+ isCallAll
746
+ })
747
+ }
748
+ } else {
749
+ VrpcAdapter._listeners[instanceId][eventId].clients.add(clientId)
708
750
  }
751
+ return VrpcAdapter._listeners[instanceId][eventId].listener
709
752
  }
710
753
 
711
- static _generateCallback (id, sender, instanceId) {
754
+ static _generateListener ({ eventId, instanceId, isCallAll }) {
712
755
  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 })
756
+ const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
757
+ VrpcAdapter._callback({ a, i: eventId })
716
758
  }
717
759
  }
718
760
 
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 })
761
+ static _unregisterListener ({ eventId, clientId, instanceId }) {
762
+ if (
763
+ VrpcAdapter._listeners[instanceId] &&
764
+ VrpcAdapter._listeners[instanceId][eventId]
765
+ ) {
766
+ const { listener, clients } = VrpcAdapter._listeners[instanceId][eventId]
767
+ clients.delete(clientId)
768
+ if (clients.size === 0) {
769
+ delete VrpcAdapter._listeners[instanceId][eventId]
770
+ if (Object.keys(VrpcAdapter._listeners[instanceId]).length === 0) {
771
+ delete VrpcAdapter._listeners[instanceId]
772
+ }
773
+ return listener
774
+ }
726
775
  }
727
776
  }
728
777
 
778
+ static _removeAllListeners (event, clientId, instanceId) {
779
+ const eventIds = VrpcAdapter._listeners[instanceId]
780
+ if (!eventIds) return
781
+ Object.entries(eventIds).forEach(([ek, ev]) => {
782
+ if (ev.event === event && ev.clients.has(clientId)) {
783
+ ev.clients.delete(clientId)
784
+ if (ev.clients.size === 0) {
785
+ VrpcAdapter.getInstance(instanceId).removeListener(
786
+ ev.event,
787
+ ev.listener
788
+ )
789
+ delete VrpcAdapter._listeners[instanceId][ek]
790
+ }
791
+ }
792
+ })
793
+ }
794
+
729
795
  static _handlePromise (json, promise) {
730
796
  try {
731
797
  const cid = VrpcAdapter._correlationId++ % Number.MAX_SAFE_INTEGER
package/vrpc/VrpcAgent.js CHANGED
@@ -92,7 +92,7 @@ class VrpcAgent extends EventEmitter {
92
92
  *
93
93
  * @constructor
94
94
  * @param {Object} obj
95
- * @param {String} [obj.username='<user>-<pathId>@<hostname>-<platform>-js'] MQTT username
95
+ * @param {String} [obj.username] MQTT username
96
96
  * @param {String} [obj.password] MQTT password (if no token is provided)
97
97
  * @param {String} [obj.token] Access token
98
98
  * @param {String} [obj.domain='vrpc'] The domain under which the agent-provided code is reachable
@@ -101,6 +101,7 @@ class VrpcAgent extends EventEmitter {
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({
@@ -109,15 +110,16 @@ class VrpcAgent extends EventEmitter {
109
110
  * })
110
111
  */
111
112
  constructor ({
112
- token,
113
+ username,
113
114
  password,
114
- username = VrpcAgent._generateAgentName(),
115
+ token,
115
116
  domain = 'vrpc',
116
117
  agent = VrpcAgent._generateAgentName(),
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,23 +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) {
176
- username = username || this._agent
179
+ username = `${this._domain}/${this._agent}`
177
180
  password = this._token
181
+ } else if (!this._password) {
182
+ username = `${this._domain}/${this._agent}`
183
+ password = this._generateToken()
178
184
  }
179
185
  this._options = {
180
186
  username,
181
187
  password,
182
188
  keepalive: 30,
183
189
  connectTimeout: 10 * 1000,
184
- clientId: `va3${md5}`,
190
+ clientId: this._mqttClientId,
185
191
  rejectUnauthorized: false,
186
192
  will: {
187
193
  topic: `${this._baseTopic}/__agentInfo__`,
@@ -194,12 +200,7 @@ class VrpcAgent extends EventEmitter {
194
200
  this._log.info(`Agent : ${this._agent}`)
195
201
  this._log.info(`Broker : ${this._broker}`)
196
202
  this._log.info('Connecting to MQTT server...')
197
- const wasEnded = await this._clearPersistedSession()
198
- if (wasEnded) return
199
- this._client = mqtt.connect(this._broker, {
200
- ...this._options,
201
- clean: false
202
- })
203
+ this._client = mqtt.connect(this._broker, this._options)
203
204
  this._client.on('connect', this._handleConnect.bind(this))
204
205
  this._client.on('reconnect', this._handleReconnect.bind(this))
205
206
  this._client.on('error', this._handleError.bind(this))
@@ -238,16 +239,11 @@ class VrpcAgent extends EventEmitter {
238
239
  }
239
240
  }
240
241
  await new Promise(resolve => this._client.end(false, {}, resolve))
241
- await this._clearPersistedSession()
242
242
  } catch (err) {
243
243
  this._log.error(err, `Problem during disconnecting agent: ${err.message}`)
244
244
  }
245
245
  }
246
246
 
247
- static _generateUserName () {
248
- return `${os.userInfo().username}@${os.hostname()}-${os.platform()}-js`
249
- }
250
-
251
247
  static _generateAgentName () {
252
248
  const { username } = os.userInfo()
253
249
  const pathId = crypto
@@ -258,6 +254,27 @@ class VrpcAgent extends EventEmitter {
258
254
  return `${username}-${pathId}@${os.hostname()}-${os.platform()}-js`
259
255
  }
260
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
+
261
278
  _validateDomain (domain) {
262
279
  if (!domain) throw new Error('The domain must be specified')
263
280
  if (domain.match(/[+/#*]/)) {
@@ -285,27 +302,6 @@ class VrpcAgent extends EventEmitter {
285
302
  })
286
303
  }
287
304
 
288
- /**
289
- * @return {Boolean} true if `agent.end()` was explicitly called
290
- */
291
- async _clearPersistedSession () {
292
- // Clear potentially existing persisted sessions
293
- const client = mqtt.connect(this._broker, { ...this._options, clean: true })
294
- client.on('error', err => this.emit('error', err))
295
- client.on('offline', () => this.emit('offline'))
296
- client.on('reconnect', () => this.emit('reconnect'))
297
- return new Promise(resolve => {
298
- client.on('connect', () => {
299
- client.end(false, {}, () => resolve(false))
300
- })
301
- // Will be triggered if the user called 'agent.end()'
302
- this.once('end', () => {
303
- client.end()
304
- resolve(true)
305
- })
306
- })
307
- }
308
-
309
305
  _mqttPublish (topic, message, options) {
310
306
  this._client.publish(
311
307
  topic,
@@ -328,9 +324,38 @@ class VrpcAgent extends EventEmitter {
328
324
  (err, granted) => {
329
325
  if (err) {
330
326
  this._log.warn(
331
- `Could not subscribe to topic '${topic}', because: ${err.message}`
327
+ `Could not subscribe to topic(s) '${topic}', because: ${err.message}`
332
328
  )
333
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
+ }
334
359
  if (granted.length === 0) {
335
360
  this._log.debug(`Already subscribed to topic '${topic}'`)
336
361
  }
@@ -375,6 +400,8 @@ class VrpcAgent extends EventEmitter {
375
400
  resolve()
376
401
  } else {
377
402
  this._client.once('connect', resolve)
403
+ // Will be triggered if the user called 'agent.end()'
404
+ this.once('end', resolve)
378
405
  }
379
406
  })
380
407
  }
@@ -413,16 +440,6 @@ class VrpcAgent extends EventEmitter {
413
440
 
414
441
  _handleConnect () {
415
442
  this._log.info('[OK]')
416
- if (this._isReconnect) {
417
- this._publishAgentInfoMessage()
418
- const classes = this._getClasses()
419
- classes.forEach(className => {
420
- this._publishClassInfoMessage(className)
421
- })
422
- this.emit('connect')
423
- this._isReconnect = false
424
- return
425
- }
426
443
  try {
427
444
  const topics = this._generateTopics()
428
445
  if (topics.length > 0) this._mqttSubscribe(topics)
@@ -670,7 +687,6 @@ class VrpcAgent extends EventEmitter {
670
687
  }
671
688
 
672
689
  _handleReconnect () {
673
- this._isReconnect = true
674
690
  this._log.warn(`Reconnecting to ${this._broker}`)
675
691
  this.emit('reconnect')
676
692
  }