bare-rpc 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,40 +8,153 @@ 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
- }
17
+ const [a, b] = IPC.open().map((port) => port.connect())
18
+
19
+ const server = new RPC(a, (req) => req.reply('pong'))
20
+
21
+ const client = new RPC(b)
22
+
23
+ const req = client.request(1)
24
+ req.send('ping')
25
+
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())
20
80
  })
21
81
 
22
- // On the other end
23
- const req = rpc.request(42)
82
+ const req = client.request(1)
83
+ const outgoing = req.createRequestStream()
84
+ const incoming = req.createResponseStream()
85
+
86
+ outgoing.end('ping')
87
+
88
+ for await (const chunk of incoming) console.log(chunk.toString()) // ping
89
+ ```
90
+
91
+ A stream only carries data once both ends have opened it, so data written before then is held back until the peer has opened its end. The handler opens the body when it calls `req.createRequestStream()`. If the peer answers before it opens the body, the body is destroyed, with the error of the reply if it has one.
92
+
93
+ An incoming stream asks the peer to pause once it buffers its `highWaterMark`, which is 16 KiB by default, and to resume once it is read. While the peer has paused an outgoing stream, its writes are held back. If the peer keeps sending anyway, an incoming stream holds at most `maxBufferedBytes`, 16 MiB by default, before the session fails with an `RPCError` with the code `STREAM_OVERFLOW`.
94
+
95
+ The body of a request and the reply to a request can each be read once. Calling `createRequestStream()` on a request of the peer again, or `createResponseStream()` or `reply()` on a request you sent again, throws an `RPCError` with the code `ALREADY_RECEIVED`.
96
+
97
+ ## Cancellation
98
+
99
+ A request you sent can be cancelled with `req.cancel(reason)`. Its reply rejects with `reason`, its streams are destroyed with it, and the peer is told to stop handling the request. `reason` defaults to an `RPCError` with the code `CANCELLED`. Cancelling a request that has finished does nothing, and cancelling one that has not been sent yet keeps it from being sent. A request has finished once its answer has arrived: a reply, an error, or the end of a streamed reply, whether it ends cleanly or with an error. A request stream that is still open then stays open, so destroy it to stop it. Destroying the response stream before the answer has arrived cancels the request, with the error it was destroyed with if there is one.
100
+
101
+ The peer receives the `message`, `code` and `errno` of `reason`. If `reason` has no `code`, the peer receives the code `TIMEOUT` when `reason` is a `TimeoutError`, such as the reason of a signal from `AbortSignal.timeout()`, and `CANCELLED` otherwise. This only changes what the peer receives; the reply still rejects with `reason` as is.
102
+
103
+ `rpc.request(command, opts)` can also cancel the request for you:
104
+
105
+ - `signal`: An `AbortSignal` that cancels the request with its reason when it aborts.
106
+ - `timeout`: Milliseconds after the request is sent to cancel it with an `RPCError` with the code `TIMEOUT`.
107
+
108
+ ```js
109
+ const req = rpc.request(1, { timeout: 5000 })
24
110
  req.send('ping')
25
111
 
