vrpc 3.7.0 → 3.8.1

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.1",
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...')
@@ -296,7 +321,7 @@ class VrpcClient extends EventEmitter {
296
321
  if (removed.length !== 0) {
297
322
  this.emit('instanceGone', removed, { domain, agent, className })
298
323
  removed.forEach(lostInstance =>
299
- this._clearCachedSubscriptions({ lostInstance })
324
+ this._clearCachedSubscriptions({ lostInstance, agent, className })
300
325
  )
301
326
  }
302
327
  if (added.length !== 0) {
@@ -409,11 +434,18 @@ 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)
416
- if (instance && cacheProxy) this._proxies[instance] = proxy
441
+ if (instance && cacheProxy) {
442
+ this._proxies[`${agent}/${className}/${instance}`] = {
443
+ agent,
444
+ className,
445
+ instance,
446
+ proxy
447
+ }
448
+ }
417
449
  return proxy
418
450
  }
419
451
 
@@ -435,7 +467,8 @@ class VrpcClient extends EventEmitter {
435
467
  if (!this._isConnected()) {
436
468
  throw new Error('Client is not connected')
437
469
  }
438
- if (this._proxies[instance]) return this._proxies[instance]
470
+ const cached = this._cachedProxy(instance, options)
471
+ if (cached) return cached
439
472
  const { agent, className } = await this._getInstanceData(instance, options)
440
473
  return this._createProxy(agent, className, instance)
441
474
  }
@@ -462,12 +495,12 @@ class VrpcClient extends EventEmitter {
462
495
  f: '__delete__',
463
496
  a: [instance],
464
497
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
465
- s: this._vrpcClientId,
498
+ s: this._vrpcConnectionId,
466
499
  v: VRPC_PROTOCOL_VERSION
467
500
  }
468
501
  const topic = `${this._domain}/${agent}/${className}/__static__/__delete__`
469
502
  this._mqttPublish(topic, JSON.stringify(json))
470
- if (this._proxies[instance]) delete this._proxies[instance]
503
+ delete this._proxies[`${agent}/${className}/${instance}`]
471
504
  return this._handleAgentAnswer(json, agent)
472
505
  }
473
506
 
@@ -502,7 +535,7 @@ class VrpcClient extends EventEmitter {
502
535
  f: functionName,
503
536
  a: wrapped,
504
537
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
505
- s: this._vrpcClientId,
538
+ s: this._vrpcConnectionId,
506
539
  v: VRPC_PROTOCOL_VERSION
507
540
  }
508
541
  const topic = `${this._domain}/${agent}/${className}/__static__/${functionName}`
@@ -545,7 +578,7 @@ class VrpcClient extends EventEmitter {
545
578
  f: functionName,
546
579
  a: [],
547
580
  i: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
548
- s: this._vrpcClientId,
581
+ s: this._vrpcConnectionId,
549
582
  v: VRPC_PROTOCOL_VERSION
550
583
  }
