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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vrpc",
3
- "version": "3.6.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",
@@ -45,9 +45,9 @@
45
45
  "bugs": {
46
46
  "url": "https://github.com/heisenware/vrpc/issues"
47
47
  },
48
- "homepage": "https://vrpc.io",
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
@@ -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._vrpcClientId)
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._vrpcClientId,
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._vrpcClientId,
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._vrpcClientId,
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._vrpcClientId,
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 as generated by: https://app.vrpc.io
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._vrpcClientId}/__clientInfo__`,
759
- 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
+ })
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 = 20) {
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 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)
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 subscriptions = this._cachedSubscriptions[topic]
1025
- subscriptions.forEach(({ handler }) => {
1052
+ const sub = this._cachedSubscriptions[topic]
1053
+
1054
+ sub.listeners.forEach(({ handler }) => {
1026
1055
  this._eventEmitter.removeListener(id, handler)
1027
1056
  })
1028
- if (!topic.includes('__e__')) {
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._vrpcClientId,
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._vrpcClientId,
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._vrpcClientId,
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
- // Route directly to the client's single event inbox wildcard.
1288
- const topic = `${this._vrpcClientId}/__e__/${agent}/${className}/${
1314
+ // Generate a short, sanitized hash
1315
+ const hashInput = `${agent}-${className}-${
1289
1316
  instance || '__static__'
1290
- }/${event}/${callbackId}`
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
- this._cachedSubscriptions[topic] = [{ callback, handler }]
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
- // We MUST push duplicates to act as a reference counter!
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).filter(
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 index = this._cachedSubscriptions[topic].findIndex(
1322
- x => x.callback === callback
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
- this._cachedSubscriptions[topic].splice(index, 1)
1362
+ sub.listeners.splice(index, 1)
1328
1363
 
1329
1364
  // Only remove from the local event emitter if NO references remain
1330
- if (this._cachedSubscriptions[topic].length === 0) {
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 tokens = topic.split('/')
1351
- if (tokens[3] === '__e__') {
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 subscriptions = this._cachedSubscriptions[topic]
1366
- subscriptions.forEach(({ handler }) => {
1388
+ const sub = this._cachedSubscriptions[topic]
1389
+
1390
+ sub.listeners.forEach(({ handler }) => {
1367
1391
  this._eventEmitter.removeListener(id, handler)
1368
1392
  })
1369
- // this async function will never reject
1370
- this._mqttUnsubscribe(topic)
1393
+
1371
1394
  delete this._cachedSubscriptions[topic]
1372
1395
  })
1373
1396
  }
@@ -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
  }