vrpc 3.1.0 → 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.
@@ -65,11 +65,13 @@ class VrpcClient extends EventEmitter {
65
65
  * @param {String} options.domain Sets the domain
66
66
  * @param {String} [options.agent="*"] Sets default agent
67
67
  * @param {String} [options.broker="mqtts://vrpc.io:8883"] Broker url in form: `<scheme>://<host>:<port>`
68
- * @param {Number} [options.timeout=6000] Maximum time in ms to wait for a RPC answer
68
+ * @param {Number} [options.timeout=12000] Maximum time in ms to wait for a RPC answer
69
69
  * @param {Object} [options.log=console] Log object (must support debug, info, warn, and error level)
70
- * @param {Boolean} [options.bestEffort=false] If true, message will be sent with best effort, i.e. no caching if offline
70
+ * @param {Boolean} [options.bestEffort=true] If true, message will be sent with best effort, i.e. no caching if offline
71
71
  * @param {String} [options.mqttClientId='<generated()>'] Explicitly sets the mqtt client id
72
72
  * @param {String} [options.identity] Explicitly sets a vrpc client identity
73
+ * @param {String} [options.keepalive] Sets the MQTT keepalive interval (in seconds)
74
+ * @param {Boolean} [options.requiresSchema=false] If true, any available schema information is shipped
73
75
  * @example
74
76
  * const client = new VrpcClient({
75
77
  * domain: 'vrpc',
@@ -83,11 +85,13 @@ class VrpcClient extends EventEmitter {
83
85
  domain,
84
86
  agent = '*',
85
87
  broker = 'mqtts://vrpc.io:8883',
86
- timeout = 6 * 1000,
88
+ timeout = 12 * 1000,
87
89
  log = 'console',
88
- bestEffort = false,
90
+ bestEffort = true,
89
91
  mqttClientId = null,
90
- identity = null
92
+ identity = null,
93
+ keepalive = 30,
94
+ requiresSchema = false
91
95
  } = {}) {
92
96
  super()
93
97
  // domain sanity check
@@ -117,7 +121,9 @@ class VrpcClient extends EventEmitter {
117
121
  this._broker = broker
118
122
  this._timeout = timeout
119
123
  this._identity = identity
124
+ this._keepalive = keepalive
120
125
  this._qos = this._qos = bestEffort ? 0 : 1
126
+ this._requiresSchema = requiresSchema
121
127
 
122
128
  this._instance = nanoid(8)
123
129
  this._mqttClientId = mqttClientId || this._createMqttClientId()
@@ -170,7 +176,7 @@ class VrpcClient extends EventEmitter {
170
176
  username,
171
177
  password,
172
178
  clean: true,
173
- keepalive: 30,
179
+ keepalive: this._keepalive,
174
180
  clientId: this._mqttClientId,
175
181
  rejectUnauthorized: false,
176
182
  connectTimeout: this._timeout,
@@ -201,7 +207,11 @@ class VrpcClient extends EventEmitter {
201
207
  // Agent info
202
208
  this._mqttSubscribe(`${this._domain}/${agent}/__agentInfo__`)
203
209
  // Class info
204
- this._mqttSubscribe(`${this._domain}/${agent}/+/__classInfo__`)
210
+ if (this._requiresSchema) {
211
+ this._mqttSubscribe(`${this._domain}/${agent}/+/__classInfo__`)
212
+ } else {
213
+ this._mqttSubscribe(`${this._domain}/${agent}/+/__classInfoConcise__`)
214
+ }
205
215
  // RPC responses
206
216
  this._mqttSubscribe(this._vrpcClientId)
207
217
  this.emit('connect')
@@ -222,10 +232,16 @@ class VrpcClient extends EventEmitter {
222
232
  this._agents[agent].status = status
223
233
  this._agents[agent].hostname = hostname
224
234
  this._agents[agent].version = version
235
+ if (status === 'offline') {
236
+ this._clearCachedSubscriptions({ lostAgent: agent })
237
+ }
225
238
  this.emit('agent', { domain, agent, status, hostname, version })
226
239
  // ClassInfo message
227
- } else if (instance === '__classInfo__') {
228
- // Json properties: { klass, instances, memberFunctions, staticFunctions }
240
+ } else if (
241
+ instance === '__classInfo__' ||
242
+ instance === '__classInfoConcise__'
243
+ ) {
244
+ // Json properties: { className, instances, memberFunctions, staticFunctions }
229
245
  const json = JSON.parse(message.toString())
230
246
  this._createIfNotExist(agent)
231
247
  const oldClassInfo = this._agents[agent].classes[klass]
@@ -234,15 +250,13 @@ class VrpcClient extends EventEmitter {
234
250
  const removed = oldInstances.filter(x => !newInstances.includes(x))
235
251
  const added = newInstances.filter(x => !oldInstances.includes(x))
236
252
  this._agents[agent].classes[klass] = json
237
- const {
238
- className,
239
- instances,
240
- memberFunctions,
241
- staticFunctions,
242
- meta
243
- } = json
253
+ const { className, instances, memberFunctions, staticFunctions, meta } =
254
+ json
244
255
  if (removed.length !== 0) {
245
256
  this.emit('instanceGone', removed, { domain, agent, className })
257
+ removed.forEach(lostInstance =>
258
+ this._clearCachedSubscriptions({ lostInstance })
259
+ )
246
260
  }
247
261
  if (added.length !== 0) {
248
262
  this.emit('instanceNew', added, { domain, agent, className })
@@ -254,7 +268,7 @@ class VrpcClient extends EventEmitter {
254
268
  instances,
255
269
  memberFunctions,
256
270
  staticFunctions,
257
- meta
271
+ meta: meta || {}
258
272
  })
259
273
  // RPC message
260
274
  } else {
@@ -710,10 +724,12 @@ class VrpcClient extends EventEmitter {
710
724
  `Could not publish MQTT message because: ${err.message}`
711
725
  )
712
726
  if (err.message === 'client disconnecting') {
713
- // Workaround for an already reported bug in MQTT.js (#1284)
727
+ // This most certainly relates to a bug in MQTT.js (#1284)
728
+ this._log.error(`Failed to publish, because: ${err.message}`)
729
+ this.emit('error', err)
714
730
  this._log.info('Forcing reconnect now...')
715
731
  this._client.disconnecting = false
716
- this._client.end(true, {}, () => this._client.reconnect())
732
+ this._client.reconnect()
717
733
  }
718
734
  }
719
735
  }
@@ -730,10 +746,12 @@ class VrpcClient extends EventEmitter {
730
746
  `Could not subscribe to topic(s): '${topic}', because: ${err.message}`
731
747
  )
732
748
  if (err.message === 'client disconnecting') {
733
- // Workaround for an already reported bug in MQTT.js (#1284)
749
+ // This most certainly relates to a bug in MQTT.js (#1284)
750
+ this._log.error(`Failed to subscribe, because: ${err.message}`)
751
+ this.emit('error', err)
734
752
  this._log.info('Forcing reconnect now...')
735
753
  this._client.disconnecting = false
736
- this._client.end(true, {}, () => this._client.reconnect())
754
+ this._client.reconnect()
737
755
  }
738
756
  } else {
739
757
  const topicArray = Array.isArray(topic) ? topic : [topic]
@@ -880,8 +898,9 @@ class VrpcClient extends EventEmitter {
880
898
  c: instance,
881
899
  f: functionName,
882
900
  a: [eventName],
883
- i: `${this._instance}-${this._invokeId++ %
884
- Number.MAX_SAFE_INTEGER}`,
901
+ i: `${this._instance}-${
902
+ this._invokeId++ % Number.MAX_SAFE_INTEGER
903
+ }`,
885
904
  s: this._vrpcClientId,
886
905
  v: VRPC_PROTOCOL_VERSION
887
906
  }
@@ -913,8 +932,9 @@ class VrpcClient extends EventEmitter {
913
932
  c: instance,
914
933
  f: functionName,
915
934
  a: wrapped,
916
- i: `${this._instance}-${this._invokeId++ %
917
- Number.MAX_SAFE_INTEGER}`,
935
+ i: `${this._instance}-${
936
+ this._invokeId++ % Number.MAX_SAFE_INTEGER
937
+ }`,
918
938
  s: this._vrpcClientId,
919
939
  v: VRPC_PROTOCOL_VERSION
920
940
  }
@@ -947,8 +967,9 @@ class VrpcClient extends EventEmitter {
947
967
  c: instance,
948
968
  f: functionName,
949
969
  a: wrapped.slice(1), // first argument was remote function name
950
- i: `${this._instance}-${this._invokeId++ %
951
- Number.MAX_SAFE_INTEGER}`,
970
+ i: `${this._instance}-${
971
+ this._invokeId++ % Number.MAX_SAFE_INTEGER
972
+ }`,
952
973
  s: this._vrpcClientId,
953
974
  v: VRPC_PROTOCOL_VERSION
954
975
  }
@@ -982,16 +1003,23 @@ class VrpcClient extends EventEmitter {
982
1003
  this._eventEmitter.once(i, data => {
983
1004
  clearTimeout(timer)
984
1005
  if (data.e) {
985
- reject(new Error(`[vrpc ${agent}-${c}-${f}]: ${data.e}`))
1006
+ const { message, cause } = VrpcClient._prepareError(data.e)
1007
+ reject(new Error(`[vrpc ${agent}-${c}-${f}]: ${message}`, { cause }))
986
1008
  } else {
987
1009
  const ret = data.r
988
1010
  // Handle functions returning a promise
989
1011
  if (typeof ret === 'string' && ret.substring(0, 5) === '__p__') {
990
1012
  this._eventEmitter.once(ret, promiseData => {
991
- // TODO improve error message here, like above
992
- if (promiseData.e)
993
- reject(new Error(`[vrpc ${agent}-${c}-${f}]: ${promiseData.e}`))
994
- else resolve(promiseData.r)
1013
+ if (promiseData.e) {
1014
+ const { message, cause } = VrpcClient._prepareError(
1015
+ promiseData.e
1016
+ )
1017
+ reject(
1018
+ new Error(`[vrpc ${agent}-${c}-${f}]: ${message}`, { cause })
1019
+ )
1020
+ } else {
1021
+ resolve(promiseData.r)
1022
+ }
995
1023
  })
996
1024
  } else {
997
1025
  resolve(ret)
@@ -1001,17 +1029,26 @@ class VrpcClient extends EventEmitter {
1001
1029
  })
1002
1030
  }
1003
1031
 
1032
+ static _prepareError (rpcError) {
1033
+ if (typeof rpcError === 'object') {
1034
+ return { message: rpcError.message, cause: rpcError.cause }
1035
+ }
1036
+ return { message: rpcError, cause: undefined }
1037
+ }
1038
+
1004
1039
  async _waitForInstance (instance, options = {}) {
1005
1040
  return new Promise((resolve, reject) => {
1006
- const handler = timer => (instances, { agent, className }) => {
1007
- if (instances.includes(instance)) {
1008
- if (options.agent && agent !== options.agent) return
1009
- if (options.className && className !== options.className) return
1010
- clearTimeout(timer)
1011
- this.removeListener('instanceNew', handler)
1012
- resolve({ agent, className, instance })
1041
+ const handler =
1042
+ timer =>
1043
+ (instances, { agent, className }) => {
1044
+ if (instances.includes(instance)) {
1045
+ if (options.agent && agent !== options.agent) return
1046
+ if (options.className && className !== options.className) return
1047
+ clearTimeout(timer)
1048
+ this.removeListener('instanceNew', handler)
1049
+ resolve({ agent, className, instance })
1050
+ }
1013
1051
  }
1014
- }
1015
1052
  const timer = setTimeout(() => {
1016
1053
  this.removeListener('instanceNew', handler(timer))
1017
1054
  const msg = `Could not find instance: ${instance} (> ${this._timeout} ms)`
@@ -1068,8 +1105,9 @@ class VrpcClient extends EventEmitter {
1068
1105
  } else {
1069
1106
  // Regular function callback (can be static or member function)
1070
1107
  const remoteId = proxyId || `${agent}-${className}`
1071
- const id = `__f__${remoteId}-${functionName}-${i}-${this._invokeId++ %
1072
- Number.MAX_SAFE_INTEGER}`
1108
+ const id = `__f__${remoteId}-${functionName}-${i}-${
1109
+ this._invokeId++ % Number.MAX_SAFE_INTEGER
1110
+ }`
1073
1111
  wrapped.push(id)
1074
1112
  this._eventEmitter.once(id, ({ a }) => x.apply(null, a))
1075
1113
  continue
@@ -1147,6 +1185,29 @@ class VrpcClient extends EventEmitter {
1147
1185
  return id
1148
1186
  }
1149
1187
 
1188
+ _clearCachedSubscriptions ({ lostAgent, lostInstance }) {
1189
+ const obsoleteTopics = Object.keys(this._cachedSubscriptions).filter(
1190
+ topic => {
1191
+ const [, agent, , instanceEvent] = topic.split('/')
1192
+ const [instance] = instanceEvent.split(':')
1193
+ if (agent === lostAgent || instance === lostInstance) {
1194
+ return true
1195
+ }
1196
+ return false
1197
+ }
1198
+ )
1199
+ obsoleteTopics.forEach(topic => {
1200
+ const id = `__e__${topic}`
1201
+ this._log.info(`Clearing subscriptions for obsolete topic: ${topic}`)
1202
+ const subscriptions = this._cachedSubscriptions[topic]
1203
+ subscriptions.forEach(({ handler }) => {
1204
+ this._eventEmitter.removeListener(id, handler)
1205
+ })
1206
+ this._mqttUnsubscribe(topic)
1207
+ delete this._cachedSubscriptions[topic]
1208
+ })
1209
+ }
1210
+
1150
1211
  _stripSignature (method) {
1151
1212
  const pos = method.indexOf('-')
1152
1213
  if (pos > 0) return method.substring(0, pos)
@@ -1164,7 +1225,7 @@ class VrpcClient extends EventEmitter {
1164
1225
 
1165
1226
  _isEmitter (variable) {
1166
1227
  return (
1167
- typeof variable === 'object' &&
1228
+ variable && typeof variable === 'object' &&
1168
1229
  variable.hasOwnProperty('emitter') &&
1169
1230
  variable.hasOwnProperty('event') &&
1170
1231
  typeof variable.emitter === 'object' &&