26
- const replyBuffer = await req.reply()
27
- console.log(replyBuffer.toString()) // pong
112
+ await req.reply() // Rejects with the code TIMEOUT if no reply arrives within 5 seconds
113
+ ```
114
+
115
+ On the answering side, `req.signal` is an `AbortSignal` that aborts when the peer cancels the request, with the `message`, `code` and `errno` it cancelled with. It also aborts when the session ends while the handler still runs, with the error it ended with, even if the handler has already replied. Events have a `signal` too, which only aborts when the session ends. Once the peer has cancelled the request, its streams are destroyed with that error, `reply()` sends nothing, and an error thrown by the handler is not sent either.
116
+
117
+ `RPC` does not create signals on its own. Pass an `AbortController` class as the `AbortController` option, such as the one from [`bare-abort-controller`](https://github.com/holepunchto/bare-abort-controller). Without it, `req.signal` is `null`.
118
+
119
+ ```js
120
+ const AbortController = require('bare-abort-controller')
121
+
122
+ const rpc = new RPC(stream, router, { AbortController })
123
+
124
+ router.respond(1, async (req, data) => {
125
+ return await search(data, { signal: req.signal })
126
+ })
28
127
  ```
29
128
 
30
- ## API
129
+ Cancelling a request needs a peer that supports it. A peer without support fails the session with the code `UNKNOWN_MESSAGE` when it receives a cancel.
130
+
131
+ ## Termination
132
+
133
+ The session ends with an `RPCError` with the code `CHANNEL_CLOSED` when `stream` closes. It fails with an error instead when:
31
134
 
32
- See the [`bare-rpc` reference](https://docs.pears.com/reference/bare/modules/bare-rpc).
135
+ - `stream` emits an error, with that error.
136
+ - `stream` ends partway through a frame, with the code `INVALID_MESSAGE`.
137
+ - An event handler throws, with that error.
138
+ - The peer sends a bad frame, as described under [Untrusted peers](#untrusted-peers).
139
+
140
+ Pending replies then reject with the error, and so do later replies. Open streams are destroyed with it, and so are streams created later. The signals of requests and events still being handled abort with it. Events sent after the end are dropped.
141
+
142
+ `RPC` does not close `stream` when the session ends cleanly. When it fails for any reason but an error of `stream`, `RPC` destroys `stream` with the error, so the frames still queued in it are not written. To end the session yourself, destroy `stream`. `rpc.idle` tells whether no requests or streams are in flight, so that it is safe to do so.
33
143
 
34
144
  ## Untrusted peers
35
145
 
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:
146
+ `RPC` does not trust the peer to send valid frames. If a frame is larger than `maxFrameSize`, cannot be decoded, or breaks the protocol, the session fails with an `RPCError`. The code tells you why:
37
147
 
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.
148
+ - `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.
149
+ - `UNKNOWN_MESSAGE`: The frame has a message type that `RPC` does not know.
150
+ - `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, stream or cancel 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.
151
+ - `STREAM_OVERFLOW`: An incoming stream holds more than `maxBufferedBytes` of data that has not been read. This only happens when the peer keeps sending after it was asked to pause.
41
152
 
42
153
  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
154
 
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.
155
+ 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.
156
+
157
+ If you expect a plain reply but the peer replies with a stream, `reply()` rejects with an `RPCError` with the code `UNEXPECTED_STREAM`. The session destroys the stream toward the peer and stays open.
45
158
 
46
159
  ## License
47
160
 
package/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import AbortController, { AbortSignal } from 'bare-abort-controller'
1
2
  import Buffer, { BufferEncoding } from 'bare-buffer'
2
3
  import { Readable, ReadableOptions, Writable, WritableOptions, Duplex } from 'bare-stream'
3
4
 
4
5
  declare const constants: {
5
- type: { REQUEST: 1; RESPONSE: 2; STREAM: 3 }
6
+ type: { REQUEST: 1; RESPONSE: 2; STREAM: 3; CANCEL: 4 }
6
7
  stream: {
7
8
  OPEN: number
8
9
  CLOSE: number
@@ -23,6 +24,11 @@ interface RPCIncomingEvent {
23
24
  readonly command: number
24
25
  /** The data buffer sent with the request. */
25
26
  readonly data: Buffer | null
27
+ /**
28
+ * Aborted when the connection ends while the event is still being handled, with the error it
29
+ * ended with. `null` unless `RPC` was given an `AbortController`.
30
+ */
31
+ readonly signal: AbortSignal | null
26
32
  }
27
33
 
28
34
  declare class RPCIncomingEvent {
@@ -43,11 +49,17 @@ interface RPCIncomingRequest {
43
49
  readonly sent: boolean
44
50
  /** A boolean for whether the request has received a reply. */
45
51
  readonly received: boolean
52
+ /**
53
+ * Aborted when the remote end cancels the request, with the error it cancelled with, or when the
54
+ * connection ends, with the error it ended with. `null` unless `RPC` was given an
55
+ * `AbortController`.
56
+ */
57
+ readonly signal: AbortSignal | null
46
58
 
47
59
  /**
48
60
  * Send a reply to the request. `data` may be a buffer or a string encoded using `encoding`.
49
61
  * @param data - The payload to send: a `Buffer`, or a string encoded using `encoding`. Omit to
50
- * send no data.
62
+ * send no data. Once the remote end has cancelled the request, nothing is sent.
51
63
  * @param encoding - The encoding used when `data` is a string (defaults to `utf8`).
52
64
  * @throws {ALREADY_SENT} a response has already been sent for this request.
53
65
  */
@@ -124,7 +136,9 @@ interface RPCOutgoingRequest {
124
136
  * @param encoding - If given, decodes the reply payload to a string using this encoding; omit to
125
137
  * receive the raw `Buffer`.
126
138
  * @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.
139
+ * channel's teardown error if it closes before a reply arrives. If the remote end replies with a
140
+ * stream, the promise rejects with an error whose code is `UNEXPECTED_STREAM`, the remote end's
141
+ * stream is destroyed, and the connection stays open.
128
142
  * @throws {ALREADY_RECEIVED} a reply is already being received for this request.
129
143
  */
130
144
  reply(encoding?: BufferEncoding): Promise<Buffer | string | null>
@@ -137,6 +151,15 @@ interface RPCOutgoingRequest {
137
151
  * @throws {ALREADY_SENT} the request has already been sent.
138
152
  */
139
153
  createRequestStream(opts?: WritableOptions): RPCOutgoingStream
154
+ /**
155
+ * Cancel the request. The reply rejects with `reason`, the streams of the request are destroyed
156
+ * with it, and the remote end is told to stop handling the request. Does nothing once the request
157
+ * has finished, which it has once its answer has arrived, even while its request stream is open.
158
+ * @param reason - The error to cancel with (defaults to an `RPCError` with the code `CANCELLED`).
159
+ * If it has no `code`, the remote end receives the code `TIMEOUT` when it is a `TimeoutError` and
160
+ * `CANCELLED` otherwise.
161
+ */
162
+ cancel(reason?: unknown): void
140
163
  /**
141
164
  * Create a [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream for
142
165
  * receiving data in reply to the request. If the remote end replies without a stream, the reply
@@ -213,18 +236,20 @@ interface RPCRespondOptions {
213
236
  }
214
237
 
215
238
  /**
216
- * @param req - The incoming request.
239
+ * @param req - The incoming request. For an event, this is an `RPCIncomingEvent`.
217
240
  * @param data - The request data, decoded with the request encoding of the command.
218
241
  * @returns the response data, encoded with the response encoding of the command and sent as the
219
- * reply unless the handler has already replied.
242
+ * reply unless the handler has already replied. For an event, the return value is ignored and
243
+ * nothing is sent.
220
244
  */
221
245
  type RPCRequestHandler = (req: RPCIncomingRequest, data: any) => unknown
222
246
 
223
247
  interface RPCCommandRouter {
224
248
  /**
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.
249
+ * Answer requests for `command` with `onrequest`, in place of any handler registered before.
250
+ * Events for `command` are also passed to `onrequest`, but are never answered. A request for a
251
+ * command with no handler is answered with an error whose code is `UNKNOWN_COMMAND`, and an event
252
+ * for such a command is dropped.
228
253
  * @param command - The command number to answer.
229
254
  * @param opts - The encodings of the command.
230
255
  * @param onrequest - The handler of the requests.
@@ -247,6 +272,31 @@ interface RPCOptions {
247
272
  * Defaults to 16 MiB.
248
273
  */
249
274
  maxFrameSize?: number
275
+ /**
276
+ * The most data, in bytes, that one incoming stream may hold before it is read. A remote end that
277
+ * keeps sending past this limit, rather than stop when asked to pause, fails the connection with
278
+ * `STREAM_OVERFLOW`. Defaults to 16 MiB.
279
+ */
280
+ maxBufferedBytes?: number
281
+ /**
282
+ * The `AbortController` class to create the `signal` of each incoming request with. Without it,
283
+ * `signal` is `null`.
284
+ */
285
+ AbortController?: typeof AbortController
286
+ }
287
+
288
+ interface RPCRequestOptions {
289
+ /**
290
+ * Cancels the request with the reason of the signal when it aborts. If the reason has no `code`,
291
+ * the remote end receives the code `TIMEOUT` when it is a `TimeoutError`, as from
292
+ * `AbortSignal.timeout()`, and `CANCELLED` otherwise.
293
+ */
294
+ signal?: AbortSignal
295
+ /**
296
+ * Cancels the request with an `RPCError` with the code `TIMEOUT` if its answer has not arrived
297
+ * this many milliseconds after it is sent. Defaults to no timeout.
298
+ */
299
+ timeout?: number
250
300
  }
251
301
 
252
302
  interface RPC {
@@ -265,10 +315,11 @@ interface RPC {
265
315
  /**
266
316
  * @param command - A unique number identifying the request; the remote end differentiates
267
317
  * requests by it.
318
+ * @param opts - Options for cancelling the request.
268
319
  * @returns an `RPCOutgoingRequest`; call `send()` on it to dispatch the request and `await
