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.
- package/package.json +1 -1
- package/vrpc/VrpcClient.js +90 -91
package/package.json
CHANGED
package/vrpc/VrpcClient.js
CHANGED
|
@@ -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
|
-
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1211
|
-
|
|
1233
|
+
((functionName === 'off' || functionName === 'removeListener') &&
|
|
1234
|
+
i === 1 &&
|
|
1235
|
+
typeof args[0] === 'string') ||
|
|
1236
|
+
isContinuousRemover
|
|
1212
1237
|
) {
|
|
1213
|
-
const
|
|
1214
|
-
|
|
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
|
-
|
|
1295
|
-
|
|
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
|
-
//
|
|
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 ({
|
|
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
|
-
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
-
|
|
1346
|
-
this._log.warn(`Can not unsubscribe from non-existing event: ${event}`)
|
|
1341
|
+
foundId = id
|
|
1347
1342
|
}
|
|
1348
|
-
|
|
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
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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
|
-
|
|
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 => {
|