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