bare-rpc 1.3.8 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -115
- package/index.d.ts +201 -2
- package/index.js +227 -175
- package/lib/command-router.js +6 -1
- package/lib/errors.d.ts +3 -0
- package/lib/errors.js +12 -0
- package/lib/incoming-stream.js +2 -1
- package/lib/messages.js +25 -13
- package/lib/outgoing-request.js +4 -0
- package/lib/outgoing-stream.js +18 -15
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -29,126 +29,19 @@ console.log(replyBuffer.toString()) // pong
|
|
|
29
29
|
|
|
30
30
|
## API
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
See the [`bare-rpc` reference](https://docs.pears.com/reference/bare/modules/bare-rpc).
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
## Untrusted peers
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
`bare-rpc` does not trust the remote end to send valid frames. If a frame is larger than `maxFrameSize`, cannot be decoded, or breaks the protocol, the connection fails: `stream` is destroyed with an `RPCError`, and every request and stream in flight is rejected or destroyed with that error. The error code tells you why:
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
req.reply('pong')
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
See [`RPCIncomingRequest`](#rpcincomingrequest) for properties and methods for processing the request.
|
|
47
|
-
|
|
48
|
-
`onrequest` can also be a [`RPCCommandRouter`](#rpccommandrouter).
|
|
49
|
-
|
|
50
|
-
#### `const req = rpc.request(command)`
|
|
51
|
-
|
|
52
|
-
Create a request for `command`. `command` is a unique number that should be used to differentiate different requests on the remote end. Returns a `RPCOutgoingRequest`.
|
|
53
|
-
|
|
54
|
-
### `RPCOutgoingRequest`
|
|
55
|
-
|
|
56
|
-
#### `req.command`
|
|
57
|
-
|
|
58
|
-
The command that the request was created with. A command is a unique number.
|
|
59
|
-
|
|
60
|
-
#### `req.sent`
|
|
61
|
-
|
|
62
|
-
A boolean for whether the request has been sent.
|
|
63
|
-
|
|
64
|
-
#### `req.received`
|
|
65
|
-
|
|
66
|
-
A boolean for whether the request has received a reply.
|
|
67
|
-
|
|
68
|
-
#### `req.send([data[, encoding]])`
|
|
69
|
-
|
|
70
|
-
Send the request with the provided `data`. `data` can be a buffer or a string which will be encoded using `encoding`.
|
|
71
|
-
|
|
72
|
-
`.send()` can only be called once per request.
|
|
73
|
-
|
|
74
|
-
#### `const data = await req.reply([encoding])`
|
|
75
|
-
|
|
76
|
-
Await the reply from the remote end to the request. `encoding` can be defined for decoding the response `data` buffer back into a string.
|
|
77
|
-
|
|
78
|
-
#### `const stream = req.createRequestStream([options])`
|
|
79
|
-
|
|
80
|
-
Create a [`Writable`](https://github.com/mafintosh/streamx#writable-stream) stream for sending data with the request.
|
|
81
|
-
|
|
82
|
-
#### `const stream = req.createResponseStream([options])`
|
|
83
|
-
|
|
84
|
-
Create a [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream for receiving data in reply to the request.
|
|
85
|
-
|
|
86
|
-
### `RPCIncomingRequest`
|
|
87
|
-
|
|
88
|
-
#### `req.command`
|
|
89
|
-
|
|
90
|
-
The command that the request was sent as. A command is a unique number.
|
|
38
|
+
- `FRAME_TOO_LARGE`: The length prefix is larger than `maxFrameSize`. This is checked before the rest of the frame is read.
|
|
39
|
+
- `UNKNOWN_MESSAGE`: The frame has a message type that `bare-rpc` does not know.
|
|
40
|
+
- `INVALID_MESSAGE`: The frame cannot be decoded, the stream ends partway through a frame, or the frame is one that a valid peer never sends. Examples are a response or stream message with an id of 0, a stream message with an invalid set of flags, and a new request that reuses the id of a request that is still in flight.
|
|
91
41
|
|
|
92
|
-
|
|
42
|
+
A frame that refers to a request or stream that has already finished, or that was never known, is ignored. The same goes for frames that arrive for a stream that has already ended or been destroyed.
|
|
93
43
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
#### `req.sent`
|
|
97
|
-
|
|
98
|
-
A boolean for whether a reply has been sent.
|
|
99
|
-
|
|
100
|
-
#### `req.received`
|
|
101
|
-
|
|
102
|
-
A boolean for whether the request has been received as a stream. See [`req.createRequestStream()`](#const-stream--reqcreaterequeststream) for receiving requests as a stream.
|
|
103
|
-
|
|
104
|
-
#### `req.reply([data, [encoding]])`
|
|
105
|
-
|
|
106
|
-
Reply to the request with the provided `data`. `data` can be a buffer or a string which will be encoded using `encoding`.
|
|
107
|
-
|
|
108
|
-
#### `const stream = req.createRequestStream([options])`
|
|
109
|
-
|
|
110
|
-
Create a `Readable` stream for receiving data from the request.
|
|
111
|
-
|
|
112
|
-
#### `const stream = req.createResponseStream([options])`
|
|
113
|
-
|
|
114
|
-
Create a `Writable` stream for sending data in reply to the request.
|
|
115
|
-
|
|
116
|
-
### `RPCCommandRouter`
|
|
117
|
-
|
|
118
|
-
An alternative way to define commands and handlers for receiving them.
|
|
119
|
-
|
|
120
|
-
#### `const router = new RPC.CommandRouter()`
|
|
121
|
-
|
|
122
|
-
Create a new command router. This router can then be used when creating an `rpc`. For example:
|
|
123
|
-
|
|
124
|
-
```js
|
|
125
|
-
const router = new RPC.CommandRouter()
|
|
126
|
-
|
|
127
|
-
router.respond(42, (req, data) => {
|
|
128
|
-
console.log(data.toString()) // ping
|
|
129
|
-
|
|
130
|
-
return Buffer.from('pong')
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
const rpc = new RPC(stream, router)
|
|
134
|
-
const req = rpc.request(42)
|
|
135
|
-
req.send('ping')
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
#### `router.respond(command[, options], async (req, data) => {})`
|
|
139
|
-
|
|
140
|
-
Define a command and the handler for it. The callback for a command receives both the request (`req`) and the `data` buffer and can return a value to respond. If the request is responded to in the callback, the return value is ignored.
|
|
141
|
-
|
|
142
|
-
Options include:
|
|
143
|
-
|
|
144
|
-
```js
|
|
145
|
-
options = {
|
|
146
|
-
// Encoding for incoming request
|
|
147
|
-
requestEncoding: c.raw,
|
|
148
|
-
// Encoding for outgoing response
|
|
149
|
-
responseEncoding: c.raw
|
|
150
|
-
}
|
|
151
|
-
```
|
|
44
|
+
If you read the response to a request as a stream but the remote end replies without one, the reply arrives on the response stream as a single chunk, unless it is empty, and the stream then ends. An error reply destroys the response stream with that error.
|
|
152
45
|
|
|
153
46
|
## License
|
|
154
47
|
|
package/index.d.ts
CHANGED
|
@@ -19,11 +19,18 @@ declare const constants: {
|
|
|
19
19
|
|
|
20
20
|
interface RPCIncomingEvent {
|
|
21
21
|
readonly rpc: RPC
|
|
22
|
+
/** The command that the request was created with. A command is a unique number. */
|
|
22
23
|
readonly command: number
|
|
24
|
+
/** The data buffer sent with the request. */
|
|
23
25
|
readonly data: Buffer | null
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
declare class RPCIncomingEvent {
|
|
29
|
+
/**
|
|
30
|
+
* @param rpc - The `RPC` instance the event arrived on.
|
|
31
|
+
* @param command - The command number the event was sent with.
|
|
32
|
+
* @param data - The payload buffer sent with the event.
|
|
33
|
+
*/
|
|
27
34
|
constructor(rpc: RPC, command: number, data: Buffer)
|
|
28
35
|
}
|
|
29
36
|
|
|
@@ -32,16 +39,45 @@ interface RPCIncomingRequest {
|
|
|
32
39
|
readonly id: number
|
|
33
40
|
readonly command: number
|
|
34
41
|
readonly data: Buffer | null
|
|
42
|
+
/** A boolean for whether the request has been sent. */
|
|
35
43
|
readonly sent: boolean
|
|
44
|
+
/** A boolean for whether the request has received a reply. */
|
|
36
45
|
readonly received: boolean
|
|
37
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Send a reply to the request. `data` may be a buffer or a string encoded using `encoding`.
|
|
49
|
+
* @param data - The payload to send: a `Buffer`, or a string encoded using `encoding`. Omit to
|
|
50
|
+
* send no data.
|
|
51
|
+
* @param encoding - The encoding used when `data` is a string (defaults to `utf8`).
|
|
52
|
+
* @throws {ALREADY_SENT} a response has already been sent for this request.
|
|
53
|
+
*/
|
|
38
54
|
reply(data?: Buffer | string | null, encoding?: BufferEncoding): void
|
|
39
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Create a [`Writable`](https://github.com/mafintosh/streamx#writable-stream) stream for sending
|
|
58
|
+
* the streamed reply to the request.
|
|
59
|
+
* @param opts - Options for the returned
|
|
60
|
+
* [`Writable`](https://github.com/mafintosh/streamx#writable-stream) stream.
|
|
61
|
+
* @throws {ALREADY_SENT} a response has already been sent for this request.
|
|
62
|
+
*/
|
|
40
63
|
createResponseStream(opts?: WritableOptions): RPCOutgoingStream
|
|
64
|
+
/**
|
|
65
|
+
* Create a [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream for
|
|
66
|
+
* receiving the request's streamed data.
|
|
67
|
+
* @param opts - Options for the returned
|
|
68
|
+
* [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream.
|
|
69
|
+
* @throws {ALREADY_RECEIVED} the request has already been received.
|
|
70
|
+
*/
|
|
41
71
|
createRequestStream(opts?: ReadableOptions): RPCIncomingStream
|
|
42
72
|
}
|
|
43
73
|
|
|
44
74
|
declare class RPCIncomingRequest {
|
|
75
|
+
/**
|
|
76
|
+
* @param rpc - The `RPC` instance the request arrived on.
|
|
77
|
+
* @param id - The request id, used to correlate the reply with the request.
|
|
78
|
+
* @param command - The command number the request was sent with.
|
|
79
|
+
* @param data - The payload buffer sent with the request.
|
|
80
|
+
*/
|
|
45
81
|
constructor(rpc: RPC, id: number, command: number, data: Buffer)
|
|
46
82
|
}
|
|
47
83
|
|
|
@@ -50,10 +86,20 @@ interface RPCOutgoingEvent {
|
|
|
50
86
|
readonly command: number
|
|
51
87
|
readonly sent: boolean
|
|
52
88
|
|
|
89
|
+
/**
|
|
90
|
+
* @param data - The payload to send: a `Buffer`, or a string encoded using `encoding`. Omit to
|
|
91
|
+
* send no data.
|
|
92
|
+
* @param encoding - The encoding used when `data` is a string (defaults to `utf8`).
|
|
93
|
+
* @throws {ALREADY_SENT} the event has already been sent.
|
|
94
|
+
*/
|
|
53
95
|
send(data?: Buffer | string | null, encoding?: BufferEncoding): void
|
|
54
96
|
}
|
|
55
97
|
|
|
56
98
|
declare class RPCOutgoingEvent {
|
|
99
|
+
/**
|
|
100
|
+
* @param rpc - The `RPC` instance to send the event on.
|
|
101
|
+
* @param command - The command number to send.
|
|
102
|
+
*/
|
|
57
103
|
constructor(rpc: RPC, command: number)
|
|
58
104
|
}
|
|
59
105
|
|
|
@@ -64,19 +110,62 @@ interface RPCOutgoingRequest {
|
|
|
64
110
|
readonly sent: boolean
|
|
65
111
|
readonly received: boolean
|
|
66
112
|
|
|
113
|
+
/**
|
|
114
|
+
* @param data - The payload to send: a `Buffer`, or a string encoded using `encoding`. Omit to
|
|
115
|
+
* send no data.
|
|
116
|
+
* @param encoding - The encoding used when `data` is a string (defaults to `utf8`).
|
|
117
|
+
* @throws {ALREADY_SENT} the request has already been sent.
|
|
118
|
+
*/
|
|
67
119
|
send(data?: Buffer | string | null, encoding?: BufferEncoding): void
|
|
68
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Await the reply from the remote end to the request. `encoding` can be defined for decoding the
|
|
123
|
+
* response `data` buffer back into a string.
|
|
124
|
+
* @param encoding - If given, decodes the reply payload to a string using this encoding; omit to
|
|
125
|
+
* receive the raw `Buffer`.
|
|
126
|
+
* @returns a promise that resolves with the remote end's reply payload, or rejects with the
|
|
127
|
+
* channel's teardown error if it closes before a reply arrives.
|
|
128
|
+
* @throws {ALREADY_RECEIVED} a reply is already being received for this request.
|
|
129
|
+
*/
|
|
69
130
|
reply(encoding?: BufferEncoding): Promise<Buffer | string | null>
|
|
70
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Create a [`Writable`](https://github.com/mafintosh/streamx#writable-stream) stream for sending
|
|
134
|
+
* data with the request.
|
|
135
|
+
* @param opts - Options for the returned
|
|
136
|
+
* [`Writable`](https://github.com/mafintosh/streamx#writable-stream) stream.
|
|
137
|
+
* @throws {ALREADY_SENT} the request has already been sent.
|
|
138
|
+
*/
|
|
71
139
|
createRequestStream(opts?: WritableOptions): RPCOutgoingStream
|
|
140
|
+
/**
|
|
141
|
+
* Create a [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream for
|
|
142
|
+
* receiving data in reply to the request. If the remote end replies without a stream, the reply
|
|
143
|
+
* arrives as a single chunk, unless it is empty, and the stream then ends.
|
|
144
|
+
* @param opts - Options for the returned
|
|
145
|
+
* [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream.
|
|
146
|
+
* @throws {ALREADY_RECEIVED} the response has already been received.
|
|
147
|
+
*/
|
|
72
148
|
createResponseStream(opts?: ReadableOptions): RPCIncomingStream
|
|
73
149
|
}
|
|
74
150
|
|
|
75
151
|
declare class RPCOutgoingRequest {
|
|
152
|
+
/**
|
|
153
|
+
* @param rpc - The `RPC` instance to send the request on.
|
|
154
|
+
* @param id - The request id, used to correlate the reply with the request.
|
|
155
|
+
* @param command - The command number to send.
|
|
156
|
+
*/
|
|
76
157
|
constructor(rpc: RPC, id: number, command: number)
|
|
77
158
|
}
|
|
78
159
|
|
|
79
160
|
declare class RPCIncomingStream extends Readable {
|
|
161
|
+
/**
|
|
162
|
+
* @param rpc - The `RPC` instance the stream belongs to.
|
|
163
|
+
* @param request - The request the stream carries data for.
|
|
164
|
+
* @param type - Whether the stream carries the request body (`constants.type.REQUEST`) or the
|
|
165
|
+
* response body (`constants.type.RESPONSE`).
|
|
166
|
+
* @param opts - Options for the underlying
|
|
167
|
+
* [`Readable`](https://github.com/mafintosh/streamx#readable-stream) stream.
|
|
168
|
+
*/
|
|
80
169
|
constructor(
|
|
81
170
|
rpc: RPC,
|
|
82
171
|
request: RPCIncomingRequest | RPCOutgoingRequest,
|
|
@@ -86,6 +175,14 @@ declare class RPCIncomingStream extends Readable {
|
|
|
86
175
|
}
|
|
87
176
|
|
|
88
177
|
declare class RPCOutgoingStream extends Writable {
|
|
178
|
+
/**
|
|
179
|
+
* @param rpc - The `RPC` instance the stream belongs to.
|
|
180
|
+
* @param request - The request the stream carries data for.
|
|
181
|
+
* @param type - Whether the stream carries the request body (`constants.type.REQUEST`) or the
|
|
182
|
+
* response body (`constants.type.RESPONSE`).
|
|
183
|
+
* @param opts - Options for the underlying
|
|
184
|
+
* [`Writable`](https://github.com/mafintosh/streamx#writable-stream) stream.
|
|
185
|
+
*/
|
|
89
186
|
constructor(
|
|
90
187
|
rpc: RPC,
|
|
91
188
|
request: RPCIncomingRequest | RPCOutgoingRequest,
|
|
@@ -94,17 +191,113 @@ declare class RPCOutgoingStream extends Writable {
|
|
|
94
191
|
)
|
|
95
192
|
}
|
|
96
193
|
|
|
194
|
+
/** A [`compact-encoding`](https://github.com/holepunchto/compact-encoding) encoding, such as `c.raw`. */
|
|
195
|
+
interface RPCValueEncoding {
|
|
196
|
+
preencode(state: unknown, value: unknown): void
|
|
197
|
+
encode(state: unknown, value: unknown): void
|
|
198
|
+
decode(state: unknown): unknown
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface RPCCommandRouterOptions {
|
|
202
|
+
/** The encoding of request and response data for commands that set none (defaults to `c.raw`). */
|
|
203
|
+
valueEncoding?: RPCValueEncoding
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
interface RPCRespondOptions {
|
|
207
|
+
/** The encoding of both the request and the response data. */
|
|
208
|
+
valueEncoding?: RPCValueEncoding
|
|
209
|
+
/** The encoding to decode the request data with, or `null` to pass the raw buffer. */
|
|
210
|
+
requestEncoding?: RPCValueEncoding | null
|
|
211
|
+
/** The encoding to encode the response data with, or `null` to reply with the value as is. */
|
|
212
|
+
responseEncoding?: RPCValueEncoding | null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param req - The incoming request.
|
|
217
|
+
* @param data - The request data, decoded with the request encoding of the command.
|
|
218
|
+
* @returns the response data, encoded with the response encoding of the command and sent as the
|
|
219
|
+
* reply unless the handler has already replied.
|
|
220
|
+
*/
|
|
221
|
+
type RPCRequestHandler = (req: RPCIncomingRequest, data: any) => unknown
|
|
222
|
+
|
|
223
|
+
interface RPCCommandRouter {
|
|
224
|
+
/**
|
|
225
|
+
* Answer requests for `command` with `onrequest`, in place of any handler registered before. A
|
|
226
|
+
* request for a command with no handler is answered with an error whose code is
|
|
227
|
+
* `UNKNOWN_COMMAND`, and an event for such a command is dropped.
|
|
228
|
+
* @param command - The command number to answer.
|
|
229
|
+
* @param opts - The encodings of the command.
|
|
230
|
+
* @param onrequest - The handler of the requests.
|
|
231
|
+
*/
|
|
232
|
+
respond(command: number, onrequest: RPCRequestHandler): void
|
|
233
|
+
respond(command: number, opts: RPCRespondOptions, onrequest: RPCRequestHandler): void
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
declare class RPCCommandRouter {
|
|
237
|
+
/**
|
|
238
|
+
* @param opts - The default encodings of the commands.
|
|
239
|
+
*/
|
|
240
|
+
constructor(opts?: RPCCommandRouterOptions)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
interface RPCOptions {
|
|
244
|
+
/**
|
|
245
|
+
* The largest frame to accept from the remote end, in bytes, counting the 4-byte length prefix. A
|
|
246
|
+
* larger frame fails the connection with `FRAME_TOO_LARGE` as soon as its length prefix arrives.
|
|
247
|
+
* Defaults to 16 MiB.
|
|
248
|
+
*/
|
|
249
|
+
maxFrameSize?: number
|
|
250
|
+
}
|
|
251
|
+
|
|
97
252
|
interface RPC {
|
|
253
|
+
/**
|
|
254
|
+
* Whether there are no requests or responses currently in flight. Useful for determining when it
|
|
255
|
+
* is safe to tear down the underlying stream.
|
|
256
|
+
*/
|
|
98
257
|
readonly idle: boolean
|
|
99
258
|
|
|
259
|
+
/**
|
|
260
|
+
* @param command - A unique number identifying the event; the remote end differentiates events by
|
|
261
|
+
* it.
|
|
262
|
+
* @returns an `RPCOutgoingEvent`; call `send()` on it to dispatch the one-way event.
|
|
263
|
+
*/
|
|
100
264
|
event(command: number): RPCOutgoingEvent
|
|
265
|
+
/**
|
|
266
|
+
* @param command - A unique number identifying the request; the remote end differentiates
|
|
267
|
+
* requests by it.
|
|
268
|
+
* @returns an `RPCOutgoingRequest`; call `send()` on it to dispatch the request and `await
|
|
269
|
+
* reply()` for the response.
|
|
270
|
+
*/
|
|
101
271
|
request(command: number): RPCOutgoingRequest
|
|
102
272
|
}
|
|
103
273
|
|
|
104
274
|
declare class RPC {
|
|
105
|
-
|
|
275
|
+
/**
|
|
276
|
+
* @param stream - The duplex stream to frame RPC messages over, such as a pipe or socket.
|
|
277
|
+
* @param onrequest - Callback run for each incoming request or event, receiving an
|
|
278
|
+
* `RPCIncomingRequest` (or `RPCIncomingEvent`) to inspect and `reply()` to, or an
|
|
279
|
+
* `RPCCommandRouter` that answers each request with the handler of its command. Defaults to a
|
|
280
|
+
* no-op.
|
|
281
|
+
* @param opts - Options for the connection.
|
|
282
|
+
*
|
|
283
|
+
* The remote end is not trusted to send valid frames. A frame that is too large, cannot be
|
|
284
|
+
* decoded, or breaks the protocol fails the connection: `stream` is destroyed with an `RPCError`
|
|
285
|
+
* whose code is `FRAME_TOO_LARGE`, `UNKNOWN_MESSAGE` or `INVALID_MESSAGE`, and every request and
|
|
286
|
+
* stream in flight is rejected or destroyed with it. A frame that refers to a request or stream
|
|
287
|
+
* that has already finished, or that was never known, is ignored.
|
|
288
|
+
*/
|
|
289
|
+
constructor(
|
|
290
|
+
stream: Duplex,
|
|
291
|
+
onrequest?: ((req: RPCIncomingRequest) => void | Promise<void>) | RPCCommandRouter,
|
|
292
|
+
opts?: RPCOptions
|
|
293
|
+
)
|
|
106
294
|
}
|
|
107
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Create an RPC instance using a duplex `stream`. `onrequest` is an optional callback run when a
|
|
298
|
+
* remote request is received. This is where processing and responding to an RPC request happens.
|
|
299
|
+
* `onrequest` receives a `RPCIncomingRequest` as an argument.
|
|
300
|
+
*/
|
|
108
301
|
declare namespace RPC {
|
|
109
302
|
export {
|
|
110
303
|
type RPCIncomingEvent as IncomingEvent,
|
|
@@ -112,7 +305,13 @@ declare namespace RPC {
|
|
|
112
305
|
type RPCIncomingRequest as IncomingRequest,
|
|
113
306
|
type RPCOutgoingRequest as OutgoingRequest,
|
|
114
307
|
type RPCIncomingStream as IncomingStream,
|
|
115
|
-
type RPCOutgoingStream as OutgoingStream
|
|
308
|
+
type RPCOutgoingStream as OutgoingStream,
|
|
309
|
+
RPCCommandRouter as CommandRouter,
|
|
310
|
+
type RPCCommandRouterOptions as CommandRouterOptions,
|
|
311
|
+
type RPCRespondOptions as RespondOptions,
|
|
312
|
+
type RPCRequestHandler as RequestHandler,
|
|
313
|
+
type RPCValueEncoding as ValueEncoding,
|
|
314
|
+
type RPCOptions as Options
|
|
116
315
|
}
|
|
117
316
|
}
|
|
118
317
|
|
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const safetyCatch = require('safety-catch')
|
|
2
2
|
const b4a = require('b4a')
|
|
3
3
|
const c = require('compact-encoding')
|
|
4
|
+
const { isEnding } = require('bare-stream')
|
|
4
5
|
const m = require('./lib/messages')
|
|
5
6
|
const errors = require('./lib/errors')
|
|
6
7
|
const { type: t, stream: s } = require('./lib/constants')
|
|
@@ -12,17 +13,42 @@ const OutgoingRequest = require('./lib/outgoing-request')
|
|
|
12
13
|
const OutgoingStream = require('./lib/outgoing-stream')
|
|
13
14
|
const CommandRouter = require('./lib/command-router')
|
|
14
15
|
|
|
16
|
+
const DEFAULT_MAX_FRAME_SIZE = 16 * 1024 * 1024
|
|
17
|
+
const MAX_PENDING_RESPONSES = 1024
|
|
18
|
+
|
|
19
|
+
const streamFlags = new Set()
|
|
20
|
+
|
|
21
|
+
for (const direction of [s.REQUEST, s.RESPONSE]) {
|
|
22
|
+
for (const action of [
|
|
23
|
+
s.OPEN,
|
|
24
|
+
s.CLOSE,
|
|
25
|
+
s.CLOSE | s.ERROR,
|
|
26
|
+
s.PAUSE,
|
|
27
|
+
s.RESUME,
|
|
28
|
+
s.DATA,
|
|
29
|
+
s.END,
|
|
30
|
+
s.DESTROY,
|
|
31
|
+
s.DESTROY | s.ERROR
|
|
32
|
+
]) {
|
|
33
|
+
streamFlags.add(direction | action)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
15
37
|
module.exports = exports = class RPC {
|
|
16
|
-
constructor(stream, onrequest = noop) {
|
|
38
|
+
constructor(stream, onrequest = noop, opts = {}) {
|
|
39
|
+
const { maxFrameSize = DEFAULT_MAX_FRAME_SIZE } = opts
|
|
40
|
+
|
|
17
41
|
this._stream = stream
|
|
42
|
+
this._maxFrameSize = maxFrameSize
|
|
18
43
|
this._id = 0
|
|
19
44
|
|
|
20
45
|
this._outgoingRequests = new Map()
|
|
21
46
|
this._outgoingResponses = new Map()
|
|
22
47
|
this._incomingRequests = new Map()
|
|
23
48
|
this._incomingResponses = new Map()
|
|
24
|
-
this._pendingRequests = new Set()
|
|
25
49
|
this._pendingResponses = new Set()
|
|
50
|
+
this._unansweredRequests = new Set()
|
|
51
|
+
this._highestRequestId = 0
|
|
26
52
|
|
|
27
53
|
this._buffer = []
|
|
28
54
|
this._buffered = 0
|
|
@@ -42,12 +68,14 @@ module.exports = exports = class RPC {
|
|
|
42
68
|
this._onerror = this._onerror.bind(this)
|
|
43
69
|
this._onclose = this._onclose.bind(this)
|
|
44
70
|
this._ondata = this._ondata.bind(this)
|
|
71
|
+
this._onend = this._onend.bind(this)
|
|
45
72
|
this._ondrain = this._ondrain.bind(this)
|
|
46
73
|
|
|
47
74
|
this._stream
|
|
48
75
|
.on('error', this._onerror)
|
|
49
76
|
.on('close', this._onclose)
|
|
50
77
|
.on('data', this._ondata)
|
|
78
|
+
.on('end', this._onend)
|
|
51
79
|
.on('drain', this._ondrain)
|
|
52
80
|
}
|
|
53
81
|
|
|
@@ -59,7 +87,6 @@ module.exports = exports = class RPC {
|
|
|
59
87
|
this._outgoingResponses.size === 0 &&
|
|
60
88
|
this._incomingRequests.size === 0 &&
|
|
61
89
|
this._incomingResponses.size === 0 &&
|
|
62
|
-
this._pendingRequests.size === 0 &&
|
|
63
90
|
this._pendingResponses.size === 0
|
|
64
91
|
)
|
|
65
92
|
}
|
|
@@ -85,7 +112,7 @@ module.exports = exports = class RPC {
|
|
|
85
112
|
|
|
86
113
|
let flushed = this._stream.write(header)
|
|
87
114
|
|
|
88
|
-
if (message.data) flushed = this._stream.write(message.data)
|
|
115
|
+
if (message.data && m.hasData(message)) flushed = this._stream.write(message.data)
|
|
89
116
|
|
|
90
117
|
if (cb) {
|
|
91
118
|
if (flushed) cb(null)
|
|
@@ -149,6 +176,8 @@ module.exports = exports = class RPC {
|
|
|
149
176
|
// settle.
|
|
150
177
|
if (this._closed) return
|
|
151
178
|
|
|
179
|
+
this._onanswered(request)
|
|
180
|
+
|
|
152
181
|
this._sendMessage({
|
|
153
182
|
type: t.RESPONSE,
|
|
154
183
|
id: request.id,
|
|
@@ -168,6 +197,8 @@ module.exports = exports = class RPC {
|
|
|
168
197
|
|
|
169
198
|
this._outgoingResponses.set(request.id, request)
|
|
170
199
|
|
|
200
|
+
if (this._onanswered(request)) request._responseStream._continueOpen()
|
|
201
|
+
|
|
171
202
|
request._responseStream.on('close', () => this._outgoingResponses.delete(request.id))
|
|
172
203
|
} else {
|
|
173
204
|
request._responseStream = new IncomingStream(this, request, t.RESPONSE, opts)
|
|
@@ -186,15 +217,23 @@ module.exports = exports = class RPC {
|
|
|
186
217
|
}
|
|
187
218
|
|
|
188
219
|
_sendError(request, err) {
|
|
220
|
+
this._onanswered(request)
|
|
221
|
+
|
|
189
222
|
this._sendMessage({
|
|
190
223
|
type: t.RESPONSE,
|
|
191
224
|
id: request.id,
|
|
192
225
|
stream: 0,
|
|
193
|
-
error: err,
|
|
226
|
+
error: m.toError(err),
|
|
194
227
|
data: null
|
|
195
228
|
})
|
|
196
229
|
}
|
|
197
230
|
|
|
231
|
+
// Returns whether the remote end had already opened its response stream.
|
|
232
|
+
_onanswered(request) {
|
|
233
|
+
this._unansweredRequests.delete(request.id)
|
|
234
|
+
return this._pendingResponses.delete(request.id)
|
|
235
|
+
}
|
|
236
|
+
|
|
198
237
|
_gcOutgoingRequest(request) {
|
|
199
238
|
// The entry must stay reachable for as long as an incoming message might
|
|
200
239
|
// still target it; until the response has settled and, if the request was
|
|
@@ -255,13 +294,19 @@ module.exports = exports = class RPC {
|
|
|
255
294
|
this._outgoingResponses.clear()
|
|
256
295
|
this._incomingRequests.clear()
|
|
257
296
|
this._incomingResponses.clear()
|
|
258
|
-
this._pendingRequests.clear()
|
|
259
297
|
this._pendingResponses.clear()
|
|
298
|
+
this._unansweredRequests.clear()
|
|
260
299
|
|
|
261
300
|
for (const stream of streams) stream.destroy(err)
|
|
262
301
|
}
|
|
263
302
|
|
|
303
|
+
_fail(err) {
|
|
304
|
+
this._stream.destroy(err)
|
|
305
|
+
}
|
|
306
|
+
|
|
264
307
|
_ondata(data) {
|
|
308
|
+
if (this._stream.destroying) return
|
|
309
|
+
|
|
265
310
|
this._buffer.push(data)
|
|
266
311
|
this._buffered += data.byteLength
|
|
267
312
|
|
|
@@ -272,20 +317,39 @@ module.exports = exports = class RPC {
|
|
|
272
317
|
}
|
|
273
318
|
}
|
|
274
319
|
|
|
275
|
-
|
|
276
|
-
if (this._buffered
|
|
320
|
+
_onend() {
|
|
321
|
+
if (this._buffered > 0) this._fail(errors.INVALID_MESSAGE('Truncated frame'))
|
|
322
|
+
}
|
|
277
323
|
|
|
278
|
-
|
|
324
|
+
_onbeforeframe() {
|
|
325
|
+
while (this._buffered >= 4 && !this._stream.destroying) {
|
|
326
|
+
const buffer = this._buffer.length === 1 ? this._buffer[0] : b4a.concat(this._buffer)
|
|
327
|
+
|
|
328
|
+
this._buffer = [buffer]
|
|
329
|
+
this._frame = 4 + c.uint32.decode(c.state(0, 4, buffer))
|
|
330
|
+
|
|
331
|
+
if (this._frame > this._maxFrameSize) {
|
|
332
|
+
return this._fail(
|
|
333
|
+
errors.FRAME_TOO_LARGE(
|
|
334
|
+
`Frame of ${this._frame} bytes exceeds the limit of ${this._maxFrameSize} bytes`
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
}
|
|
279
338
|
|
|
280
|
-
|
|
281
|
-
this._frame = 4 + c.uint32.decode(c.state(0, 4, buffer))
|
|
339
|
+
if (this._buffered < this._frame) return
|
|
282
340
|
|
|
283
|
-
|
|
341
|
+
this._onframe()
|
|
342
|
+
}
|
|
284
343
|
}
|
|
285
344
|
|
|
286
345
|
_onafterframe() {
|
|
287
346
|
if (this._buffered < this._frame) return
|
|
288
347
|
|
|
348
|
+
this._onframe()
|
|
349
|
+
this._onbeforeframe()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
_onframe() {
|
|
289
353
|
const buffer = this._buffer.length === 1 ? this._buffer[0] : b4a.concat(this._buffer)
|
|
290
354
|
|
|
291
355
|
const frame = this._frame
|
|
@@ -295,55 +359,74 @@ module.exports = exports = class RPC {
|
|
|
295
359
|
this._frame = -1
|
|
296
360
|
|
|
297
361
|
this._onmessage(buffer.subarray(0, frame))
|
|
298
|
-
this._onbeforeframe()
|
|
299
362
|
}
|
|
300
363
|
|
|
301
|
-
|
|
364
|
+
_onmessage(buffer) {
|
|
302
365
|
let message
|
|
303
366
|
try {
|
|
304
367
|
message = m.message.decode(c.state(0, buffer.length, buffer))
|
|
305
368
|
} catch (err) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
369
|
+
return this._fail(
|
|
370
|
+
err instanceof errors ? err : errors.INVALID_MESSAGE(`Malformed message: ${err.message}`)
|
|
371
|
+
)
|
|
309
372
|
}
|
|
310
373
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
else this._stream.destroy(err)
|
|
325
|
-
}
|
|
326
|
-
break
|
|
327
|
-
}
|
|
328
|
-
case t.RESPONSE:
|
|
329
|
-
try {
|
|
374
|
+
const violation = protocolViolation(message)
|
|
375
|
+
|
|
376
|
+
if (violation !== null) return this._fail(errors.INVALID_MESSAGE(violation))
|
|
377
|
+
|
|
378
|
+
// Handling a message can run user code, such as stream listeners, so route
|
|
379
|
+
// anything it throws through the connection failure path rather than let it
|
|
380
|
+
// escape into the underlying stream.
|
|
381
|
+
try {
|
|
382
|
+
switch (message.type) {
|
|
383
|
+
case t.REQUEST:
|
|
384
|
+
this._onrequestmessage(message)
|
|
385
|
+
break
|
|
386
|
+
case t.RESPONSE:
|
|
330
387
|
this._onresponse(message)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
break
|
|
335
|
-
case t.STREAM:
|
|
336
|
-
try {
|
|
388
|
+
break
|
|
389
|
+
case t.STREAM:
|
|
337
390
|
this._onstream(message)
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
391
|
+
}
|
|
392
|
+
} catch (err) {
|
|
393
|
+
this._fail(err)
|
|
341
394
|
}
|
|
342
395
|
}
|
|
343
396
|
|
|
344
|
-
|
|
345
|
-
|
|
397
|
+
async _onrequestmessage(message) {
|
|
398
|
+
// A repeated id can only be caught while its request is tracked here, which
|
|
399
|
+
// is also the only time it could corrupt any state.
|
|
400
|
+
if (
|
|
401
|
+
this._unansweredRequests.has(message.id) ||
|
|
402
|
+
this._incomingRequests.has(message.id) ||
|
|
403
|
+
this._outgoingResponses.has(message.id)
|
|
404
|
+
) {
|
|
405
|
+
return this._fail(errors.INVALID_MESSAGE(`Request '${message.id}' is already in flight`))
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let request
|
|
409
|
+
|
|
410
|
+
if (message.id === 0) {
|
|
411
|
+
request = new IncomingEvent(this, message.command, message.data)
|
|
412
|
+
} else {
|
|
413
|
+
request = new IncomingRequest(this, message.id, message.command, message.data)
|
|
414
|
+
|
|
415
|
+
this._unansweredRequests.add(message.id)
|
|
416
|
+
this._highestRequestId = Math.max(this._highestRequestId, message.id)
|
|
417
|
+
}
|
|
346
418
|
|
|
419
|
+
try {
|
|
420
|
+
await this._onrequest(request)
|
|
421
|
+
} catch (err) {
|
|
422
|
+
safetyCatch(err)
|
|
423
|
+
|
|
424
|
+
if (message.id) this._sendError(request, err)
|
|
425
|
+
else this._stream.destroy(err)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
_onresponse(message) {
|
|
347
430
|
const request = this._outgoingRequests.get(message.id)
|
|
348
431
|
if (request === undefined) return
|
|
349
432
|
|
|
@@ -352,17 +435,36 @@ module.exports = exports = class RPC {
|
|
|
352
435
|
this._gcOutgoingRequest(request)
|
|
353
436
|
|
|
354
437
|
request._reject(message.error)
|
|
438
|
+
|
|
439
|
+
if (request._responseStream !== null) request._responseStream.destroy(message.error)
|
|
440
|
+
|
|
441
|
+
this._releaseRequestStream(request, message.error)
|
|
355
442
|
} else if (message.stream === 0) {
|
|
356
443
|
request._responded = true
|
|
357
444
|
this._gcOutgoingRequest(request)
|
|
358
445
|
|
|
359
446
|
request._resolve(message.data)
|
|
447
|
+
|
|
448
|
+
const stream = request._responseStream
|
|
449
|
+
|
|
450
|
+
if (isPushable(stream)) {
|
|
451
|
+
if (message.data.byteLength > 0) stream.push(message.data)
|
|
452
|
+
stream.push(null)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
this._releaseRequestStream(request, null)
|
|
360
456
|
}
|
|
361
457
|
}
|
|
362
458
|
|
|
363
|
-
|
|
364
|
-
|
|
459
|
+
// Once the request is answered, a request stream that the remote end never
|
|
460
|
+
// opened will never be read, and would otherwise wait for its open forever.
|
|
461
|
+
_releaseRequestStream(request, err) {
|
|
462
|
+
const stream = request._requestStream
|
|
463
|
+
|
|
464
|
+
if (stream !== null && !stream._acknowledged) stream.destroy(err)
|
|
465
|
+
}
|
|
365
466
|
|
|
467
|
+
_onstream(message) {
|
|
366
468
|
if (message.stream & s.OPEN) this._onstreamopen(message)
|
|
367
469
|
else if (message.stream & s.CLOSE) this._onstreamclose(message)
|
|
368
470
|
else if (message.stream & s.PAUSE) this._onstreampause(message)
|
|
@@ -373,123 +475,56 @@ module.exports = exports = class RPC {
|
|
|
373
475
|
}
|
|
374
476
|
|
|
375
477
|
_onstreamopen(message) {
|
|
376
|
-
let stream
|
|
377
|
-
|
|
378
478
|
if (message.stream & s.REQUEST) {
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
479
|
+
const stream = this._outgoingStream(message)
|
|
480
|
+
if (stream !== null) stream._continueOpen()
|
|
481
|
+
} else {
|
|
482
|
+
const request = this._outgoingResponses.get(message.id)
|
|
384
483
|
|
|
385
|
-
|
|
484
|
+
if (request !== undefined) return request._responseStream._continueOpen()
|
|
386
485
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (
|
|
394
|
-
|
|
486
|
+
// The remote end opens its response stream as soon as it is created,
|
|
487
|
+
// which may be before its request arrives or before the handler here
|
|
488
|
+
// creates the response stream. Remember the open until the response
|
|
489
|
+
// stream is created, the request is answered, or the remote end destroys
|
|
490
|
+
// its stream. Any other open is for a request that has already been
|
|
491
|
+
// answered.
|
|
492
|
+
if (this._pendingResponses.has(message.id)) return
|
|
493
|
+
|
|
494
|
+
if (!this._unansweredRequests.has(message.id) && message.id <= this._highestRequestId) {
|
|
395
495
|
return
|
|
396
496
|
}
|
|
397
497
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (stream === null || stream._pendingOpen === null) {
|
|
401
|
-
this._pendingResponses.add(message.id)
|
|
402
|
-
return
|
|
498
|
+
if (this._pendingResponses.size >= MAX_PENDING_RESPONSES) {
|
|
499
|
+
return this._fail(errors.INVALID_MESSAGE('Too many response streams opened early'))
|
|
403
500
|
}
|
|
404
|
-
} else {
|
|
405
|
-
return
|
|
406
|
-
}
|
|
407
501
|
|
|
408
|
-
|
|
502
|
+
this._pendingResponses.add(message.id)
|
|
503
|
+
}
|
|
409
504
|
}
|
|
410
505
|
|
|
411
506
|
_onstreamclose(message) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (message.stream & s.REQUEST) {
|
|
415
|
-
const request = this._incomingRequests.get(message.id)
|
|
416
|
-
if (request === undefined) return
|
|
417
|
-
|
|
418
|
-
stream = request._requestStream
|
|
419
|
-
} else if (message.stream & s.RESPONSE) {
|
|
420
|
-
const request = this._incomingResponses.get(message.id)
|
|
421
|
-
if (request === undefined) return
|
|
422
|
-
|
|
423
|
-
stream = request._responseStream
|
|
424
|
-
} else {
|
|
425
|
-
return
|
|
426
|
-
}
|
|
427
|
-
|
|
507
|
+
const stream = this._incomingStream(message)
|
|
428
508
|
if (stream === null) return
|
|
509
|
+
|
|
429
510
|
if (message.error) stream.destroy(message.error)
|
|
430
511
|
else stream.push(null)
|
|
431
512
|
}
|
|
432
513
|
|
|
433
514
|
_onstreampause(message) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (message.stream & s.REQUEST) {
|
|
437
|
-
const request = this._outgoingRequests.get(message.id)
|
|
438
|
-
if (request === undefined) return
|
|
439
|
-
|
|
440
|
-
stream = request._requestStream
|
|
441
|
-
} else if (message.stream & s.RESPONSE) {
|
|
442
|
-
const request = this._outgoingResponses.get(message.id)
|
|
443
|
-
if (request === undefined) return
|
|
444
|
-
|
|
445
|
-
stream = request._responseStream
|
|
446
|
-
} else {
|
|
447
|
-
return
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (stream === null) return
|
|
451
|
-
stream.cork()
|
|
515
|
+
const stream = this._outgoingStream(message)
|
|
516
|
+
if (stream !== null) stream.cork()
|
|
452
517
|
}
|
|
453
518
|
|
|
454
519
|
_onstreamresume(message) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (message.stream & s.REQUEST) {
|
|
458
|
-
const request = this._outgoingRequests.get(message.id)
|
|
459
|
-
if (request === undefined) return
|
|
460
|
-
|
|
461
|
-
stream = request._requestStream
|
|
462
|
-
} else if (message.stream & s.RESPONSE) {
|
|
463
|
-
const request = this._outgoingResponses.get(message.id)
|
|
464
|
-
if (request === undefined) return
|
|
465
|
-
|
|
466
|
-
stream = request._responseStream
|
|
467
|
-
} else {
|
|
468
|
-
return
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (stream === null) return
|
|
472
|
-
stream.uncork()
|
|
520
|
+
const stream = this._outgoingStream(message)
|
|
521
|
+
if (stream !== null) stream.uncork()
|
|
473
522
|
}
|
|
474
523
|
|
|
475
524
|
_onstreamdata(message) {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (message.stream & s.REQUEST) {
|
|
479
|
-
const request = this._incomingRequests.get(message.id)
|
|
480
|
-
if (request === undefined) return
|
|
481
|
-
|
|
482
|
-
stream = request._requestStream
|
|
483
|
-
} else if (message.stream & s.RESPONSE) {
|
|
484
|
-
const request = this._incomingResponses.get(message.id)
|
|
485
|
-
if (request === undefined) return
|
|
525
|
+
const stream = this._incomingStream(message)
|
|
526
|
+
if (!isPushable(stream)) return
|
|
486
527
|
|
|
487
|
-
stream = request._responseStream
|
|
488
|
-
} else {
|
|
489
|
-
return
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if (stream === null) return
|
|
493
528
|
if (stream.push(message.data) === false) {
|
|
494
529
|
this._sendMessage({
|
|
495
530
|
type: t.STREAM,
|
|
@@ -502,45 +537,23 @@ module.exports = exports = class RPC {
|
|
|
502
537
|
}
|
|
503
538
|
|
|
504
539
|
_onstreamend(message) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if (message.stream & s.REQUEST) {
|
|
508
|
-
const request = this._incomingRequests.get(message.id)
|
|
509
|
-
if (request === undefined) return
|
|
510
|
-
|
|
511
|
-
stream = request._requestStream
|
|
512
|
-
} else if (message.stream & s.RESPONSE) {
|
|
513
|
-
const request = this._incomingResponses.get(message.id)
|
|
514
|
-
if (request === undefined) return
|
|
515
|
-
|
|
516
|
-
stream = request._responseStream
|
|
517
|
-
} else {
|
|
518
|
-
return
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (stream === null) return
|
|
522
|
-
stream.push(null)
|
|
540
|
+
const stream = this._incomingStream(message)
|
|
541
|
+
if (stream !== null) stream.push(null)
|
|
523
542
|
}
|
|
524
543
|
|
|
525
544
|
_onstreamdestroy(message) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (message.stream & s.REQUEST) {
|
|
529
|
-
const request = this._outgoingRequests.get(message.id)
|
|
530
|
-
if (request === undefined) return
|
|
545
|
+
if (message.stream & s.RESPONSE) this._pendingResponses.delete(message.id)
|
|
531
546
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (request === undefined) return
|
|
547
|
+
const stream = this._outgoingStream(message)
|
|
548
|
+
if (stream !== null) stream.destroy(message.error)
|
|
549
|
+
}
|
|
536
550
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
551
|
+
_incomingStream(message) {
|
|
552
|
+
return findStream(message, this._incomingRequests, this._incomingResponses)
|
|
553
|
+
}
|
|
541
554
|
|
|
542
|
-
|
|
543
|
-
|
|
555
|
+
_outgoingStream(message) {
|
|
556
|
+
return findStream(message, this._outgoingRequests, this._outgoingResponses)
|
|
544
557
|
}
|
|
545
558
|
|
|
546
559
|
_ondrain(err = null) {
|
|
@@ -554,4 +567,43 @@ module.exports = exports = class RPC {
|
|
|
554
567
|
|
|
555
568
|
exports.CommandRouter = CommandRouter
|
|
556
569
|
|
|
570
|
+
// Messages that decode cleanly but that no valid peer ever sends, as derived
|
|
571
|
+
// from what the outgoing side encodes.
|
|
572
|
+
function protocolViolation(message) {
|
|
573
|
+
switch (message.type) {
|
|
574
|
+
case t.REQUEST:
|
|
575
|
+
if (message.stream === 0) return null
|
|
576
|
+
if (message.stream !== s.OPEN) return `Invalid request stream flags '${message.stream}'`
|
|
577
|
+
if (message.id === 0) return 'Stream request without an id'
|
|
578
|
+
return null
|
|
579
|
+
|
|
580
|
+
case t.RESPONSE:
|
|
581
|
+
if (message.id === 0) return 'Response without an id'
|
|
582
|
+
if (message.stream === 0) return null
|
|
583
|
+
if (message.stream !== s.OPEN || message.error) {
|
|
584
|
+
return `Invalid response stream flags '${message.stream}'`
|
|
585
|
+
}
|
|
586
|
+
return null
|
|
587
|
+
|
|
588
|
+
case t.STREAM:
|
|
589
|
+
if (message.id === 0) return 'Stream message without an id'
|
|
590
|
+
if (!streamFlags.has(message.stream)) return `Invalid stream flags '${message.stream}'`
|
|
591
|
+
return null
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function findStream(message, requests, responses) {
|
|
596
|
+
if (message.stream & s.REQUEST) {
|
|
597
|
+
const request = requests.get(message.id)
|
|
598
|
+
return request === undefined ? null : request._requestStream
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const request = responses.get(message.id)
|
|
602
|
+
return request === undefined ? null : request._responseStream
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function isPushable(stream) {
|
|
606
|
+
return stream !== null && !stream.destroying && !isEnding(stream)
|
|
607
|
+
}
|
|
608
|
+
|
|
557
609
|
function noop() {}
|
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,7 +31,11 @@ module.exports = class RPCCommandRouter {
|
|
|
30
31
|
async _onrequest(req) {
|
|
31
32
|
const responder = this._responders.get(req.command)
|
|
32
33
|
|
|
33
|
-
if (responder === undefined)
|
|
34
|
+
if (responder === undefined) {
|
|
35
|
+
if (req.id === undefined) return
|
|
36
|
+
|
|
37
|
+
throw errors.UNKNOWN_COMMAND(`Unknown command ${req.command}`)
|
|
38
|
+
}
|
|
34
39
|
|
|
35
40
|
const { onrequest, requestEncoding, responseEncoding } = responder
|
|
36
41
|
|
package/lib/errors.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
declare class RPCError extends Error {
|
|
2
2
|
static UNKNOWN_MESSAGE(msg: string): RPCError
|
|
3
|
+
static UNKNOWN_COMMAND(msg: string): RPCError
|
|
4
|
+
static INVALID_MESSAGE(msg: string): RPCError
|
|
5
|
+
static FRAME_TOO_LARGE(msg: string): RPCError
|
|
3
6
|
static ALREADY_SENT(msg: string): RPCError
|
|
4
7
|
static ALREADY_RECEIVED(msg: string): RPCError
|
|
5
8
|
static CHANNEL_CLOSED(msg: string): RPCError
|
package/lib/errors.js
CHANGED
|
@@ -16,6 +16,18 @@ module.exports = class RPCError extends Error {
|
|
|
16
16
|
return new RPCError(msg, 'UNKNOWN_MESSAGE', RPCError.UNKNOWN_MESSAGE)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
static UNKNOWN_COMMAND(msg) {
|
|
20
|
+
return new RPCError(msg, 'UNKNOWN_COMMAND', RPCError.UNKNOWN_COMMAND)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static INVALID_MESSAGE(msg) {
|
|
24
|
+
return new RPCError(msg, 'INVALID_MESSAGE', RPCError.INVALID_MESSAGE)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static FRAME_TOO_LARGE(msg) {
|
|
28
|
+
return new RPCError(msg, 'FRAME_TOO_LARGE', RPCError.FRAME_TOO_LARGE)
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
static ALREADY_SENT(msg) {
|
|
20
32
|
return new RPCError(msg, 'ALREADY_SENT', RPCError.ALREADY_SENT)
|
|
21
33
|
}
|
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,19 +21,36 @@ const error = {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
exports.toError = function toError(err) {
|
|
25
|
+
if (typeof err === 'object' && err !== null && typeof err.message === 'string') return err
|
|
26
|
+
return new Error(String(err))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hasData(m) {
|
|
30
|
+
switch (m.type) {
|
|
31
|
+
case t.REQUEST:
|
|
32
|
+
return m.stream === 0
|
|
33
|
+
case t.RESPONSE:
|
|
34
|
+
return !m.error && m.stream === 0
|
|
35
|
+
case t.STREAM:
|
|
36
|
+
return (m.stream & s.ERROR) === 0 && (m.stream & s.DATA) !== 0
|
|
37
|
+
default:
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
exports.hasData = hasData
|
|
43
|
+
|
|
24
44
|
exports.header = {
|
|
25
45
|
preencode(state, m) {
|
|
26
46
|
c.uint32.preencode(state, 0) // Frame
|
|
27
47
|
c.uint.preencode(state, m.type)
|
|
28
48
|
c.uint.preencode(state, m.id)
|
|
29
49
|
|
|
30
|
-
let hasData = false
|
|
31
|
-
|
|
32
50
|
switch (m.type) {
|
|
33
51
|
case t.REQUEST:
|
|
34
52
|
c.uint.preencode(state, m.command)
|
|
35
53
|
c.uint.preencode(state, m.stream)
|
|
36
|
-
if (m.stream === 0) hasData = true
|
|
37
54
|
break
|
|
38
55
|
|
|
39
56
|
case t.RESPONSE:
|
|
@@ -41,18 +58,16 @@ exports.header = {
|
|
|
41
58
|
c.uint.preencode(state, m.stream)
|
|
42
59
|
|
|
43
60
|
if (m.error) error.preencode(state, m.error)
|
|
44
|
-
else if (m.stream === 0) hasData = true
|
|
45
61
|
break
|
|
46
62
|
|
|
47
63
|
case t.STREAM:
|
|
48
64
|
c.uint.preencode(state, m.stream)
|
|
49
65
|
|
|
50
66
|
if (m.stream & s.ERROR) error.preencode(state, m.error)
|
|
51
|
-
else if (m.stream & s.DATA) hasData = true
|
|
52
67
|
break
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
if (hasData) c.uint.preencode(state, m.data ? m.data.byteLength : 0)
|
|
70
|
+
if (hasData(m)) c.uint.preencode(state, m.data ? m.data.byteLength : 0)
|
|
56
71
|
},
|
|
57
72
|
encode(state, m) {
|
|
58
73
|
const frame = state.start
|
|
@@ -64,13 +79,10 @@ exports.header = {
|
|
|
64
79
|
c.uint.encode(state, m.type)
|
|
65
80
|
c.uint.encode(state, m.id)
|
|
66
81
|
|
|
67
|
-
let hasData = false
|
|
68
|
-
|
|
69
82
|
switch (m.type) {
|
|
70
83
|
case t.REQUEST:
|
|
71
84
|
c.uint.encode(state, m.command)
|
|
72
85
|
c.uint.encode(state, m.stream)
|
|
73
|
-
if (m.stream === 0) hasData = true
|
|
74
86
|
break
|
|
75
87
|
|
|
76
88
|
case t.RESPONSE:
|
|
@@ -78,24 +90,24 @@ exports.header = {
|
|
|
78
90
|
c.uint.encode(state, m.stream)
|
|
79
91
|
|
|
80
92
|
if (m.error) error.encode(state, m.error)
|
|
81
|
-
else if (m.stream === 0) hasData = true
|
|
82
93
|
break
|
|
83
94
|
|
|
84
95
|
case t.STREAM:
|
|
85
96
|
c.uint.encode(state, m.stream)
|
|
86
97
|
|
|
87
98
|
if (m.stream & s.ERROR) error.encode(state, m.error)
|
|
88
|
-
else if (m.stream & s.DATA) hasData = true
|
|
89
99
|
break
|
|
90
100
|
}
|
|
91
101
|
|
|
92
|
-
|
|
102
|
+
const data = hasData(m)
|
|
103
|
+
|
|
104
|
+
if (data) c.uint.encode(state, m.data ? m.data.byteLength : 0)
|
|
93
105
|
|
|
94
106
|
const end = state.start
|
|
95
107
|
|
|
96
108
|
state.start = frame
|
|
97
109
|
|
|
98
|
-
c.uint32.encode(state, end - start + (
|
|
110
|
+
c.uint32.encode(state, end - start + (data && m.data ? m.data.byteLength : 0))
|
|
99
111
|
|
|
100
112
|
state.start = end
|
|
101
113
|
}
|
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,18 @@ module.exports = class RPCOutgoingStream extends Writable {
|
|
|
11
12
|
this._mask = type === t.REQUEST ? s.REQUEST : s.RESPONSE
|
|
12
13
|
|
|
13
14
|
this._pendingOpen = null
|
|
15
|
+
this._acknowledged = false
|
|
16
|
+
this._ended = false
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
_open(cb) {
|
|
17
|
-
let pending
|
|
18
|
-
|
|
19
20
|
const onflushed = () => {
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
cb(null)
|
|
24
|
-
} else {
|
|
25
|
-
this._pendingOpen = cb
|
|
26
|
-
}
|
|
21
|
+
if (this._acknowledged) cb(null)
|
|
22
|
+
else this._pendingOpen = cb
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
switch (this._type) {
|
|
30
26
|
case t.REQUEST:
|
|
31
|
-
pending = this._rpc._pendingRequests
|
|
32
|
-
|
|
33
27
|
this._rpc._sendMessage(
|
|
34
28
|
{
|
|
35
29
|
type: t.REQUEST,
|
|
@@ -43,8 +37,6 @@ module.exports = class RPCOutgoingStream extends Writable {
|
|
|
43
37
|
break
|
|
44
38
|
|
|
45
39
|
case t.RESPONSE:
|
|
46
|
-
pending = this._rpc._pendingResponses
|
|
47
|
-
|
|
48
40
|
this._rpc._sendMessage(
|
|
49
41
|
{
|
|
50
42
|
type: t.RESPONSE,
|
|
@@ -60,12 +52,19 @@ module.exports = class RPCOutgoingStream extends Writable {
|
|
|
60
52
|
}
|
|
61
53
|
|
|
62
54
|
_continueOpen() {
|
|
55
|
+
this._acknowledged = true
|
|
56
|
+
|
|
63
57
|
if (this._pendingOpen === null) return
|
|
58
|
+
|
|
64
59
|
const cb = this._pendingOpen
|
|
65
60
|
this._pendingOpen = null
|
|
66
61
|
cb()
|
|
67
62
|
}
|
|
68
63
|
|
|
64
|
+
_predestroy() {
|
|
65
|
+
this._continueOpen()
|
|
66
|
+
}
|
|
67
|
+
|
|
69
68
|
_write(data, encoding, cb) {
|
|
70
69
|
this._rpc._sendMessage(
|
|
71
70
|
{
|
|
@@ -80,6 +79,8 @@ module.exports = class RPCOutgoingStream extends Writable {
|
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
_final(cb) {
|
|
82
|
+
this._ended = true
|
|
83
|
+
|
|
83
84
|
this._rpc._sendMessage(
|
|
84
85
|
{
|
|
85
86
|
type: t.STREAM,
|
|
@@ -93,13 +94,15 @@ module.exports = class RPCOutgoingStream extends Writable {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
_destroy(err, cb) {
|
|
96
|
-
if (
|
|
97
|
+
if (this._ended) {
|
|
98
|
+
cb(null)
|
|
99
|
+
} else if (err) {
|
|
97
100
|
this._rpc._sendMessage(
|
|
98
101
|
{
|
|
99
102
|
type: t.STREAM,
|
|
100
103
|
id: this._request.id,
|
|
101
104
|
stream: this._mask | s.CLOSE | s.ERROR,
|
|
102
|
-
error: err,
|
|
105
|
+
error: toError(err),
|
|
103
106
|
data: null
|
|
104
107
|
},
|
|
105
108
|
cb
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bare-rpc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "librpc ABI compatible RPC for Bare",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"bare-buffer": "^3.0.1",
|
|
46
46
|
"bare-ipc": "^1.1.0",
|
|
47
|
+
"bare-rpc-test": "^0.1.0",
|
|
47
48
|
"brittle": "^3.2.1",
|
|
48
49
|
"lunte": "^1.7.0",
|
|
49
50
|
"prettier": "^3.4.2",
|