vrpc 3.5.3 → 3.7.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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/vrpc/VrpcClient.js +83 -99
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vrpc",
3
- "version": "3.5.3",
3
+ "version": "3.7.0",
4
4
  "description": "Variadic Remote Procedure Calls",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -45,7 +45,7 @@
45
45
  "bugs": {
46
46
  "url": "https://github.com/heisenware/vrpc/issues"
47
47
  },
48
- "homepage": "https://vrpc.io",
48
+ "homepage": "https://docs.heisenware.com/developers/vrpc",
49
49
  "devDependencies": {
50
50
  "@heisenware/storage": "^1.2.2",
51
51
  "chai": "^4.3.10",
@@ -235,8 +235,8 @@ class VrpcClient extends EventEmitter {
235
235
  `${this._domain}/${agent}/+/__classInfoConcise__`
236
236
  )
237
237
  }
238
- // RPC responses
239
- await this._mqttSubscribe(this._vrpcClientId)
238
+ // Single wildcard catches BOTH RPC responses (length 3) AND events (length 4)
239
+ await this._mqttSubscribe(`${this._vrpcClientId}/#`)
240
240
 
241
241
  // Re-subscribe to all cached event topics
242
242
  this._log.debug('Restoring event subscriptions after reconnect...')
@@ -764,7 +764,7 @@ class VrpcClient extends EventEmitter {
764
764
 
765
765
  // Private functions
766
766
 
767
- static _createHash (str, length = 20) {
767
+ static _createHash (str, length = 16) {
768
768
  // Extended DJB2 hash with two accumulators
769
769
  let hash1 = 5381
770
770
  let hash2 = 52711 // different seed
@@ -1001,13 +1001,24 @@ class VrpcClient extends EventEmitter {
1001
1001
  if (!eventName) {
1002
1002
  throw new Error('VRPC does not support removing all listeners')
1003
1003
  }
1004
- const topic = `${this._domain}/${agent}/${className}/${instance}:${eventName}`
1005
- const id = `__e__${topic}`
1006
- if (this._cachedSubscriptions[topic]) {
1007
- this._eventEmitter.removeAllListeners(id)
1008
- await this._mqttUnsubscribe(topic)
1004
+
1005
+ const obsoleteTopics = Object.keys(this._cachedSubscriptions).filter(
1006
+ topic => {
1007
+ const sub = this._cachedSubscriptions[topic]
1008
+ return sub.instance === instance && sub.event === eventName
1009
+ }
1010
+ )
1011
+
1012
+ obsoleteTopics.forEach(topic => {
1013
+ const id = `__e__${topic}`
1014
+ const sub = this._cachedSubscriptions[topic]
1015
+
1016
+ sub.listeners.forEach(({ handler }) => {
1017
+ this._eventEmitter.removeListener(id, handler)
1018
+ })
1019
+
1009
1020
  delete this._cachedSubscriptions[topic]
1010
- }
1021
+ })
1011
1022
  try {
1012
1023
  const json = {
1013
1024
  c: instance,
@@ -1182,20 +1193,22 @@ class VrpcClient extends EventEmitter {
1182
1193
  args
1183
1194
  }) {
1184
1195
  const wrapped = []
1196
+
1197
+ // Heuristic for continuous event emitters and their removal
1198
+ const isContinuousEmitter = /^(on|notify|monitor|signal)/.test(functionName)
1199
+ const isContinuousRemover = /^(off|remove|unmonitor|unsignal)/.test(
1200
+ functionName
1201
+ )
1202
+
1185
1203
  for (const [i, x] of args.entries()) {
1186
- // Check whether provided argument is a function
1187
1204
  if (this._isFunction(x)) {
1188
1205
  const callback = x
1189
1206
  if (
1190
1207
  functionName === 'vrpcOn' ||
1191
- (functionName === 'on' && i === 1 && typeof args[0] === 'string')
1208
+ (functionName === 'on' && i === 1 && typeof args[0] === 'string') ||
1209
+ isContinuousEmitter
1192
1210
  ) {
1193
- // special case of an (remote-)event emitter registration
1194
- // we test three conditions:
1195
- // 1) functionName must be "on"
1196
- // 2) callback is second argument
1197
- // 3) first argument was string
1198
- const event = args[0]
1211
+ const event = typeof args[0] === 'string' ? args[0] : functionName
1199
1212
  const id = await this._onRemoteEvent({
1200
1213
  agent,
1201
1214
  className,
@@ -1206,19 +1219,13 @@ class VrpcClient extends EventEmitter {
1206
1219
  wrapped.push(id)
1207
1220
  continue
1208
1221
  } else if (
1209
- (functionName === 'off' || functionName === 'removeListener') &&
1210
- i === 1 &&
1211
- typeof args[0] === 'string'
1222
+ ((functionName === 'off' || functionName === 'removeListener') &&
1223
+ i === 1 &&
1224
+ typeof args[0] === 'string') ||
1225
+ isContinuousRemover
1212
1226
  ) {
1213
- const event = args[0]
1214
- const id = await this._offRemoteEvent({
1215
- agent,
1216
- className,
1217
- instance,
1218
- event,
1219
- callback
1220
- })
1221
- wrapped.push(id)
1227
+ const id = await this._offRemoteEvent({ callback })
1228
+ wrapped.push(id || null)
1222
1229
  continue
1223
1230
  } else {
1224
1231
  // Regular function callback (can be static or member function)
@@ -1231,6 +1238,7 @@ class VrpcClient extends EventEmitter {
1231
1238
  continue
1232
1239
  }
1233
1240
  }
1241
+
1234
1242
  // special case of an EventEmitter provided as argument
1235
1243
  if (this._isEmitter(x)) {
1236
1244
  const { emitter, event } = x
@@ -1264,111 +1272,87 @@ class VrpcClient extends EventEmitter {
1264
1272
 
1265
1273
  async _onRemoteEvent ({ agent, className, instance, event, callback }) {
1266
1274
  const callbackId = this._getCallbackId(callback)
1267
- const eventCallback = `${event}-${callbackId}`
1268
-
1269
- // the topic on which any remote event will be published to
1270
- const topic = instance
1271
- ? `${this._domain}/${agent}/${className}/${instance}:${eventCallback}`
1272
- : `${this._domain}/${agent}/${className}/${eventCallback}`
1273
1275
 
1274
- // prepare a special id for the agent to know that events should be
1275
- // published to the provided topic after the prefix
1276
+ // Generate a short, sanitized hash
1277
+ const hashInput = `${agent}-${className}-${
1278
+ instance || '__static__'
1279
+ }-${event}-${callbackId}`
1280
+ const topicId = VrpcClient._createHash(hashInput, 12)
1276
1281
 
1282
+ // Route directly to the client's single event inbox wildcard
1283
+ const topic = `${this._vrpcClientId}/${topicId}`
1277
1284
  const id = `__e__${topic}`
1278
1285
 
1279
- // If a subscription for this topic is already in progress, wait for it to finish.
1280
- if (this._pendingSubscriptions.has(topic)) {
1281
- await this._pendingSubscriptions.get(topic)
1282
- }
1283
-
1284
- // call our proxy callback when remote events were received
1285
1286
  const handler = ({ a }) => callback.apply(null, a)
1287
+
1288
+ // Only bind to the actual emitter on the first registration
1286
1289
  if (this._eventEmitter.listenerCount(id) === 0) {
1287
1290
  this._eventEmitter.on(id, handler)
1288
1291
  }
1289
- if (!this._cachedSubscriptions[topic]) {
1290
- // when not yet subscribed to this topic do it now
1291
- const subscribePromise = this._mqttSubscribe(topic)
1292
- this._pendingSubscriptions.set(topic, subscribePromise) // set lock
1293
1292
 
1294
- try {
1295
- await subscribePromise
1296
- this._cachedSubscriptions[topic] = [{ callback, handler }]
1297
- } finally {
1298
- this._pendingSubscriptions.delete(topic) // release lock
1293
+ if (!this._cachedSubscriptions[topic]) {
1294
+ // Store metadata explicitly
1295
+ this._cachedSubscriptions[topic] = {
1296
+ agent,
1297
+ className,
1298
+ instance,
1299
+ event,
1300
+ listeners: [{ callback, handler }]
1299
1301
  }
1300
1302
  } else {
1301
- // otherwise just add the new callback-handler pair
1302
- this._cachedSubscriptions[topic].push({ callback, handler })
1303
+ this._cachedSubscriptions[topic].listeners.push({ callback, handler })
1303
1304
  }
1305
+
1304
1306
  return id
1305
1307
  }
1306
1308
 
1307
- async _offRemoteEvent ({ agent, className, instance, event, callback }) {
1308
- const callbackId = this._getCallbackId(callback, {
1309
- readOnly: true
1310
- })
1311
- // If the callback was never registered, we can't do anything.
1309
+ async _offRemoteEvent ({ callback }) {
1310
+ const callbackId = this._getCallbackId(callback, { readOnly: true })
1312
1311
  if (!callbackId) return null
1313
1312
 
1314
- const eventCallback = `${event}-${callbackId}`
1315
- const topic = instance
1316
- ? `${this._domain}/${agent}/${className}/${instance}:${eventCallback}`
1317
- : `${this._domain}/${agent}/${className}/${eventCallback}`
1313
+ let foundId = null
1314
+ const topics = Object.keys(this._cachedSubscriptions)
1318
1315
 
1319
- const id = `__e__${topic}`
1316
+ topics.forEach(topic => {
1317
+ const id = `__e__${topic}`
1318
+ const sub = this._cachedSubscriptions[topic]
1320
1319
 
1321
- if (this._pendingUnsubscriptions.has(topic)) {
1322
- await this._pendingUnsubscriptions.get(topic)
1323
- }
1320
+ const index = sub.listeners.findIndex(x => x.callback === callback)
1324
1321
 
1325
- if (this._cachedSubscriptions[topic]) {
1326
- const index = this._cachedSubscriptions[topic].findIndex(
1327
- x => x.callback === callback
1328
- )
1329
- if (index === -1) {
1330
- this._log.warn(`Failed removing event listener ${id}`)
1331
- return null
1332
- }
1333
- const [{ handler }] = this._cachedSubscriptions[topic].splice(index, 1)
1334
- if (this._cachedSubscriptions[topic].length === 0) {
1335
- delete this._cachedSubscriptions[topic]
1336
- const unsubscribePromise = this._mqttUnsubscribe(topic)
1337
- this._pendingUnsubscriptions.set(topic, unsubscribePromise) // set lock
1322
+ if (index !== -1) {
1323
+ // Decrease the "reference count" by removing one instance
1324
+ sub.listeners.splice(index, 1)
1338
1325
 
1339
- try {
1340
- await unsubscribePromise
1341
- this._eventEmitter.removeListener(id, handler)
1342
- } finally {
1343
- this._pendingUnsubscriptions.delete(topic) // release lock
1326
+ // Only remove from the local event emitter if NO references remain
1327
+ if (sub.listeners.length === 0) {
1328
+ delete this._cachedSubscriptions[topic]
1329
+
1330
+ // Cleanly wipe all listeners for this specific id
1331
+ this._eventEmitter.removeAllListeners(id)
1344
1332
  }
1345
- } else {
1346
- this._log.warn(`Can not unsubscribe from non-existing event: ${event}`)
1333
+ foundId = id
1347
1334
  }
1348
- return id
1349
- }
1335
+ })
1336
+ return foundId
1350
1337
  }
1351
1338
 
1352
1339
  _clearCachedSubscriptions ({ lostAgent, lostInstance }) {
1353
1340
  const obsoleteTopics = Object.keys(this._cachedSubscriptions).filter(
1354
1341
  topic => {
1355
- const [, agent, , instanceEvent] = topic.split('/')
1356
- const [instance] = instanceEvent.split(':')
1357
- if (agent === lostAgent || instance === lostInstance) {
1358
- return true
1359
- }
1360
- return false
1342
+ const sub = this._cachedSubscriptions[topic]
1343
+ return sub.agent === lostAgent || sub.instance === lostInstance
1361
1344
  }
1362
1345
  )
1346
+
1363
1347
  obsoleteTopics.forEach(topic => {
1364
1348
  const id = `__e__${topic}`
1365
1349
  this._log.debug(`Clearing subscriptions for obsolete topic: ${topic}`)
1366
- const subscriptions = this._cachedSubscriptions[topic]
1367
- subscriptions.forEach(({ handler }) => {
1350
+ const sub = this._cachedSubscriptions[topic]
1351
+
1352
+ sub.listeners.forEach(({ handler }) => {
1368
1353
  this._eventEmitter.removeListener(id, handler)
1369
1354
  })
1370
- // this async function will never reject
1371
- this._mqttUnsubscribe(topic)
1355
+
1372
1356
  delete this._cachedSubscriptions[topic]
1373
1357
  })
1374
1358
  }