vrpc 3.6.0 → 3.8.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/README.md +113 -37
- package/browser/vrpc.js +2 -2
- package/package.json +6 -6
- package/vrpc/VrpcAgent.js +17 -9
- package/vrpc/VrpcBrowserAgent.js +16 -8
- package/vrpc/VrpcClient.js +90 -67
- package/vrpc/VrpcPersistor.js +20 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vrpc",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"description": "Variadic Remote Procedure Calls",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"test": "npm run test:adapter && npm run test:agent && npm run test:client && npm run test:persistor && npm run test:production",
|
|
16
16
|
"test:adapter": "./node_modules/.bin/mocha tests/adapter/*.js --exit",
|
|
17
17
|
"test:native": "./node_modules/.bin/mocha tests/native/*.js --exit",
|
|
18
|
-
"test:agent": "tests/agent/test.sh"
|
|
18
|
+
"test:agent": "tests/agent/test.sh",
|
|
19
19
|
"test:client": "tests/client/test.sh",
|
|
20
20
|
"test:performance": "tests/performance/test.sh",
|
|
21
21
|
"test:persistor": "./node_modules/.bin/mocha tests/persistor/*.js --timeout 30000 --exit",
|
|
@@ -45,9 +45,9 @@
|
|
|
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
|
-
"@heisenware/storage": "^
|
|
50
|
+
"@heisenware/storage": "^3.0.2",
|
|
51
51
|
"chai": "^4.3.10",
|
|
52
52
|
"crypto-browserify": "^3.12.1",
|
|
53
53
|
"dockerode": "^4.0.9",
|
|
@@ -78,11 +78,11 @@
|
|
|
78
78
|
"process": "^0.11.10"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
|
-
"@heisenware/storage": "^1.2.2"
|
|
81
|
+
"@heisenware/storage": "^1.2.2 || ^2.0.0 || ^3.0.0"
|
|
82
82
|
},
|
|
83
83
|
"peerDependenciesMeta": {
|
|
84
84
|
"@heisenware/storage": {
|
|
85
85
|
"optional": true
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
}
|
|
88
|
+
}
|
package/vrpc/VrpcAgent.js
CHANGED
|
@@ -23,7 +23,7 @@ class VrpcAgent extends EventEmitter {
|
|
|
23
23
|
* @param {String} defaults.agent This agent's name
|
|
24
24
|
* @param {String} defaults.username MQTT username (if no token is used)
|
|
25
25
|
* @param {String} defaults.password MQTT password (if no token is provided)
|
|
26
|
-
* @param {String} defaults.token Access token
|
|
26
|
+
* @param {String} defaults.token Access token
|
|
27
27
|
* @param {String} defaults.broker Broker url in form: `<scheme>://<host>:<port>`
|
|
28
28
|
* @param {String} defaults.version The (user-defined) version of this agent
|
|
29
29
|
* @returns {Agent} Agent instance
|
|
@@ -641,11 +641,13 @@ class VrpcAgent extends EventEmitter {
|
|
|
641
641
|
}
|
|
642
642
|
|
|
643
643
|
_handleClientInfoMessage (topic, json) {
|
|
644
|
-
//
|
|
645
|
-
|
|
644
|
+
// A client connection went offline. The topic carries the connection
|
|
645
|
+
// id (unique per VrpcClient instance) - the key of all bookkeeping -
|
|
646
|
+
// so ending one connection never touches others sharing its identity.
|
|
647
|
+
const connectionId = topic.slice(0, -15) // /__clientInfo__ = 15
|
|
646
648
|
if (json.status === 'offline') {
|
|
647
|
-
VrpcAdapter._unregisterClient(
|
|
648
|
-
const entry = this._isolatedInstances.get(
|
|
649
|
+
VrpcAdapter._unregisterClient(connectionId)
|
|
650
|
+
const entry = this._isolatedInstances.get(connectionId)
|
|
649
651
|
if (entry) {
|
|
650
652
|
entry.forEach(instanceId => {
|
|
651
653
|
const json = { f: '__delete__', a: [instanceId], r: null }
|
|
@@ -655,8 +657,10 @@ class VrpcAgent extends EventEmitter {
|
|
|
655
657
|
}
|
|
656
658
|
})
|
|
657
659
|
}
|
|
658
|
-
this._mqttUnsubscribe(`${
|
|
659
|
-
|
|
660
|
+
this._mqttUnsubscribe(`${connectionId}/__clientInfo__`)
|
|
661
|
+
// clientId: the identity-derived id shared by all connections of one
|
|
662
|
+
// identity (undefined for clients < 3.8.0)
|
|
663
|
+
this.emit('clientGone', connectionId, { clientId: json.clientId })
|
|
660
664
|
}
|
|
661
665
|
}
|
|
662
666
|
|
|
@@ -825,10 +829,14 @@ class VrpcAgent extends EventEmitter {
|
|
|
825
829
|
/**
|
|
826
830
|
* Event 'clientGone'
|
|
827
831
|
*
|
|
828
|
-
* Emitted when a tracked VRPC client
|
|
832
|
+
* Emitted when a tracked VRPC client connection ended. Listeners receive
|
|
833
|
+
* the connection id (unique per VrpcClient instance) and an info object
|
|
834
|
+
* whose `clientId` is the identity-derived id shared by all connections
|
|
835
|
+
* of one identity (undefined for clients < 3.8.0).
|
|
829
836
|
*
|
|
830
837
|
* @event VrpcAgent#clientGone
|
|
831
|
-
* @type {String}
|
|
838
|
+
* @type {String} connectionId
|
|
839
|
+
* @type {Object} info
|
|
832
840
|
*/
|
|
833
841
|
|
|
834
842
|
module.exports = VrpcAgent
|
package/vrpc/VrpcBrowserAgent.js
CHANGED
|
@@ -495,11 +495,13 @@ class VrpcAgent extends EventEmitter {
|
|
|
495
495
|
}
|
|
496
496
|
|
|
497
497
|
_handleClientInfoMessage (topic, json) {
|
|
498
|
-
//
|
|
499
|
-
|
|
498
|
+
// A client connection went offline. The topic carries the connection
|
|
499
|
+
// id (unique per VrpcClient instance) - the key of all bookkeeping -
|
|
500
|
+
// so ending one connection never touches others sharing its identity.
|
|
501
|
+
const connectionId = topic.slice(0, -15) // /__clientInfo__ = 15
|
|
500
502
|
if (json.status === 'offline') {
|
|
501
|
-
VrpcAdapter._unregisterClient(
|
|
502
|
-
const entry = this._isolatedInstances.get(
|
|
503
|
+
VrpcAdapter._unregisterClient(connectionId)
|
|
504
|
+
const entry = this._isolatedInstances.get(connectionId)
|
|
503
505
|
if (entry) {
|
|
504
506
|
entry.forEach(instanceId => {
|
|
505
507
|
const json = { f: '__delete__', a: [instanceId], r: null }
|
|
@@ -509,8 +511,10 @@ class VrpcAgent extends EventEmitter {
|
|
|
509
511
|
}
|
|
510
512
|
})
|
|
511
513
|
}
|
|
512
|
-
this._mqttUnsubscribe(`${
|
|
513
|
-
|
|
514
|
+
this._mqttUnsubscribe(`${connectionId}/__clientInfo__`)
|
|
515
|
+
// clientId: the identity-derived id shared by all connections of one
|
|
516
|
+
// identity (undefined for clients < 3.8.0)
|
|
517
|
+
this.emit('clientGone', connectionId, { clientId: json.clientId })
|
|
514
518
|
}
|
|
515
519
|
}
|
|
516
520
|
|
|
@@ -679,10 +683,14 @@ class VrpcAgent extends EventEmitter {
|
|
|
679
683
|
/**
|
|
680
684
|
* Event 'clientGone'
|
|
681
685
|
*
|
|
682
|
-
* Emitted when a tracked VRPC client
|
|
686
|
+
* Emitted when a tracked VRPC client connection ended. Listeners receive
|
|
687
|
+
* the connection id (unique per VrpcClient instance) and an info object
|
|
688
|
+
* whose `clientId` is the identity-derived id shared by all connections
|
|
689
|
+
* of one identity (undefined for clients < 3.8.0).
|
|
683
690
|
*
|
|
684
691
|
* @event VrpcAgent#clientGone
|
|
685
|
-
* @type {String}
|
|
692
|
+
* @type {String} connectionId
|
|
693
|
+
* @type {Object} info
|
|
686
694
|
*/
|
|
687
695
|
|
|
688
696
|
module.exports = VrpcAgent
|
package/vrpc/VrpcClient.js
CHANGED
|
@@ -74,7 +74,7 @@ class VrpcClient extends EventEmitter {
|
|
|
74
74
|
* @example
|
|
75
75
|
* const client = new VrpcClient({
|
|
76
76
|
* domain: 'vrpc',
|
|
77
|
-
* broker: '
|
|
77
|
+
* broker: 'mqtts://broker.hivemq.com:8883'
|
|
78
78
|
* })
|
|
79
79
|
*/
|
|
80
80
|
constructor ({
|
|
@@ -127,6 +127,7 @@ class VrpcClient extends EventEmitter {
|
|
|
127
127
|
this._instance = nanoid(8)
|
|
128
128
|
this._mqttClientId = mqttClientId || this._createMqttClientId()
|
|
129
129
|
this._vrpcClientId = this._createVrpcClientId()
|
|
130
|
+
this._vrpcConnectionId = this._createVrpcConnectionId()
|
|
130
131
|
this._agents = {}
|
|
131
132
|
this._eventEmitter = new EventEmitter()
|
|
132
133
|
this._invokeId = 0
|
|
@@ -147,7 +148,13 @@ class VrpcClient extends EventEmitter {
|
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
/**
|
|
150
|
-
* Provides
|
|
151
|
+
* Provides the identity-derived id of this client
|
|
152
|
+
*
|
|
153
|
+
* Every client constructed with the same domain, host and identity
|
|
154
|
+
* reports the same client id: it names the principal, not the
|
|
155
|
+
* connection. Agents receive it as `clientId` in the presence messages
|
|
156
|
+
* and can group the connections (e.g. browser tabs) of one identity by
|
|
157
|
+
* it. See getConnectionId() for the id that is unique per instance.
|
|
151
158
|
*
|
|
152
159
|
* @returns {String} clientId
|
|
153
160
|
*/
|
|
@@ -155,6 +162,21 @@ class VrpcClient extends EventEmitter {
|
|
|
155
162
|
return this._vrpcClientId
|
|
156
163
|
}
|
|
157
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Provides the unique id of this connection
|
|
167
|
+
*
|
|
168
|
+
* Unique per VrpcClient instance: two clients sharing one identity still
|
|
169
|
+
* have distinct connection ids. Agents key all their bookkeeping by it
|
|
170
|
+
* (response routing, event listeners, isolated instances, presence), so
|
|
171
|
+
* one connection ending never disturbs the others of the same identity.
|
|
172
|
+
* Without an identity the connection id equals the client id.
|
|
173
|
+
*
|
|
174
|
+
* @returns {String} connectionId
|
|
175
|
+
*/
|
|
176
|
+
getConnectionId () {
|
|
177
|
+
return this._vrpcConnectionId
|
|
178
|
+
}
|
|
179
|
+
|
|
158
180
|
/**
|
|
159
181
|
* Actually connects to the MQTT broker.
|
|
160
182
|
*
|
|
@@ -184,8 +206,11 @@ class VrpcClient extends EventEmitter {
|
|
|
184
206
|
rejectUnauthorized: false,
|
|
185
207
|
connectTimeout: this._timeout,
|
|
186
208
|
will: {
|
|
187
|
-
topic: `${this.
|
|
188
|
-
payload: JSON.stringify({
|
|
209
|
+
topic: `${this._vrpcConnectionId}/__clientInfo__`,
|
|
210
|
+
payload: JSON.stringify({
|
|
211
|
+
status: 'offline',
|
|
212
|
+
clientId: this._vrpcClientId
|
|
213
|
+
})
|
|
189
214
|
}
|
|
190
215
|
}
|
|
191
216
|
if (username === undefined) delete options.username
|
|
@@ -235,11 +260,8 @@ class VrpcClient extends EventEmitter {
|
|
|
235
260
|
`${this._domain}/${agent}/+/__classInfoConcise__`
|
|
236
261
|
)
|
|
237
262
|
}
|
|
238
|
-
// RPC responses
|
|
239
|
-
await this._mqttSubscribe(this.
|
|
240
|
-
|
|
241
|
-
// Single wildcard subscription for ALL continuous remote events
|
|
242
|
-
await this._mqttSubscribe(`${this._vrpcClientId}/__e__/#`)
|
|
263
|
+
// Single wildcard catches BOTH RPC responses (length 3) AND events (length 4)
|
|
264
|
+
await this._mqttSubscribe(`${this._vrpcConnectionId}/#`)
|
|
243
265
|
|
|
244
266
|
// Re-subscribe to all cached event topics
|
|
245
267
|
this._log.debug('Restoring event subscriptions after reconnect...')
|
|
@@ -412,7 +434,7 @@ class VrpcClient extends EventEmitter {
|
|
|
412
434
|
f: isIsolated ? '__createIsolated__' : '__createShared__',
|
|
413
435
|
a: [instance, ...args],
|
|
414
436
|
i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
|
|
415
|
-
s: this.
|
|
437
|
+
s: this._vrpcConnectionId,
|
|
416
438
|
v: VRPC_PROTOCOL_VERSION
|
|
417
439
|
}
|
|
418
440
|
const proxy = await this._getProxy(agent, className, json)
|
|
@@ -465,7 +487,7 @@ class VrpcClient extends EventEmitter {
|
|
|
465
487
|
f: '__delete__',
|
|
466
488
|
a: [instance],
|
|
467
489
|
i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
|
|
468
|
-
s: this.
|
|
490
|
+
s: this._vrpcConnectionId,
|
|
469
491
|
v: VRPC_PROTOCOL_VERSION
|
|
470
492
|
}
|
|
471
493
|
const topic = `${this._domain}/${agent}/${className}/__static__/__delete__`
|
|
@@ -505,7 +527,7 @@ class VrpcClient extends EventEmitter {
|
|
|
505
527
|
f: functionName,
|
|
506
528
|
a: wrapped,
|
|
507
529
|
i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
|
|
508
|
-
s: this.
|
|
530
|
+
s: this._vrpcConnectionId,
|
|
509
531
|
v: VRPC_PROTOCOL_VERSION
|
|
510
532
|
}
|
|
511
533
|
const topic = `${this._domain}/${agent}/${className}/__static__/${functionName}`
|
|
@@ -548,7 +570,7 @@ class VrpcClient extends EventEmitter {
|
|
|
548
570
|
f: functionName,
|
|
549
571
|
a: [],
|
|
550
572
|
i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
|
|
551
|
-
s: this.
|
|
573
|
+
s: this._vrpcConnectionId,
|
|
552
574
|
v: VRPC_PROTOCOL_VERSION
|
|
553
575
|
}
|
|
554
576
|
if (agent === '*') {
|
|
@@ -711,7 +733,7 @@ class VrpcClient extends EventEmitter {
|
|
|
711
733
|
/**
|
|
712
734
|
* Reconnects to the broker by using a different token
|
|
713
735
|
*
|
|
714
|
-
* @param {String} token Access token
|
|
736
|
+
* @param {String} token Access token
|
|
715
737
|
* @param {Object} [options]
|
|
716
738
|
* @param {String} options.agent Agent name. If not provided class default is used
|
|
717
739
|
* @returns {Promise} Promise that resolves once re-connected
|
|
@@ -755,8 +777,12 @@ class VrpcClient extends EventEmitter {
|
|
|
755
777
|
async end () {
|
|
756
778
|
if (!this._client) return
|
|
757
779
|
this._mqttPublish(
|
|
758
|
-
`${this.
|
|
759
|
-
JSON.stringify({
|
|
780
|
+
`${this._vrpcConnectionId}/__clientInfo__`,
|
|
781
|
+
JSON.stringify({
|
|
782
|
+
status: 'offline',
|
|
783
|
+
clientId: this._vrpcClientId,
|
|
784
|
+
v: VRPC_PROTOCOL_VERSION
|
|
785
|
+
})
|
|
760
786
|
)
|
|
761
787
|
this._eventEmitter.removeAllListeners()
|
|
762
788
|
this.removeAllListeners()
|
|
@@ -767,7 +793,7 @@ class VrpcClient extends EventEmitter {
|
|
|
767
793
|
|
|
768
794
|
// Private functions
|
|
769
795
|
|
|
770
|
-
static _createHash (str, length =
|
|
796
|
+
static _createHash (str, length = 16) {
|
|
771
797
|
// Extended DJB2 hash with two accumulators
|
|
772
798
|
let hash1 = 5381
|
|
773
799
|
let hash2 = 52711 // different seed
|
|
@@ -822,6 +848,14 @@ class VrpcClient extends EventEmitter {
|
|
|
822
848
|
return `${this._domain}/${os.hostname()}/${identity}`
|
|
823
849
|
}
|
|
824
850
|
|
|
851
|
+
_createVrpcConnectionId () {
|
|
852
|
+
// Without an identity the client id already is unique per instance.
|
|
853
|
+
// With one, the instance token makes every connection of that identity
|
|
854
|
+
// distinct while keeping the three-segment layout agents parse.
|
|
855
|
+
if (!this._identity) return this._vrpcClientId
|
|
856
|
+
return `${this._vrpcClientId}:${this._instance}`
|
|
857
|
+
}
|
|
858
|
+
|
|
825
859
|
_mqttPublish (topic, message, options) {
|
|
826
860
|
this._client.publish(
|
|
827
861
|
topic,
|
|
@@ -984,6 +1018,7 @@ class VrpcClient extends EventEmitter {
|
|
|
984
1018
|
const proxyId = `${this._instance}-${this._proxyId++}`
|
|
985
1019
|
const proxy = {
|
|
986
1020
|
vrpcClientId: this._vrpcClientId,
|
|
1021
|
+
vrpcConnectionId: this._vrpcConnectionId,
|
|
987
1022
|
vrpcInstanceId: instance,
|
|
988
1023
|
vrpcProxyId: proxyId
|
|
989
1024
|
}
|
|
@@ -1007,27 +1042,19 @@ class VrpcClient extends EventEmitter {
|
|
|
1007
1042
|
|
|
1008
1043
|
const obsoleteTopics = Object.keys(this._cachedSubscriptions).filter(
|
|
1009
1044
|
topic => {
|
|
1010
|
-
const
|
|
1011
|
-
|
|
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)
|
|
1045
|
+
const sub = this._cachedSubscriptions[topic]
|
|
1046
|
+
return sub.instance === instance && sub.event === eventName
|
|
1019
1047
|
}
|
|
1020
1048
|
)
|
|
1021
1049
|
|
|
1022
1050
|
obsoleteTopics.forEach(topic => {
|
|
1023
1051
|
const id = `__e__${topic}`
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1052
|
+
const sub = this._cachedSubscriptions[topic]
|
|
1053
|
+
|
|
1054
|
+
sub.listeners.forEach(({ handler }) => {
|
|
1026
1055
|
this._eventEmitter.removeListener(id, handler)
|
|
1027
1056
|
})
|
|
1028
|
-
|
|
1029
|
-
this._mqttUnsubscribe(topic).catch(() => {})
|
|
1030
|
-
}
|
|
1057
|
+
|
|
1031
1058
|
delete this._cachedSubscriptions[topic]
|
|
1032
1059
|
})
|
|
1033
1060
|
try {
|
|
@@ -1038,7 +1065,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1038
1065
|
i: `${this._instance}-${
|
|
1039
1066
|
this._invokeId++ % Number.MAX_SAFE_INTEGER
|
|
1040
1067
|
}`,
|
|
1041
|
-
s: this.
|
|
1068
|
+
s: this._vrpcConnectionId,
|
|
1042
1069
|
v: VRPC_PROTOCOL_VERSION
|
|
1043
1070
|
}
|
|
1044
1071
|
this._mqttPublish(
|
|
@@ -1072,7 +1099,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1072
1099
|
i: `${this._instance}-${
|
|
1073
1100
|
this._invokeId++ % Number.MAX_SAFE_INTEGER
|
|
1074
1101
|
}`,
|
|
1075
|
-
s: this.
|
|
1102
|
+
s: this._vrpcConnectionId,
|
|
1076
1103
|
v: VRPC_PROTOCOL_VERSION
|
|
1077
1104
|
}
|
|
1078
1105
|
this._mqttPublish(
|
|
@@ -1107,7 +1134,7 @@ class VrpcClient extends EventEmitter {
|
|
|
1107
1134
|
i: `${this._instance}-${
|
|
1108
1135
|
this._invokeId++ % Number.MAX_SAFE_INTEGER
|
|
1109
1136
|
}`,
|
|
1110
|
-
s: this.
|
|
1137
|
+
s: this._vrpcConnectionId,
|
|
1111
1138
|
v: VRPC_PROTOCOL_VERSION
|
|
1112
1139
|
}
|
|
1113
1140
|
this._mqttPublish(
|
|
@@ -1284,10 +1311,14 @@ class VrpcClient extends EventEmitter {
|
|
|
1284
1311
|
async _onRemoteEvent ({ agent, className, instance, event, callback }) {
|
|
1285
1312
|
const callbackId = this._getCallbackId(callback)
|
|
1286
1313
|
|
|
1287
|
-
//
|
|
1288
|
-
const
|
|
1314
|
+
// Generate a short, sanitized hash
|
|
1315
|
+
const hashInput = `${agent}-${className}-${
|
|
1289
1316
|
instance || '__static__'
|
|
1290
|
-
}
|
|
1317
|
+
}-${event}-${callbackId}`
|
|
1318
|
+
const topicId = VrpcClient._createHash(hashInput, 12)
|
|
1319
|
+
|
|
1320
|
+
// Route directly to the client's single event inbox wildcard
|
|
1321
|
+
const topic = `${this._vrpcConnectionId}/${topicId}`
|
|
1291
1322
|
const id = `__e__${topic}`
|
|
1292
1323
|
|
|
1293
1324
|
const handler = ({ a }) => callback.apply(null, a)
|
|
@@ -1298,10 +1329,16 @@ class VrpcClient extends EventEmitter {
|
|
|
1298
1329
|
}
|
|
1299
1330
|
|
|
1300
1331
|
if (!this._cachedSubscriptions[topic]) {
|
|
1301
|
-
|
|
1332
|
+
// Store metadata explicitly
|
|
1333
|
+
this._cachedSubscriptions[topic] = {
|
|
1334
|
+
agent,
|
|
1335
|
+
className,
|
|
1336
|
+
instance,
|
|
1337
|
+
event,
|
|
1338
|
+
listeners: [{ callback, handler }]
|
|
1339
|
+
}
|
|
1302
1340
|
} else {
|
|
1303
|
-
|
|
1304
|
-
this._cachedSubscriptions[topic].push({ callback, handler })
|
|
1341
|
+
this._cachedSubscriptions[topic].listeners.push({ callback, handler })
|
|
1305
1342
|
}
|
|
1306
1343
|
|
|
1307
1344
|
return id
|
|
@@ -1312,29 +1349,22 @@ class VrpcClient extends EventEmitter {
|
|
|
1312
1349
|
if (!callbackId) return null
|
|
1313
1350
|
|
|
1314
1351
|
let foundId = null
|
|
1315
|
-
const topics = Object.keys(this._cachedSubscriptions)
|
|
1316
|
-
t => t.endsWith(`/${callbackId}`) || t.endsWith(`-${callbackId}`)
|
|
1317
|
-
)
|
|
1352
|
+
const topics = Object.keys(this._cachedSubscriptions)
|
|
1318
1353
|
|
|
1319
1354
|
topics.forEach(topic => {
|
|
1320
1355
|
const id = `__e__${topic}`
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
)
|
|
1356
|
+
const sub = this._cachedSubscriptions[topic]
|
|
1357
|
+
|
|
1358
|
+
const index = sub.listeners.findIndex(x => x.callback === callback)
|
|
1324
1359
|
|
|
1325
1360
|
if (index !== -1) {
|
|
1326
1361
|
// Decrease the "reference count" by removing one instance
|
|
1327
|
-
|
|
1362
|
+
sub.listeners.splice(index, 1)
|
|
1328
1363
|
|
|
1329
1364
|
// Only remove from the local event emitter if NO references remain
|
|
1330
|
-
if (
|
|
1365
|
+
if (sub.listeners.length === 0) {
|
|
1331
1366
|
delete this._cachedSubscriptions[topic]
|
|
1332
1367
|
|
|
1333
|
-
// Legacy cleanup just in case
|
|
1334
|
-
if (!topic.includes('__e__')) {
|
|
1335
|
-
this._mqttUnsubscribe(topic).catch(() => {})
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
1368
|
// Cleanly wipe all listeners for this specific id
|
|
1339
1369
|
this._eventEmitter.removeAllListeners(id)
|
|
1340
1370
|
}
|
|
@@ -1347,27 +1377,20 @@ class VrpcClient extends EventEmitter {
|
|
|
1347
1377
|
_clearCachedSubscriptions ({ lostAgent, lostInstance }) {
|
|
1348
1378
|
const obsoleteTopics = Object.keys(this._cachedSubscriptions).filter(
|
|
1349
1379
|
topic => {
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
const topicAgent = tokens[4]
|
|
1353
|
-
const topicInstance = tokens[6]
|
|
1354
|
-
return topicAgent === lostAgent || topicInstance === lostInstance
|
|
1355
|
-
}
|
|
1356
|
-
// Legacy fallback
|
|
1357
|
-
const [, agent, , instanceEvent] = topic.split('/')
|
|
1358
|
-
const [instance] = (instanceEvent || '').split(':')
|
|
1359
|
-
return agent === lostAgent || instance === lostInstance
|
|
1380
|
+
const sub = this._cachedSubscriptions[topic]
|
|
1381
|
+
return sub.agent === lostAgent || sub.instance === lostInstance
|
|
1360
1382
|
}
|
|
1361
1383
|
)
|
|
1384
|
+
|
|
1362
1385
|
obsoleteTopics.forEach(topic => {
|
|
1363
1386
|
const id = `__e__${topic}`
|
|
1364
1387
|
this._log.debug(`Clearing subscriptions for obsolete topic: ${topic}`)
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1388
|
+
const sub = this._cachedSubscriptions[topic]
|
|
1389
|
+
|
|
1390
|
+
sub.listeners.forEach(({ handler }) => {
|
|
1367
1391
|
this._eventEmitter.removeListener(id, handler)
|
|
1368
1392
|
})
|
|
1369
|
-
|
|
1370
|
-
this._mqttUnsubscribe(topic)
|
|
1393
|
+
|
|
1371
1394
|
delete this._cachedSubscriptions[topic]
|
|
1372
1395
|
})
|
|
1373
1396
|
}
|
package/vrpc/VrpcPersistor.js
CHANGED
|
@@ -47,6 +47,9 @@ const VrpcAdapter = require('./VrpcAdapter')
|
|
|
47
47
|
* for an 'update' event on instances to persist their state after creation.
|
|
48
48
|
*
|
|
49
49
|
* @requires @heisenware/storage - This peer dependency must be installed.
|
|
50
|
+
* Storage 1.x (synchronous constructor) and >= 2.x (async `Storage.open`)
|
|
51
|
+
* are both supported; the layer is opened lazily and every operation waits
|
|
52
|
+
* for it.
|
|
50
53
|
*/
|
|
51
54
|
class VrpcPersistor {
|
|
52
55
|
/**
|
|
@@ -76,8 +79,8 @@ class VrpcPersistor {
|
|
|
76
79
|
.toLocaleLowerCase()
|
|
77
80
|
.replace(/[^a-zA-Z0-9]/g, '-')}`
|
|
78
81
|
|
|
79
|
-
this._storage =
|
|
80
|
-
this._isInitialized = this._init()
|
|
82
|
+
this._storage = null
|
|
83
|
+
this._isInitialized = this._init(Storage)
|
|
81
84
|
|
|
82
85
|
this._log.info(
|
|
83
86
|
`[VrpcPersistor] Persistence layer enabled. Storage path: ${this._dir}`
|
|
@@ -166,8 +169,12 @@ class VrpcPersistor {
|
|
|
166
169
|
* Initializes the persistor by attaching listeners to VRPC adapter events.
|
|
167
170
|
* @private
|
|
168
171
|
*/
|
|
169
|
-
async _init () {
|
|
172
|
+
async _init (Storage) {
|
|
170
173
|
try {
|
|
174
|
+
// Listeners go first and synchronously: an instance created right
|
|
175
|
+
// after construction must be caught. The operations they trigger wait
|
|
176
|
+
// for the storage layer opened below.
|
|
177
|
+
|
|
171
178
|
// Persist new instance creation
|
|
172
179
|
VrpcAdapter.on('create', async ({ instance, className, args }) => {
|
|
173
180
|
this._log.info(
|
|
@@ -211,6 +218,14 @@ class VrpcPersistor {
|
|
|
211
218
|
)
|
|
212
219
|
)
|
|
213
220
|
})
|
|
221
|
+
|
|
222
|
+
// storage >= 2 constructs through the async factory only; 1.x has a
|
|
223
|
+
// synchronous constructor. No watcher: this persistor is the sole
|
|
224
|
+
// writer of its directory.
|
|
225
|
+
this._storage =
|
|
226
|
+
typeof Storage.open === 'function'
|
|
227
|
+
? await Storage.open({ dir: this._dir, log: this._log, watch: false })
|
|
228
|
+
: new Storage({ dir: this._dir, log: this._log })
|
|
214
229
|
} catch (err) {
|
|
215
230
|
this._log.error(
|
|
216
231
|
`[VrpcPersistor] Could not initialize persistence layer: ${err.message}`
|
|
@@ -225,6 +240,7 @@ class VrpcPersistor {
|
|
|
225
240
|
* @private
|
|
226
241
|
*/
|
|
227
242
|
async _persist (id, className, args) {
|
|
243
|
+
await this._isInitialized
|
|
228
244
|
await this._storage.setItem(id, { className, args }, { folder: className })
|
|
229
245
|
}
|
|
230
246
|
|
|
@@ -233,6 +249,7 @@ class VrpcPersistor {
|
|
|
233
249
|
* @private
|
|
234
250
|
*/
|
|
235
251
|
async _delete (id) {
|
|
252
|
+
await this._isInitialized
|
|
236
253
|
await this._storage.removeItem(id)
|
|
237
254
|
}
|
|
238
255
|
}
|