269
320
  * reply()` for the response.
270
321
  */
271
- request(command: number): RPCOutgoingRequest
322
+ request(command: number, opts?: RPCRequestOptions): RPCOutgoingRequest
272
323
  }
273
324
 
274
325
  declare class RPC {
@@ -276,19 +327,23 @@ declare class RPC {
276
327
  * @param stream - The duplex stream to frame RPC messages over, such as a pipe or socket.
277
328
  * @param onrequest - Callback run for each incoming request or event, receiving an
278
329
  * `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.
330
+ * `RPCCommandRouter` that answers each request with the handler of its command. If it throws, the
331
+ * request is answered with the error, or, if it has already created a response stream, that
332
+ * stream is destroyed with the error. If it has already replied, nothing more is sent. If
333
+ * omitted, every request is answered with an error whose code is `UNKNOWN_COMMAND` and every
334
+ * event is dropped.
281
335
  * @param opts - Options for the connection.
282
336
  *
283
337
  * 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
338
+ * decoded, or breaks the protocol fails the connection, and so does a stream that holds more
339
+ * unread data than `maxBufferedBytes`: `stream` is destroyed with an `RPCError` whose code is
340
+ * `FRAME_TOO_LARGE`, `UNKNOWN_MESSAGE`, `INVALID_MESSAGE` or `STREAM_OVERFLOW`, and every request
341
+ * and stream in flight is rejected or destroyed with it. A frame that refers to a request or stream
287
342
  * that has already finished, or that was never known, is ignored.
288
343
  */
289
344
  constructor(
290
345
  stream: Duplex,
291
- onrequest?: ((req: RPCIncomingRequest) => void | Promise<void>) | RPCCommandRouter,
346
+ onrequest?: ((req: RPCIncomingRequest) => void | Promise<void>) | RPCCommandRouter | null,
292
347
  opts?: RPCOptions
293
348
  )
294
349
  }
