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.
- package/package.json +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.
|
|
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
|
|
48
|
+
"homepage": "https://docs.heisenware.com/developers/vrpc",
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@heisenware/storage": "^1.2.2",
|
|
51
51
|
"chai": "^4.3.10",
|
package/vrpc/VrpcClient.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1211
|
-
|
|
1222
|
+
((functionName === 'off' || functionName === 'removeListener') &&
|
|
1223
|
+
i === 1 &&
|
|
1224
|
+
typeof args[0] === 'string') ||
|
|
1225
|
+
isContinuousRemover
|
|
1212
1226
|
) {
|
|
1213
|
-
const
|
|
1214
|
-
|
|
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
|
-
//
|
|
1275
|
-
|
|
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
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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
|
-
|
|
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 ({
|
|
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
|
-
|
|
1315
|
-
const
|
|
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
|
-
|
|
1316
|
+
topics.forEach(topic => {
|
|
1317
|
+
const id = `__e__${topic}`
|
|
1318
|
+
const sub = this._cachedSubscriptions[topic]
|
|
1320
1319
|
|
|
1321
|
-
|
|
1322
|
-
await this._pendingUnsubscriptions.get(topic)
|
|
1323
|
-
}
|
|
1320
|
+
const index = sub.listeners.findIndex(x => x.callback === callback)
|
|
1324
1321
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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
|
-
|
|
1340
|
-
|
|
1341
|
-
this.
|
|
1342
|
-
|
|
1343
|
-
this
|
|
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
|
-
|
|
1346
|
-
this._log.warn(`Can not unsubscribe from non-existing event: ${event}`)
|
|
1333
|
+
foundId = id
|
|
1347
1334
|
}
|
|
1348
|
-
|
|
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
|
|
1356
|
-
|
|
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
|
|
1367
|
-
|
|
1350
|
+
const sub = this._cachedSubscriptions[topic]
|
|
1351
|
+
|
|
1352
|
+
sub.listeners.forEach(({ handler }) => {
|
|
1368
1353
|
this._eventEmitter.removeListener(id, handler)
|
|
1369
1354
|
})
|
|
1370
|
-
|
|
1371
|
-
this._mqttUnsubscribe(topic)
|
|
1355
|
+
|
|
1372
1356
|
delete this._cachedSubscriptions[topic]
|
|
1373
1357
|
})
|
|
1374
1358
|
}
|