vrpc 3.15.0 → 3.16.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.15.0",
3
+ "version": "3.16.0",
4
4
  "description": "Remote objects over MQTT: call classes in other programs as if they were local - Node.js, browser, C++, Python, Arduino",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -1177,13 +1177,15 @@ class VrpcAdapter {
1177
1177
  }
1178
1178
 
1179
1179
  /**
1180
- * Answers a promise twice: at once with `r: '__p__<f>-<n>'`, and once it
1181
- * settled with a second answer under that id, `{ s, i, r }` or
1182
- * `{ s, i, e }`.
1180
+ * Answers a promise twice: at once with `r: '__p__<f>-<nonce>-<n>'`, and
1181
+ * once it settled with a second answer under that id, `{ s, i, r }` or
1182
+ * `{ s, i, e }`. A client waits for the second answer under the bare id,
1183
+ * whichever agent sends it: the nonce keeps the ids of two agents apart,
1184
+ * whose counters may well stand at the same value.
1183
1185
  */
1184
1186
  static _handlePromise (json, promise) {
1185
1187
  const cid = VrpcAdapter._correlationId++ % Number.MAX_SAFE_INTEGER
1186
- const id = `__p__${json.f}-${cid}`
1188
+ const id = `__p__${json.f}-${VrpcAdapter._promiseNonce}-${cid}`
1187
1189
  const { s } = json
1188
1190
  json.r = id
1189
1191
  promise.then(
@@ -1391,6 +1393,8 @@ VrpcAdapter._functionRegistry = new Map()
1391
1393
  VrpcAdapter._instances = new Map()
1392
1394
  VrpcAdapter._emitter = new EventEmitter()
1393
1395
  VrpcAdapter._correlationId = 0
1396
+ // tells this process's promise ids from those of every other agent
1397
+ VrpcAdapter._promiseNonce = nanoid(8)
1394
1398
  // the event registrations of every context and their subscriptions
1395
1399
  VrpcAdapter._registrations = new Registrations({
1396
1400
  publish: message => VrpcAdapter._callback(message),
@@ -69,7 +69,9 @@ class VrpcAdapter {
69
69
  * @param {Boolean} [options.withNew=true] If true, class will be constructed
70
70
  * using the `new` operator
71
71
  * @param {Object} [options.schema=null] If provided is used to validate ctor
72
- * parameters (only works if registered code reflects a single class)
72
+ * parameters (only works if registered code reflects a single class). The
73
+ * browser bundle (`browser/vrpc.js`) does not validate: it warns once and
74
+ * constructs with the arguments as given
73
75
  */
74
76
  static registerClass (
75
77
  Klass,
@@ -912,6 +914,17 @@ class VrpcAdapter {
912
914
  }
913
915
 
914
916
  static _validate (schema, params) {
917
+ // the browser bundle leaves ajv out - a fifth of its size, for agents in
918
+ // a page, which are rare: there a schema is not checked
919
+ if (typeof Ajv !== 'function') {
920
+ if (!VrpcAdapter._warnedNoSchemas) {
921
+ VrpcAdapter._warnedNoSchemas = true
922
+ console.warn(
923
+ 'vrpc: schema validation is not available in the browser build, constructor arguments are not checked'
924
+ )
925
+ }
926
+ return
927
+ }
915
928
  if (!VrpcAdapter._ajv) {
916
929
  VrpcAdapter._ajv = new Ajv({ useDefaults: true })
917
930
  }
@@ -983,13 +996,15 @@ class VrpcAdapter {
983
996
  }
984
997
 
985
998
  /**
986
- * Answers a promise twice: at once with `r: '__p__<f>-<n>'`, and once it
987
- * settled with a second answer under that id, `{ s, i, r }` or
988
- * `{ s, i, e }`.
999
+ * Answers a promise twice: at once with `r: '__p__<f>-<nonce>-<n>'`, and
1000
+ * once it settled with a second answer under that id, `{ s, i, r }` or
1001
+ * `{ s, i, e }`. A client waits for the second answer under the bare id,
1002
+ * whichever agent sends it: the nonce keeps the ids of two agents apart,
1003
+ * whose counters may well stand at the same value.
989
1004
  */
990
1005
  static _handlePromise (json, promise) {
991
1006
  const cid = VrpcAdapter._correlationId++ % Number.MAX_SAFE_INTEGER
992
- const id = `__p__${json.f}-${cid}`
1007
+ const id = `__p__${json.f}-${VrpcAdapter._promiseNonce}-${cid}`
993
1008
  const { s } = json
994
1009
  json.r = id
995
1010
  promise.then(
@@ -1157,6 +1172,8 @@ VrpcAdapter._functionRegistry = new Map()
1157
1172
  VrpcAdapter._instances = new Map()
1158
1173
  VrpcAdapter._emitter = new EventEmitter()
1159
1174
  VrpcAdapter._correlationId = 0
1175
+ // tells this process's promise ids from those of every other agent
1176
+ VrpcAdapter._promiseNonce = nanoid(8)
1160
1177
  // the event registrations of every context and their subscriptions
1161
1178
  VrpcAdapter._registrations = new Registrations({
1162
1179
  publish: message => VrpcAdapter._callback(message),
@@ -180,6 +180,9 @@ class VrpcClient extends EventEmitter {
180
180
  this._pendingWithdrawals = new Map()
181
181
  // the client's own inbox is subscribed (answers can arrive)
182
182
  this._inboxReady = false
183
+ // the answers this client waits for ({ agent, second, lose, forget }),
184
+ // lost when the path an answer would travel breaks (see _loseAnswers)
185
+ this._waiting = new Set()
183
186
  // labels callbacks carry towards the agent's list of registrations
184
187
  this._subscriberLabels = new WeakMap()
185
188
  if (log === 'console') {
@@ -323,6 +326,12 @@ class VrpcClient extends EventEmitter {
323
326
  this.emit('error', err)
324
327
  })
325
328
 
329
+ // a clean session keeps nothing for this connection while it is gone:
330
+ // an answer published meanwhile never arrives
331
+ this._client.on('close', () => {
332
+ this._loseAnswers(() => true, 'this client lost its connection')
333
+ })
334
+
326
335
  this._client.stream.on('error', err => {
327
336
  this.emit('error', err)
328
337
  })
@@ -377,7 +386,7 @@ class VrpcClient extends EventEmitter {
377
386
  }
378
387
  })
379
388
 
380
- this._client.on('message', (topic, message) => {
389
+ this._client.on('message', (topic, message, packet) => {
381
390
  if (message.length === 0) return
382
391
  try {
383
392
  const tokens = topic.split('/')
@@ -398,8 +407,21 @@ class VrpcClient extends EventEmitter {
398
407
  // the agent's protocol version (undefined before 3)
399
408
  this._agents[agent].v = v
400
409
  if (status === 'offline') {
410
+ this._loseAnswers(
411
+ w => w.second && w.agent === agent,
412
+ 'the agent went offline'
413
+ )
401
414
  this._loseAgent(agent, v)
402
415
  } else {
416
+ // an agent announces itself from a new connection only (a
417
+ // retained announcement is old news a subscription brings):
418
+ // the promises its former connection was to answer are gone
419
+ if (!packet.retain) {
420
+ this._loseAnswers(
421
+ w => w.second && w.agent === agent,
422
+ 'the agent connected anew'
423
+ )
424
+ }
403
425
  this._redeclareTo(agent)
404
426
  this._flushWithdrawals(agent)
405
427
  }
@@ -1400,6 +1422,7 @@ class VrpcClient extends EventEmitter {
1400
1422
  v: VRPC_PROTOCOL_VERSION
1401
1423
  })
1402
1424
  )
1425
+ this._loseAnswers(() => true, 'the client ended')
1403
1426
  this._eventEmitter.removeAllListeners()
1404
1427
  this.removeAllListeners()
1405
1428
  // the agents withdraw what this connection held: nothing to declare
@@ -1604,12 +1627,16 @@ class VrpcClient extends EventEmitter {
1604
1627
  await this._waitUntilClassIsOnline(agent, className)
1605
1628
  this._mqttPublish(topic, JSON.stringify(json))
1606
1629
  return new Promise((resolve, reject) => {
1607
- const msg = `Proxy creation for class "${className}" on agent "${agent}" and domain "${this._domain}" timed out (> ${this._timeout} ms)`
1630
+ const call = `Proxy creation for class "${className}" on agent "${agent}" and domain "${this._domain}"`
1608
1631
  const timer = setTimeout(() => {
1609
- this._eventEmitter.removeAllListeners(i)
1610
- reject(new Error(msg))
1632
+ forget()
1633
+ reject(new Error(`${call} timed out (> ${this._timeout} ms)`))
1611
1634
  }, this._timeout)
1612
- this._eventEmitter.once(i, ({ e, r }) => {
1635
+ const lose = reason => {
1636
+ clearTimeout(timer)
1637
+ reject(new Error(`${call} lost its answer: ${reason}`))
1638
+ }
1639
+ const forget = this._awaitAnswer(i, { agent, lose }, ({ e, r }) => {
1613
1640
  clearTimeout(timer)
1614
1641
  if (e) {
1615
1642
  // the agent answers a throwing constructor with { message, cause }
@@ -1813,27 +1840,33 @@ class VrpcClient extends EventEmitter {
1813
1840
 
1814
1841
  async _handleAgentAnswer ({ i, c, f, a }, agent) {
1815
1842
  return new Promise((resolve, reject) => {
1816
- const msg = `Function call "${c}::${f}()" on agent "${agent}" timed out (> ${this._timeout} ms)`
1843
+ const call = `Function call "${c}::${f}()" on agent "${agent}"`
1817
1844
  const timer = setTimeout(() => {
1818
- this._eventEmitter.removeAllListeners(i)
1819
- reject(new Error(msg))
1845
+ forget()
1846
+ reject(new Error(`${call} timed out (> ${this._timeout} ms)`))
1820
1847
  }, this._timeout)
1848
+ const lose = reason => {
1849
+ clearTimeout(timer)
1850
+ reject(new Error(`${call} lost its answer: ${reason}`))
1851
+ }
1821
1852
  // a protocol-4 agent that answers a declaring call without an error
1822
1853
  // holds every subscription the call carried
1823
1854
  const confirm = () => {
1824
1855
  const info = this._agents[agent]
1825
1856
  if (info && info.v >= 4) this._confirmSubscriptions(a)
1826
1857
  }
1827
- this._eventEmitter.once(i, data => {
1858
+ const forget = this._awaitAnswer(i, { agent, lose }, data => {
1828
1859
  clearTimeout(timer)
1829
1860
  if (data.e) {
1830
1861
  const { message, cause } = VrpcClient._prepareError(data.e)
1831
1862
  reject(new Error(`[vrpc ${agent}-${c}-${f}]: ${message}`, { cause }))
1832
1863
  } else {
1833
1864
  const ret = data.r
1834
- // Handle functions returning a promise
1865
+ // Handle functions returning a promise: the result follows when
1866
+ // it settles, however long that takes - unless the path it would
1867
+ // travel breaks first
1835
1868
  if (typeof ret === 'string' && ret.substring(0, 5) === '__p__') {
1836
- this._eventEmitter.once(ret, promiseData => {
1869
+ this._awaitAnswer(ret, { agent, lose, second: true }, promiseData => {
1837
1870
  if (promiseData.e) {
1838
1871
  const { message, cause } = VrpcClient._prepareError(
1839
1872
  promiseData.e
@@ -1855,6 +1888,44 @@ class VrpcClient extends EventEmitter {
1855
1888
  })
1856
1889
  }
1857
1890
 
1891
+ /**
1892
+ * Waits for the answer with id `id`.
1893
+ *
1894
+ * @param {String} id
1895
+ * @param {Object} waiter
1896
+ * @param {String} waiter.agent the agent that answers
1897
+ * @param {Function} waiter.lose called with a reason when the answer can
1898
+ * no longer arrive
1899
+ * @param {Boolean} [waiter.second] a promise's result, the second answer
1900
+ * to its call
1901
+ * @param {Function} onAnswer called with the answer
1902
+ * @returns {Function} stops waiting, without a call to `lose`
1903
+ */
1904
+ _awaitAnswer (id, { agent, lose, second = false }, onAnswer) {
1905
+ const waiter = { agent, lose, second }
1906
+ const listener = data => {
1907
+ this._waiting.delete(waiter)
1908
+ onAnswer(data)
1909
+ }
1910
+ waiter.forget = () => {
1911
+ this._waiting.delete(waiter)
1912
+ this._eventEmitter.removeListener(id, listener)
1913
+ }
1914
+ this._waiting.add(waiter)
1915
+ this._eventEmitter.once(id, listener)
1916
+ return waiter.forget
1917
+ }
1918
+
1919
+ // the answers that can no longer arrive: their calls reject at once, and
1920
+ // a late answer finds no listener
1921
+ _loseAnswers (selects, reason) {
1922
+ for (const waiter of Array.from(this._waiting)) {
1923
+ if (!selects(waiter)) continue
1924
+ waiter.forget()
1925
+ waiter.lose(reason)
1926
+ }
1927
+ }
1928
+
1858
1929
  static _prepareError (rpcError) {
1859
1930
  if (typeof rpcError === 'object') {
1860
1931
  return { message: rpcError.message, cause: rpcError.cause }