bare-rpc 1.3.9 → 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,28 +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()
30
+ ```
31
+
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.
64
+
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
28
89
  ```
29
90
 
30
- ## API
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.
109
+
110
+ ## Untrusted peers
111
+
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:
113
+
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.
118
+
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.
120
+
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.
31
122
 
32
- See the [`bare-rpc` reference](https://docs.pears.com/reference/bare/modules/bare-rpc).
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.
33
124
 
34
125
  ## License
35
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>
@@ -139,7 +141,8 @@ interface RPCOutgoingRequest {
139
141
  createRequestStream(opts?: WritableOptions): RPCOutgoingStream
140
142
  /**
141
143
  * Create a [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream for
142
- * receiving data in reply to the request.
144
+ * receiving data in reply to the request. If the remote end replies without a stream, the reply
145
+ * arrives as a single chunk, unless it is empty, and the stream then ends.
143
146
  * @param opts - Options for the returned
144
147
  * [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream.
145
148
  * @throws {ALREADY_RECEIVED} the response has already been received.
@@ -212,16 +215,20 @@ interface RPCRespondOptions {
212
215
  }
213
216
 
214
217
  /**
215
- * @param req - The incoming request.
218
+ * @param req - The incoming request. For an event, this is an `RPCIncomingEvent`.
216
219
  * @param data - The request data, decoded with the request encoding of the command.
217
220
  * @returns the response data, encoded with the response encoding of the command and sent as the
218
- * 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.
219
223
  */
220
224
  type RPCRequestHandler = (req: RPCIncomingRequest, data: any) => unknown
221
225
 
222
226
  interface RPCCommandRouter {
223
227
  /**
224
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.
225
232
  * @param command - The command number to answer.
226
233
  * @param opts - The encodings of the command.
227
234
  * @param onrequest - The handler of the requests.
@@ -237,6 +244,21 @@ declare class RPCCommandRouter {
237
244
  constructor(opts?: RPCCommandRouterOptions)
238
245
  }
239
246
 
247
+ interface RPCOptions {
248
+ /**
249
+ * The largest frame to accept from the remote end, in bytes, counting the 4-byte length prefix. A
250
+ * larger frame fails the connection with `FRAME_TOO_LARGE` as soon as its length prefix arrives.
251
+ * Defaults to 16 MiB.
252
+ */
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
260
+ }
261
+
240
262
  interface RPC {
241
263
  /**
242
264
  * Whether there are no requests or responses currently in flight. Useful for determining when it
@@ -264,12 +286,24 @@ declare class RPC {
264
286
  * @param stream - The duplex stream to frame RPC messages over, such as a pipe or socket.
265
287
  * @param onrequest - Callback run for each incoming request or event, receiving an
266
288
  * `RPCIncomingRequest` (or `RPCIncomingEvent`) to inspect and `reply()` to, or an
267
- * `RPCCommandRouter` that answers each request with the handler of its command. Defaults to a
268
- * 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.
294
+ * @param opts - Options for the connection.
295
+ *
296
+ * The remote end is not trusted to send valid frames. A frame that is too large, cannot be
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
301
+ * that has already finished, or that was never known, is ignored.
269
302
  */
270
303
  constructor(
271
304
  stream: Duplex,
272
- onrequest?: ((req: RPCIncomingRequest) => void | Promise<void>) | RPCCommandRouter
305
+ onrequest?: ((req: RPCIncomingRequest) => void | Promise<void>) | RPCCommandRouter | null,
306
+ opts?: RPCOptions
273
307
  )
274
308
  }
275
309
 
@@ -290,7 +324,8 @@ declare namespace RPC {
290
324
  type RPCCommandRouterOptions as CommandRouterOptions,
291
325
  type RPCRespondOptions as RespondOptions,
292
326
  type RPCRequestHandler as RequestHandler,
293
- type RPCValueEncoding as ValueEncoding
327
+ type RPCValueEncoding as ValueEncoding,
328
+ type RPCOptions as Options
294
329
  }
295
330
  }
296
331
 
package/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const safetyCatch = require('safety-catch')
2
2
  const b4a = require('b4a')
3
3
  const c = require('compact-encoding')
4
+ const { isEnding } = require('bare-stream')
4
5
  const m = require('./lib/messages')
5
6
  const errors = require('./lib/errors')
6
7
  const { type: t, stream: s } = require('./lib/constants')
@@ -12,17 +13,45 @@ const OutgoingRequest = require('./lib/outgoing-request')
12
13
  const OutgoingStream = require('./lib/outgoing-stream')
13
14
  const CommandRouter = require('./lib/command-router')
14
15
 
16
+ const DEFAULT_MAX_FRAME_SIZE = 16 * 1024 * 1024
17
+ const DEFAULT_MAX_BUFFERED_BYTES = 16 * 1024 * 1024
18
+ const MAX_PENDING_RESPONSES = 1024
19
+
20
+ const streamFlags = new Set()
21
+
22
+ for (const direction of [s.REQUEST, s.RESPONSE]) {
23
+ for (const action of [
24
+ s.OPEN,
25
+ s.CLOSE,
26
+ s.CLOSE | s.ERROR,
27
+ s.PAUSE,
28
+ s.RESUME,
29
+ s.DATA,
30
+ s.END,
31
+ s.DESTROY,
32
+ s.DESTROY | s.ERROR
33
+ ]) {
34
+ streamFlags.add(direction | action)
35
+ }
36
+ }
37
+
15
38
  module.exports = exports = class RPC {
16
- constructor(stream, onrequest = noop) {
39
+ constructor(stream, onrequest = null, opts = {}) {
40
+ const { maxFrameSize = DEFAULT_MAX_FRAME_SIZE, maxBufferedBytes = DEFAULT_MAX_BUFFERED_BYTES } =
41
+ opts
42
+
17
43
  this._stream = stream
44
+ this._maxFrameSize = maxFrameSize
45
+ this._maxBufferedBytes = maxBufferedBytes
18
46
  this._id = 0
19
47
 
20
48
  this._outgoingRequests = new Map()
21
49
  this._outgoingResponses = new Map()
22
50
  this._incomingRequests = new Map()
23
51
  this._incomingResponses = new Map()
24
- this._pendingRequests = new Set()
25
52
  this._pendingResponses = new Set()
53
+ this._unansweredRequests = new Set()
54
+ this._highestRequestId = 0
26
55
 
27
56
  this._buffer = []
28
57
  this._buffered = 0
@@ -32,6 +61,10 @@ module.exports = exports = class RPC {
32
61
  this._closed = false
33
62
  this._error = null
34
63
 
64
+ if (onrequest === null) {
65
+ onrequest = new CommandRouter()
66
+ }
67
+
35
68
  if (typeof onrequest === 'function') {
36
69
  onrequest = onrequest.bind(this)
37
70
  } else {
@@ -42,12 +75,14 @@ module.exports = exports = class RPC {
42
75
  this._onerror = this._onerror.bind(this)
43
76
  this._onclose = this._onclose.bind(this)
44
77
  this._ondata = this._ondata.bind(this)
78
+ this._onend = this._onend.bind(this)
45
79
  this._ondrain = this._ondrain.bind(this)
46
80
 
47
81
  this._stream
48
82
  .on('error', this._onerror)
49
83
  .on('close', this._onclose)
50
84
  .on('data', this._ondata)
85
+ .on('end', this._onend)
51
86
  .on('drain', this._ondrain)
52
87
  }
53
88
 
@@ -59,7 +94,6 @@ module.exports = exports = class RPC {
59
94
  this._outgoingResponses.size === 0 &&
60
95
  this._incomingRequests.size === 0 &&
61
96
  this._incomingResponses.size === 0 &&
62
- this._pendingRequests.size === 0 &&
63
97
  this._pendingResponses.size === 0
64
98
  )
65
99
  }
@@ -130,6 +164,10 @@ module.exports = exports = class RPC {
130
164
 
131
165
  this._outgoingRequests.set(request.id, request)
132
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
+
133
171
  request._requestStream.on('close', () => this._gcOutgoingRequest(request))
134
172
  } else {
135
173
  request._requestStream = new IncomingStream(this, request, t.REQUEST, opts)
@@ -149,6 +187,8 @@ module.exports = exports = class RPC {
149
187
  // settle.
150
188
  if (this._closed) return
151
189
 
190
+ this._onanswered(request)
191
+
152
192
  this._sendMessage({
153
193
  type: t.RESPONSE,
154
194
  id: request.id,
@@ -168,6 +208,10 @@ module.exports = exports = class RPC {
168
208
 
169
209
  this._outgoingResponses.set(request.id, request)
170
210
 
211
+ request._responseStream._sendOpen()
212
+
213
+ if (this._onanswered(request)) request._responseStream._continueOpen()
214
+
171
215
  request._responseStream.on('close', () => this._outgoingResponses.delete(request.id))
172
216
  } else {
173
217
  request._responseStream = new IncomingStream(this, request, t.RESPONSE, opts)
@@ -186,15 +230,23 @@ module.exports = exports = class RPC {
186
230
  }
187
231
 
188
232
  _sendError(request, err) {
233
+ this._onanswered(request)
234
+
189
235
  this._sendMessage({
190
236
  type: t.RESPONSE,
191
237
  id: request.id,
192
238
  stream: 0,
193
- error: err,
239
+ error: m.toError(err),
194
240
  data: null
195
241
  })
196
242
  }
197
243
 
244
+ // Returns whether the remote end had already opened its response stream.
245
+ _onanswered(request) {
246
+ this._unansweredRequests.delete(request.id)
247
+ return this._pendingResponses.delete(request.id)
248
+ }
249
+
198
250
  _gcOutgoingRequest(request) {
199
251
  // The entry must stay reachable for as long as an incoming message might
200
252
  // still target it; until the response has settled and, if the request was
@@ -255,13 +307,19 @@ module.exports = exports = class RPC {
255
307
  this._outgoingResponses.clear()
256
308
  this._incomingRequests.clear()
257
309
  this._incomingResponses.clear()
258
- this._pendingRequests.clear()
259
310
  this._pendingResponses.clear()
311
+ this._unansweredRequests.clear()
260
312
 
261
313
  for (const stream of streams) stream.destroy(err)
262
314
  }
263
315
 
316
+ _fail(err) {
317
+ this._stream.destroy(err)
318
+ }
319
+
264
320
  _ondata(data) {
321
+ if (this._stream.destroying) return
322
+
265
323
  this._buffer.push(data)
266
324
  this._buffered += data.byteLength
267
325
 
@@ -272,13 +330,25 @@ module.exports = exports = class RPC {
272
330
  }
273
331
  }
274
332
 
333
+ _onend() {
334
+ if (this._buffered > 0) this._fail(errors.INVALID_MESSAGE('Truncated frame'))
335
+ }
336
+
275
337
  _onbeforeframe() {
276
- while (this._buffered >= 4) {
338
+ while (this._buffered >= 4 && !this._stream.destroying) {
277
339
  const buffer = this._buffer.length === 1 ? this._buffer[0] : b4a.concat(this._buffer)
278
340
 
279
341
  this._buffer = [buffer]
280
342
  this._frame = 4 + c.uint32.decode(c.state(0, 4, buffer))
281
343
 
344
+ if (this._frame > this._maxFrameSize) {
345
+ return this._fail(
346
+ errors.FRAME_TOO_LARGE(
347
+ `Frame of ${this._frame} bytes exceeds the limit of ${this._maxFrameSize} bytes`
348
+ )
349
+ )
350
+ }
351
+
282
352
  if (this._buffered < this._frame) return
283
353
 
284
354
  this._onframe()
@@ -304,71 +374,127 @@ module.exports = exports = class RPC {
304
374
  this._onmessage(buffer.subarray(0, frame))
305
375
  }
306
376
 
307
- async _onmessage(buffer) {
377
+ _onmessage(buffer) {
308
378
  let message
309
379
  try {
310
380
  message = m.message.decode(c.state(0, buffer.length, buffer))
311
381
  } catch (err) {
312
- safetyCatch(err)
313
-
314
- return this._stream.destroy(err)
382
+ return this._fail(
383
+ err instanceof errors ? err : errors.INVALID_MESSAGE(`Malformed message: ${err.message}`)
384
+ )
315
385
  }
316
386
 
317
- switch (message.type) {
318
- case t.REQUEST: {
319
- const request =
320
- message.id === 0
321
- ? new IncomingEvent(this, message.command, message.data)
322
- : new IncomingRequest(this, message.id, message.command, message.data)
323
-
324
- try {
325
- await this._onrequest(request)
326
- } catch (err) {
327
- safetyCatch(err)
328
-
329
- if (message.id) this._sendError(request, err)
330
- else this._stream.destroy(err)
331
- }
332
- break
333
- }
334
- case t.RESPONSE:
335
- try {
387
+ const violation = protocolViolation(message)
388
+
389
+ if (violation !== null) return this._fail(errors.INVALID_MESSAGE(violation))
390
+
391
+ // Handling a message can run user code, such as stream listeners, so route
392
+ // anything it throws through the connection failure path rather than let it
393
+ // escape into the underlying stream.
394
+ try {
395
+ switch (message.type) {
396
+ case t.REQUEST:
397
+ this._onrequestmessage(message)
398
+ break
399
+ case t.RESPONSE:
336
400
  this._onresponse(message)
337
- } catch (err) {
338
- safetyCatch(err)
339
- }
340
- break
341
- case t.STREAM:
342
- try {
401
+ break
402
+ case t.STREAM:
343
403
  this._onstream(message)
344
- } catch (err) {
345
- safetyCatch(err)
346
- }
404
+ }
405
+ } catch (err) {
406
+ this._fail(err)
347
407
  }
348
408
  }
349
409
 
350
- _onresponse(message) {
351
- if (message.id === 0) return
410
+ async _onrequestmessage(message) {
411
+ // A repeated id can only be caught while its request is tracked here, which
412
+ // is also the only time it could corrupt any state.
413
+ if (
414
+ this._unansweredRequests.has(message.id) ||
415
+ this._incomingRequests.has(message.id) ||
416
+ this._outgoingResponses.has(message.id)
417
+ ) {
418
+ return this._fail(errors.INVALID_MESSAGE(`Request '${message.id}' is already in flight`))
419
+ }
420
+
421
+ let request
422
+
423
+ if (message.id === 0) {
424
+ request = new IncomingEvent(this, message.command, message.data)
425
+ } else {
426
+ request = new IncomingRequest(this, message.id, message.command, message.data)
427
+
428
+ this._unansweredRequests.add(message.id)
429
+ this._highestRequestId = Math.max(this._highestRequestId, message.id)
430
+ }
352
431
 
432
+ try {
433
+ await this._onrequest(request)
434
+ } catch (err) {
435
+ safetyCatch(err)
436
+
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)
440
+ }
441
+ }
442
+
443
+ _onresponse(message) {
353
444
  const request = this._outgoingRequests.get(message.id)
354
445
  if (request === undefined) return
355
446
 
356
447
  if (message.error) {
357
- request._responded = true
358
- this._gcOutgoingRequest(request)
448
+ this._settle(request, message.error, null)
449
+
450
+ if (request._responseStream !== null) request._responseStream.destroy(message.error)
359
451
 
360
- request._reject(message.error)
452
+ this._releaseRequestStream(request, message.error)
361
453
  } else if (message.stream === 0) {
362
- request._responded = true
363
- this._gcOutgoingRequest(request)
454
+ this._settle(request, null, message.data)
364
455
 
365
- request._resolve(message.data)
456
+ const stream = request._responseStream
457
+
458
+ if (isPushable(stream)) {
459
+ if (message.data.byteLength > 0) stream.push(message.data)
460
+ stream.push(null)
461
+ }
462
+
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)
366
478
  }
367
479
  }
368
480
 
369
- _onstream(message) {
370
- if (message.id === 0) return
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
+ }
371
488
 
489
+ // Once the request is answered, a request stream that the remote end never
490
+ // opened will never be read, and would otherwise wait for its open forever.
491
+ _releaseRequestStream(request, err) {
492
+ const stream = request._requestStream
493
+
494
+ if (stream !== null && !stream._acknowledged) stream.destroy(err)
495
+ }
496
+
497
+ _onstream(message) {
372
498
  if (message.stream & s.OPEN) this._onstreamopen(message)
373
499
  else if (message.stream & s.CLOSE) this._onstreamclose(message)
374
500
  else if (message.stream & s.PAUSE) this._onstreampause(message)
@@ -379,124 +505,67 @@ module.exports = exports = class RPC {
379
505
  }
380
506
 
381
507
  _onstreamopen(message) {
382
- let stream
383
-
384
508
  if (message.stream & s.REQUEST) {
385
- const request = this._outgoingRequests.get(message.id)
386
- if (request === undefined) {
387
- this._pendingRequests.add(message.id)
388
- return
389
- }
509
+ const stream = this._outgoingStream(message)
510
+ if (stream !== null) stream._continueOpen()
511
+ } else {
512
+ const request = this._outgoingResponses.get(message.id)
390
513
 
391
- stream = request._requestStream
514
+ if (request !== undefined) return request._responseStream._continueOpen()
392
515
 
393
- if (stream === null || stream._pendingOpen === null) {
394
- this._pendingRequests.add(message.id)
395
- return
396
- }
397
- } else if (message.stream & s.RESPONSE) {
398
- const request = this._outgoingResponses.get(message.id)
399
- if (request === undefined) {
400
- this._pendingResponses.add(message.id)
516
+ // The remote end opens its response stream as soon as it is created,
517
+ // which may be before its request arrives or before the handler here
518
+ // creates the response stream. Remember the open until the response
519
+ // stream is created, the request is answered, or the remote end destroys
520
+ // its stream. Any other open is for a request that has already been
521
+ // answered.
522
+ if (this._pendingResponses.has(message.id)) return
523
+
524
+ if (!this._unansweredRequests.has(message.id) && message.id <= this._highestRequestId) {
401
525
  return
402
526
  }
403
527
 
404
- stream = request._responseStream
405
-
406
- if (stream === null || stream._pendingOpen === null) {
407
- this._pendingResponses.add(message.id)
408
- return
528
+ if (this._pendingResponses.size >= MAX_PENDING_RESPONSES) {
529
+ return this._fail(errors.INVALID_MESSAGE('Too many response streams opened early'))
409
530
  }
410
- } else {
411
- return
412
- }
413
531
 
414
- stream._continueOpen()
532
+ this._pendingResponses.add(message.id)
533
+ }
415
534
  }
416
535
 
417
536
  _onstreamclose(message) {
418
- let stream
419
-
420
- if (message.stream & s.REQUEST) {
421
- const request = this._incomingRequests.get(message.id)
422
- if (request === undefined) return
423
-
424
- stream = request._requestStream
425
- } else if (message.stream & s.RESPONSE) {
426
- const request = this._incomingResponses.get(message.id)
427
- if (request === undefined) return
428
-
429
- stream = request._responseStream
430
- } else {
431
- return
432
- }
433
-
537
+ const stream = this._incomingStream(message)
434
538
  if (stream === null) return
539
+
435
540
  if (message.error) stream.destroy(message.error)
436
541
  else stream.push(null)
437
542
  }
438
543
 
439
544
  _onstreampause(message) {
440
- let stream
441
-
442
- if (message.stream & s.REQUEST) {
443
- const request = this._outgoingRequests.get(message.id)
444
- if (request === undefined) return
445
-
446
- stream = request._requestStream
447
- } else if (message.stream & s.RESPONSE) {
448
- const request = this._outgoingResponses.get(message.id)
449
- if (request === undefined) return
450
-
451
- stream = request._responseStream
452
- } else {
453
- return
454
- }
455
-
456
- if (stream === null) return
457
- stream.cork()
545
+ const stream = this._outgoingStream(message)
546
+ if (stream !== null) stream.cork()
458
547
  }
459
548
 
460
549
  _onstreamresume(message) {
461
- let stream
462
-
463
- if (message.stream & s.REQUEST) {
464
- const request = this._outgoingRequests.get(message.id)
465
- if (request === undefined) return
466
-
467
- stream = request._requestStream
468
- } else if (message.stream & s.RESPONSE) {
469
- const request = this._outgoingResponses.get(message.id)
470
- if (request === undefined) return
471
-
472
- stream = request._responseStream
473
- } else {
474
- return
475
- }
476
-
477
- if (stream === null) return
478
- stream.uncork()
550
+ const stream = this._outgoingStream(message)
551
+ if (stream !== null) stream.uncork()
479
552
  }
480
553
 
481
554
  _onstreamdata(message) {
482
- let stream
483
-
484
- if (message.stream & s.REQUEST) {
485
- const request = this._incomingRequests.get(message.id)
486
- if (request === undefined) return
555
+ const stream = this._incomingStream(message)
556
+ if (!isPushable(stream)) return
487
557
 
488
- stream = request._requestStream
489
- } else if (message.stream & s.RESPONSE) {
490
- const request = this._incomingResponses.get(message.id)
491
- if (request === undefined) return
558
+ const flushed = stream.push(message.data)
492
559
 
493
- stream = request._responseStream
494
- } else {
495
- return
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
+ )
496
566
  }
497
567
 
498
- if (stream === null) return
499
- if (stream.push(message.data) === false) {
568
+ if (flushed === false) {
500
569
  this._sendMessage({
501
570
  type: t.STREAM,
502
571
  id: stream._request.id,
@@ -508,45 +577,23 @@ module.exports = exports = class RPC {
508
577
  }
509
578
 
510
579
  _onstreamend(message) {
511
- let stream
512
-
513
- if (message.stream & s.REQUEST) {
514
- const request = this._incomingRequests.get(message.id)
515
- if (request === undefined) return
516
-
517
- stream = request._requestStream
518
- } else if (message.stream & s.RESPONSE) {
519
- const request = this._incomingResponses.get(message.id)
520
- if (request === undefined) return
521
-
522
- stream = request._responseStream
523
- } else {
524
- return
525
- }
526
-
527
- if (stream === null) return
528
- stream.push(null)
580
+ const stream = this._incomingStream(message)
581
+ if (stream !== null) stream.push(null)
529
582
  }
530
583
 
531
584
  _onstreamdestroy(message) {
532
- let stream
533
-
534
- if (message.stream & s.REQUEST) {
535
- const request = this._outgoingRequests.get(message.id)
536
- if (request === undefined) return
585
+ if (message.stream & s.RESPONSE) this._pendingResponses.delete(message.id)
537
586
 
538
- stream = request._requestStream
539
- } else if (message.stream & s.RESPONSE) {
540
- const request = this._outgoingResponses.get(message.id)
541
- if (request === undefined) return
587
+ const stream = this._outgoingStream(message)
588
+ if (stream !== null) stream.destroy(message.error)
589
+ }
542
590
 
543
- stream = request._responseStream
544
- } else {
545
- return
546
- }
591
+ _incomingStream(message) {
592
+ return findStream(message, this._incomingRequests, this._incomingResponses)
593
+ }
547
594
 
548
- if (stream === null) return
549
- stream.destroy(message.error)
595
+ _outgoingStream(message) {
596
+ return findStream(message, this._outgoingRequests, this._outgoingResponses)
550
597
  }
551
598
 
552
599
  _ondrain(err = null) {
@@ -560,4 +607,41 @@ module.exports = exports = class RPC {
560
607
 
561
608
  exports.CommandRouter = CommandRouter
562
609
 
563
- function noop() {}
610
+ // Messages that decode cleanly but that no valid peer ever sends, as derived
611
+ // from what the outgoing side encodes.
612
+ function protocolViolation(message) {
613
+ switch (message.type) {
614
+ case t.REQUEST:
615
+ if (message.stream === 0) return null
616
+ if (message.stream !== s.OPEN) return `Invalid request stream flags '${message.stream}'`
617
+ if (message.id === 0) return 'Stream request without an id'
618
+ return null
619
+
620
+ case t.RESPONSE:
621
+ if (message.id === 0) return 'Response without an id'
622
+ if (message.stream === 0) return null
623
+ if (message.stream !== s.OPEN || message.error) {
624
+ return `Invalid response stream flags '${message.stream}'`
625
+ }
626
+ return null
627
+
628
+ case t.STREAM:
629
+ if (message.id === 0) return 'Stream message without an id'
630
+ if (!streamFlags.has(message.stream)) return `Invalid stream flags '${message.stream}'`
631
+ return null
632
+ }
633
+ }
634
+
635
+ function findStream(message, requests, responses) {
636
+ if (message.stream & s.REQUEST) {
637
+ const request = requests.get(message.id)
638
+ return request === undefined ? null : request._requestStream
639
+ }
640
+
641
+ const request = responses.get(message.id)
642
+ return request === undefined ? null : request._responseStream
643
+ }
644
+
645
+ function isPushable(stream) {
646
+ return stream !== null && !stream.destroying && !isEnding(stream)
647
+ }
@@ -1,4 +1,5 @@
1
1
  const c = require('compact-encoding')
2
+ const errors = require('./errors')
2
3
 
3
4
  module.exports = class RPCCommandRouter {
4
5
  constructor(opts = {}) {
@@ -30,20 +31,31 @@ module.exports = class RPCCommandRouter {
30
31
  async _onrequest(req) {
31
32
  const responder = this._responders.get(req.command)
32
33
 
33
- if (responder === undefined) return
34
-
35
- const { onrequest, requestEncoding, responseEncoding } = responder
34
+ if (req.id === undefined) {
35
+ if (responder !== undefined) await this._dispatch(responder, req)
36
+ return
37
+ }
36
38
 
37
- let data = req.data
39
+ if (responder === undefined) throw errors.UNKNOWN_COMMAND(`Unknown command ${req.command}`)
38
40
 
39
- if (requestEncoding) data = c.decode(requestEncoding, data)
40
-
41
- data = await onrequest(req, data)
41
+ let data = await this._dispatch(responder, req)
42
42
 
43
43
  if (req.sent) return
44
44
 
45
+ const { responseEncoding } = responder
46
+
45
47
  if (responseEncoding) data = c.encode(responseEncoding, data)
46
48
 
47
49
  req.reply(data)
48
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
+ }
49
61
  }
package/lib/errors.d.ts CHANGED
@@ -1,8 +1,13 @@
1
1
  declare class RPCError extends Error {
2
2
  static UNKNOWN_MESSAGE(msg: string): RPCError
3
+ static UNKNOWN_COMMAND(msg: string): RPCError
4
+ static INVALID_MESSAGE(msg: string): RPCError
5
+ static FRAME_TOO_LARGE(msg: string): RPCError
3
6
  static ALREADY_SENT(msg: string): RPCError
4
7
  static ALREADY_RECEIVED(msg: string): RPCError
5
8
  static CHANNEL_CLOSED(msg: string): RPCError
9
+ static UNEXPECTED_STREAM(msg: string): RPCError
10
+ static STREAM_OVERFLOW(msg: string): RPCError
6
11
  }
7
12
 
8
13
  export = RPCError
package/lib/errors.js CHANGED
@@ -16,6 +16,18 @@ module.exports = class RPCError extends Error {
16
16
  return new RPCError(msg, 'UNKNOWN_MESSAGE', RPCError.UNKNOWN_MESSAGE)
17
17
  }
18
18
 
19
+ static UNKNOWN_COMMAND(msg) {
20
+ return new RPCError(msg, 'UNKNOWN_COMMAND', RPCError.UNKNOWN_COMMAND)
21
+ }
22
+
23
+ static INVALID_MESSAGE(msg) {
24
+ return new RPCError(msg, 'INVALID_MESSAGE', RPCError.INVALID_MESSAGE)
25
+ }
26
+
27
+ static FRAME_TOO_LARGE(msg) {
28
+ return new RPCError(msg, 'FRAME_TOO_LARGE', RPCError.FRAME_TOO_LARGE)
29
+ }
30
+
19
31
  static ALREADY_SENT(msg) {
20
32
  return new RPCError(msg, 'ALREADY_SENT', RPCError.ALREADY_SENT)
21
33
  }
@@ -27,4 +39,12 @@ module.exports = class RPCError extends Error {
27
39
  static CHANNEL_CLOSED(msg) {
28
40
  return new RPCError(msg, 'CHANNEL_CLOSED', RPCError.CHANNEL_CLOSED)
29
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
+ }
30
50
  }
@@ -1,5 +1,6 @@
1
1
  const { Readable } = require('bare-stream')
2
2
  const { type: t, stream: s } = require('./constants')
3
+ const { toError } = require('./messages')
3
4
 
4
5
  module.exports = class RPCIncomingStream extends Readable {
5
6
  constructor(rpc, request, type, opts) {
@@ -41,7 +42,7 @@ module.exports = class RPCIncomingStream extends Readable {
41
42
  type: t.STREAM,
42
43
  id: this._request.id,
43
44
  stream: this._mask | s.DESTROY | s.ERROR,
44
- error: err,
45
+ error: toError(err),
45
46
  data: null
46
47
  },
47
48
  cb
package/lib/messages.js CHANGED
@@ -21,6 +21,11 @@ const error = {
21
21
  }
22
22
  }
23
23
 
24
+ exports.toError = function toError(err) {
25
+ if (typeof err === 'object' && err !== null && typeof err.message === 'string') return err
26
+ return new Error(String(err))
27
+ }
28
+
24
29
  function hasData(m) {
25
30
  switch (m.type) {
26
31
  case t.REQUEST:
@@ -16,6 +16,8 @@ module.exports = class RPCOutgoingRequest {
16
16
  this._reject = reject
17
17
  })
18
18
 
19
+ this._promise.catch(noop)
20
+
19
21
  this._requestStream = null
20
22
  this._responseStream = null
21
23
  }
@@ -79,3 +81,5 @@ module.exports = class RPCOutgoingRequest {
79
81
  return this._responseStream
80
82
  }
81
83
  }
84
+
85
+ function noop() {}
@@ -1,5 +1,6 @@
1
1
  const { Writable } = require('bare-stream')
2
2
  const { type: t, stream: s } = require('./constants')
3
+ const { toError } = require('./messages')
3
4
 
4
5
  module.exports = class RPCOutgoingStream extends Writable {
5
6
  constructor(rpc, request, type, opts) {
@@ -11,25 +12,19 @@ module.exports = class RPCOutgoingStream extends Writable {
11
12
  this._mask = type === t.REQUEST ? s.REQUEST : s.RESPONSE
12
13
 
13
14
  this._pendingOpen = null
15
+ this._flushed = false
16
+ this._acknowledged = false
17
+ this._ended = false
14
18
  }
15
19
 
16
- _open(cb) {
17
- let pending
18
-
20
+ _sendOpen() {
19
21
  const onflushed = () => {
20
- if (pending.has(this._request.id)) {
21
- pending.delete(this._request.id)
22
-
23
- cb(null)
24
- } else {
25
- this._pendingOpen = cb
26
- }
22
+ this._flushed = true
23
+ this._finishOpen()
27
24
  }
28
25
 
29
26
  switch (this._type) {
30
27
  case t.REQUEST:
31
- pending = this._rpc._pendingRequests
32
-
33
28
  this._rpc._sendMessage(
34
29
  {
35
30
  type: t.REQUEST,
@@ -43,8 +38,6 @@ module.exports = class RPCOutgoingStream extends Writable {
43
38
  break
44
39
 
45
40
  case t.RESPONSE:
46
- pending = this._rpc._pendingResponses
47
-
48
41
  this._rpc._sendMessage(
49
42
  {
50
43
  type: t.RESPONSE,
@@ -59,11 +52,26 @@ module.exports = class RPCOutgoingStream extends Writable {
59
52
  }
60
53
  }
61
54
 
55
+ _open(cb) {
56
+ this._pendingOpen = cb
57
+ this._finishOpen()
58
+ }
59
+
62
60
  _continueOpen() {
63
- if (this._pendingOpen === null) return
61
+ this._acknowledged = true
62
+ this._finishOpen()
63
+ }
64
+
65
+ _finishOpen() {
66
+ if (!this._flushed || !this._acknowledged || this._pendingOpen === null) return
67
+
64
68
  const cb = this._pendingOpen
65
69
  this._pendingOpen = null
66
- cb()
70
+ cb(null)
71
+ }
72
+
73
+ _predestroy() {
74
+ this._continueOpen()
67
75
  }
68
76
 
69
77
  _write(data, encoding, cb) {
@@ -80,6 +88,8 @@ module.exports = class RPCOutgoingStream extends Writable {
80
88
  }
81
89
 
82
90
  _final(cb) {
91
+ this._ended = true
92
+
83
93
  this._rpc._sendMessage(
84
94
  {
85
95
  type: t.STREAM,
@@ -93,13 +103,15 @@ module.exports = class RPCOutgoingStream extends Writable {
93
103
  }
94
104
 
95
105
  _destroy(err, cb) {
96
- if (err) {
106
+ if (this._ended) {
107
+ cb(null)
108
+ } else if (err) {
97
109
  this._rpc._sendMessage(
98
110
  {
99
111
  type: t.STREAM,
100
112
  id: this._request.id,
101
113
  stream: this._mask | s.CLOSE | s.ERROR,
102
- error: err,
114
+ error: toError(err),
103
115
  data: null
104
116
  },
105
117
  cb
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-rpc",
3
- "version": "1.3.9",
3
+ "version": "1.5.0",
4
4
  "description": "librpc ABI compatible RPC for Bare",
5
5
  "exports": {
6
6
  ".": {