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 +130 -17
- package/index.d.ts +71 -15
- package/index.js +202 -41
- package/lib/command-router.js +17 -10
- package/lib/constants.js +2 -1
- package/lib/errors.d.ts +4 -0
- package/lib/errors.js +16 -0
- package/lib/incoming-event.js +5 -2
- package/lib/incoming-request.js +6 -2
- package/lib/incoming-stream.js +9 -1
- package/lib/incoming.js +30 -0
- package/lib/messages.js +11 -0
- package/lib/outgoing-request.js +41 -1
- package/lib/outgoing-stream.js +15 -6
- package/package.json +6 -1
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
23
|
-
const
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
|
39
|
-
- `UNKNOWN_MESSAGE`: The frame has a message type that `
|
|
40
|
-
- `INVALID_MESSAGE`: The frame cannot be decoded,
|
|
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
|
|
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.
|
|
226
|
-
*
|
|
227
|
-
*
|
|
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.
|
|
280
|
-
*
|
|
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
|
|
285
|
-
*
|
|
286
|
-
*
|
|
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 =
|
|
39
|
-
const {
|
|
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.
|
|
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
|
|
135
|
-
// request. Skip registering it so that it never
|
|
136
|
-
//
|
|
137
|
-
|
|
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.
|
|
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
|
|
155
|
-
// stream, so destroy it right away rather than
|
|
156
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
174
|
-
// response, so drop it rather than
|
|
175
|
-
// only be discarded. The reply is fire-and-forget, so there is
|
|
176
|
-
// settle.
|
|
177
|
-
if (this.
|
|
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
|
|
195
|
-
// stream, so destroy it right away rather than
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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.
|
|
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.
|
|
425
|
-
else
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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() {}
|
package/lib/command-router.js
CHANGED
|
@@ -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 (
|
|
35
|
-
if (
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
let data = req.data
|
|
39
|
+
if (responder === undefined) throw errors.UNKNOWN_COMMAND(`Unknown command ${req.command}`)
|
|
43
40
|
|
|
44
|
-
|
|
41
|
+
let data = await this._dispatch(responder, req)
|
|
45
42
|
|
|
46
|
-
|
|
43
|
+
if (req.sent || req._reason !== null) return
|
|
47
44
|
|
|
48
|
-
|
|
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
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
|
}
|
package/lib/incoming-event.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
const RPCIncoming = require('./incoming')
|
|
2
|
+
|
|
3
|
+
module.exports = class RPCIncomingEvent extends RPCIncoming {
|
|
2
4
|
constructor(rpc, command, data) {
|
|
3
|
-
|
|
5
|
+
super(rpc)
|
|
6
|
+
|
|
4
7
|
this.command = command
|
|
5
8
|
this.data = data
|
|
6
9
|
}
|
package/lib/incoming-request.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/lib/incoming-stream.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/lib/incoming.js
ADDED
|
@@ -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
|
}
|
package/lib/outgoing-request.js
CHANGED
|
@@ -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() {}
|
package/lib/outgoing-stream.js
CHANGED
|
@@ -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
|
-
|
|
20
|
+
_sendOpen() {
|
|
20
21
|
const onflushed = () => {
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
}
|