551
584
  if (agent === '*') {
@@ -708,7 +741,7 @@ class VrpcClient extends EventEmitter {
708
741
  /**
709
742
  * Reconnects to the broker by using a different token
710
743
  *
711
- * @param {String} token Access token as generated by: https://app.vrpc.io
744
+ * @param {String} token Access token
712
745
  * @param {Object} [options]
713
746
  * @param {String} options.agent Agent name. If not provided class default is used
714
747
  * @returns {Promise} Promise that resolves once re-connected
@@ -752,8 +785,12 @@ class VrpcClient extends EventEmitter {
752
785
  async end () {
753
786
  if (!this._client) return
754
787
  this._mqttPublish(
755
- `${this._vrpcClientId}/__clientInfo__`,
756
- JSON.stringify({ status: 'offline', v: VRPC_PROTOCOL_VERSION })
788
+ `${this._vrpcConnectionId}/__clientInfo__`,
789
+ JSON.stringify({
790
+ status: 'offline',
791
+ clientId: this._vrpcClientId,
792
+ v: VRPC_PROTOCOL_VERSION
793
+ })
757
794
  )
758
795
  this._eventEmitter.removeAllListeners()
759
796
  this.removeAllListeners()
@@ -819,6 +856,14 @@ class VrpcClient extends EventEmitter {
819
856
  return `${this._domain}/${os.hostname()}/${identity}`
820
857
  }
821
858
 
859
+ _createVrpcConnectionId () {
860
+ // Without an identity the client id already is unique per instance.
861
+ // With one, the instance token makes every connection of that identity
862
+ // distinct while keeping the three-segment layout agents parse.
863
+ if (!this._identity) return this._vrpcClientId
864
+ return `${this._vrpcClientId}:${this._instance}`
865
+ }
866
+
822
867
  _mqttPublish (topic, message, options) {
823
868
  this._client.publish(
824
869
  topic,
@@ -981,6 +1026,7 @@ class VrpcClient extends EventEmitter {
981
1026
  const proxyId = `${this._instance}-${this._proxyId++}`
982
1027
  const proxy = {
983
1028
  vrpcClientId: this._vrpcClientId,
1029
+ vrpcConnectionId: this._vrpcConnectionId,
984
1030
  vrpcInstanceId: instance,
985
1031
  vrpcProxyId: proxyId
986
1032
  }
@@ -1027,7 +1073,7 @@ class VrpcClient extends EventEmitter {
1027
1073
  i: `${this._instance}-${
1028
1074
  this._invokeId++ % Number.MAX_SAFE_INTEGER
1029
1075
  }`,
1030
- s: this._vrpcClientId,
1076
+ s: this._vrpcConnectionId,
1031
1077
  v: VRPC_PROTOCOL_VERSION
1032
1078
  }
1033
1079
  this._mqttPublish(
@@ -1061,7 +1107,7 @@ class VrpcClient extends EventEmitter {
1061
1107
  i: `${this._instance}-${
1062
1108
  this._invokeId++ % Number.MAX_SAFE_INTEGER
1063
1109
  }`,
1064
- s: this._vrpcClientId,
1110
+ s: this._vrpcConnectionId,
1065
1111
  v: VRPC_PROTOCOL_VERSION
1066
1112
  }
1067
1113
  this._mqttPublish(
@@ -1096,7 +1142,7 @@ class VrpcClient extends EventEmitter {
1096
1142
  i: `${this._instance}-${
1097
1143
  this._invokeId++ % Number.MAX_SAFE_INTEGER
1098
1144
  }`,
1099
- s: this._vrpcClientId,
1145
+ s: this._vrpcConnectionId,
1100
1146
  v: VRPC_PROTOCOL_VERSION
1101
1147
  }
1102
1148
  this._mqttPublish(
@@ -1280,7 +1326,7 @@ class VrpcClient extends EventEmitter {
1280
1326
  const topicId = VrpcClient._createHash(hashInput, 12)
1281
1327
 
1282
1328
  // Route directly to the client's single event inbox wildcard
1283
- const topic = `${this._vrpcClientId}/${topicId}`
1329
+ const topic = `${this._vrpcConnectionId}/${topicId}`
1284
1330
  const id = `__e__${topic}`
1285
1331
 
1286
1332
  const handler = ({ a }) => callback.apply(null, a)
@@ -1336,11 +1382,19 @@ class VrpcClient extends EventEmitter {
1336
1382
  return foundId
1337
1383
  }
1338
1384
 
1339
- _clearCachedSubscriptions ({ lostAgent, lostInstance }) {
1385
+ _clearCachedSubscriptions ({ lostAgent, lostInstance, agent, className }) {
1340
1386
  const obsoleteTopics = Object.keys(this._cachedSubscriptions).filter(
1341
1387
  topic => {
1342
1388
  const sub = this._cachedSubscriptions[topic]
1343
- return sub.agent === lostAgent || sub.instance === lostInstance
1389
+ if (lostAgent !== undefined) return sub.agent === lostAgent
1390
+ // Instance ids are unique per agent only: a twin of the same id on
1391
+ // another agent (a deployed app next to its builder) keeps its
1392
+ // subscriptions when this one goes
1393
+ return (
1394
+ sub.instance === lostInstance &&
1395
+ sub.agent === agent &&
1396
+ sub.className === className
1397
+ )
1344
1398
  }
1345
1399
  )
1346
1400
 
@@ -1394,12 +1448,25 @@ class VrpcClient extends EventEmitter {
1394
1448
  return instanceData
1395
1449
  }
1396
1450
 
1451
+ _cachedProxy (instance, { agent, className } = {}) {
1452
+ // Instance ids are unique per agent only: a cached proxy is served
1453
+ // solely when it is the one match for the given agent and class - twins
1454
+ // on several agents are resolved through the topology instead
1455
+ const hits = Object.values(this._proxies).filter(
1456
+ x =>
1457
+ x.instance === instance &&
1458
+ (!agent || x.agent === agent) &&
1459
+ (!className || x.className === className)
1460
+ )
1461
+ return hits.length === 1 ? hits[0].proxy : null
1462
+ }
1463
+
1397
1464
  _getInstanceFromCache (instance, options = {}) {
1398
1465
  // when no agent is specified, but an explicit class default exists give
1399
1466
  // such instances priority in being found
1400
1467
  if (!options.agent && this._agent !== '*') {
1401
1468
  const { classes, status } = this._agents[this._agent] || {}
1402
- if (!classes || status === 'offline') {
1469
+ if (classes && status !== 'offline') {
1403
1470
  for (const className in classes) {
1404
1471
  if (options.className && className !== options.className) continue
1405
1472
  const { instances } = classes[className]
@@ -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
  }