vrpc 3.5.3 → 3.6.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 +1 -1
  2. package/vrpc/VrpcClient.js +90 -91
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vrpc",
3
- "version": "3.5.3",
3
+ "version": "3.6.0",
4
4
  "description": "Variadic Remote Procedure Calls",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -238,6 +238,9 @@ class VrpcClient extends EventEmitter {
238
238
  // RPC responses
239
239
  await this._mqttSubscribe(this._vrpcClientId)
240
240
 
241
+ // Single wildcard subscription for ALL continuous remote events
242
+ await this._mqttSubscribe(`${this._vrpcClientId}/__e__/#`)
243
+
241
244
  // Re-subscribe to all cached event topics
242
245
  this._log.debug('Restoring event subscriptions after reconnect...')
243
246
  const promises = []
@@ -1001,13 +1004,32 @@ class VrpcClient extends EventEmitter {
1001
1004
  if (!eventName) {
1002
1005
  throw new Error('VRPC does not support removing all listeners')
1003
1006
  }
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)
1007
+
1008
+ const obsoleteTopics = Object.keys(this._cachedSubscriptions).filter(
1009
+ topic => {
1010
+ const tokens = topic.split('/')
1011
+ if (tokens[3] === '__e__') {
1012
+ const topicInstance = tokens[6]
1013
+ const topicEvent = tokens[7]
1014
+ return topicInstance === instance && topicEvent === eventName
1015
+ }
1016
+ // Legacy fallback
1017
+ const expectedOld = `${this._domain}/${agent}/${className}/${instance}:${eventName}`
1018
+ return topic.startsWith(expectedOld)
1019
+ }
1020
+ )
1021
+
1022
+ obsoleteTopics.forEach(topic => {
1023
+ const id = `__e__${topic}`
1024
+ const subscriptions = this._cachedSubscriptions[topic]
1025
+ subscriptions.forEach(({ handler }) => {
1026
+ this._eventEmitter.removeListener(id, handler)
1027
+ })
1028
+ if (!topic.includes('__e__')) {
1029
+ this._mqttUnsubscribe(topic).catch(() => {})
1030
+ }
1009
1031
  delete this._cachedSubscriptions[topic]
1010
- }
1032
+ })
1011
1033
  try {
1012
1034
  const json = {
1013
1035
  c: instance,
@@ -1182,20 +1204,22 @@ class VrpcClient extends EventEmitter {
1182
1204
  args
1183
1205
  }) {
1184
1206
  const wrapped = []
1207
+
1208
+ // Heuristic for continuous event emitters and their removal
1209
+ const isContinuousEmitter = /^(on|notify|monitor|signal)/.test(functionName)
1210
+ const isContinuousRemover = /^(off|remove|unmonitor|unsignal)/.test(
1211
+ functionName
1212
+ )
1213
+
1185
1214
  for (const [i, x] of args.entries()) {
1186
- // Check whether provided argument is a function
1187
1215
  if (this._isFunction(x)) {
1188
1216
  const callback = x
1189
1217
  if (
1190
1218
  functionName === 'vrpcOn' ||
1191
- (functionName === 'on' && i === 1 && typeof args[0] === 'string')
1219
+ (functionName === 'on' && i === 1 && typeof args[0] === 'string') ||
1220
+ isContinuousEmitter
1192
1221
  ) {
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]
1222
+ const event = typeof args[0] === 'string' ? args[0] : functionName
1199
1223
  const id = await this._onRemoteEvent({
1200
1224
  agent,
1201
1225
  className,
@@ -1206,19 +1230,13 @@ class VrpcClient extends EventEmitter {
1206
1230
  wrapped.push(id)
1207
1231
  continue
1208
1232
  } else if (
1209
- (functionName === 'off' || functionName === 'removeListener') &&
1210
- i === 1 &&
1211
- typeof args[0] === 'string'
1233
+ ((functionName === 'off' || functionName === 'removeListener') &&
1234
+ i === 1 &&
1235
+ typeof args[0] === 'string') ||
1236
+ isContinuousRemover
1212
1237
  ) {
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)
1238
+ const id = await this._offRemoteEvent({ callback })
1239
+ wrapped.push(id || null)
1222
1240
  continue
1223
1241
  } else {
1224
1242
  // Regular function callback (can be static or member function)
@@ -1231,6 +1249,7 @@ class VrpcClient extends EventEmitter {
1231
1249
  continue
1232
1250
  }
1233
1251
  }
1252
+
1234
1253
  // special case of an EventEmitter provided as argument
1235
1254
  if (this._isEmitter(x)) {
1236
1255
  const { emitter, event } = x
@@ -1264,100 +1283,80 @@ class VrpcClient extends EventEmitter {
1264
1283
 
1265
1284
  async _onRemoteEvent ({ agent, className, instance, event, callback }) {
1266
1285
  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
-
1274
- // prepare a special id for the agent to know that events should be
1275
- // published to the provided topic after the prefix
1276
1286
 
1287
+ // Route directly to the client's single event inbox wildcard.
1288
+ const topic = `${this._vrpcClientId}/__e__/${agent}/${className}/${
1289
+ instance || '__static__'
1290
+ }/${event}/${callbackId}`
1277
1291
  const id = `__e__${topic}`
1278
1292
 
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
1293
  const handler = ({ a }) => callback.apply(null, a)
1294
+
1295
+ // Only bind to the actual emitter on the first registration
1286
1296
  if (this._eventEmitter.listenerCount(id) === 0) {
1287
1297
  this._eventEmitter.on(id, handler)
1288
1298
  }
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
1299
 
1294
- try {
1295
- await subscribePromise
1296
- this._cachedSubscriptions[topic] = [{ callback, handler }]
1297
- } finally {
1298
- this._pendingSubscriptions.delete(topic) // release lock
1299
- }
1300
+ if (!this._cachedSubscriptions[topic]) {
1301
+ this._cachedSubscriptions[topic] = [{ callback, handler }]
1300
1302
  } else {
1301
- // otherwise just add the new callback-handler pair
1303
+ // We MUST push duplicates to act as a reference counter!
1302
1304
  this._cachedSubscriptions[topic].push({ callback, handler })
1303
1305
  }
1306
+
1304
1307
  return id
1305
1308
  }
1306
1309
 
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.
1310
+ async _offRemoteEvent ({ callback }) {
1311
+ const callbackId = this._getCallbackId(callback, { readOnly: true })
1312
1312
  if (!callbackId) return null
1313
1313
 
1314
- const eventCallback = `${event}-${callbackId}`
1315
- const topic = instance
1316
- ? `${this._domain}/${agent}/${className}/${instance}:${eventCallback}`
1317
- : `${this._domain}/${agent}/${className}/${eventCallback}`
1318
-
1319
- const id = `__e__${topic}`
1320
-
1321
- if (this._pendingUnsubscriptions.has(topic)) {
1322
- await this._pendingUnsubscriptions.get(topic)
1323
- }
1314
+ let foundId = null
1315
+ const topics = Object.keys(this._cachedSubscriptions).filter(
1316
+ t => t.endsWith(`/${callbackId}`) || t.endsWith(`-${callbackId}`)
1317
+ )
1324
1318
 
1325
- if (this._cachedSubscriptions[topic]) {
1319
+ topics.forEach(topic => {
1320
+ const id = `__e__${topic}`
1326
1321
  const index = this._cachedSubscriptions[topic].findIndex(
1327
1322
  x => x.callback === callback
1328
1323
  )
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
1338
1324
 
1339
- try {
1340
- await unsubscribePromise
1341
- this._eventEmitter.removeListener(id, handler)
1342
- } finally {
1343
- this._pendingUnsubscriptions.delete(topic) // release lock
1325
+ if (index !== -1) {
1326
+ // Decrease the "reference count" by removing one instance
1327
+ this._cachedSubscriptions[topic].splice(index, 1)
1328
+
1329
+ // Only remove from the local event emitter if NO references remain
1330
+ if (this._cachedSubscriptions[topic].length === 0) {
1331
+ delete this._cachedSubscriptions[topic]
1332
+
1333
+ // Legacy cleanup just in case
1334
+ if (!topic.includes('__e__')) {
1335
+ this._mqttUnsubscribe(topic).catch(() => {})
1336
+ }
1337
+
1338
+ // Cleanly wipe all listeners for this specific id
1339
+ this._eventEmitter.removeAllListeners(id)
1344
1340
  }
1345
- } else {
1346
- this._log.warn(`Can not unsubscribe from non-existing event: ${event}`)
1341
+ foundId = id
1347
1342
  }
1348
- return id
1349
- }
1343
+ })
1344
+ return foundId
1350
1345
  }
1351
1346
 
1352
1347
  _clearCachedSubscriptions ({ lostAgent, lostInstance }) {
1353
1348
  const obsoleteTopics = Object.keys(this._cachedSubscriptions).filter(
1354
1349
  topic => {
1355
- const [, agent, , instanceEvent] = topic.split('/')
1356
- const [instance] = instanceEvent.split(':')
1357
- if (agent === lostAgent || instance === lostInstance) {
1358
- return true
1350
+ const tokens = topic.split('/')
1351
+ if (tokens[3] === '__e__') {
1352
+ const topicAgent = tokens[4]
1353
+ const topicInstance = tokens[6]
1354
+ return topicAgent === lostAgent || topicInstance === lostInstance
1359
1355
  }
1360
- return false
1356
+ // Legacy fallback
1357
+ const [, agent, , instanceEvent] = topic.split('/')
1358
+ const [instance] = (instanceEvent || '').split(':')
1359
+ return agent === lostAgent || instance === lostInstance
1361
1360
  }
1362
1361
  )
1363
1362
  obsoleteTopics.forEach(topic => {