bare-rpc 1.4.0 → 1.5.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 CHANGED
@@ -8,40 +8,119 @@ npm i bare-rpc
8
8
 
9
9
  ## Usage
10
10
 
11
+ This example wires two peers together over IPC and makes one request:
12
+
11
13
  ```js
14
+ import IPC from 'bare-ipc'
12
15
  import RPC from 'bare-rpc'
13
16
 
14
- // On one end
15
- const rpc = new RPC(stream, (req) => {
16
- if (req.command === 42) {
17
- console.log(req.data.toString()) // ping
18
- req.reply('pong')
19
- }
20
- })
17
+ const [a, b] = IPC.open().map((port) => port.connect())
18
+
19
+ const server = new RPC(a, (req) => req.reply('pong'))
21
20
 
22
- // On the other end
23
- const req = rpc.request(42)
21
+ const client = new RPC(b)
22
+
23
+ const req = client.request(1)
24
24
  req.send('ping')
25
25
 
26
- const replyBuffer = await req.reply()
27
- console.log(replyBuffer.toString()) // pong
26
+ console.log((await req.reply()).toString()) // pong
27
+
28
+ a.destroy()
29
+ b.destroy()
28
30
  ```
29
31
 
30
- ## API
32
+ `new RPC(stream, onrequest, opts)` runs a session over a duplex `stream`, such as a pipe, a socket or an IPC port. `onrequest` is a function that is called with each request and event of the peer, or a `CommandRouter`. `opts` takes `maxFrameSize` and `maxBufferedBytes`, as described under [Untrusted peers](#untrusted-peers).
33
+
34
+ See the [`bare-rpc` reference](https://docs.pears.com/reference/bare/modules/bare-rpc) for the full API.
35
+
36
+ ## Command routing
37
+
38
+ A `CommandRouter` sends each request and event to the handler registered for its command. Pass it to `RPC` as `onrequest`:
39
+
40
+ ```js
41
+ const router = new RPC.CommandRouter()
42
+
43
+ router.respond(1, (req, data) => Buffer.from('pong'))
44
+ router.respond(2, (req, data) => console.log(req.command))
45
+
46
+ const rpc = new RPC(stream, router)
47
+ ```
48
+
49
+ A handler is called with the request and its data, and returns the data to reply with, unless it has answered the request itself. Events for a command go to the same handler, and its return value is ignored. A request for a command with no handler is rejected with the code `UNKNOWN_COMMAND`. An event for a command with no handler is dropped.
50
+
51
+ The data is decoded and the reply encoded with a [compact-encoding](https://github.com/holepunchto/compact-encoding), `c.raw` by default. Pass `valueEncoding` to `new CommandRouter()` to change the default, or `valueEncoding`, `requestEncoding` and `responseEncoding` to `respond(command, opts, handler)` for one command.
52
+
53
+ ## Requests and responses
54
+
55
+ `rpc.request(command)` returns a request. Call `send(data)` on it to send it, and `await reply()` to get its reply. `rpc.event(command)` returns an event, which gets no reply. Call `send(data)` on it to send it. Each can be sent once, and a reply can be read once. Sending again throws an `RPCError` with the code `ALREADY_SENT`, and reading again throws one with the code `ALREADY_RECEIVED`.
56
+
57
+ Data can be a buffer, or a string that `send(data, encoding)` and `reply(data, encoding)` encode with `encoding`, `utf8` by default. `reply(encoding)` on a request you sent decodes the reply to a string.
58
+
59
+ A request of the peer is answered once, with `reply()` or `createResponseStream()`. Answering again throws an `RPCError` with the code `ALREADY_SENT`, and `req.sent` tells whether the request has been answered. Events reach `onrequest` as requests without `reply()`. Each request and event of the peer is handled on its own, so a slow async handler does not hold back the others.
60
+
61
+ If a request handler throws before it answers, the request is rejected with the `message` of the error, and its `code` and `errno` if it has them. If the handler had opened a response stream, the stream is destroyed with the error instead.
62
+
63
+ When the peer rejects a request, `reply()` rejects with an `Error` with the `message`, `code` and `errno` of the peer.
31
64
 
32
- See the [`bare-rpc` reference](https://docs.pears.com/reference/bare/modules/bare-rpc).
65
+ Without `onrequest`, every request is rejected with the code `UNKNOWN_COMMAND`, and events are dropped.
66
+
67
+ ## Streaming requests and responses
68
+
69
+ A request can stream its body, its reply, or both:
70
+
71
+ - `createRequestStream()` returns a `Writable` for the body, and `await reply()` gets a single reply.
72
+ - `send(data)` sends a plain request, and `createResponseStream()` returns a `Readable` for the reply.
73
+ - `createRequestStream()` and `createResponseStream()` together stream both.
74
+
75
+ On the answering side, `req.createRequestStream()` returns a `Readable` for the body, and `req.createResponseStream()` returns a `Writable` for the reply. This handler echoes the body back:
76
+
77
+ ```js
78
+ const server = new RPC(stream, (req) => {
79
+ req.createRequestStream().pipe(req.createResponseStream())
80
+ })
81
+
82
+ const req = client.request(1)
83
+ const outgoing = req.createRequestStream()
84
+ const incoming = req.createResponseStream()
85
+
86
+ outgoing.end('ping')
87
+
88
+ for await (const chunk of incoming) console.log(chunk.toString()) // ping
89
+ ```
90
+
91
+ A stream only carries data once both ends have opened it, so data written before then is held back until the peer has opened its end. The handler opens the body when it calls `req.createRequestStream()`. If the peer answers before it opens the body, the body is destroyed, with the error of the reply if it has one.
92
+
93
+ An incoming stream asks the peer to pause once it buffers its `highWaterMark`, which is 16 KiB by default, and to resume once it is read. While the peer has paused an outgoing stream, its writes are held back. If the peer keeps sending anyway, an incoming stream holds at most `maxBufferedBytes`, 16 MiB by default, before the session fails with an `RPCError` with the code `STREAM_OVERFLOW`.
94
+
95
+ The body of a request and the reply to a request can each be read once. Calling `createRequestStream()` on a request of the peer again, or `createResponseStream()` or `reply()` on a request you sent again, throws an `RPCError` with the code `ALREADY_RECEIVED`.
96
+
97
+ ## Termination
98
+
99
+ The session ends with an `RPCError` with the code `CHANNEL_CLOSED` when `stream` closes. It fails with an error instead when:
100
+
101
+ - `stream` emits an error, with that error.
102
+ - `stream` ends partway through a frame, with the code `INVALID_MESSAGE`.
103
+ - An event handler throws, with that error.
104
+ - The peer sends a bad frame, as described under [Untrusted peers](#untrusted-peers).
105
+
106
+ Pending replies then reject with the error, and so do later replies. Open streams are destroyed with it, and so are streams created later. Events sent after the end are dropped.
107
+
108
+ `RPC` does not close `stream` when the session ends cleanly. When it fails for any reason but an error of `stream`, `RPC` destroys `stream` with the error, so the frames still queued in it are not written. To end the session yourself, destroy `stream`. `rpc.idle` tells whether no requests or streams are in flight, so that it is safe to do so.
33
109
 
34
110
  ## Untrusted peers
35
111
 
36
- `bare-rpc` does not trust the remote end to send valid frames. If a frame is larger than `maxFrameSize`, cannot be decoded, or breaks the protocol, the connection fails: `stream` is destroyed with an `RPCError`, and every request and stream in flight is rejected or destroyed with that error. The error code tells you why:
112
+ `RPC` does not trust the peer to send valid frames. If a frame is larger than `maxFrameSize`, cannot be decoded, or breaks the protocol, the session fails with an `RPCError`. The code tells you why:
37
113
 
38
- - `FRAME_TOO_LARGE`: The length prefix is larger than `maxFrameSize`. This is checked before the rest of the frame is read.
39
- - `UNKNOWN_MESSAGE`: The frame has a message type that `bare-rpc` does not know.
40
- - `INVALID_MESSAGE`: The frame cannot be decoded, the stream ends partway through a frame, or the frame is one that a valid peer never sends. Examples are a response or stream message with an id of 0, a stream message with an invalid set of flags, and a new request that reuses the id of a request that is still in flight.
114
+ - `FRAME_TOO_LARGE`: The frame, with its 4-byte length prefix, is larger than `maxFrameSize`, 16 MiB by default. This is checked as soon as the length prefix arrives, before the rest of the frame is read.
115
+ - `UNKNOWN_MESSAGE`: The frame has a message type that `RPC` does not know.
116
+ - `INVALID_MESSAGE`: The frame cannot be decoded, `stream` ends partway through a frame, or the frame is one that a valid peer never sends. Examples are a response or stream message with an id of 0, a stream message with an invalid set of flags, and a new request that reuses the id of a request that is still in flight.
117
+ - `STREAM_OVERFLOW`: An incoming stream holds more than `maxBufferedBytes` of data that has not been read. This only happens when the peer keeps sending after it was asked to pause.
41
118
 
42
119
  A frame that refers to a request or stream that has already finished, or that was never known, is ignored. The same goes for frames that arrive for a stream that has already ended or been destroyed.
43
120
 
44
- If you read the response to a request as a stream but the remote end replies without one, the reply arrives on the response stream as a single chunk, unless it is empty, and the stream then ends. An error reply destroys the response stream with that error.
121
+ If you read the reply as a stream but the peer replies without one, the reply arrives on the stream as a single chunk, unless it is empty, and the stream then ends. An error reply destroys the stream with the error instead.
122
+
123
+ If you expect a plain reply but the peer replies with a stream, `reply()` rejects with an `RPCError` with the code `UNEXPECTED_STREAM`. The session destroys the stream toward the peer and stays open.
45
124
 
46
125
  ## License
47
126
 
package/index.d.ts CHANGED
@@ -124,7 +124,9 @@ interface RPCOutgoingRequest {
124
124
  * @param encoding - If given, decodes the reply payload to a string using this encoding; omit to
125
125
  * receive the raw `Buffer`.
126
126
  * @returns a promise that resolves with the remote end's reply payload, or rejects with the
127
- * channel's teardown error if it closes before a reply arrives.
127
+ * channel's teardown error if it closes before a reply arrives. If the remote end replies with a
128
+ * stream, the promise rejects with an error whose code is `UNEXPECTED_STREAM`, the remote end's
129
+ * stream is destroyed, and the connection stays open.
128
130
  * @throws {ALREADY_RECEIVED} a reply is already being received for this request.
129
131
  */
130
132
  reply(encoding?: BufferEncoding): Promise<Buffer | string | null>
@@ -213,18 +215,20 @@ interface RPCRespondOptions {
213
215
  }
214
216
 
215
217
  /**
216
- * @param req - The incoming request.
218
+ * @param req - The incoming request. For an event, this is an `RPCIncomingEvent`.
217
219
  * @param data - The request data, decoded with the request encoding of the command.
218
220
  * @returns the response data, encoded with the response encoding of the command and sent as the
219
- * reply unless the handler has already replied.
221
+ * reply unless the handler has already replied. For an event, the return value is ignored and
222
+ * nothing is sent.
220
223
  */
221
224
  type RPCRequestHandler = (req: RPCIncomingRequest, data: any) => unknown
222
225
 
223
226
  interface RPCCommandRouter {
224
227
  /**
225
- * Answer requests for `command` with `onrequest`, in place of any handler registered before. A
226
- * request for a command with no handler is answered with an error whose code is
227
- * `UNKNOWN_COMMAND`, and an event for such a command is dropped.
228
+ * Answer requests for `command` with `onrequest`, in place of any handler registered before.
229
+ * Events for `command` are also passed to `onrequest`, but are never answered. A request for a
230
+ * command with no handler is answered with an error whose code is `UNKNOWN_COMMAND`, and an event
231
+ * for such a command is dropped.
228
232
  * @param command - The command number to answer.
229
233
  * @param opts - The encodings of the command.
230
234
  * @param onrequest - The handler of the requests.
@@ -247,6 +251,12 @@ interface RPCOptions {
247
251
  * Defaults to 16 MiB.
248
252
  */
249
253
  maxFrameSize?: number
254
+ /**
255
+ * The most data, in bytes, that one incoming stream may hold before it is read. A remote end that
256
+ * keeps sending past this limit, rather than stop when asked to pause, fails the connection with
257
+ * `STREAM_OVERFLOW`. Defaults to 16 MiB.
258
+ */
259
+ maxBufferedBytes?: number
250
260
  }
251
261
 
252
262
  interface RPC {
@@ -276,19 +286,23 @@ declare class RPC {
276
286
  * @param stream - The duplex stream to frame RPC messages over, such as a pipe or socket.
277
287
  * @param onrequest - Callback run for each incoming request or event, receiving an
278
288
  * `RPCIncomingRequest` (or `RPCIncomingEvent`) to inspect and `reply()` to, or an
279
- * `RPCCommandRouter` that answers each request with the handler of its command. Defaults to a
280
- * no-op.
289
+ * `RPCCommandRouter` that answers each request with the handler of its command. If it throws, the
290
+ * request is answered with the error, or, if it has already created a response stream, that
291
+ * stream is destroyed with the error. If it has already replied, nothing more is sent. If
292
+ * omitted, every request is answered with an error whose code is `UNKNOWN_COMMAND` and every
293
+ * event is dropped.
281
294
  * @param opts - Options for the connection.
282
295
  *
283
296
  * The remote end is not trusted to send valid frames. A frame that is too large, cannot be
284
- * decoded, or breaks the protocol fails the connection: `stream` is destroyed with an `RPCError`
285
- * whose code is `FRAME_TOO_LARGE`, `UNKNOWN_MESSAGE` or `INVALID_MESSAGE`, and every request and
286
- * stream in flight is rejected or destroyed with it. A frame that refers to a request or stream
297
+ * decoded, or breaks the protocol fails the connection, and so does a stream that holds more
298
+ * unread data than `maxBufferedBytes`: `stream` is destroyed with an `RPCError` whose code is
299
+ * `FRAME_TOO_LARGE`, `UNKNOWN_MESSAGE`, `INVALID_MESSAGE` or `STREAM_OVERFLOW`, and every request
300
+ * and stream in flight is rejected or destroyed with it. A frame that refers to a request or stream
287
301
  * that has already finished, or that was never known, is ignored.
288
302
  */
289
303
  constructor(
290
304
  stream: Duplex,
291
- onrequest?: ((req: RPCIncomingRequest) => void | Promise<void>) | RPCCommandRouter,
305
+ onrequest?: ((req: RPCIncomingRequest) => void | Promise<void>) | RPCCommandRouter | null,
292
306
  opts?: RPCOptions
293
307
  )
294
308
  }
package/index.js CHANGED
@@ -14,6 +14,7 @@ const OutgoingStream = require('./lib/outgoing-stream')
14
14
  const CommandRouter = require('./lib/command-router')
15
15
 
16
16
  const DEFAULT_MAX_FRAME_SIZE = 16 * 1024 * 1024
17
+ const DEFAULT_MAX_BUFFERED_BYTES = 16 * 1024 * 1024
17
18
  const MAX_PENDING_RESPONSES = 1024
18
19
 
19
20
  const streamFlags = new Set()
@@ -35,11 +36,13 @@ for (const direction of [s.REQUEST, s.RESPONSE]) {
35
36
  }
36
37
 
37
38
  module.exports = exports = class RPC {
38
- constructor(stream, onrequest = noop, opts = {}) {
39
- const { maxFrameSize = DEFAULT_MAX_FRAME_SIZE } = opts
39
+ constructor(stream, onrequest = null, opts = {}) {
40
+ const { maxFrameSize = DEFAULT_MAX_FRAME_SIZE, maxBufferedBytes = DEFAULT_MAX_BUFFERED_BYTES } =
41
+ opts
40
42
 
41
43
  this._stream = stream
42
44
  this._maxFrameSize = maxFrameSize
45
+ this._maxBufferedBytes = maxBufferedBytes
43
46
  this._id = 0
44
47
 
45
48
  this._outgoingRequests = new Map()
@@ -58,6 +61,10 @@ module.exports = exports = class RPC {
58
61
  this._closed = false
59
62
  this._error = null
60
63
 
64
+ if (onrequest === null) {
65
+ onrequest = new CommandRouter()
66
+ }
67
+
61
68
  if (typeof onrequest === 'function') {
62
69
  onrequest = onrequest.bind(this)
63
70
  } else {
@@ -157,6 +164,10 @@ module.exports = exports = class RPC {
157
164
 
158
165
  this._outgoingRequests.set(request.id, request)
159
166
 
167
+ // Announce the stream right away rather than when it opens, so that the
168
+ // remote end also learns of a stream that is destroyed before it opens.
169
+ request._requestStream._sendOpen()
170
+
160
171
  request._requestStream.on('close', () => this._gcOutgoingRequest(request))
161
172
  } else {
162
173
  request._requestStream = new IncomingStream(this, request, t.REQUEST, opts)
@@ -197,6 +208,8 @@ module.exports = exports = class RPC {
197
208
 
198
209
  this._outgoingResponses.set(request.id, request)
199
210
 
211
+ request._responseStream._sendOpen()
212
+
200
213
  if (this._onanswered(request)) request._responseStream._continueOpen()
201
214
 
202
215
  request._responseStream.on('close', () => this._outgoingResponses.delete(request.id))
@@ -421,8 +434,9 @@ module.exports = exports = class RPC {
421
434
  } catch (err) {
422
435
  safetyCatch(err)
423
436
 
424
- if (message.id) this._sendError(request, err)
425
- else this._stream.destroy(err)
437
+ if (message.id === 0) this._stream.destroy(err)
438
+ else if (request._responseStream !== null) request._responseStream.destroy(err)
439
+ else if (!request.sent) this._sendError(request, err)
426
440
  }
427
441
  }
428
442
 
@@ -431,19 +445,13 @@ module.exports = exports = class RPC {
431
445
  if (request === undefined) return
432
446
 
433
447
  if (message.error) {
434
- request._responded = true
435
- this._gcOutgoingRequest(request)
436
-
437
- request._reject(message.error)
448
+ this._settle(request, message.error, null)
438
449
 
439
450
  if (request._responseStream !== null) request._responseStream.destroy(message.error)
440
451
 
441
452
  this._releaseRequestStream(request, message.error)
442
453
  } else if (message.stream === 0) {
443
- request._responded = true
444
- this._gcOutgoingRequest(request)
445
-
446
- request._resolve(message.data)
454
+ this._settle(request, null, message.data)
447
455
 
448
456
  const stream = request._responseStream
449
457
 
@@ -453,9 +461,31 @@ module.exports = exports = class RPC {
453
461
  }
454
462
 
455
463
  this._releaseRequestStream(request, null)
464
+ } else if (request._responseStream === null) {
465
+ const err = errors.UNEXPECTED_STREAM(`Request '${request.id}' was answered with a stream`)
466
+
467
+ this._settle(request, err, null)
468
+
469
+ this._sendMessage({
470
+ type: t.STREAM,
471
+ id: request.id,
472
+ stream: s.RESPONSE | s.DESTROY,
473
+ error: null,
474
+ data: null
475
+ })
476
+
477
+ this._releaseRequestStream(request, err)
456
478
  }
457
479
  }
458
480
 
481
+ _settle(request, err, data) {
482
+ request._responded = true
483
+ this._gcOutgoingRequest(request)
484
+
485
+ if (err) request._reject(err)
486
+ else request._resolve(data)
487
+ }
488
+
459
489
  // Once the request is answered, a request stream that the remote end never
460
490
  // opened will never be read, and would otherwise wait for its open forever.
461
491
  _releaseRequestStream(request, err) {
@@ -525,7 +555,17 @@ module.exports = exports = class RPC {
525
555
  const stream = this._incomingStream(message)
526
556
  if (!isPushable(stream)) return
527
557
 
528
- if (stream.push(message.data) === false) {
558
+ const flushed = stream.push(message.data)
559
+
560
+ if (stream._readableState.buffered > this._maxBufferedBytes) {
561
+ return this._fail(
562
+ errors.STREAM_OVERFLOW(
563
+ `Stream buffered more than the limit of ${this._maxBufferedBytes} bytes`
564
+ )
565
+ )
566
+ }
567
+
568
+ if (flushed === false) {
529
569
  this._sendMessage({
530
570
  type: t.STREAM,
531
571
  id: stream._request.id,
@@ -605,5 +645,3 @@ function findStream(message, requests, responses) {
605
645
  function isPushable(stream) {
606
646
  return stream !== null && !stream.destroying && !isEnding(stream)
607
647
  }
608
-
609
- function noop() {}
@@ -31,24 +31,31 @@ module.exports = class RPCCommandRouter {
31
31
  async _onrequest(req) {
32
32
  const responder = this._responders.get(req.command)
33
33
 
34
- if (responder === undefined) {
35
- if (req.id === undefined) return
36
-
37
- throw errors.UNKNOWN_COMMAND(`Unknown command ${req.command}`)
34
+ if (req.id === undefined) {
35
+ if (responder !== undefined) await this._dispatch(responder, req)
36
+ return
38
37
  }
39
38
 
40
- const { onrequest, requestEncoding, responseEncoding } = responder
41
-
42
- let data = req.data
39
+ if (responder === undefined) throw errors.UNKNOWN_COMMAND(`Unknown command ${req.command}`)
43
40
 
44
- if (requestEncoding) data = c.decode(requestEncoding, data)
45
-
46
- data = await onrequest(req, data)
41
+ let data = await this._dispatch(responder, req)
47
42
 
48
43
  if (req.sent) return
49
44
 
45
+ const { responseEncoding } = responder
46
+
50
47
  if (responseEncoding) data = c.encode(responseEncoding, data)
51
48
 
52
49
  req.reply(data)
53
50
  }
51
+
52
+ _dispatch(responder, req) {
53
+ const { onrequest, requestEncoding } = responder
54
+
55
+ let data = req.data
56
+
57
+ if (requestEncoding) data = c.decode(requestEncoding, data)
58
+
59
+ return onrequest(req, data)
60
+ }
54
61
  }
package/lib/errors.d.ts CHANGED
@@ -6,6 +6,8 @@ declare class RPCError extends Error {
6
6
  static ALREADY_SENT(msg: string): RPCError
7
7
  static ALREADY_RECEIVED(msg: string): RPCError
8
8
  static CHANNEL_CLOSED(msg: string): RPCError
9
+ static UNEXPECTED_STREAM(msg: string): RPCError
10
+ static STREAM_OVERFLOW(msg: string): RPCError
9
11
  }
10
12
 
11
13
  export = RPCError
package/lib/errors.js CHANGED
@@ -39,4 +39,12 @@ module.exports = class RPCError extends Error {
39
39
  static CHANNEL_CLOSED(msg) {
40
40
  return new RPCError(msg, 'CHANNEL_CLOSED', RPCError.CHANNEL_CLOSED)
41
41
  }
42
+
43
+ static UNEXPECTED_STREAM(msg) {
44
+ return new RPCError(msg, 'UNEXPECTED_STREAM', RPCError.UNEXPECTED_STREAM)
45
+ }
46
+
47
+ static STREAM_OVERFLOW(msg) {
48
+ return new RPCError(msg, 'STREAM_OVERFLOW', RPCError.STREAM_OVERFLOW)
49
+ }
42
50
  }
@@ -12,14 +12,15 @@ module.exports = class RPCOutgoingStream extends Writable {
12
12
  this._mask = type === t.REQUEST ? s.REQUEST : s.RESPONSE
13
13
 
14
14
  this._pendingOpen = null
15
+ this._flushed = false
15
16
  this._acknowledged = false
16
17
  this._ended = false
17
18
  }
18
19
 
19
- _open(cb) {
20
+ _sendOpen() {
20
21
  const onflushed = () => {
21
- if (this._acknowledged) cb(null)
22
- else this._pendingOpen = cb
22
+ this._flushed = true
23
+ this._finishOpen()
23
24
  }
24
25
 
25
26
  switch (this._type) {
@@ -51,14 +52,22 @@ module.exports = class RPCOutgoingStream extends Writable {
51
52
  }
52
53
  }
53
54
 
55
+ _open(cb) {
56
+ this._pendingOpen = cb
57
+ this._finishOpen()
58
+ }
59
+
54
60
  _continueOpen() {
55
61
  this._acknowledged = true
62
+ this._finishOpen()
63
+ }
56
64
 
57
- if (this._pendingOpen === null) return
65
+ _finishOpen() {
66
+ if (!this._flushed || !this._acknowledged || this._pendingOpen === null) return
58
67
 
59
68
  const cb = this._pendingOpen
60
69
  this._pendingOpen = null
61
- cb()
70
+ cb(null)
62
71
  }
63
72
 
64
73
  _predestroy() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-rpc",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "librpc ABI compatible RPC for Bare",
5
5
  "exports": {
6
6
  ".": {