starpc 0.52.0 → 0.52.2
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 +37 -8
- package/dist/echo/client-test.d.ts +1 -1
- package/dist/echo/client-test.js +110 -2
- package/dist/integration/cross-language/tcp-packet-stream.d.ts +3 -0
- package/dist/integration/cross-language/tcp-packet-stream.js +112 -0
- package/dist/integration/cross-language/tcp-packet-stream.test.d.ts +1 -0
- package/dist/integration/cross-language/tcp-packet-stream.test.js +121 -0
- package/dist/integration/cross-language/ts-client.js +50 -37
- package/dist/integration/cross-language/ts-server.js +1 -36
- package/dist/rpcstream/rpcstream.d.ts +5 -1
- package/dist/rpcstream/rpcstream.js +75 -28
- package/dist/rpcstream/rpcstream.test.d.ts +1 -0
- package/dist/rpcstream/rpcstream.test.js +92 -0
- package/dist/srpc/client-rpc.d.ts +1 -0
- package/dist/srpc/client-rpc.js +9 -0
- package/dist/srpc/client-rpc.test.d.ts +1 -0
- package/dist/srpc/client-rpc.test.js +50 -0
- package/dist/srpc/client.js +18 -4
- package/dist/srpc/common-rpc.d.ts +5 -0
- package/dist/srpc/common-rpc.js +37 -8
- package/dist/srpc/common-rpc.test.js +2 -0
- package/dist/srpc/errors.d.ts +2 -0
- package/dist/srpc/errors.js +14 -0
- package/dist/srpc/index.d.ts +1 -1
- package/dist/srpc/index.js +1 -1
- package/dist/srpc/packet-codec.test.d.ts +1 -0
- package/dist/srpc/packet-codec.test.js +75 -0
- package/dist/srpc/packet.d.ts +1 -1
- package/dist/srpc/packet.js +11 -1
- package/dist/srpc/server.js +19 -6
- package/dist/srpc/server.test.js +43 -0
- package/dist/srpc/stream.d.ts +4 -1
- package/dist/srpc/stream.js +62 -3
- package/dist/srpc/stream.test.js +110 -1
- package/dist/srpc/termination.d.ts +27 -0
- package/dist/srpc/termination.js +56 -0
- package/dist/srpc/termination.test.d.ts +1 -0
- package/dist/srpc/termination.test.js +24 -0
- package/dist/testdata/packet-codec-vectors.json +64 -0
- package/echo/Cargo.toml +8 -0
- package/echo/client-test.ts +124 -2
- package/echo/echo_e2e_test.cpp +542 -985
- package/echo/echo_pb2.py +40 -0
- package/echo/echo_pb2.pyi +13 -0
- package/echo/echo_srpc.pb.cpp +3 -5
- package/echo/echo_srpc.pb.go +6 -9
- package/echo/echo_srpc.pb.hpp +23 -3
- package/echo/echo_srpc.pb.rs +61 -26
- package/echo/echo_srpc.py +309 -0
- package/echo/echo_srpc.pyi +85 -0
- package/echo/half_close_test.rs +178 -0
- package/echo/integration_server.rs +14 -21
- package/echo/main.rs +28 -48
- package/go.mod +3 -3
- package/go.sum +6 -8
- package/integration/cross-language/cpp-client.cpp +446 -402
- package/integration/cross-language/cpp-pipe-server.cpp +139 -0
- package/integration/cross-language/cpp-pipe_test.go +114 -0
- package/integration/cross-language/cpp-server.cpp +222 -186
- package/integration/cross-language/go-client/main.go +79 -3
- package/integration/cross-language/python-client.py +146 -0
- package/integration/cross-language/python-server.py +140 -0
- package/integration/cross-language/run.bash +190 -65
- package/integration/cross-language/tcp-packet-stream.test.ts +154 -0
- package/integration/cross-language/tcp-packet-stream.ts +121 -0
- package/integration/cross-language/ts-client.ts +62 -40
- package/integration/cross-language/ts-server.ts +1 -45
- package/mock/mock_pb2.py +38 -0
- package/mock/mock_pb2.pyi +11 -0
- package/mock/mock_srpc.pb.cpp +1 -1
- package/mock/mock_srpc.pb.hpp +1 -1
- package/mock/mock_srpc.pb.rs +10 -9
- package/mock/mock_srpc.py +71 -0
- package/mock/mock_srpc.pyi +27 -0
- package/package.json +19 -5
- package/srpc/CMakeLists.txt +3 -58
- package/srpc/README-cpp.md +69 -0
- package/srpc/__init__.py +0 -0
- package/srpc/accept.go +10 -4
- package/srpc/accept_test.go +25 -0
- package/srpc/build.rs +129 -34
- package/srpc/client-half-close_test.go +152 -0
- package/srpc/client-invoker.go +32 -42
- package/srpc/client-invoker_test.go +53 -0
- package/srpc/client-rpc.cpp +65 -80
- package/srpc/client-rpc.go +32 -33
- package/srpc/client-rpc.hpp +52 -38
- package/srpc/client-rpc.test.ts +67 -0
- package/srpc/client-rpc.ts +10 -0
- package/srpc/client-rpc_test.go +76 -0
- package/srpc/client.cpp +99 -87
- package/srpc/client.hpp +46 -35
- package/srpc/client.rs +78 -38
- package/srpc/client.ts +20 -4
- package/srpc/client_half_close_test.cc +331 -0
- package/srpc/client_invoker.rs +75 -0
- package/srpc/codec.rs +6 -0
- package/srpc/common-rpc.cpp +186 -146
- package/srpc/common-rpc.go +5 -4
- package/srpc/common-rpc.hpp +119 -89
- package/srpc/common-rpc.test.ts +2 -0
- package/srpc/common-rpc.ts +43 -8
- package/srpc/common-rpc_test.go +23 -0
- package/srpc/error.rs +19 -2
- package/srpc/errors.hpp +80 -50
- package/srpc/errors.ts +16 -0
- package/srpc/handler.hpp +9 -7
- package/srpc/handler.rs +7 -45
- package/srpc/index.ts +2 -0
- package/srpc/invoker.hpp +82 -55
- package/srpc/invoker.rs +14 -52
- package/srpc/lib.rs +2 -0
- package/srpc/message.hpp +17 -12
- package/srpc/msg-stream.go +1 -0
- package/srpc/msg-stream.hpp +108 -61
- package/srpc/mux.cpp +84 -84
- package/srpc/mux.hpp +48 -38
- package/srpc/mux.rs +4 -4
- package/srpc/net.go +12 -3
- package/srpc/packet-codec-vectors_test.go +195 -0
- package/srpc/packet-codec.test.ts +139 -0
- package/srpc/packet-rw.go +9 -2
- package/srpc/packet.cpp +60 -58
- package/srpc/packet.hpp +35 -20
- package/srpc/packet.rs +8 -27
- package/srpc/packet.ts +15 -2
- package/srpc/py.typed +0 -0
- package/srpc/rpc.rs +332 -115
- package/srpc/rpc_lifecycle_test.cc +333 -0
- package/srpc/rpcproto_pb2.py +40 -0
- package/srpc/rpcproto_pb2.pyi +40 -0
- package/srpc/server-cancel_test.go +39 -0
- package/srpc/server-rpc.cpp +83 -73
- package/srpc/server-rpc.go +21 -19
- package/srpc/server-rpc.hpp +56 -37
- package/srpc/server.rs +76 -131
- package/srpc/server.test.ts +50 -0
- package/srpc/server.ts +22 -6
- package/srpc/starpc.hpp +4 -2
- package/srpc/stream.go +4 -0
- package/srpc/stream.hpp +95 -34
- package/srpc/stream.rs +7 -2
- package/srpc/stream.test.ts +132 -1
- package/srpc/stream.ts +65 -9
- package/srpc/termination.test.ts +30 -0
- package/srpc/termination.ts +70 -0
- package/srpc/testing.rs +11 -66
- package/srpc/transport.rs +21 -71
- package/srpc/websocket.rs +106 -57
- package/srpc/writer.hpp +46 -32
- package/srpc/yamux.rs +82 -26
- package/srpc/tests/integration_test.rs +0 -495
package/srpc/stream.hpp
CHANGED
|
@@ -1,61 +1,122 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
|
-
#include <functional>
|
|
4
|
-
#include <memory>
|
|
5
|
-
|
|
6
3
|
#include "errors.hpp"
|
|
7
4
|
#include "message.hpp"
|
|
8
5
|
|
|
6
|
+
#include <atomic>
|
|
7
|
+
#include <functional>
|
|
8
|
+
#include <memory>
|
|
9
|
+
#include <stop_token>
|
|
10
|
+
#include <string>
|
|
11
|
+
|
|
9
12
|
namespace starpc {
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
/*
|
|
15
|
+
* Stream is one live RPC stream between the caller and the remote. Matches
|
|
16
|
+
* the Go Stream interface in stream.go.
|
|
17
|
+
*/
|
|
13
18
|
class Stream {
|
|
14
19
|
public:
|
|
15
|
-
|
|
20
|
+
virtual ~Stream() = default;
|
|
21
|
+
|
|
22
|
+
/* StopToken interrupts work when the RPC ends or is canceled. */
|
|
23
|
+
virtual std::stop_token StopToken() const
|
|
24
|
+
{
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
/*
|
|
29
|
+
* RemoteErrorMessage returns the diagnostic accompanying
|
|
30
|
+
* Error::RemoteError.
|
|
31
|
+
*/
|
|
32
|
+
virtual std::string RemoteErrorMessage() const
|
|
33
|
+
{
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
19
36
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
virtual Error MsgRecv(Message *msg) = 0;
|
|
37
|
+
/* MsgSend serializes msg and writes it to the remote. */
|
|
38
|
+
virtual Error MsgSend(const Message &msg) = 0;
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
/*
|
|
41
|
+
* MsgRecv reads the next message from the remote and parses it into
|
|
42
|
+
* msg. Returns EOF_ after the call completes.
|
|
43
|
+
*/
|
|
44
|
+
virtual Error MsgRecv(Message *msg) = 0;
|
|
26
45
|
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
/*
|
|
47
|
+
* CloseSend half-closes: no further messages will be sent. Once the
|
|
48
|
+
* request is written, the half-close is only a notice: it fails when
|
|
49
|
+
* the call already ended, and MsgRecv then returns the buffered
|
|
50
|
+
* messages followed by the call's outcome.
|
|
51
|
+
*/
|
|
52
|
+
virtual Error CloseSend() = 0;
|
|
53
|
+
|
|
54
|
+
/* Close ends the stream for both reading and writing. */
|
|
55
|
+
virtual Error Close() = 0;
|
|
29
56
|
};
|
|
30
57
|
|
|
31
|
-
|
|
32
|
-
|
|
58
|
+
/*
|
|
59
|
+
* StreamWithClose forwards a Stream and runs an extra callback on the first
|
|
60
|
+
* Close. Matches streamWithClose in stream.go.
|
|
61
|
+
*/
|
|
33
62
|
class StreamWithClose : public Stream {
|
|
34
63
|
public:
|
|
35
|
-
|
|
36
|
-
|
|
64
|
+
/*
|
|
65
|
+
* StreamWithClose borrows inner; it must outlive the wrapper.
|
|
66
|
+
* close_fn runs once, after inner->Close.
|
|
67
|
+
*/
|
|
68
|
+
StreamWithClose(Stream *inner, std::function<Error()> close_fn)
|
|
69
|
+
: inner(inner),
|
|
70
|
+
close_fn(std::move(close_fn))
|
|
71
|
+
{
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
Error MsgSend(const Message &msg) override
|
|
75
|
+
{
|
|
76
|
+
return inner->MsgSend(msg);
|
|
77
|
+
}
|
|
78
|
+
Error MsgRecv(Message *msg) override
|
|
79
|
+
{
|
|
80
|
+
return inner->MsgRecv(msg);
|
|
81
|
+
}
|
|
82
|
+
Error CloseSend() override
|
|
83
|
+
{
|
|
84
|
+
return inner->CloseSend();
|
|
85
|
+
}
|
|
86
|
+
std::stop_token StopToken() const override
|
|
87
|
+
{
|
|
88
|
+
return inner->StopToken();
|
|
89
|
+
}
|
|
90
|
+
std::string RemoteErrorMessage() const override
|
|
91
|
+
{
|
|
92
|
+
return inner->RemoteErrorMessage();
|
|
93
|
+
}
|
|
37
94
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
95
|
+
Error Close() override
|
|
96
|
+
{
|
|
97
|
+
if (closed.exchange(true))
|
|
98
|
+
return Error::OK;
|
|
41
99
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
100
|
+
/* Report the inner close even when the callback also fails. */
|
|
101
|
+
Error err = inner->Close();
|
|
102
|
+
Error err2 = close_fn();
|
|
103
|
+
if (err != Error::OK)
|
|
104
|
+
return err;
|
|
105
|
+
return err2;
|
|
106
|
+
}
|
|
49
107
|
|
|
50
108
|
private:
|
|
51
|
-
|
|
52
|
-
|
|
109
|
+
/* inner is borrowed; the caller owns it. */
|
|
110
|
+
Stream *inner;
|
|
111
|
+
std::function<Error()> close_fn;
|
|
112
|
+
std::atomic<bool> closed{false};
|
|
53
113
|
};
|
|
54
114
|
|
|
55
|
-
|
|
115
|
+
/* NewStreamWithClose wraps strm so close_fn runs on the first Close. */
|
|
56
116
|
inline std::unique_ptr<Stream>
|
|
57
|
-
NewStreamWithClose(Stream *strm, std::function<Error()> close_fn)
|
|
58
|
-
|
|
117
|
+
NewStreamWithClose(Stream *strm, std::function<Error()> close_fn)
|
|
118
|
+
{
|
|
119
|
+
return std::make_unique<StreamWithClose>(strm, std::move(close_fn));
|
|
59
120
|
}
|
|
60
121
|
|
|
61
122
|
} // namespace starpc
|
package/srpc/stream.rs
CHANGED
|
@@ -110,7 +110,7 @@ impl Context {
|
|
|
110
110
|
///
|
|
111
111
|
/// - All methods are async and may block waiting for I/O or messages
|
|
112
112
|
/// - `send_bytes` may return `Error::Completed` if `close_send` was already called
|
|
113
|
-
/// - `recv_bytes` returns `Error::StreamClosed` when the remote
|
|
113
|
+
/// - `recv_bytes` returns `Error::StreamClosed` when the remote completes
|
|
114
114
|
/// - `close` cancels the context and releases all resources
|
|
115
115
|
#[async_trait]
|
|
116
116
|
pub trait Stream: Send + Sync {
|
|
@@ -133,8 +133,9 @@ pub trait Stream: Send + Sync {
|
|
|
133
133
|
///
|
|
134
134
|
/// # Errors
|
|
135
135
|
///
|
|
136
|
-
/// - `Error::StreamClosed` if the remote
|
|
136
|
+
/// - `Error::StreamClosed` if the remote completed the call
|
|
137
137
|
/// - `Error::Remote` if the remote sent an error
|
|
138
|
+
/// - `Error::ClosedBeforeCompletion` if the transport closed first
|
|
138
139
|
/// - `Error::Cancelled` if the context was cancelled
|
|
139
140
|
async fn recv_bytes(&self) -> Result<Bytes>;
|
|
140
141
|
|
|
@@ -142,6 +143,10 @@ pub trait Stream: Send + Sync {
|
|
|
142
143
|
///
|
|
143
144
|
/// After calling this, `send_bytes` will return `Error::Completed`.
|
|
144
145
|
/// The receive side remains open until the remote closes.
|
|
146
|
+
///
|
|
147
|
+
/// Once the request is written, the half-close is only a notice. It fails
|
|
148
|
+
/// when the call already ended, and `recv_bytes` then returns the buffered
|
|
149
|
+
/// messages followed by the call's outcome.
|
|
145
150
|
async fn close_send(&self) -> Result<()>;
|
|
146
151
|
|
|
147
152
|
/// Closes both sides of the stream.
|
package/srpc/stream.test.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
2
|
import { pipe } from 'it-pipe'
|
|
3
|
+
import { pushable } from 'it-pushable'
|
|
4
|
+
import type { Source } from 'it-stream-types'
|
|
5
|
+
import type { Stream } from './stream-muxer.js'
|
|
6
|
+
import { streamToPacketStream } from './stream.js'
|
|
3
7
|
|
|
4
8
|
import {
|
|
5
9
|
ChannelStream,
|
|
@@ -71,6 +75,133 @@ describe('StreamConn packet stream', () => {
|
|
|
71
75
|
expect(serverError).toBeUndefined()
|
|
72
76
|
})
|
|
73
77
|
|
|
78
|
+
it('settles a blocked packet source when closed', async () => {
|
|
79
|
+
const { clientConn, cleanup } = connectStreamConns({
|
|
80
|
+
handlePacketStream() {},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const stream = await clientConn.openStream()
|
|
85
|
+
const pending = stream.source.next()
|
|
86
|
+
|
|
87
|
+
await stream.close()
|
|
88
|
+
|
|
89
|
+
await expect(pending).resolves.toEqual({ done: true, value: undefined })
|
|
90
|
+
} finally {
|
|
91
|
+
cleanup()
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('settles a blocked packet source when aborted', async () => {
|
|
96
|
+
const { clientConn, cleanup } = connectStreamConns({
|
|
97
|
+
handlePacketStream() {},
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const stream = await clientConn.openStream()
|
|
102
|
+
const pending = stream.source.next()
|
|
103
|
+
const error = new Error('stopped')
|
|
104
|
+
|
|
105
|
+
stream.abort(error)
|
|
106
|
+
|
|
107
|
+
await expect(pending).rejects.toBe(error)
|
|
108
|
+
} finally {
|
|
109
|
+
cleanup()
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('settles a blocked packet sink when closed', async () => {
|
|
114
|
+
const { clientConn, cleanup } = connectStreamConns({
|
|
115
|
+
handlePacketStream() {},
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const stream = await clientConn.openStream()
|
|
120
|
+
const input = pushable<Uint8Array>({ objectMode: true })
|
|
121
|
+
const pending = stream.sink(input)
|
|
122
|
+
|
|
123
|
+
await stream.close()
|
|
124
|
+
|
|
125
|
+
await expect(pending).resolves.toBeUndefined()
|
|
126
|
+
} finally {
|
|
127
|
+
cleanup()
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('settles a packet sink blocked in the underlying write', async () => {
|
|
132
|
+
const sinkStarted = Promise.withResolvers<void>()
|
|
133
|
+
const transport = {
|
|
134
|
+
source: (async function* () {})(),
|
|
135
|
+
sink: async (source: Source<Uint8Array>) => {
|
|
136
|
+
for await (const _chunk of source) {
|
|
137
|
+
sinkStarted.resolve()
|
|
138
|
+
await new Promise<void>(() => {})
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
close: vi.fn(async () => {}),
|
|
142
|
+
closeRead: vi.fn(async () => {}),
|
|
143
|
+
closeWrite: vi.fn(async () => {}),
|
|
144
|
+
abort: vi.fn(),
|
|
145
|
+
} satisfies Stream
|
|
146
|
+
const stream = streamToPacketStream(transport)
|
|
147
|
+
const input = pushable<Uint8Array>({ objectMode: true })
|
|
148
|
+
input.push(new Uint8Array([1]))
|
|
149
|
+
const pending = stream.sink(input)
|
|
150
|
+
await sinkStarted.promise
|
|
151
|
+
|
|
152
|
+
await stream.close()
|
|
153
|
+
|
|
154
|
+
await expect(pending).resolves.toBeUndefined()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('rejects a blocked packet sink with the abort error', async () => {
|
|
158
|
+
const { clientConn, cleanup } = connectStreamConns({
|
|
159
|
+
handlePacketStream() {},
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const stream = await clientConn.openStream()
|
|
164
|
+
const input = pushable<Uint8Array>({ objectMode: true })
|
|
165
|
+
const pending = stream.sink(input)
|
|
166
|
+
const error = new Error('stopped')
|
|
167
|
+
|
|
168
|
+
stream.abort(error)
|
|
169
|
+
|
|
170
|
+
await expect(pending).rejects.toBe(error)
|
|
171
|
+
} finally {
|
|
172
|
+
cleanup()
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('does not write ready input after close', async () => {
|
|
177
|
+
const serverStream = Promise.withResolvers<PacketStream>()
|
|
178
|
+
const { clientConn, cleanup } = connectStreamConns({
|
|
179
|
+
handlePacketStream(stream) {
|
|
180
|
+
serverStream.resolve(stream)
|
|
181
|
+
},
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const stream = await clientConn.openStream()
|
|
186
|
+
const peer = await serverStream.promise
|
|
187
|
+
const input = pushable<Uint8Array>({ objectMode: true })
|
|
188
|
+
input.push(new Uint8Array([1]))
|
|
189
|
+
|
|
190
|
+
const pending = stream.sink(input)
|
|
191
|
+
await stream.close()
|
|
192
|
+
|
|
193
|
+
await expect(pending).resolves.toBeUndefined()
|
|
194
|
+
await expect(nextWithTimeout(peer.source, 'server eof')).resolves.toEqual(
|
|
195
|
+
{
|
|
196
|
+
done: true,
|
|
197
|
+
value: undefined,
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
} finally {
|
|
201
|
+
cleanup()
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
74
205
|
it('aborts the yamux stream when the packet source errors', async () => {
|
|
75
206
|
const request = new TextEncoder().encode('request')
|
|
76
207
|
const sourceError = new Error('source failed')
|
package/srpc/stream.ts
CHANGED
|
@@ -8,16 +8,26 @@ import {
|
|
|
8
8
|
parseLengthPrefixTransform,
|
|
9
9
|
prependLengthPrefixTransform,
|
|
10
10
|
} from './packet.js'
|
|
11
|
+
import {
|
|
12
|
+
closeIterator,
|
|
13
|
+
sourceIterator,
|
|
14
|
+
TerminationGate,
|
|
15
|
+
} from './termination.js'
|
|
11
16
|
|
|
12
17
|
// PacketHandler handles incoming packets.
|
|
13
18
|
export type PacketHandler = (packet: Packet) => Promise<void>
|
|
14
19
|
|
|
15
20
|
// PacketStream represents a stream of packets where each Uint8Array represents one packet.
|
|
16
|
-
export
|
|
21
|
+
export interface PacketStream extends Duplex<
|
|
17
22
|
AsyncGenerator<Uint8Array>,
|
|
18
23
|
Source<Uint8Array>,
|
|
19
24
|
Promise<void>
|
|
20
|
-
>
|
|
25
|
+
> {
|
|
26
|
+
// close cleanly ends both directions of the stream.
|
|
27
|
+
close(): Promise<void>
|
|
28
|
+
// abort ends both directions of the stream with err.
|
|
29
|
+
abort(err: Error): void
|
|
30
|
+
}
|
|
21
31
|
|
|
22
32
|
// OpenStreamFunc is a function to start a new RPC by opening a Stream.
|
|
23
33
|
export type OpenStreamFunc = () => Promise<PacketStream>
|
|
@@ -29,20 +39,66 @@ export type HandleStreamFunc = (ch: PacketStream) => Promise<void>
|
|
|
29
39
|
|
|
30
40
|
// streamToPacketStream converts a Stream into a PacketStream using length-prefix framing.
|
|
31
41
|
export function streamToPacketStream(stream: Stream): PacketStream {
|
|
42
|
+
const termination = new TerminationGate()
|
|
32
43
|
return {
|
|
33
|
-
|
|
34
|
-
stream
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
close: async () => {
|
|
45
|
+
if (termination.terminate()) await stream.close()
|
|
46
|
+
},
|
|
47
|
+
abort: (err: Error) => {
|
|
48
|
+
if (termination.terminate(err)) stream.abort(err)
|
|
49
|
+
},
|
|
50
|
+
source: (async function* () {
|
|
51
|
+
const packets = pipe(
|
|
52
|
+
stream,
|
|
53
|
+
parseLengthPrefixTransform(),
|
|
54
|
+
combineUint8ArrayListTransform(),
|
|
55
|
+
)[Symbol.asyncIterator]()
|
|
56
|
+
try {
|
|
57
|
+
while (true) {
|
|
58
|
+
const next = await termination.next(packets)
|
|
59
|
+
if ('terminated' in next) {
|
|
60
|
+
if (next.error) throw next.error
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
if ('error' in next) throw next.error
|
|
64
|
+
if (next.result.done) return
|
|
65
|
+
yield next.result.value
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
closeIterator(packets)
|
|
69
|
+
}
|
|
70
|
+
})(),
|
|
38
71
|
sink: async (source: Source<Uint8Array>): Promise<void> => {
|
|
72
|
+
const iterator = sourceIterator(
|
|
73
|
+
pipe(source, prependLengthPrefixTransform()),
|
|
74
|
+
)
|
|
75
|
+
const gatedSource = (async function* () {
|
|
76
|
+
while (true) {
|
|
77
|
+
const next = await termination.next(iterator)
|
|
78
|
+
if ('terminated' in next) {
|
|
79
|
+
if (next.error) throw next.error
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if ('error' in next) throw next.error
|
|
83
|
+
if (next.result.done) return
|
|
84
|
+
if (termination.terminated) return
|
|
85
|
+
yield next.result.value
|
|
86
|
+
}
|
|
87
|
+
})()
|
|
39
88
|
try {
|
|
40
|
-
await
|
|
89
|
+
const result = await termination.wait(stream.sink(gatedSource))
|
|
90
|
+
if ('terminated' in result) {
|
|
91
|
+
if (result.error) throw result.error
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
if ('error' in result) throw result.error
|
|
41
95
|
await stream.closeWrite()
|
|
42
96
|
} catch (err) {
|
|
43
97
|
const error = err instanceof Error ? err : new Error(String(err))
|
|
44
|
-
stream.abort(error)
|
|
98
|
+
if (termination.terminate(error)) stream.abort(error)
|
|
45
99
|
throw error
|
|
100
|
+
} finally {
|
|
101
|
+
closeIterator(iterator)
|
|
46
102
|
}
|
|
47
103
|
},
|
|
48
104
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { TerminationGate } from './termination.js'
|
|
4
|
+
|
|
5
|
+
describe('TerminationGate', () => {
|
|
6
|
+
it('prioritizes termination over an already-ready result', async () => {
|
|
7
|
+
const gate = new TerminationGate()
|
|
8
|
+
const iterator = (async function* () {
|
|
9
|
+
yield 1
|
|
10
|
+
})()
|
|
11
|
+
const pending = gate.next(iterator)
|
|
12
|
+
const error = new Error('stopped')
|
|
13
|
+
|
|
14
|
+
gate.terminate(error)
|
|
15
|
+
|
|
16
|
+
await expect(pending).resolves.toEqual({ terminated: true, error })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('keeps the first termination result', async () => {
|
|
20
|
+
const gate = new TerminationGate()
|
|
21
|
+
const error = new Error('stopped')
|
|
22
|
+
|
|
23
|
+
expect(gate.terminate(error)).toBe(true)
|
|
24
|
+
expect(gate.terminate()).toBe(false)
|
|
25
|
+
await expect(gate.wait(new Promise<void>(() => {}))).resolves.toEqual({
|
|
26
|
+
terminated: true,
|
|
27
|
+
error,
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Source } from 'it-stream-types'
|
|
2
|
+
|
|
3
|
+
type Terminated = { terminated: true; error?: Error }
|
|
4
|
+
type Received<T> = { result: IteratorResult<T> } | { error: unknown }
|
|
5
|
+
type WaitResult<T> = { result: T } | { error: unknown }
|
|
6
|
+
|
|
7
|
+
// TerminationGate makes the first clean close or abort visible to blocked I/O.
|
|
8
|
+
export class TerminationGate {
|
|
9
|
+
private readonly _waiters = new Set<(error?: Error) => void>()
|
|
10
|
+
private _error: Error | undefined
|
|
11
|
+
private _terminated = false
|
|
12
|
+
|
|
13
|
+
public get terminated(): boolean {
|
|
14
|
+
return this._terminated
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public terminate(error?: Error): boolean {
|
|
18
|
+
if (this._terminated) return false
|
|
19
|
+
this._terminated = true
|
|
20
|
+
this._error = error
|
|
21
|
+
for (const waiter of this._waiters) waiter(error)
|
|
22
|
+
this._waiters.clear()
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async wait<T>(
|
|
27
|
+
promise: Promise<T>,
|
|
28
|
+
): Promise<WaitResult<T> | Terminated> {
|
|
29
|
+
if (this._terminated) return { terminated: true, error: this._error }
|
|
30
|
+
|
|
31
|
+
let notify!: (error?: Error) => void
|
|
32
|
+
const terminated = new Promise<Terminated>((resolve) => {
|
|
33
|
+
notify = (error) => resolve({ terminated: true, error })
|
|
34
|
+
this._waiters.add(notify)
|
|
35
|
+
})
|
|
36
|
+
const received: Promise<WaitResult<T>> = promise.then(
|
|
37
|
+
(result) => ({ result }),
|
|
38
|
+
(error: unknown) => ({ error }),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const result = await Promise.race([received, terminated])
|
|
43
|
+
if (this._terminated) {
|
|
44
|
+
return { terminated: true, error: this._error }
|
|
45
|
+
}
|
|
46
|
+
return result
|
|
47
|
+
} finally {
|
|
48
|
+
this._waiters.delete(notify)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public next<T>(
|
|
53
|
+
iterator: AsyncIterator<T>,
|
|
54
|
+
): Promise<Received<T> | Terminated> {
|
|
55
|
+
return this.wait(iterator.next())
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function sourceIterator<T>(source: Source<T>): AsyncIterator<T> {
|
|
60
|
+
if (Symbol.asyncIterator in source) return source[Symbol.asyncIterator]()
|
|
61
|
+
const iterator = source[Symbol.iterator]()
|
|
62
|
+
return { next: async () => iterator.next() }
|
|
63
|
+
}
|
|
64
|
+
// closeIterator asks an abandoned iterator to release its upstream resources.
|
|
65
|
+
// Async generators may wait for an active next call before running return;
|
|
66
|
+
// rejection is observed here so cleanup cannot create an unhandled promise.
|
|
67
|
+
export function closeIterator<T>(iterator: AsyncIterator<T>): void {
|
|
68
|
+
if (!iterator.return) return
|
|
69
|
+
void Promise.resolve(iterator.return()).catch(() => undefined)
|
|
70
|
+
}
|
package/srpc/testing.rs
CHANGED
|
@@ -13,22 +13,8 @@ use crate::error::{Error, Result};
|
|
|
13
13
|
use crate::rpc::PacketWriter;
|
|
14
14
|
use crate::transport::create_packet_channel;
|
|
15
15
|
|
|
16
|
-
/// Creates a pair of connected in-memory streams
|
|
17
|
-
///
|
|
18
|
-
/// Returns (client_stream, server_stream) that can be used for testing
|
|
19
|
-
/// client-server communication.
|
|
20
|
-
///
|
|
21
|
-
/// # Arguments
|
|
22
|
-
/// * `buffer_size` - Size of the internal buffer for each direction
|
|
23
|
-
///
|
|
24
|
-
/// # Example
|
|
25
|
-
///
|
|
26
|
-
/// ```ignore
|
|
27
|
-
/// let (client_stream, server_stream) = create_pipe(64 * 1024);
|
|
28
|
-
///
|
|
29
|
-
/// // Use client_stream with a client
|
|
30
|
-
/// // Use server_stream with a server
|
|
31
|
-
/// ```
|
|
16
|
+
/// Creates a pair of connected in-memory streams for testing client-server
|
|
17
|
+
/// communication, with `buffer_size` bytes of buffer in each direction.
|
|
32
18
|
pub fn create_pipe(buffer_size: usize) -> (DuplexStream, DuplexStream) {
|
|
33
19
|
duplex(buffer_size)
|
|
34
20
|
}
|
|
@@ -38,21 +24,8 @@ pub fn create_pipe_default() -> (DuplexStream, DuplexStream) {
|
|
|
38
24
|
create_pipe(64 * 1024)
|
|
39
25
|
}
|
|
40
26
|
|
|
41
|
-
/// In-memory stream opener for testing
|
|
42
|
-
///
|
|
43
|
-
/// This opener can hold multiple streams and returns them one at a time
|
|
44
|
-
/// on each call to `open_stream`.
|
|
45
|
-
///
|
|
46
|
-
/// # Example
|
|
47
|
-
///
|
|
48
|
-
/// ```ignore
|
|
49
|
-
/// let (client_stream, server_stream) = create_pipe_default();
|
|
50
|
-
///
|
|
51
|
-
/// let opener = InMemoryOpener::new(vec![client_stream]);
|
|
52
|
-
/// let client = SrpcClient::new(opener);
|
|
53
|
-
///
|
|
54
|
-
/// // The client will use client_stream for its RPC
|
|
55
|
-
/// ```
|
|
27
|
+
/// In-memory stream opener for testing that holds multiple streams and
|
|
28
|
+
/// returns them one at a time on each call to `open_stream`.
|
|
56
29
|
pub struct InMemoryOpener {
|
|
57
30
|
streams: Arc<Mutex<Vec<DuplexStream>>>,
|
|
58
31
|
}
|
|
@@ -108,40 +81,15 @@ impl SingleInMemoryOpener {
|
|
|
108
81
|
#[async_trait]
|
|
109
82
|
impl OpenStream for SingleInMemoryOpener {
|
|
110
83
|
async fn open_stream(&self) -> Result<(Arc<dyn PacketWriter>, PacketReceiver)> {
|
|
111
|
-
let stream = self
|
|
112
|
-
.stream
|
|
113
|
-
.lock()
|
|
114
|
-
.await
|
|
115
|
-
.take()
|
|
116
|
-
.ok_or(Error::StreamClosed)?;
|
|
84
|
+
let stream = self.stream.lock().await.take().ok_or(Error::StreamClosed)?;
|
|
117
85
|
|
|
118
86
|
let (read_half, write_half) = tokio::io::split(stream);
|
|
119
87
|
Ok(create_packet_channel(read_half, write_half))
|
|
120
88
|
}
|
|
121
89
|
}
|
|
122
90
|
|
|
123
|
-
/// Creates a connected client-server test setup
|
|
124
|
-
///
|
|
125
|
-
/// Returns (client_opener, server_stream) where:
|
|
126
|
-
/// - `client_opener` can be used to create a `SrpcClient`
|
|
127
|
-
/// - `server_stream` can be passed to `Server::handle_stream`
|
|
128
|
-
///
|
|
129
|
-
/// # Example
|
|
130
|
-
///
|
|
131
|
-
/// ```ignore
|
|
132
|
-
/// let (opener, server_stream) = create_test_pair();
|
|
133
|
-
///
|
|
134
|
-
/// let client = SrpcClient::new(opener);
|
|
135
|
-
/// let server = Server::new(mux);
|
|
136
|
-
///
|
|
137
|
-
/// // Spawn server
|
|
138
|
-
/// tokio::spawn(async move {
|
|
139
|
-
/// server.handle_stream(server_stream).await
|
|
140
|
-
/// });
|
|
141
|
-
///
|
|
142
|
-
/// // Use client
|
|
143
|
-
/// let response = client.exec_call("svc", "method", &request).await?;
|
|
144
|
-
/// ```
|
|
91
|
+
/// Creates a connected client-server test setup: an opener for a
|
|
92
|
+
/// `SrpcClient` and the server stream for `Server::handle_stream`.
|
|
145
93
|
pub fn create_test_pair() -> (SingleInMemoryOpener, DuplexStream) {
|
|
146
94
|
let (client_stream, server_stream) = create_pipe_default();
|
|
147
95
|
(SingleInMemoryOpener::new(client_stream), server_stream)
|
|
@@ -225,13 +173,10 @@ mod tests {
|
|
|
225
173
|
stream.close_send().await.unwrap();
|
|
226
174
|
|
|
227
175
|
// Read response with a timeout to handle potential races.
|
|
228
|
-
let response = tokio::time::timeout(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
.await
|
|
233
|
-
.expect("timeout")
|
|
234
|
-
.expect("recv_bytes failed");
|
|
176
|
+
let response = tokio::time::timeout(std::time::Duration::from_secs(5), stream.recv_bytes())
|
|
177
|
+
.await
|
|
178
|
+
.expect("timeout")
|
|
179
|
+
.expect("recv_bytes failed");
|
|
235
180
|
assert_eq!(&response[..], b"hello");
|
|
236
181
|
|
|
237
182
|
// Wait for server to complete.
|