@@ -309,6 +364,7 @@ declare namespace RPC {
309
364
  RPCCommandRouter as CommandRouter,
310
365
  type RPCCommandRouterOptions as CommandRouterOptions,
311
366
  type RPCRespondOptions as RespondOptions,
367
+ type RPCRequestOptions as RequestOptions,
312
368
  type RPCRequestHandler as RequestHandler,
313
369
  type RPCValueEncoding as ValueEncoding,
314
370
  type RPCOptions as Options
package/index.js CHANGED
@@ -14,6 +14,7 @@ const OutgoingStream = require('./lib/outgoing-stream')
14
14
  const CommandRouter = require('./lib/command-router')
15
15
 
16
16
  const DEFAULT_MAX_FRAME_SIZE = 16 * 1024 * 1024
17
+ const DEFAULT_MAX_BUFFERED_BYTES = 16 * 1024 * 1024
17
18
  const MAX_PENDING_RESPONSES = 1024
18
19
 
19
20
  const streamFlags = new Set()
@@ -35,11 +36,17 @@ for (const direction of [s.REQUEST, s.RESPONSE]) {
35
36
  }
36
37
 
37
38
  module.exports = exports = class RPC {
38
- constructor(stream, onrequest = noop, opts = {}) {
39
- const { maxFrameSize = DEFAULT_MAX_FRAME_SIZE } = opts
39
+ constructor(stream, onrequest = null, opts = {}) {
40
+ const {
41
+ maxFrameSize = DEFAULT_MAX_FRAME_SIZE,
42
+ maxBufferedBytes = DEFAULT_MAX_BUFFERED_BYTES,
43
+ AbortController = null
44
+ } = opts
40
45
 
41
46
  this._stream = stream
42
47
  this._maxFrameSize = maxFrameSize
48
+ this._maxBufferedBytes = maxBufferedBytes
49
+ this._AbortController = AbortController
43
50
  this._id = 0
44
51
 
45
52
  this._outgoingRequests = new Map()
@@ -47,7 +54,8 @@ module.exports = exports = class RPC {
47
54
  this._incomingRequests = new Map()
48
55
  this._incomingResponses = new Map()
49
56
  this._pendingResponses = new Set()
50
- this._unansweredRequests = new Set()
57
+ this._handling = new Set()
58
+ this._unansweredRequests = new Map()
51
59
  this._highestRequestId = 0
52
60
 
53
61
  this._buffer = []
@@ -58,6 +66,10 @@ module.exports = exports = class RPC {
58
66
  this._closed = false
59
67
  this._error = null
60
68
 
69
+ if (onrequest === null) {
70
+ onrequest = new CommandRouter()
71
+ }
72
+
61
73
  if (typeof onrequest === 'function') {
62
74
  onrequest = onrequest.bind(this)
63
75
  } else {
@@ -95,8 +107,8 @@ module.exports = exports = class RPC {
95
107
  return new OutgoingEvent(this, command)
96
108
  }
97
109
 
98
- request(command) {
99
- return new OutgoingRequest(this, ++this._id, command)
110
+ request(command, opts) {
111
+ return new OutgoingRequest(this, ++this._id, command, opts)
100
112
  }
101
113
 
102
114
  _sendMessage(message, cb) {
@@ -131,12 +143,13 @@ module.exports = exports = class RPC {
131
143
  }
132
144
 
133
145
  _sendRequest(request, data = null) {
134
- // Once the channel has been torn down there is nowhere left to send the
135
- // request. Skip registering it so that it never leaks into the maps; the
136
- // awaiting reply is rejected through `RPCOutgoingRequest.reply()` instead.
137
- if (this._closed) return
146
+ // Once the channel has been torn down or the request cancelled there is
147
+ // nowhere left to send the request. Skip registering it so that it never
148
+ // leaks into the maps; the reply has already been rejected by the cancel,
149
+ // or is rejected through `RPCOutgoingRequest.reply()` after teardown.
150
+ if (this._requestError(request) !== null) return
138
151
 
139
- this._outgoingRequests.set(request.id, request)
152
+ this._trackOutgoingRequest(request)
140
153
 
141
154
  this._sendMessage({
142
155
  type: t.REQUEST,
@@ -151,17 +164,24 @@ module.exports = exports = class RPC {
151
164
  if (isInitiator) {
152
165
  request._requestStream = new OutgoingStream(this, request, t.REQUEST, opts)
153
166
 
154
- // Once the channel has been torn down there is nowhere left to open the
155
- // stream, so destroy it right away rather than leave it hanging.
156
- if (this._closed) return request._requestStream.destroy(this._error)
167
+ // Once the channel has been torn down or the request cancelled there is
168
+ // nowhere left to open the stream, so destroy it right away rather than
169
+ // leave it hanging.
170
+ const err = this._requestError(request)
171
+ if (err !== null) return request._requestStream.destroy(err)
157
172
 
158
- this._outgoingRequests.set(request.id, request)
173
+ this._trackOutgoingRequest(request)
174
+
175
+ // Announce the stream right away rather than when it opens, so that the
176
+ // remote end also learns of a stream that is destroyed before it opens.
177
+ request._requestStream._sendOpen()
159
178
 
160
179
  request._requestStream.on('close', () => this._gcOutgoingRequest(request))
161
180
  } else {
162
181
  request._requestStream = new IncomingStream(this, request, t.REQUEST, opts)
163
182
 
164
- if (this._closed) return request._requestStream.destroy(this._error)
183
+ const err = this._requestError(request)
184
+ if (err !== null) return request._requestStream.destroy(err)
165
185
 
166
186
  this._incomingRequests.set(request.id, request)
167
187
 
@@ -170,11 +190,11 @@ module.exports = exports = class RPC {
170
190
  }
171
191
 
172
192
  _sendResponse(request, data) {
173
- // Once the channel has been torn down there is nowhere left to send the
174
- // response, so drop it rather than encode and write a message that would
175
- // only be discarded. The reply is fire-and-forget, so there is nothing to
176
- // settle.
177
- if (this._closed) return
193
+ // Once the channel has been torn down or the request cancelled there is
194
+ // no one left to read the response, so drop it rather than write a message
195
+ // that would only be discarded. The reply is fire-and-forget, so there is
196
+ // nothing to settle.
197
+ if (!this._isUnanswered(request)) return
178
198
 
179
199
  this._onanswered(request)
180
200
 
@@ -191,32 +211,40 @@ module.exports = exports = class RPC {
191
211
  if (isInitiator) {
192
212
  request._responseStream = new OutgoingStream(this, request, t.RESPONSE, opts)
193
213
 
194
- // Once the channel has been torn down there is nowhere left to open the
195
- // stream, so destroy it right away rather than leave it hanging.
196
- if (this._closed) return request._responseStream.destroy(this._error)
214
+ // Once the channel has been torn down or the request cancelled there is
215
+ // nowhere left to open the stream, so destroy it right away rather than
216
+ // leave it hanging.
217
+ const err = this._requestError(request)
218
+ if (err !== null) return request._responseStream.destroy(err)
197
219
 
198
220
  this._outgoingResponses.set(request.id, request)
199
221
 
222
+ request._responseStream._sendOpen()
223
+
200
224
  if (this._onanswered(request)) request._responseStream._continueOpen()
201
225
 
202
226
  request._responseStream.on('close', () => this._outgoingResponses.delete(request.id))
203
227
  } else {
204
228
  request._responseStream = new IncomingStream(this, request, t.RESPONSE, opts)
205
229
 
206
- if (this._closed) return request._responseStream.destroy(this._error)
230
+ const err = this._requestError(request)
231
+ if (err !== null) return request._responseStream.destroy(err)
207
232
 
208
233
  this._incomingResponses.set(request.id, request)
209
234
 
210
235
  request._responseStream.on('close', () => {
211
236
  this._incomingResponses.delete(request.id)
212
237
 
213
- request._responded = true
214
- this._gcOutgoingRequest(request)
238
+ // A response stream destroyed before the answer has arrived leaves no
239
+ // way to read it, so the request is cancelled.
240
+ request.cancel(request._responseStream.errored)
215
241
  })
216
242
  }
217
243
  }
218
244
 
219
245
  _sendError(request, err) {
246
+ if (!this._isUnanswered(request)) return
247
+
220
248
  this._onanswered(request)
221
249
 
222
250
  this._sendMessage({
@@ -228,23 +256,68 @@ module.exports = exports = class RPC {
228
256
  })
229
257
  }
230
258
 
259
+ _isUnanswered(request) {
260
+ return this._unansweredRequests.get(request.id) === request
261
+ }
262
+
231
263
  // Returns whether the remote end had already opened its response stream.
232
264
  _onanswered(request) {
233
265
  this._unansweredRequests.delete(request.id)
234
266
  return this._pendingResponses.delete(request.id)
235
267
  }
236
268
 
269
+ _onresponded(request) {
270
+ request._responded = true
271
+ request._detach()
272
+ this._gcOutgoingRequest(request)
273
+ }
274
+
237
275
  _gcOutgoingRequest(request) {
238
276
  // The entry must stay reachable for as long as an incoming message might
239
277
  // still target it; until the response has settled and, if the request was
240
278
  // streamed, until the outgoing request stream has closed. Either can be the
241
279
  // last to arrive, so only remove the entry once both have completed.
242
280
  if (!request._responded) return
281
+
243
282
  if (request._requestStream !== null && !request._requestStream.destroyed) return
244
283
 
245
284
  this._outgoingRequests.delete(request.id)
246
285
  }
247
286
 
287
+ _trackOutgoingRequest(request) {
288
+ this._outgoingRequests.set(request.id, request)
289
+
290
+ request._attach()
291
+ }
292
+
293
+ // The error that keeps a request from using the channel, or `null` if it may.
294
+ _requestError(request) {
295
+ return this._closed ? this._error : request._reason
296
+ }
297
+
298
+ _cancelRequest(request, reason) {
299
+ // A request that has its answer has finished, even while its request stream
300
+ // is still open, and so has one that was sent but is no longer tracked.
301
+ if (request._reason !== null || request._responded) return
302
+ if (request.sent && this._outgoingRequests.get(request.id) !== request) return
303
+
304
+ request._reason = reason
305
+
306
+ if (request.sent) {
307
+ request._cancelled = true
308
+
309
+ this._outgoingRequests.delete(request.id)
310
+
311
+ request._detach()
312
+
313
+ this._sendMessage({ type: t.CANCEL, id: request.id, error: cancelError(reason), data: null })
314
+ }
315
+
316
+ request._reject(reason)
317
+
318
+ destroyStreams(request, reason)
319
+ }
320
+
248
321
  _onerror(err) {
249
322
  this._teardown(err)
250
323
  }
@@ -267,9 +340,16 @@ module.exports = exports = class RPC {
267
340
  // Reject any request awaiting a plain reply. Requests whose response is
268
341
  // delivered over a stream are torn down through that stream below.
269
342
  for (const request of this._outgoingRequests.values()) {
343
+ request._detach()
344
+
270
345
  if (request.received && request._responseStream === null) request._reject(err)
271
346
  }
272
347
 
348
+ for (const request of this._unansweredRequests.values()) request._abort(err)
349
+ for (const request of this._handling) request._abort(err)
350
+ for (const request of this._incomingRequests.values()) request._abort(err)
351
+ for (const request of this._outgoingResponses.values()) request._abort(err)
352
+
273
353
  // Collect the streams first, deduplicating as a single request may appear
274
354
  // in more than one map, then destroy them once the maps have been cleared.
275
355
  const streams = new Set()
@@ -388,6 +468,9 @@ module.exports = exports = class RPC {
388
468
  break
389
469
  case t.STREAM:
390
470
  this._onstream(message)
471
+ break
472
+ case t.CANCEL:
473
+ this._oncancel(message)
391
474
  }
392
475
  } catch (err) {
393
476
  this._fail(err)
@@ -412,17 +495,22 @@ module.exports = exports = class RPC {
412
495
  } else {
413
496
  request = new IncomingRequest(this, message.id, message.command, message.data)
414
497
 
415
- this._unansweredRequests.add(message.id)
498
+ this._unansweredRequests.set(message.id, request)
416
499
  this._highestRequestId = Math.max(this._highestRequestId, message.id)
417
500
  }
418
501
 
502
+ this._handling.add(request)
503
+
419
504
  try {
420
505
  await this._onrequest(request)
421
506
  } catch (err) {
422
507
  safetyCatch(err)
423
508
 
424
- if (message.id) this._sendError(request, err)
425
- else this._stream.destroy(err)
509
+ if (message.id === 0) this._stream.destroy(err)
510
+ else if (request._responseStream !== null) request._responseStream.destroy(err)
511
+ else if (!request.sent) this._sendError(request, err)
512
+ } finally {
513
+ this._handling.delete(request)
426
514
  }
427
515
  }
428
516
 
@@ -431,19 +519,13 @@ module.exports = exports = class RPC {
431
519
  if (request === undefined) return
432
520
 
433
521
  if (message.error) {
434
- request._responded = true
435
- this._gcOutgoingRequest(request)
436
-
437
- request._reject(message.error)
522
+ this._settle(request, message.error, null)
438
523
 
439
524
  if (request._responseStream !== null) request._responseStream.destroy(message.error)
440
525
 
441
526
  this._releaseRequestStream(request, message.error)
442
527
  } else if (message.stream === 0) {
443
- request._responded = true
444
- this._gcOutgoingRequest(request)
445
-
446
- request._resolve(message.data)
528
+ this._settle(request, null, message.data)
447
529
 
448
530
  const stream = request._responseStream
449
531
 
@@ -453,9 +535,30 @@ module.exports = exports = class RPC {
453
535
  }
454
536
 
455
537
  this._releaseRequestStream(request, null)
538
+ } else if (request._responseStream === null) {
539
+ const err = errors.UNEXPECTED_STREAM(`Request '${request.id}' was answered with a stream`)
540
+
541
+ this._settle(request, err, null)
542
+
543
+ this._sendMessage({
544
+ type: t.STREAM,
545
+ id: request.id,
546
+ stream: s.RESPONSE | s.DESTROY,
547
+ error: null,
548
+ data: null
549
+ })
550
+
551
+ this._releaseRequestStream(request, err)
456
552
  }
457
553
  }
458
554
 
555
+ _settle(request, err, data) {
556
+ this._onresponded(request)
557
+
558
+ if (err) request._reject(err)
559
+ else request._resolve(data)
560
+ }
561
+
459
562
  // Once the request is answered, a request stream that the remote end never
460
563
  // opened will never be read, and would otherwise wait for its open forever.
461
564
  _releaseRequestStream(request, err) {
@@ -464,6 +567,23 @@ module.exports = exports = class RPC {
464
567
  if (stream !== null && !stream._acknowledged) stream.destroy(err)
465
568
  }
466
569
 
570
+ _oncancel(message) {
571
+ const request =
572
+ this._unansweredRequests.get(message.id) ??
573
+ this._incomingRequests.get(message.id) ??
574
+ this._outgoingResponses.get(message.id)
575
+
576
+ if (request === undefined) return
577
+
578
+ request._cancelled = true
579
+
580
+ this._onanswered(request)
581
+
582
+ request._abort(message.error)
583
+
584
+ destroyStreams(request, message.error)
585
+ }
586
+
467
587
  _onstream(message) {
468
588
  if (message.stream & s.OPEN) this._onstreamopen(message)
469
589
  else if (message.stream & s.CLOSE) this._onstreamclose(message)
@@ -507,6 +627,8 @@ module.exports = exports = class RPC {
507
627
  const stream = this._incomingStream(message)
508
628
  if (stream === null) return
509
629
 
630
+ this._onstreamfinish(stream)
631
+
510
632
  if (message.error) stream.destroy(message.error)
511
633
  else stream.push(null)
512
634
  }
@@ -525,7 +647,17 @@ module.exports = exports = class RPC {
525
647
  const stream = this._incomingStream(message)
526
648
  if (!isPushable(stream)) return
527
649
 
528
- if (stream.push(message.data) === false) {
650
+ const flushed = stream.push(message.data)
651
+
652
+ if (stream._readableState.buffered > this._maxBufferedBytes) {
653
+ return this._fail(
654
+ errors.STREAM_OVERFLOW(
655
+ `Stream buffered more than the limit of ${this._maxBufferedBytes} bytes`
656
+ )
657
+ )
658
+ }
659
+
660
+ if (flushed === false) {
529
661
  this._sendMessage({
530
662
  type: t.STREAM,
531
663
  id: stream._request.id,
@@ -538,7 +670,17 @@ module.exports = exports = class RPC {
538
670
 
539
671
  _onstreamend(message) {
540
672
  const stream = this._incomingStream(message)
541
- if (stream !== null) stream.push(null)
673
+ if (stream === null) return
674
+
675
+ this._onstreamfinish(stream)
676
+
677
+ stream.push(null)
678
+ }
679
+
680
+ // A streamed reply has its answer once its end arrives, even if the response
681
+ // stream has not been read to the end yet.
682
+ _onstreamfinish(stream) {
683
+ if (stream._type === t.RESPONSE) this._onresponded(stream._request)
542
684
  }
543
685
 
544
686
  _onstreamdestroy(message) {
@@ -589,9 +731,30 @@ function protocolViolation(message) {
589
731
  if (message.id === 0) return 'Stream message without an id'
590
732
  if (!streamFlags.has(message.stream)) return `Invalid stream flags '${message.stream}'`
591
733
  return null
734
+
735
+ case t.CANCEL:
736
+ if (message.id === 0) return 'Cancel without an id'
737
+ return null
592
738
  }
593
739
  }
594
740
 
741
+ function cancelError(reason) {
742
+ const err = m.toError(reason)
743
+
744
+ if (typeof err.code === 'string' && err.code !== '') return err
745
+
746
+ return {
747
+ message: err.message,
748
+ code: err.name === 'TimeoutError' ? 'TIMEOUT' : 'CANCELLED',
749
+ errno: err.errno
750
+ }
751
+ }
752
+
753
+ function destroyStreams(request, err) {
754
+ if (request._requestStream !== null) request._requestStream.destroy(err)
755
+ if (request._responseStream !== null) request._responseStream.destroy(err)
756
+ }
757
+
595
758
  function findStream(message, requests, responses) {
596
759
  if (message.stream & s.REQUEST) {
597
760
  const request = requests.get(message.id)
@@ -605,5 +768,3 @@ function findStream(message, requests, responses) {
605
768
  function isPushable(stream) {
606
769
  return stream !== null && !stream.destroying && !isEnding(stream)
607
770
  }
608
-
609
- function noop() {}
@@ -31,24 +31,31 @@ module.exports = class RPCCommandRouter {
31
31
  async _onrequest(req) {
32
32
  const responder = this._responders.get(req.command)
33
33
 
34
- if (responder === undefined) {
35
- if (req.id === undefined) return
36
-
37
- throw errors.UNKNOWN_COMMAND(`Unknown command ${req.command}`)
34
+ if (req.id === undefined) {
35
+ if (responder !== undefined) await this._dispatch(responder, req)
36
+ return
38
37
  }
39
38
 
40
- const { onrequest, requestEncoding, responseEncoding } = responder
41
-
42
- let data = req.data
39
+ if (responder === undefined) throw errors.UNKNOWN_COMMAND(`Unknown command ${req.command}`)
43
40
 
44
- if (requestEncoding) data = c.decode(requestEncoding, data)
41
+ let data = await this._dispatch(responder, req)
45
42
 
46
- data = await onrequest(req, data)
43
+ if (req.sent || req._reason !== null) return
47
44
 
48
- if (req.sent) return
45
+ const { responseEncoding } = responder
49
46
 
50
47
  if (responseEncoding) data = c.encode(responseEncoding, data)
51
48
 
52
49
  req.reply(data)
53
50
  }
51
+
52
+ _dispatch(responder, req) {
53
+ const { onrequest, requestEncoding } = responder
54
+
55
+ let data = req.data
56
+
57
+ if (requestEncoding) data = c.decode(requestEncoding, data)
58
+
59
+ return onrequest(req, data)
60
+ }
54
61
  }
package/lib/constants.js CHANGED
@@ -2,7 +2,8 @@ module.exports = {
2
2
  type: {
3
3
  REQUEST: 1,
4
4
  RESPONSE: 2,
5
- STREAM: 3
5
+ STREAM: 3,
6
+ CANCEL: 4
6
7
  },
7
8
  stream: {
8
9
  OPEN: 0x1,
package/lib/errors.d.ts CHANGED
@@ -6,6 +6,10 @@ declare class RPCError extends Error {
6
6
  static ALREADY_SENT(msg: string): RPCError
7
7
  static ALREADY_RECEIVED(msg: string): RPCError
8
8
  static CHANNEL_CLOSED(msg: string): RPCError
9
+ static UNEXPECTED_STREAM(msg: string): RPCError
10
+ static STREAM_OVERFLOW(msg: string): RPCError
11
+ static CANCELLED(msg: string): RPCError
12
+ static TIMEOUT(msg: string): RPCError
9
13
  }
10
14
 
11
15
  export = RPCError
package/lib/errors.js CHANGED
@@ -39,4 +39,20 @@ module.exports = class RPCError extends Error {
39
39
  static CHANNEL_CLOSED(msg) {
40
40
  return new RPCError(msg, 'CHANNEL_CLOSED', RPCError.CHANNEL_CLOSED)
41
41
  }
42
+
43
+ static UNEXPECTED_STREAM(msg) {
44
+ return new RPCError(msg, 'UNEXPECTED_STREAM', RPCError.UNEXPECTED_STREAM)
45
+ }
46
+
47
+ static STREAM_OVERFLOW(msg) {
48
+ return new RPCError(msg, 'STREAM_OVERFLOW', RPCError.STREAM_OVERFLOW)
49
+ }
50
+
51
+ static CANCELLED(msg) {
52
+ return new RPCError(msg, 'CANCELLED', RPCError.CANCELLED)
53
+ }
54
+
55
+ static TIMEOUT(msg) {
56
+ return new RPCError(msg, 'TIMEOUT', RPCError.TIMEOUT)
57
+ }
42
58
  }
@@ -1,6 +1,9 @@
1
- module.exports = class RPCIncomingEvent {
1
+ const RPCIncoming = require('./incoming')
2
+
3
+ module.exports = class RPCIncomingEvent extends RPCIncoming {
2
4
  constructor(rpc, command, data) {
3
- this.rpc = rpc
5
+ super(rpc)
6
+
4
7
  this.command = command
5
8
  this.data = data
6
9
  }
@@ -1,9 +1,11 @@
1
1
  const c = require('compact-encoding')
2
2
  const errors = require('./errors')
3
+ const RPCIncoming = require('./incoming')
3
4
 
4
- module.exports = class RPCIncomingRequest {
5
+ module.exports = class RPCIncomingRequest extends RPCIncoming {
5
6
  constructor(rpc, id, command, data) {
6
- this.rpc = rpc
7
+ super(rpc)
8
+
7
9
  this.id = id
8
10
  this.command = command
9
11
  this.data = data
@@ -12,6 +14,8 @@ module.exports = class RPCIncomingRequest {
12
14
 
13
15
  this._requestStream = null
14
16
  this._responseStream = null
17
+
18
+ this._cancelled = false
15
19
  }
16
20
 
17
21
  reply(data, encoding) {
@@ -10,9 +10,13 @@ module.exports = class RPCIncomingStream extends Readable {
10
10
  this._request = request
11
11
  this._type = type
12
12
  this._mask = type === t.REQUEST ? s.REQUEST : s.RESPONSE
13
+
14
+ this._opened = false
13
15
  }
14
16
 
15
17
  _open(cb) {
18
+ this._opened = true
19
+
16
20
  this._rpc._sendMessage(
17
21
  {
18
22
  type: t.STREAM,
@@ -36,7 +40,11 @@ module.exports = class RPCIncomingStream extends Readable {
36
40
  }
37
41
 
38
42
  _destroy(err, cb) {
39
- if (err) {
43
+ // A request cancelled before it was sent only has to take back an open
44
+ // that already went out, as the remote end never learns of the cancel.
45
+ if (this._request._reason !== null && (this._request._cancelled || !this._opened)) {
46
+ cb(null)
47
+ } else if (err) {
40
48
  this._rpc._sendMessage(
41
49
  {
42
50
  type: t.STREAM,
@@ -0,0 +1,30 @@
1
+ module.exports = class RPCIncoming {
2
+ constructor(rpc) {
3
+ this.rpc = rpc
4
+
5
+ this._reason = null
6
+ this._controller = null
7
+ }
8
+
9
+ get signal() {
10
+ const { _AbortController: AbortController } = this.rpc
11
+
12
+ if (AbortController === null) return null
13
+
14
+ if (this._controller === null) {
15
+ this._controller = new AbortController()
16
+
17
+ if (this._reason !== null) this._controller.abort(this._reason)
18
+ }
19
+
20
+ return this._controller.signal
21
+ }
22
+
23
+ _abort(reason) {
24
+ if (this._reason !== null) return
25
+
26
+ this._reason = reason
27
+
28
+ if (this._controller !== null) this._controller.abort(reason)
29
+ }
30
+ }
package/lib/messages.js CHANGED
@@ -65,6 +65,10 @@ exports.header = {
65
65
 
66
66
  if (m.stream & s.ERROR) error.preencode(state, m.error)
67
67
  break
68
+
69
+ case t.CANCEL:
70
+ error.preencode(state, m.error)
71
+ break
68
72
  }
69
73
 
70
74
  if (hasData(m)) c.uint.preencode(state, m.data ? m.data.byteLength : 0)
@@ -97,6 +101,10 @@ exports.header = {
97
101
 
98
102
  if (m.stream & s.ERROR) error.encode(state, m.error)
99
103
  break
104
+
105
+ case t.CANCEL:
106
+ error.encode(state, m.error)
107
+ break
100
108
  }
101
109
 
102
110
  const data = hasData(m)
@@ -160,6 +168,9 @@ exports.message = {
160
168
  return { type, id, stream, error: null, data: null }
161
169
  }
162
170
 
171
+ case t.CANCEL:
172
+ return { type, id, error: error.decode(state), data: null }
173
+
163
174
  default:
164
175
  throw errors.UNKNOWN_MESSAGE(`Unknown message '${type}'`)
165
176
  }
@@ -2,7 +2,9 @@ const c = require('compact-encoding')
2
2
  const errors = require('./errors')
3
3
 
4
4
  module.exports = class RPCOutgoingRequest {
5
- constructor(rpc, id, command) {
5
+ constructor(rpc, id, command, opts = {}) {
6
+ const { signal = null, timeout = 0 } = opts
7
+
6
8
  this.rpc = rpc
7
9
  this.id = id
8
10
  this.command = command
@@ -10,6 +12,13 @@ module.exports = class RPCOutgoingRequest {
10
12
  this.received = false
11
13
 
12
14
  this._responded = false
15
+ this._reason = null
16
+ this._cancelled = false
17
+
18
+ this._signal = signal
19
+ this._timeout = timeout
20
+ this._timer = null
21
+ this._onabort = signal === null ? null : () => this.cancel(signal.reason)
13
22
 
14
23
  this._promise = new Promise((resolve, reject) => {
15
24
  this._resolve = resolve
@@ -34,11 +43,17 @@ module.exports = class RPCOutgoingRequest {
34
43
  ? c.raw.utf8
35
44
  : null
36
45
 
46
+ this._abortIfSignalled()
47
+
37
48
  this.sent = true
38
49
 
39
50
  this.rpc._sendRequest(this, encoding ? c.encode(encoding, data) : data)
40
51
  }
41
52
 
53
+ cancel(reason) {
54
+ this.rpc._cancelRequest(this, reason ?? errors.CANCELLED('Request was cancelled'))
55
+ }
56
+
42
57
  reply(encoding) {
43
58
  if (this.received) {
44
59
  throw errors.ALREADY_RECEIVED('Response is already being received')
@@ -62,6 +77,8 @@ module.exports = class RPCOutgoingRequest {
62
77
  throw errors.ALREADY_SENT('Request has already been sent')
63
78
  }
64
79
 
80
+ this._abortIfSignalled()
81
+
65
82
  this.sent = true
66
83
 
67
84
  this.rpc._createRequestStream(this, true, opts)
@@ -80,6 +97,29 @@ module.exports = class RPCOutgoingRequest {
80
97
 
81
98
  return this._responseStream
82
99
  }
100
+
101
+ _abortIfSignalled() {
102
+ if (this._signal !== null && this._signal.aborted) this._onabort()
103
+ }
104
+
105
+ _attach() {
106
+ if (this._signal !== null) this._signal.addEventListener('abort', this._onabort)
107
+
108
+ if (this._timeout > 0) this._timer = setTimeout(ontimeout, this._timeout, this)
109
+ }
110
+
111
+ _detach() {
112
+ if (this._signal !== null) this._signal.removeEventListener('abort', this._onabort)
113
+
114
+ if (this._timer !== null) {
115
+ clearTimeout(this._timer)
116
+ this._timer = null
117
+ }
118
+ }
119
+ }
120
+
121
+ function ontimeout(request) {
122
+ request.cancel(errors.TIMEOUT(`Request timed out after ${request._timeout} ms`))
83
123
  }
84
124
 
85
125
  function noop() {}
@@ -12,14 +12,15 @@ module.exports = class RPCOutgoingStream extends Writable {
12
12
  this._mask = type === t.REQUEST ? s.REQUEST : s.RESPONSE
13
13
 
14
14
  this._pendingOpen = null
15
+ this._flushed = false
15
16
  this._acknowledged = false
16
17
  this._ended = false
17
18
  }
18
19
 
19
- _open(cb) {
20
+ _sendOpen() {
20
21
  const onflushed = () => {
21
- if (this._acknowledged) cb(null)
22
- else this._pendingOpen = cb
22
+ this._flushed = true
23
+ this._finishOpen()
23
24
  }
24
25
 
25
26
  switch (this._type) {
@@ -51,14 +52,22 @@ module.exports = class RPCOutgoingStream extends Writable {
51
52
  }
52
53
  }
53
54
 
55
+ _open(cb) {
56
+ this._pendingOpen = cb
57
+ this._finishOpen()
58
+ }
59
+
54
60
  _continueOpen() {
55
61
  this._acknowledged = true
62
+ this._finishOpen()
63
+ }
56
64
 
57
- if (this._pendingOpen === null) return
65
+ _finishOpen() {
66
+ if (!this._flushed || !this._acknowledged || this._pendingOpen === null) return
58
67
 
59
68
  const cb = this._pendingOpen
60
69
  this._pendingOpen = null
61
- cb()
70
+ cb(null)
62
71
  }
63
72
 
64
73
  _predestroy() {
@@ -94,7 +103,7 @@ module.exports = class RPCOutgoingStream extends Writable {
94
103
  }
95
104
 
96
105
  _destroy(err, cb) {
97
- if (this._ended) {
106
+ if (this._ended || this._request._reason !== null) {
98
107
  cb(null)
99
108
  } else if (err) {
100
109
  this._rpc._sendMessage(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-rpc",
3
- "version": "1.4.0",
3
+ "version": "2.0.0",
4
4
  "description": "librpc ABI compatible RPC for Bare",
5
5
  "exports": {
6
6
  ".": {
@@ -42,6 +42,7 @@
42
42
  "safety-catch": "^1.0.2"
43
43
  },
44
44
  "devDependencies": {
45
+ "bare-abort-controller": "^1.1.2",
45
46
  "bare-buffer": "^3.0.1",
46
47
  "bare-ipc": "^1.1.0",
47
48
  "bare-rpc-test": "^0.1.0",
@@ -51,9 +52,13 @@
51
52
  "prettier-config-holepunch": "^2.0.0"
52
53
  },
53
54
  "peerDependencies": {
55
+ "bare-abort-controller": "*",
54
56
  "bare-buffer": "*"
55
57
  },
56
58
  "peerDependenciesMeta": {
59
+ "bare-abort-controller": {
60
+ "optional": true
61
+ },
57
62
  "bare-buffer": {
58
63
  "optional": true
59
64
  }