vrpc 3.5.2 → 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/browser/vrpc.js +2 -2
- package/package.json +1 -1
- package/vrpc/VrpcClient.js +211 -177
package/package.json
CHANGED
package/vrpc/VrpcClient.js
CHANGED
|
@@ -136,6 +136,8 @@ class VrpcClient extends EventEmitter {
|
|
|
136
136
|
this._proxies = {}
|
|
137
137
|
this._callbackIds = new WeakMap()
|
|
138
138
|
this._emitterListener = new WeakMap()
|
|
139
|
+
this._pendingSubscriptions = new Map()
|
|
140
|
+
this._pendingUnsubscriptions = new Map()
|
|
139
141
|
if (log === 'console') {
|
|
140
142
|
this._log = console
|
|
141
143
|
this._log.debug = () => {}
|
|
@@ -200,6 +202,7 @@ class VrpcClient extends EventEmitter {
|
|
|
200
202
|
this._log.warn('MQTT client went offline.')
|
|
201
203
|
// Clear the cache to ensure a fresh state upon reconnection
|
|
202
204
|
this._agents = {}
|
|
205
|
+
this._proxies = {}
|
|
203
206
|
this.emit('offline')
|
|
204
207
|
})
|
|
205
208
|
|
|
@@ -216,27 +219,40 @@ class VrpcClient extends EventEmitter {
|
|
|
216
219
|
this.emit('error', err)
|
|
217
220
|
})
|
|
218
221
|
|
|
219
|
-
this._client.on('connect', () => {
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this._mqttSubscribe(`${this._domain}/${agent}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
222
|
+
this._client.on('connect', async () => {
|
|
223
|
+
// forward the good news
|
|
224
|
+
this.emit('connect')
|
|
225
|
+
try {
|
|
226
|
+
// This will give us an overview of all remotely available classes
|
|
227
|
+
const agent = this._agent === '*' ? '+' : this._agent
|
|
228
|
+
// Agent info
|
|
229
|
+
await this._mqttSubscribe(`${this._domain}/${agent}/__agentInfo__`)
|
|
230
|
+
// Class info
|
|
231
|
+
if (this._requiresSchema) {
|
|
232
|
+
await this._mqttSubscribe(`${this._domain}/${agent}/+/__classInfo__`)
|
|
233
|
+
} else {
|
|
234
|
+
await this._mqttSubscribe(
|
|
235
|
+
`${this._domain}/${agent}/+/__classInfoConcise__`
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
// RPC responses
|
|
239
|
+
await this._mqttSubscribe(this._vrpcClientId)
|
|
232
240
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
for (const topic of Object.keys(this._cachedSubscriptions)) {
|
|
236
|
-
this._mqttSubscribe(topic)
|
|
237
|
-
}
|
|
241
|
+
// Single wildcard subscription for ALL continuous remote events
|
|
242
|
+
await this._mqttSubscribe(`${this._vrpcClientId}/__e__/#`)
|
|
238
243
|
|
|
239
|
-
|
|
244
|
+
// Re-subscribe to all cached event topics
|
|
245
|
+
this._log.debug('Restoring event subscriptions after reconnect...')
|
|
246
|
+
const promises = []
|
|
247
|
+
for (const topic of Object.keys(this._cachedSubscriptions)) {
|
|
248
|
+
promises.push(this._mqttSubscribe(topic))
|
|
249
|
+
}
|
|
250
|
+
await Promise.allSettled(promises)
|
|
251
|
+
} catch (err) {
|
|
252
|
+
this._log.error(
|
|
253
|
+
`Encountered issue while setting up subscriptions after connect: ${err.message}`
|
|
254
|
+
)
|
|
255
|
+
}
|
|
240
256
|
})
|
|
241
257
|
|
|
242
258
|
this._client.on('message', (topic, message) => {
|
|
@@ -327,7 +343,6 @@ class VrpcClient extends EventEmitter {
|
|
|
327
343
|
resolve()
|
|
328
344
|
})
|
|
329
345
|
})
|
|
330
|
-
// --------------------------------------------------------------------------------
|
|
331
346
|
}
|
|
332
347
|
|
|
333
348
|
/**
|
|
@@ -456,25 +471,7 @@ class VrpcClient extends EventEmitter {
|
|
|
456
471
|
const topic = `${this._domain}/${agent}/${className}/__static__/__delete__`
|
|
457
472
|
this._mqttPublish(topic, JSON.stringify(json))
|
|
458
473
|
if (this._proxies[instance]) delete this._proxies[instance]
|
|
459
|
-
|
|
460
|
-
// Now that deletion is confirmed, proactively update all local caches
|
|
461
|
-
if (result) {
|
|
462
|
-
// 1. Remove from the proxy cache
|
|
463
|
-
if (this._proxies[instance]) {
|
|
464
|
-
delete this._proxies[instance]
|
|
465
|
-
}
|
|
466
|
-
// 2. Remove from the main agent/instance cache
|
|
467
|
-
if (this._agents[agent] && this._agents[agent].classes) {
|
|
468
|
-
const classInfo = this._agents[agent].classes[className]
|
|
469
|
-
if (classInfo && classInfo.instances) {
|
|
470
|
-
const index = classInfo.instances.indexOf(instance)
|
|
471
|
-
if (index > -1) {
|
|
472
|
-
classInfo.instances.splice(index, 1)
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return result
|
|
474
|
+
return this._handleAgentAnswer(json, agent)
|
|
478
475
|
}
|
|
479
476
|
|
|
480
477
|
/**
|
|
@@ -496,7 +493,7 @@ class VrpcClient extends EventEmitter {
|
|
|
496
493
|
if (!this._isConnected()) {
|
|
497
494
|
throw new Error('Client is not connected')
|
|
498
495
|
}
|
|
499
|
-
const wrapped = this._wrapArguments({
|
|
496
|
+
const wrapped = await this._wrapArguments({
|
|
500
497
|
agent,
|
|
501
498
|
className,
|
|
502
499
|
functionName,
|
|
@@ -559,7 +556,7 @@ class VrpcClient extends EventEmitter {
|
|
|
559
556
|
const agents = this.getAvailableAgents()
|
|
560
557
|
for (const agent of agents) {
|
|
561
558
|
const topic = `${this._domain}/${agent}/${className}/__static__/__callAll__`
|
|
562
|
-
const wrapped = this._wrapArguments({
|
|
559
|
+
const wrapped = await this._wrapArguments({
|
|
563
560
|
agent,
|
|
564
561
|
className,
|
|
565
562
|
functionName,
|
|
@@ -575,7 +572,7 @@ class VrpcClient extends EventEmitter {
|
|
|
575
572
|
return result
|
|
576
573
|
}
|
|
577
574
|
const topic = `${this._domain}/${agent}/${className}/__static__/__callAll__`
|
|
578
|
-
const wrapped = this._wrapArguments({
|
|
575
|
+
const wrapped = await this._wrapArguments({
|
|
579
576
|
agent,
|
|
580
577
|
className,
|
|
581
578
|
functionName,
|
|
@@ -848,68 +845,77 @@ class VrpcClient extends EventEmitter {
|
|
|
848
845
|
)
|
|
849
846
|
}
|
|
850
847
|
|
|
851
|
-
_mqttSubscribe (topic, options) {
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
if (err.message === 'client disconnecting') {
|
|
861
|
-
// This most certainly relates to a bug in MQTT.js (#1284)
|
|
862
|
-
this._log.error(`Failed to subscribe, because: ${err.message}`)
|
|
863
|
-
this.emit('error', err)
|
|
864
|
-
this._log.info('Forcing reconnect now...')
|
|
865
|
-
this._client.disconnecting = false
|
|
866
|
-
this._client.reconnect()
|
|
867
|
-
}
|
|
868
|
-
} else {
|
|
869
|
-
const topicArray = Array.isArray(topic) ? topic : [topic]
|
|
870
|
-
const erroneousGranted = granted
|
|
871
|
-
.filter(x => x.qos === 128)
|
|
872
|
-
.map(x => x.topic)
|
|
873
|
-
if (erroneousGranted.length > 0) {
|
|
874
|
-
err = new Error(
|
|
875
|
-
`Could not subscribe all ${topicArray.length} topic(s) but got error qos=128 on following ${erroneousGranted.length} topic(s): ${erroneousGranted}`
|
|
876
|
-
)
|
|
877
|
-
err.code = 'SUBSCRIBE_FAILED'
|
|
878
|
-
err.subscribeOptions = options
|
|
879
|
-
this._log.error(err)
|
|
880
|
-
this.emit('error', err)
|
|
881
|
-
}
|
|
882
|
-
const reducedQos = granted.filter(x => x.qos < this._qos)
|
|
883
|
-
if (reducedQos.length > 0) {
|
|
884
|
-
err = new Error(
|
|
885
|
-
`Could not subscribe all ${
|
|
886
|
-
topicArray.length
|
|
887
|
-
} topic(s) at desired qos=${
|
|
888
|
-
this._qos
|
|
889
|
-
} but got reduced qos on following ${
|
|
890
|
-
reducedQos.length
|
|
891
|
-
} topic(s): ${JSON.stringify(reducedQos)}`
|
|
848
|
+
async _mqttSubscribe (topic, options) {
|
|
849
|
+
return new Promise((resolve, reject) => {
|
|
850
|
+
this._client.subscribe(
|
|
851
|
+
topic,
|
|
852
|
+
{ qos: this._qos, ...options },
|
|
853
|
+
(err, granted) => {
|
|
854
|
+
if (err) {
|
|
855
|
+
this._log.warn(
|
|
856
|
+
`Could not subscribe to topic(s): '${topic}', because: ${err.message}`
|
|
892
857
|
)
|
|
893
|
-
err.
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
858
|
+
if (err.message === 'client disconnecting') {
|
|
859
|
+
// This most certainly relates to a bug in MQTT.js (#1284)
|
|
860
|
+
const msg = `Failed to subscribe, because: ${err.message}`
|
|
861
|
+
this._log.error(msg)
|
|
862
|
+
this.emit('error', err)
|
|
863
|
+
this._log.info('Forcing reconnect now...')
|
|
864
|
+
this._client.disconnecting = false
|
|
865
|
+
this._client.reconnect()
|
|
866
|
+
reject(new Error(msg))
|
|
867
|
+
}
|
|
868
|
+
} else {
|
|
869
|
+
const topicArray = Array.isArray(topic) ? topic : [topic]
|
|
870
|
+
const erroneousGranted = granted
|
|
871
|
+
.filter(x => x.qos === 128)
|
|
872
|
+
.map(x => x.topic)
|
|
873
|
+
if (erroneousGranted.length > 0) {
|
|
874
|
+
err = new Error(
|
|
875
|
+
`Could not subscribe all ${topicArray.length} topic(s) but got error qos=128 on following ${erroneousGranted.length} topic(s): ${erroneousGranted}`
|
|
876
|
+
)
|
|
877
|
+
err.code = 'SUBSCRIBE_FAILED'
|
|
878
|
+
err.subscribeOptions = options
|
|
879
|
+
this._log.error(err.message)
|
|
880
|
+
this.emit('error', err)
|
|
881
|
+
}
|
|
882
|
+
const reducedQos = granted.filter(x => x.qos < this._qos)
|
|
883
|
+
if (reducedQos.length > 0) {
|
|
884
|
+
err = new Error(
|
|
885
|
+
`Could not subscribe all ${
|
|
886
|
+
topicArray.length
|
|
887
|
+
} topic(s) at desired qos=${
|
|
888
|
+
this._qos
|
|
889
|
+
} but got reduced qos on following ${
|
|
890
|
+
reducedQos.length
|
|
891
|
+
} topic(s): ${JSON.stringify(reducedQos)}`
|
|
892
|
+
)
|
|
893
|
+
err.code = 'SUBSCRIBE_REDUCED_QOS'
|
|
894
|
+
err.subscribeOptions = options
|
|
895
|
+
this._log.warn(err.message)
|
|
896
|
+
this.emit('error', err)
|
|
897
|
+
}
|
|
898
|
+
if (granted.length === 0) {
|
|
899
|
+
this._log.debug(`Already subscribed to: ${topic}`)
|
|
900
|
+
}
|
|
901
|
+
resolve()
|
|
900
902
|
}
|
|
901
903
|
}
|
|
902
|
-
|
|
903
|
-
)
|
|
904
|
+
)
|
|
905
|
+
})
|
|
904
906
|
}
|
|
905
907
|
|
|
906
|
-
_mqttUnsubscribe (topic, options) {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
908
|
+
async _mqttUnsubscribe (topic, options) {
|
|
909
|
+
return new Promise((resolve, reject) => {
|
|
910
|
+
this._client.unsubscribe(topic, options, err => {
|
|
911
|
+
if (err) {
|
|
912
|
+
this._log.warn(
|
|
913
|
+
`Could not unsubscribe from topic: ${topic} because: ${err.message}`
|
|
914
|
+
)
|
|
915
|
+
}
|
|
916
|
+
// we resolve in any case to let the upstream logic flow
|
|
917
|
+
resolve()
|
|
918
|
+
})
|
|
913
919
|
})
|
|
914
920
|
}
|
|
915
921
|
|
|
@@ -998,13 +1004,32 @@ class VrpcClient extends EventEmitter {
|
|
|
998
1004
|
if (!eventName) {
|
|
999
1005
|
throw new Error('VRPC does not support removing all listeners')
|
|
1000
1006
|
}
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
+
}
|
|
1006
1031
|
delete this._cachedSubscriptions[topic]
|
|
1007
|
-
}
|
|
1032
|
+
})
|
|
1008
1033
|
try {
|
|
1009
1034
|
const json = {
|
|
1010
1035
|
c: instance,
|
|
@@ -1030,7 +1055,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1030
1055
|
return
|
|
1031
1056
|
}
|
|
1032
1057
|
proxy[functionName] = async (...args) => {
|
|
1033
|
-
const wrapped = this._wrapArguments({
|
|
1058
|
+
const wrapped = await this._wrapArguments({
|
|
1034
1059
|
agent,
|
|
1035
1060
|
className,
|
|
1036
1061
|
instance,
|
|
@@ -1065,7 +1090,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1065
1090
|
if (!uniqueFuncs.has('vrpcOn') && !uniqueFuncs.has('vrpcOff')) {
|
|
1066
1091
|
proxy.vrpcOn = async (functionName, ...args) => {
|
|
1067
1092
|
if (!uniqueFuncs.has(functionName)) throw new Error('Bad magic')
|
|
1068
|
-
const wrapped = this._wrapArguments({
|
|
1093
|
+
const wrapped = await this._wrapArguments({
|
|
1069
1094
|
agent,
|
|
1070
1095
|
className,
|
|
1071
1096
|
instance,
|
|
@@ -1170,23 +1195,32 @@ class VrpcClient extends EventEmitter {
|
|
|
1170
1195
|
})
|
|
1171
1196
|
}
|
|
1172
1197
|
|
|
1173
|
-
_wrapArguments ({
|
|
1198
|
+
async _wrapArguments ({
|
|
1199
|
+
agent,
|
|
1200
|
+
className,
|
|
1201
|
+
instance,
|
|
1202
|
+
proxyId,
|
|
1203
|
+
functionName,
|
|
1204
|
+
args
|
|
1205
|
+
}) {
|
|
1174
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
|
+
|
|
1175
1214
|
for (const [i, x] of args.entries()) {
|
|
1176
|
-
// Check whether provided argument is a function
|
|
1177
1215
|
if (this._isFunction(x)) {
|
|
1178
1216
|
const callback = x
|
|
1179
1217
|
if (
|
|
1180
1218
|
functionName === 'vrpcOn' ||
|
|
1181
|
-
(functionName === 'on' && i === 1 && typeof args[0] === 'string')
|
|
1219
|
+
(functionName === 'on' && i === 1 && typeof args[0] === 'string') ||
|
|
1220
|
+
isContinuousEmitter
|
|
1182
1221
|
) {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
// 1) functionName must be "on"
|
|
1186
|
-
// 2) callback is second argument
|
|
1187
|
-
// 3) first argument was string
|
|
1188
|
-
const event = args[0]
|
|
1189
|
-
const id = this._onRemoteEvent({
|
|
1222
|
+
const event = typeof args[0] === 'string' ? args[0] : functionName
|
|
1223
|
+
const id = await this._onRemoteEvent({
|
|
1190
1224
|
agent,
|
|
1191
1225
|
className,
|
|
1192
1226
|
instance,
|
|
@@ -1196,19 +1230,13 @@ class VrpcClient extends EventEmitter {
|
|
|
1196
1230
|
wrapped.push(id)
|
|
1197
1231
|
continue
|
|
1198
1232
|
} else if (
|
|
1199
|
-
(functionName === 'off' || functionName === 'removeListener') &&
|
|
1200
|
-
|
|
1201
|
-
|
|
1233
|
+
((functionName === 'off' || functionName === 'removeListener') &&
|
|
1234
|
+
i === 1 &&
|
|
1235
|
+
typeof args[0] === 'string') ||
|
|
1236
|
+
isContinuousRemover
|
|
1202
1237
|
) {
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
agent,
|
|
1206
|
-
className,
|
|
1207
|
-
instance,
|
|
1208
|
-
event,
|
|
1209
|
-
callback
|
|
1210
|
-
})
|
|
1211
|
-
wrapped.push(id)
|
|
1238
|
+
const id = await this._offRemoteEvent({ callback })
|
|
1239
|
+
wrapped.push(id || null)
|
|
1212
1240
|
continue
|
|
1213
1241
|
} else {
|
|
1214
1242
|
// Regular function callback (can be static or member function)
|
|
@@ -1221,6 +1249,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1221
1249
|
continue
|
|
1222
1250
|
}
|
|
1223
1251
|
}
|
|
1252
|
+
|
|
1224
1253
|
// special case of an EventEmitter provided as argument
|
|
1225
1254
|
if (this._isEmitter(x)) {
|
|
1226
1255
|
const { emitter, event } = x
|
|
@@ -1236,7 +1265,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1236
1265
|
this._emitterListener.set(emitter, new Map([[event, callback]]))
|
|
1237
1266
|
}
|
|
1238
1267
|
|
|
1239
|
-
const id = this._onRemoteEvent({
|
|
1268
|
+
const id = await this._onRemoteEvent({
|
|
1240
1269
|
agent,
|
|
1241
1270
|
className,
|
|
1242
1271
|
instance,
|
|
@@ -1252,87 +1281,92 @@ class VrpcClient extends EventEmitter {
|
|
|
1252
1281
|
return wrapped
|
|
1253
1282
|
}
|
|
1254
1283
|
|
|
1255
|
-
_onRemoteEvent ({ agent, className, instance, event, callback }) {
|
|
1284
|
+
async _onRemoteEvent ({ agent, className, instance, event, callback }) {
|
|
1256
1285
|
const callbackId = this._getCallbackId(callback)
|
|
1257
|
-
const eventCallback = `${event}-${callbackId}`
|
|
1258
|
-
|
|
1259
|
-
// the topic on which any remote event will be published to
|
|
1260
|
-
const topic = instance
|
|
1261
|
-
? `${this._domain}/${agent}/${className}/${instance}:${eventCallback}`
|
|
1262
|
-
: `${this._domain}/${agent}/${className}/${eventCallback}`
|
|
1263
|
-
|
|
1264
|
-
// prepare a special id for the agent to know that events should be
|
|
1265
|
-
// published to the provided topic after the prefix and before the suffix
|
|
1266
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}`
|
|
1267
1291
|
const id = `__e__${topic}`
|
|
1268
1292
|
|
|
1269
|
-
// call our proxy callback when remote events were received
|
|
1270
1293
|
const handler = ({ a }) => callback.apply(null, a)
|
|
1294
|
+
|
|
1295
|
+
// Only bind to the actual emitter on the first registration
|
|
1271
1296
|
if (this._eventEmitter.listenerCount(id) === 0) {
|
|
1272
1297
|
this._eventEmitter.on(id, handler)
|
|
1273
1298
|
}
|
|
1299
|
+
|
|
1274
1300
|
if (!this._cachedSubscriptions[topic]) {
|
|
1275
|
-
// when not yet subscribed to this topic do it now
|
|
1276
|
-
this._mqttSubscribe(topic)
|
|
1277
1301
|
this._cachedSubscriptions[topic] = [{ callback, handler }]
|
|
1278
|
-
|
|
1302
|
+
} else {
|
|
1303
|
+
// We MUST push duplicates to act as a reference counter!
|
|
1304
|
+
this._cachedSubscriptions[topic].push({ callback, handler })
|
|
1279
1305
|
}
|
|
1280
|
-
|
|
1281
|
-
this._cachedSubscriptions[topic].push({ callback, handler })
|
|
1306
|
+
|
|
1282
1307
|
return id
|
|
1283
1308
|
}
|
|
1284
1309
|
|
|
1285
|
-
_offRemoteEvent ({
|
|
1286
|
-
const callbackId = this._getCallbackId(callback, {
|
|
1287
|
-
|
|
1288
|
-
})
|
|
1289
|
-
const eventCallback = `${event}-${callbackId}`
|
|
1290
|
-
const topic = instance
|
|
1291
|
-
? `${this._domain}/${agent}/${className}/${instance}:${eventCallback}`
|
|
1292
|
-
: `${this._domain}/${agent}/${className}/${eventCallback}`
|
|
1310
|
+
async _offRemoteEvent ({ callback }) {
|
|
1311
|
+
const callbackId = this._getCallbackId(callback, { readOnly: true })
|
|
1312
|
+
if (!callbackId) return null
|
|
1293
1313
|
|
|
1294
|
-
|
|
1314
|
+
let foundId = null
|
|
1315
|
+
const topics = Object.keys(this._cachedSubscriptions).filter(
|
|
1316
|
+
t => t.endsWith(`/${callbackId}`) || t.endsWith(`-${callbackId}`)
|
|
1317
|
+
)
|
|
1295
1318
|
|
|
1296
|
-
|
|
1319
|
+
topics.forEach(topic => {
|
|
1320
|
+
const id = `__e__${topic}`
|
|
1297
1321
|
const index = this._cachedSubscriptions[topic].findIndex(
|
|
1298
1322
|
x => x.callback === callback
|
|
1299
1323
|
)
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
this.
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1324
|
+
|
|
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)
|
|
1340
|
+
}
|
|
1341
|
+
foundId = id
|
|
1310
1342
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
this._log.warn(`Can not unsubscribe from non-existing event: ${event}`)
|
|
1314
|
-
}
|
|
1315
|
-
return id
|
|
1343
|
+
})
|
|
1344
|
+
return foundId
|
|
1316
1345
|
}
|
|
1317
1346
|
|
|
1318
1347
|
_clearCachedSubscriptions ({ lostAgent, lostInstance }) {
|
|
1319
1348
|
const obsoleteTopics = Object.keys(this._cachedSubscriptions).filter(
|
|
1320
1349
|
topic => {
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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
|
|
1325
1355
|
}
|
|
1326
|
-
|
|
1356
|
+
// Legacy fallback
|
|
1357
|
+
const [, agent, , instanceEvent] = topic.split('/')
|
|
1358
|
+
const [instance] = (instanceEvent || '').split(':')
|
|
1359
|
+
return agent === lostAgent || instance === lostInstance
|
|
1327
1360
|
}
|
|
1328
1361
|
)
|
|
1329
1362
|
obsoleteTopics.forEach(topic => {
|
|
1330
1363
|
const id = `__e__${topic}`
|
|
1331
|
-
this._log.
|
|
1364
|
+
this._log.debug(`Clearing subscriptions for obsolete topic: ${topic}`)
|
|
1332
1365
|
const subscriptions = this._cachedSubscriptions[topic]
|
|
1333
1366
|
subscriptions.forEach(({ handler }) => {
|
|
1334
1367
|
this._eventEmitter.removeListener(id, handler)
|
|
1335
1368
|
})
|
|
1369
|
+
// this async function will never reject
|
|
1336
1370
|
this._mqttUnsubscribe(topic)
|
|
1337
1371
|
delete this._cachedSubscriptions[topic]
|
|
1338
1372
|
})
|