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