vrpc 3.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vrpc",
3
- "version": "3.7.0",
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",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "homepage": "https://docs.heisenware.com/developers/vrpc",
49
49
  "devDependencies": {
50
- "@heisenware/storage": "^1.2.2",
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 as generated by: https://app.vrpc.io
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
- // Client went offline
645
- const clientId = topic.slice(0, -15) // /__clientInfo__ = 15
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(clientId)
648
- const entry = this._isolatedInstances.get(clientId)
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(`${clientId}/__clientInfo__`)
659
- this.emit('clientGone', clientId)
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 exited.
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} clientId
838
+ * @type {String} connectionId
839
+ * @type {Object} info
832
840
  */
833
841
 
834
842
  module.exports = VrpcAgent
@@ -495,11 +495,13 @@ class VrpcAgent extends EventEmitter {
495
495
  }
496
496
 
497
497
  _handleClientInfoMessage (topic, json) {
498
- // Client went offline
499
- const clientId = topic.slice(0, -15) // /__clientInfo__ = 15
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(clientId)
502
- const entry = this._isolatedInstances.get(clientId)
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(`${clientId}/__clientInfo__`)
513
- this.emit('clientGone', clientId)
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 exited.
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} clientId
692
+ * @type {String} connectionId
693
+ * @type {Object} info
686
694
  */
687
695
 
688
696
  module.exports = VrpcAgent
