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