bare-rpc 1.3.8 → 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
@@ -29,126 +29,19 @@ console.log(replyBuffer.toString()) // pong
29
29
 
30
30
  ## API
31
31
 
32
- ### `RPC`
32
+ See the [`bare-rpc` reference](https://docs.pears.com/reference/bare/modules/bare-rpc).
33
33
 
34
- #### `const rpc = new RPC(stream[, onrequest])`
34
+ ## Untrusted peers
35
35
 
36
- Create an RPC instance using a duplex `stream`. `onrequest` is an optional callback run when a remote request is received. This is where processing and responding to an RPC request happens. `onrequest` receives a `RPCIncomingRequest` as an argument, for example:
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
37
 
38
- ```js
39
- const rpc = new RPC(stream, (req) => {
40
- if (req.command === 42) {
41
- req.reply('pong')
42
- }
43
- })
44
- ```
45
-
46
- See [`RPCIncomingRequest`](#rpcincomingrequest) for properties and methods for processing the request.
47
-
48
- `onrequest` can also be a [`RPCCommandRouter`](#rpccommandrouter).
49
-
50
- #### `const req = rpc.request(command)`
51
-
52
- Create a request for `command`. `command` is a unique number that should be used to differentiate different requests on the remote end. Returns a `RPCOutgoingRequest`.
53
-
54
- ### `RPCOutgoingRequest`
55
-
56
- #### `req.command`
57
-
58
- The command that the request was created with. A command is a unique number.
59
-
60
- #### `req.sent`
61
-
62
- A boolean for whether the request has been sent.
63
-
64
- #### `req.received`
65
-
66
- A boolean for whether the request has received a reply.
67
-
68
- #### `req.send([data[, encoding]])`
69
-
70
- Send the request with the provided `data`. `data` can be a buffer or a string which will be encoded using `encoding`.
71
-
72
- `.send()` can only be called once per request.
73
-
74
- #### `const data = await req.reply([encoding])`
75
-
76
- Await the reply from the remote end to the request. `encoding` can be defined for decoding the response `data` buffer back into a string.
77
-
78
- #### `const stream = req.createRequestStream([options])`
79
-
80
- Create a [`Writable`](https://github.com/mafintosh/streamx#writable-stream) stream for sending data with the request.
81
-
82
- #### `const stream = req.createResponseStream([options])`
83
-
84
- Create a [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream for receiving data in reply to the request.
85
-
86
- ### `RPCIncomingRequest`
87
-
88
- #### `req.command`
89
-
90
- The command that the request was sent as. A command is a unique number.
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.
91
41
 
92
- #### `req.data`
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.
93
43
 
94
- The data buffer sent with the request.
95
-
96
- #### `req.sent`
97
-
98
- A boolean for whether a reply has been sent.
99
-
100
- #### `req.received`
101
-
102
- A boolean for whether the request has been received as a stream. See [`req.createRequestStream()`](#const-stream--reqcreaterequeststream) for receiving requests as a stream.
103
-
104
- #### `req.reply([data, [encoding]])`
105
-
106
- Reply to the request with the provided `data`. `data` can be a buffer or a string which will be encoded using `encoding`.
107
-
108
- #### `const stream = req.createRequestStream([options])`
109
-
110
- Create a `Readable` stream for receiving data from the request.
111
-
112
- #### `const stream = req.createResponseStream([options])`
113
-
114
- Create a `Writable` stream for sending data in reply to the request.
115
-
116
- ### `RPCCommandRouter`
117
-
118
- An alternative way to define commands and handlers for receiving them.
119
-
120
- #### `const router = new RPC.CommandRouter()`
121
-
122
- Create a new command router. This router can then be used when creating an `rpc`. For example:
123
-
124
- ```js
125
- const router = new RPC.CommandRouter()
126
-
127
- router.respond(42, (req, data) => {
128
- console.log(data.toString()) // ping
129
-
130
- return Buffer.from('pong')
131
- })
132
-
133
- const rpc = new RPC(stream, router)
134
- const req = rpc.request(42)
135
- req.send('ping')
136
- ```
137
-
138
- #### `router.respond(command[, options], async (req, data) => {})`
139
-
140
- Define a command and the handler for it. The callback for a command receives both the request (`req`) and the `data` buffer and can return a value to respond. If the request is responded to in the callback, the return value is ignored.
141
-
142
- Options include:
143
-
144
- ```js
145
- options = {
146
- // Encoding for incoming request
147
- requestEncoding: c.raw,
148
- // Encoding for outgoing response
149
- responseEncoding: c.raw
150
- }
151
- ```
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.
152
45
 
153
46
  ## License
154
47
 
package/index.d.ts CHANGED
@@ -19,11 +19,18 @@ declare const constants: {
19
19
 
20
20
  interface RPCIncomingEvent {
21
21
  readonly rpc: RPC
22
+ /** The command that the request was created with. A command is a unique number. */
22
23
  readonly command: number
24
+ /** The data buffer sent with the request. */
23
25
  readonly data: Buffer | null
24
26
  }
25
27
 
26
28
  declare class RPCIncomingEvent {
29
+ /**
30
+ * @param rpc - The `RPC` instance the event arrived on.
31
+ * @param command - The command number the event was sent with.
32
+ * @param data - The payload buffer sent with the event.
33
+ */
27
34
  constructor(rpc: RPC, command: number, data: Buffer)
28
35
  }
29
36
 
@@ -32,16 +39,45 @@ interface RPCIncomingRequest {
32
39
  readonly id: number
33
40
  readonly command: number
34
41
  readonly data: Buffer | null
42
+ /** A boolean for whether the request has been sent. */
35
43
  readonly sent: boolean
44
+ /** A boolean for whether the request has received a reply. */
36
45
  readonly received: boolean
37
46
 
47
+ /**
48
+ * Send a reply to the request. `data` may be a buffer or a string encoded using `encoding`.
49
+ * @param data - The payload to send: a `Buffer`, or a string encoded using `encoding`. Omit to
50
+ * send no data.
51
+ * @param encoding - The encoding used when `data` is a string (defaults to `utf8`).
52
+ * @throws {ALREADY_SENT} a response has already been sent for this request.
53
+ */
38
54
  reply(data?: Buffer | string | null, encoding?: BufferEncoding): void
39
55
 
56
+ /**
57
+ * Create a [`Writable`](https://github.com/mafintosh/streamx#writable-stream) stream for sending
58
+ * the streamed reply to the request.
59
+ * @param opts - Options for the returned
60
+ * [`Writable`](https://github.com/mafintosh/streamx#writable-stream) stream.
61
+ * @throws {ALREADY_SENT} a response has already been sent for this request.
62
+ */
40
63
  createResponseStream(opts?: WritableOptions): RPCOutgoingStream
64
+ /**
65
+ * Create a [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream for
66
+ * receiving the request's streamed data.
67
+ * @param opts - Options for the returned
68
+ * [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream.
69
+ * @throws {ALREADY_RECEIVED} the request has already been received.
70
+ */
41
71
  createRequestStream(opts?: ReadableOptions): RPCIncomingStream
42
72
  }
43
73
 
44
74
  declare class RPCIncomingRequest {
75
+ /**
76
+ * @param rpc - The `RPC` instance the request arrived on.
77
+ * @param id - The request id, used to correlate the reply with the request.
78
+ * @param command - The command number the request was sent with.
79
+ * @param data - The payload buffer sent with the request.
80
+ */
45
81
  constructor(rpc: RPC, id: number, command: number, data: Buffer)
46
82
  }
47
83
 
@@ -50,10 +86,20 @@ interface RPCOutgoingEvent {
50
86
  readonly command: number
51
87
  readonly sent: boolean
52
88
 
89
+ /**
90
+ * @param data - The payload to send: a `Buffer`, or a string encoded using `encoding`. Omit to
91
+ * send no data.
92
+ * @param encoding - The encoding used when `data` is a string (defaults to `utf8`).
93
+ * @throws {ALREADY_SENT} the event has already been sent.
94
+ */
53
95
  send(data?: Buffer | string | null, encoding?: BufferEncoding): void
54
96
  }
55
97
 
56
98
  declare class RPCOutgoingEvent {
99
+ /**
100
+ * @param rpc - The `RPC` instance to send the event on.
101
+ * @param command - The command number to send.
102
+ */
57
103
  constructor(rpc: RPC, command: number)
58
104
  }
59
105
 
@@ -64,19 +110,62 @@ interface RPCOutgoingRequest {
64
110
  readonly sent: boolean
65
111
  readonly received: boolean
66
112
 
113
+ /**
114
+ * @param data - The payload to send: a `Buffer`, or a string encoded using `encoding`. Omit to
115
+ * send no data.
116
+ * @param encoding - The encoding used when `data` is a string (defaults to `utf8`).
117
+ * @throws {ALREADY_SENT} the request has already been sent.
118
+ */
67
119
  send(data?: Buffer | string | null, encoding?: BufferEncoding): void
68
120
 
121
+ /**
122
+ * Await the reply from the remote end to the request. `encoding` can be defined for decoding the
123
+ * response `data` buffer back into a string.
124
+ * @param encoding - If given, decodes the reply payload to a string using this encoding; omit to
125
+ * receive the raw `Buffer`.
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.
128
+ * @throws {ALREADY_RECEIVED} a reply is already being received for this request.
129
+ */
69
130
  reply(encoding?: BufferEncoding): Promise<Buffer | string | null>
70
131
 
132
+ /**
133
+ * Create a [`Writable`](https://github.com/mafintosh/streamx#writable-stream) stream for sending
134
+ * data with the request.
135
+ * @param opts - Options for the returned
136
+ * [`Writable`](https://github.com/mafintosh/streamx#writable-stream) stream.
137
+ * @throws {ALREADY_SENT} the request has already been sent.
138
+ */
71
139
  createRequestStream(opts?: WritableOptions): RPCOutgoingStream
140
+ /**
141
+ * Create a [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream for
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.
144
+ * @param opts - Options for the returned
145
+ * [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream.
146
+ * @throws {ALREADY_RECEIVED} the response has already been received.
147
+ */
72
148
  createResponseStream(opts?: ReadableOptions): RPCIncomingStream
73
149
  }
74
150
 
75
151
  declare class RPCOutgoingRequest {
152
+ /**
153
+ * @param rpc - The `RPC` instance to send the request on.
154
+ * @param id - The request id, used to correlate the reply with the request.
155
+ * @param command - The command number to send.
156
+ */
76
157
  constructor(rpc: RPC, id: number, command: number)
77
158
  }
78
159
 
79
160
  declare class RPCIncomingStream extends Readable {
161
+ /**
162
+ * @param rpc - The `RPC` instance the stream belongs to.
163
+ * @param request - The request the stream carries data for.
164
+ * @param type - Whether the stream carries the request body (`constants.type.REQUEST`) or the
165
+ * response body (`constants.type.RESPONSE`).
166
+ * @param opts - Options for the underlying
167
+ * [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream.
168
+ */
80
169
  constructor(
81
170
  rpc: RPC,
82
171
  request: RPCIncomingRequest | RPCOutgoingRequest,
@@ -86,6 +175,14 @@ declare class RPCIncomingStream extends Readable {
86
175
  }
87
176
 
88
177
  declare class RPCOutgoingStream extends Writable {
178
+ /**
179
+ * @param rpc - The `RPC` instance the stream belongs to.
180
+ * @param request - The request the stream carries data for.
181
+ * @param type - Whether the stream carries the request body (`constants.type.REQUEST`) or the
182
+ * response body (`constants.type.RESPONSE`).
183
+ * @param opts - Options for the underlying
184
+ * [`Writable`](https://github.com/mafintosh/streamx#writable-stream) stream.
185
+ */
89
186
  constructor(
90
187
  rpc: RPC,
91
188
  request: RPCIncomingRequest | RPCOutgoingRequest,
@@ -94,17 +191,113 @@ declare class RPCOutgoingStream extends Writable {
94
191
  )
95
192
  }
96
193
 
194
+ /** A [`compact-encoding`](https://github.com/holepunchto/compact-encoding) encoding, such as `c.raw`. */
195
+ interface RPCValueEncoding {
196
+ preencode(state: unknown, value: unknown): void
197
+ encode(state: unknown, value: unknown): void
198
+ decode(state: unknown): unknown
199
+ }
200
+
201
+ interface RPCCommandRouterOptions {
202
+ /** The encoding of request and response data for commands that set none (defaults to `c.raw`). */
203
+ valueEncoding?: RPCValueEncoding
204
+ }
205
+
206
+ interface RPCRespondOptions {
207
+ /** The encoding of both the request and the response data. */
208
+ valueEncoding?: RPCValueEncoding
209
+ /** The encoding to decode the request data with, or `null` to pass the raw buffer. */
210
+ requestEncoding?: RPCValueEncoding | null
211
+ /** The encoding to encode the response data with, or `null` to reply with the value as is. */
212
+ responseEncoding?: RPCValueEncoding | null
213
+ }
214
+
215
+ /**
216
+ * @param req - The incoming request.
217
+ * @param data - The request data, decoded with the request encoding of the command.
218
+ * @returns the response data, encoded with the response encoding of the command and sent as the
219
+ * reply unless the handler has already replied.
220
+ */
221
+ type RPCRequestHandler = (req: RPCIncomingRequest, data: any) => unknown
222
+
223
+ interface RPCCommandRouter {
224
+ /**
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
+ * @param command - The command number to answer.
229
+ * @param opts - The encodings of the command.
230
+ * @param onrequest - The handler of the requests.
231
+ */
232
+ respond(command: number, onrequest: RPCRequestHandler): void
233
+ respond(command: number, opts: RPCRespondOptions, onrequest: RPCRequestHandler): void
234
+ }
235
+
236
+ declare class RPCCommandRouter {
237
+ /**
238
+ * @param opts - The default encodings of the commands.
239
+ */
240
+ constructor(opts?: RPCCommandRouterOptions)
241
+ }
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
+
97
252
  interface RPC {
253
+ /**
254
+ * Whether there are no requests or responses currently in flight. Useful for determining when it
255
+ * is safe to tear down the underlying stream.
256
+ */
98
257
  readonly idle: boolean
99
258
 
259
+ /**
260
+ * @param command - A unique number identifying the event; the remote end differentiates events by
261
+ * it.
262
+ * @returns an `RPCOutgoingEvent`; call `send()` on it to dispatch the one-way event.
263
+ */
100
264
  event(command: number): RPCOutgoingEvent
265
+ /**
266
+ * @param command - A unique number identifying the request; the remote end differentiates
267
+ * requests by it.
268
+ * @returns an `RPCOutgoingRequest`; call `send()` on it to dispatch the request and `await
269
+ * reply()` for the response.
270
+ */
101
271
  request(command: number): RPCOutgoingRequest
102
272
  }
103
273
 
104
274
  declare class RPC {
105
- constructor(stream: Duplex, onrequest: (req: RPCIncomingRequest) => void | Promise<void>)
275
+ /**
276
+ * @param stream - The duplex stream to frame RPC messages over, such as a pipe or socket.
277
+ * @param onrequest - Callback run for each incoming request or event, receiving an
278
+ * `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.
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.
288
+ */
289
+ constructor(
290
+ stream: Duplex,
291
+ onrequest?: ((req: RPCIncomingRequest) => void | Promise<void>) | RPCCommandRouter,
292
+ opts?: RPCOptions
293
+ )
106
294
  }
107
295
 
296
+ /**
297
+ * Create an RPC instance using a duplex `stream`. `onrequest` is an optional callback run when a
298
+ * remote request is received. This is where processing and responding to an RPC request happens.
299
+ * `onrequest` receives a `RPCIncomingRequest` as an argument.
300
+ */
108
301
  declare namespace RPC {
109
302
  export {
110
303
  type RPCIncomingEvent as IncomingEvent,
@@ -112,7 +305,13 @@ declare namespace RPC {
112
305
  type RPCIncomingRequest as IncomingRequest,
113
306
  type RPCOutgoingRequest as OutgoingRequest,
114
307
  type RPCIncomingStream as IncomingStream,
115
- type RPCOutgoingStream as OutgoingStream
308
+ type RPCOutgoingStream as OutgoingStream,
309
+ RPCCommandRouter as CommandRouter,
310
+ type RPCCommandRouterOptions as CommandRouterOptions,
311
+ type RPCRespondOptions as RespondOptions,
312
+ type RPCRequestHandler as RequestHandler,
313
+ type RPCValueEncoding as ValueEncoding,
314
+ type RPCOptions as Options
116
315
  }
117
316
  }
118
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
  }
@@ -85,7 +112,7 @@ module.exports = exports = class RPC {
85
112
 
86
113
  let flushed = this._stream.write(header)
87
114
 
88
- if (message.data) flushed = this._stream.write(message.data)
115
+ if (message.data && m.hasData(message)) flushed = this._stream.write(message.data)
89
116
 
90
117
  if (cb) {
91
118
  if (flushed) cb(null)
@@ -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,20 +317,39 @@ module.exports = exports = class RPC {
272
317
  }
273
318
  }
274
319
 
275
- _onbeforeframe() {
276
- if (this._buffered < 4) return
320
+ _onend() {
321
+ if (this._buffered > 0) this._fail(errors.INVALID_MESSAGE('Truncated frame'))
322
+ }
277
323
 
278
- const buffer = this._buffer.length === 1 ? this._buffer[0] : b4a.concat(this._buffer)
324
+ _onbeforeframe() {
325
+ while (this._buffered >= 4 && !this._stream.destroying) {
326
+ const buffer = this._buffer.length === 1 ? this._buffer[0] : b4a.concat(this._buffer)
327
+
328
+ this._buffer = [buffer]
329
+ this._frame = 4 + c.uint32.decode(c.state(0, 4, buffer))
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
+ }
279
338
 
280
- this._buffer = [buffer]
281
- this._frame = 4 + c.uint32.decode(c.state(0, 4, buffer))
339
+ if (this._buffered < this._frame) return
282
340
 
283
- this._onafterframe()
341
+ this._onframe()
342
+ }
284
343
  }
285
344
 
286
345
  _onafterframe() {
287
346
  if (this._buffered < this._frame) return
288
347
 
348
+ this._onframe()
349
+ this._onbeforeframe()
350
+ }
351
+
352
+ _onframe() {
289
353
  const buffer = this._buffer.length === 1 ? this._buffer[0] : b4a.concat(this._buffer)
290
354
 
291
355
  const frame = this._frame
@@ -295,55 +359,74 @@ module.exports = exports = class RPC {
295
359
  this._frame = -1
296
360
 
297
361
  this._onmessage(buffer.subarray(0, frame))
298
- this._onbeforeframe()
299
362
  }
300
363
 
301
- async _onmessage(buffer) {
364
+ _onmessage(buffer) {
302
365
  let message
303
366
  try {
304
367
  message = m.message.decode(c.state(0, buffer.length, buffer))
305
368
  } catch (err) {
306
- safetyCatch(err)
307
-
308
- return this._stream.destroy(err)
369
+ return this._fail(
370
+ err instanceof errors ? err : errors.INVALID_MESSAGE(`Malformed message: ${err.message}`)
371
+ )
309
372
  }
310
373
 
311
- switch (message.type) {
312
- case t.REQUEST: {
313
- const request =
314
- message.id === 0
315
- ? new IncomingEvent(this, message.command, message.data)
316
- : new IncomingRequest(this, message.id, message.command, message.data)
317
-
318
- try {
319
- await this._onrequest(request)
320
- } catch (err) {
321
- safetyCatch(err)
322
-
323
- if (message.id) this._sendError(request, err)
324
- else this._stream.destroy(err)
325
- }
326
- break
327
- }
328
- case t.RESPONSE:
329
- 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:
330
387
  this._onresponse(message)
331
- } catch (err) {
332
- safetyCatch(err)
333
- }
334
- break
335
- case t.STREAM:
336
- try {
388
+ break
389
+ case t.STREAM:
337
390
  this._onstream(message)
338
- } catch (err) {
339
- safetyCatch(err)
340
- }
391
+ }
392
+ } catch (err) {
393
+ this._fail(err)
341
394
  }
342
395
  }
343
396
 
344
- _onresponse(message) {
345
- 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)
414
+
415
+ this._unansweredRequests.add(message.id)
416
+ this._highestRequestId = Math.max(this._highestRequestId, message.id)
417
+ }
346
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) {
347
430
  const request = this._outgoingRequests.get(message.id)
348
431
  if (request === undefined) return
349
432
 
@@ -352,17 +435,36 @@ module.exports = exports = class RPC {
352
435
  this._gcOutgoingRequest(request)
353
436
 
354
437
  request._reject(message.error)
438
+
439
+ if (request._responseStream !== null) request._responseStream.destroy(message.error)
440
+
441
+ this._releaseRequestStream(request, message.error)
355
442
  } else if (message.stream === 0) {
356
443
  request._responded = true
357
444
  this._gcOutgoingRequest(request)
358
445
 
359
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)
360
456
  }
361
457
  }
362
458
 
363
- _onstream(message) {
364
- 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
+ }
365
466
 
467
+ _onstream(message) {
366
468
  if (message.stream & s.OPEN) this._onstreamopen(message)
367
469
  else if (message.stream & s.CLOSE) this._onstreamclose(message)
368
470
  else if (message.stream & s.PAUSE) this._onstreampause(message)
@@ -373,123 +475,56 @@ module.exports = exports = class RPC {
373
475
  }
374
476
 
375
477
  _onstreamopen(message) {
376
- let stream
377
-
378
478
  if (message.stream & s.REQUEST) {
379
- const request = this._outgoingRequests.get(message.id)
380
- if (request === undefined) {
381
- this._pendingRequests.add(message.id)
382
- return
383
- }
479
+ const stream = this._outgoingStream(message)
480
+ if (stream !== null) stream._continueOpen()
481
+ } else {
482
+ const request = this._outgoingResponses.get(message.id)
384
483
 
385
- stream = request._requestStream
484
+ if (request !== undefined) return request._responseStream._continueOpen()
386
485
 
387
- if (stream === null || stream._pendingOpen === null) {
388
- this._pendingRequests.add(message.id)
389
- return
390
- }
391
- } else if (message.stream & s.RESPONSE) {
392
- const request = this._outgoingResponses.get(message.id)
393
- if (request === undefined) {
394
- 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) {
395
495
  return
396
496
  }
397
497
 
398
- stream = request._responseStream
399
-
400
- if (stream === null || stream._pendingOpen === null) {
401
- this._pendingResponses.add(message.id)
402
- return
498
+ if (this._pendingResponses.size >= MAX_PENDING_RESPONSES) {
499
+ return this._fail(errors.INVALID_MESSAGE('Too many response streams opened early'))
403
500
  }
404
- } else {
405
- return
406
- }
407
501
 
408
- stream._continueOpen()
502
+ this._pendingResponses.add(message.id)
503
+ }
409
504
  }
410
505
 
411
506
  _onstreamclose(message) {
412
- let stream
413
-
414
- if (message.stream & s.REQUEST) {
415
- const request = this._incomingRequests.get(message.id)
416
- if (request === undefined) return
417
-
418
- stream = request._requestStream
419
- } else if (message.stream & s.RESPONSE) {
420
- const request = this._incomingResponses.get(message.id)
421
- if (request === undefined) return
422
-
423
- stream = request._responseStream
424
- } else {
425
- return
426
- }
427
-
507
+ const stream = this._incomingStream(message)
428
508
  if (stream === null) return
509
+
429
510
  if (message.error) stream.destroy(message.error)
430
511
  else stream.push(null)
431
512
  }
432
513
 
433
514
  _onstreampause(message) {
434
- let stream
435
-
436
- if (message.stream & s.REQUEST) {
437
- const request = this._outgoingRequests.get(message.id)
438
- if (request === undefined) return
439
-
440
- stream = request._requestStream
441
- } else if (message.stream & s.RESPONSE) {
442
- const request = this._outgoingResponses.get(message.id)
443
- if (request === undefined) return
444
-
445
- stream = request._responseStream
446
- } else {
447
- return
448
- }
449
-
450
- if (stream === null) return
451
- stream.cork()
515
+ const stream = this._outgoingStream(message)
516
+ if (stream !== null) stream.cork()
452
517
  }
453
518
 
454
519
  _onstreamresume(message) {
455
- let stream
456
-
457
- if (message.stream & s.REQUEST) {
458
- const request = this._outgoingRequests.get(message.id)
459
- if (request === undefined) return
460
-
461
- stream = request._requestStream
462
- } else if (message.stream & s.RESPONSE) {
463
- const request = this._outgoingResponses.get(message.id)
464
- if (request === undefined) return
465
-
466
- stream = request._responseStream
467
- } else {
468
- return
469
- }
470
-
471
- if (stream === null) return
472
- stream.uncork()
520
+ const stream = this._outgoingStream(message)
521
+ if (stream !== null) stream.uncork()
473
522
  }
474
523
 
475
524
  _onstreamdata(message) {
476
- let stream
477
-
478
- if (message.stream & s.REQUEST) {
479
- const request = this._incomingRequests.get(message.id)
480
- if (request === undefined) return
481
-
482
- stream = request._requestStream
483
- } else if (message.stream & s.RESPONSE) {
484
- const request = this._incomingResponses.get(message.id)
485
- if (request === undefined) return
525
+ const stream = this._incomingStream(message)
526
+ if (!isPushable(stream)) return
486
527
 
487
- stream = request._responseStream
488
- } else {
489
- return
490
- }
491
-
492
- if (stream === null) return
493
528
  if (stream.push(message.data) === false) {
494
529
  this._sendMessage({
495
530
  type: t.STREAM,
@@ -502,45 +537,23 @@ module.exports = exports = class RPC {
502
537
  }
503
538
 
504
539
  _onstreamend(message) {
505
- let stream
506
-
507
- if (message.stream & s.REQUEST) {
508
- const request = this._incomingRequests.get(message.id)
509
- if (request === undefined) return
510
-
511
- stream = request._requestStream
512
- } else if (message.stream & s.RESPONSE) {
513
- const request = this._incomingResponses.get(message.id)
514
- if (request === undefined) return
515
-
516
- stream = request._responseStream
517
- } else {
518
- return
519
- }
520
-
521
- if (stream === null) return
522
- stream.push(null)
540
+ const stream = this._incomingStream(message)
541
+ if (stream !== null) stream.push(null)
523
542
  }
524
543
 
525
544
  _onstreamdestroy(message) {
526
- let stream
527
-
528
- if (message.stream & s.REQUEST) {
529
- const request = this._outgoingRequests.get(message.id)
530
- if (request === undefined) return
545
+ if (message.stream & s.RESPONSE) this._pendingResponses.delete(message.id)
531
546
 
532
- stream = request._requestStream
533
- } else if (message.stream & s.RESPONSE) {
534
- const request = this._outgoingResponses.get(message.id)
535
- if (request === undefined) return
547
+ const stream = this._outgoingStream(message)
548
+ if (stream !== null) stream.destroy(message.error)
549
+ }
536
550
 
537
- stream = request._responseStream
538
- } else {
539
- return
540
- }
551
+ _incomingStream(message) {
552
+ return findStream(message, this._incomingRequests, this._incomingResponses)
553
+ }
541
554
 
542
- if (stream === null) return
543
- stream.destroy(message.error)
555
+ _outgoingStream(message) {
556
+ return findStream(message, this._outgoingRequests, this._outgoingResponses)
544
557
  }
545
558
 
546
559
  _ondrain(err = null) {
@@ -554,4 +567,43 @@ module.exports = exports = class RPC {
554
567
 
555
568
  exports.CommandRouter = CommandRouter
556
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
+
557
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,19 +21,36 @@ 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
+
29
+ function hasData(m) {
30
+ switch (m.type) {
31
+ case t.REQUEST:
32
+ return m.stream === 0
33
+ case t.RESPONSE:
34
+ return !m.error && m.stream === 0
35
+ case t.STREAM:
36
+ return (m.stream & s.ERROR) === 0 && (m.stream & s.DATA) !== 0
37
+ default:
38
+ return false
39
+ }
40
+ }
41
+
42
+ exports.hasData = hasData
43
+
24
44
  exports.header = {
25
45
  preencode(state, m) {
26
46
  c.uint32.preencode(state, 0) // Frame
27
47
  c.uint.preencode(state, m.type)
28
48
  c.uint.preencode(state, m.id)
29
49
 
30
- let hasData = false
31
-
32
50
  switch (m.type) {
33
51
  case t.REQUEST:
34
52
  c.uint.preencode(state, m.command)
35
53
  c.uint.preencode(state, m.stream)
36
- if (m.stream === 0) hasData = true
37
54
  break
38
55
 
39
56
  case t.RESPONSE:
@@ -41,18 +58,16 @@ exports.header = {
41
58
  c.uint.preencode(state, m.stream)
42
59
 
43
60
  if (m.error) error.preencode(state, m.error)
44
- else if (m.stream === 0) hasData = true
45
61
  break
46
62
 
47
63
  case t.STREAM:
48
64
  c.uint.preencode(state, m.stream)
49
65
 
50
66
  if (m.stream & s.ERROR) error.preencode(state, m.error)
51
- else if (m.stream & s.DATA) hasData = true
52
67
  break
53
68
  }
54
69
 
55
- if (hasData) c.uint.preencode(state, m.data ? m.data.byteLength : 0)
70
+ if (hasData(m)) c.uint.preencode(state, m.data ? m.data.byteLength : 0)
56
71
  },
57
72
  encode(state, m) {
58
73
  const frame = state.start
@@ -64,13 +79,10 @@ exports.header = {
64
79
  c.uint.encode(state, m.type)
65
80
  c.uint.encode(state, m.id)
66
81
 
67
- let hasData = false
68
-
69
82
  switch (m.type) {
70
83
  case t.REQUEST:
71
84
  c.uint.encode(state, m.command)
72
85
  c.uint.encode(state, m.stream)
73
- if (m.stream === 0) hasData = true
74
86
  break
75
87
 
76
88
  case t.RESPONSE:
@@ -78,24 +90,24 @@ exports.header = {
78
90
  c.uint.encode(state, m.stream)
79
91
 
80
92
  if (m.error) error.encode(state, m.error)
81
- else if (m.stream === 0) hasData = true
82
93
  break
83
94
 
84
95
  case t.STREAM:
85
96
  c.uint.encode(state, m.stream)
86
97
 
87
98
  if (m.stream & s.ERROR) error.encode(state, m.error)
88
- else if (m.stream & s.DATA) hasData = true
89
99
  break
90
100
  }
91
101
 
92
- if (hasData) c.uint.encode(state, m.data ? m.data.byteLength : 0)
102
+ const data = hasData(m)
103
+
104
+ if (data) c.uint.encode(state, m.data ? m.data.byteLength : 0)
93
105
 
94
106
  const end = state.start
95
107
 
96
108
  state.start = frame
97
109
 
98
- c.uint32.encode(state, end - start + (hasData && m.data ? m.data.byteLength : 0))
110
+ c.uint32.encode(state, end - start + (data && m.data ? m.data.byteLength : 0))
99
111
 
100
112
  state.start = end
101
113
  }
@@ -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.8",
3
+ "version": "1.4.0",
4
4
  "description": "librpc ABI compatible RPC for Bare",
5
5
  "exports": {
6
6
  ".": {
@@ -44,6 +44,7 @@
44
44
  "devDependencies": {
45
45
  "bare-buffer": "^3.0.1",
46
46
  "bare-ipc": "^1.1.0",
47
+ "bare-rpc-test": "^0.1.0",
47
48
  "brittle": "^3.2.1",
48
49
  "lunte": "^1.7.0",
49
50
  "prettier": "^3.4.2",