@@ -74,7 +74,7 @@ class VrpcClient extends EventEmitter {
74
74
  * @example
75
75
  * const client = new VrpcClient({
76
76
  * domain: 'vrpc',
77
- * broker: 'mqtt://vrpc.io'
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 a unique id for this client instance
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._vrpcClientId}/__clientInfo__`,
188
- payload: JSON.stringify({ status: 'offline' })
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
@@ -236,7 +261,7 @@ class VrpcClient extends EventEmitter {
236
261
  )
237
262
  }
238
263
  // Single wildcard catches BOTH RPC responses (length 3) AND events (length 4)
239
- await this._mqttSubscribe(`${this._vrpcClientId}/#`)
264
+ await this._mqttSubscribe(`${this._vrpcConnectionId}/#`)
240
265
 
241
266
  // Re-subscribe to all cached event topics
242
267
  this._log.debug('Restoring event subscriptions after reconnect...')
@@ -409,7 +434,7 @@ class VrpcClient extends EventEmitter {
409
434
  f: isIsolated ? '__createIsolated__' : '__createShared__',
410
435
  a: [instance, ...args],
411
436
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
412
- s: this._vrpcClientId,
437
+ s: this._vrpcConnectionId,
413
438
  v: VRPC_PROTOCOL_VERSION
414
439
  }
415
440
  const proxy = await this._getProxy(agent, className, json)
@@ -462,7 +487,7 @@ class VrpcClient extends EventEmitter {
462
487
  f: '__delete__',
463
488
  a: [instance],
464
489
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
465
- s: this._vrpcClientId,
490
+ s: this._vrpcConnectionId,
466
491
  v: VRPC_PROTOCOL_VERSION
467
492
  }
468
493
  const topic = `${this._domain}/${agent}/${className}/__static__/__delete__`
@@ -502,7 +527,7 @@ class VrpcClient extends EventEmitter {
502
527
  f: functionName,
503
528
  a: wrapped,
504
529
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
505
- s: this._vrpcClientId,
530
+ s: this._vrpcConnectionId,
506
531
  v: VRPC_PROTOCOL_VERSION
507
532
  }
508
533
  const topic = `${this._domain}/${agent}/${className}/__static__/${functionName}`
@@ -545,7 +570,7 @@ class VrpcClient extends EventEmitter {
545
570
  f: functionName,
546
571
  a: [],
547
572
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
548
- s: this._vrpcClientId,
573
+ s: this._vrpcConnectionId,
549
574
  v: VRPC_PROTOCOL_VERSION
550
575
  }
551
576
  if (agent === '*') {
@@ -708,7 +733,7 @@ class VrpcClient extends EventEmitter {
708
733
  /**
709
734
  * Reconnects to the broker by using a different token
710
735
  *
711
- * @param {String} token Access token as generated by: https://app.vrpc.io
736
+ * @param {String} token Access token
712
737
  * @param {Object} [options]
713
738
  * @param {String} options.agent Agent name. If not provided class default is used
714
739
  * @returns {Promise} Promise that resolves once re-connected
@@ -752,8 +777,12 @@ class VrpcClient extends EventEmitter {
752
777
  async end () {
753
778
  if (!this._client) return
754
779
  this._mqttPublish(
755
- `${this._vrpcClientId}/__clientInfo__`,
756
- JSON.stringify({ status: 'offline', v: VRPC_PROTOCOL_VERSION })
780
+ `${this._vrpcConnectionId}/__clientInfo__`,
781
+ JSON.stringify({
782
+ status: 'offline',
783
+ clientId: this._vrpcClientId,
784
+ v: VRPC_PROTOCOL_VERSION
785
+ })
757
786
  )
758
787
  this._eventEmitter.removeAllListeners()
759
788
  this.removeAllListeners()
@@ -819,6 +848,14 @@ class VrpcClient extends EventEmitter {
819
848
  return `${this._domain}/${os.hostname()}/${identity}`
820
849
  }
821
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
+
822
859
  _mqttPublish (topic, message, options) {
823
860
  this._client.publish(
824
861
  topic,
@@ -981,6 +1018,7 @@ class VrpcClient extends EventEmitter {
981
1018
  const proxyId = `${this._instance}-${this._proxyId++}`
982
1019
  const proxy = {
983
1020
  vrpcClientId: this._vrpcClientId,
1021
+ vrpcConnectionId: this._vrpcConnectionId,
984
1022
  vrpcInstanceId: instance,
985
1023
  vrpcProxyId: proxyId
986
1024
  }
@@ -1027,7 +1065,7 @@ class VrpcClient extends EventEmitter {
1027
1065
  i: `${this._instance}-${
1028
1066
  this._invokeId++ % Number.MAX_SAFE_INTEGER
1029
1067
  }`,
1030
- s: this._vrpcClientId,
1068
+ s: this._vrpcConnectionId,
1031
1069
  v: VRPC_PROTOCOL_VERSION
1032
1070
  }
1033
1071
  this._mqttPublish(
@@ -1061,7 +1099,7 @@ class VrpcClient extends EventEmitter {
1061
1099
  i: `${this._instance}-${
1062
1100
  this._invokeId++ % Number.MAX_SAFE_INTEGER
1063
1101
  }`,
1064
- s: this._vrpcClientId,
1102
+ s: this._vrpcConnectionId,
1065
1103
  v: VRPC_PROTOCOL_VERSION
1066
1104
  }
1067
1105
  this._mqttPublish(
@@ -1096,7 +1134,7 @@ class VrpcClient extends EventEmitter {
1096
1134
  i: `${this._instance}-${
1097
1135
  this._invokeId++ % Number.MAX_SAFE_INTEGER
1098
1136
  }`,
1099
- s: this._vrpcClientId,
1137
+ s: this._vrpcConnectionId,
1100
1138
  v: VRPC_PROTOCOL_VERSION
1101
1139
  }
1102
1140
  this._mqttPublish(
@@ -1280,7 +1318,7 @@ class VrpcClient extends EventEmitter {
1280
1318
  const topicId = VrpcClient._createHash(hashInput, 12)
1281
1319
 
1282
1320
  // Route directly to the client's single event inbox wildcard
1283
- const topic = `${this._vrpcClientId}/${topicId}`
1321
+ const topic = `${this._vrpcConnectionId}/${topicId}`
1284
1322
  const id = `__e__${topic}`
1285
1323
 
1286
1324
  const handler = ({ a }) => callback.apply(null, a)
@@ -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 = new Storage({ log: this._log, dir: this._dir })
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
  }