starpc 0.3.2 → 0.3.5
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 +41 -31
- package/dist/srpc/client.d.ts +4 -2
- package/dist/srpc/client.js +36 -5
- package/dist/srpc/index.d.ts +0 -1
- package/dist/srpc/index.js +0 -1
- package/integration/integration.bash +3 -0
- package/integration/integration.ts +1 -1
- package/package.json +1 -1
- package/srpc/client.ts +37 -8
- package/srpc/index.ts +0 -1
- package/srpc/server-http.go +1 -1
- package/srpc/server-pipe.go +2 -2
- package/srpc/server.go +29 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Stream RPC
|
|
2
2
|
|
|
3
|
-
**starpc** implements [Proto3 services] in both TypeScript and Go.
|
|
3
|
+
**starpc** implements [Proto3 services] (server & client) in both TypeScript and Go.
|
|
4
4
|
|
|
5
5
|
[Proto3 services]: https://developers.google.com/protocol-buffers/docs/proto3#services
|
|
6
6
|
|
|
@@ -13,8 +13,6 @@ Can use any Stream multiplexer: defaults to [libp2p-mplex] over a WebSocket.
|
|
|
13
13
|
|
|
14
14
|
[libp2p-mplex]: https://github.com/libp2p/js-libp2p-mplex
|
|
15
15
|
|
|
16
|
-
Note: the server has not yet been implemented in TypeScript.
|
|
17
|
-
|
|
18
16
|
# Usage
|
|
19
17
|
|
|
20
18
|
Starting with the [protobuf-project] repository on the "starpc" branch.
|
|
@@ -30,6 +28,8 @@ The demo/boilerplate project implements the Echo example below.
|
|
|
30
28
|
|
|
31
29
|
[protobuf-project]: https://github.com/aperturerobotics/protobuf-project/tree/starpc
|
|
32
30
|
|
|
31
|
+
This repository uses protowrap, see the [Makefile](./Makefile).
|
|
32
|
+
|
|
33
33
|
## Protobuf
|
|
34
34
|
|
|
35
35
|
The following examples use the [echo](./echo/echo.proto) protobuf sample.
|
|
@@ -56,9 +56,9 @@ message EchoMsg {
|
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
## Go
|
|
59
|
+
## Go
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
This example demonstrates both the server and client:
|
|
62
62
|
|
|
63
63
|
```go
|
|
64
64
|
// construct the server
|
|
@@ -96,39 +96,58 @@ if out.GetBody() != bodyTxt {
|
|
|
96
96
|
|
|
97
97
|
See the ts-proto README to generate the TypeScript for your protobufs.
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
For an example of Go <-> TypeScript interop, see the [integration] test. For an
|
|
100
|
+
example of TypeScript <-> TypeScript interop, see the [e2e] test.
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
[e2e]: ./e2e/e2e.ts
|
|
103
|
+
[integration]: ./integration/integration.ts
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
Supports any AsyncIterable communication channel. `DuplexConn`,
|
|
106
|
+
`MessagePortConn`, and `WebSocketConn` use [js-libp2p-mplex] to multiplex
|
|
107
|
+
streams, but any multiplexer can be used.
|
|
107
108
|
|
|
108
109
|
[js-libp2p-mplex]: https://github.com/libp2p/js-libp2p-mplex
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
This example demonstrates both the server and client:
|
|
111
112
|
|
|
112
113
|
```typescript
|
|
113
|
-
import {
|
|
114
|
-
import {
|
|
114
|
+
import { pipe } from 'it-pipe'
|
|
115
|
+
import { createHandler, createMux, Server, Client, Conn } from 'srpc'
|
|
116
|
+
import { EchoerDefinition, EchoerServer, runClientTest } from 'srpc/echo'
|
|
115
117
|
|
|
116
118
|
const mux = createMux()
|
|
117
|
-
|
|
119
|
+
const echoer = new EchoerServer()
|
|
120
|
+
mux.register(createHandler(EchoerDefinition, echoer))
|
|
118
121
|
const server = new Server(mux)
|
|
119
122
|
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
const clientConn = new Conn()
|
|
124
|
+
const serverConn = new Conn(server)
|
|
125
|
+
pipe(clientConn, serverConn, clientConn)
|
|
126
|
+
const client = new Client(clientConn.buildOpenStreamFunc())
|
|
127
|
+
|
|
128
|
+
console.log('Calling Echo: unary call...')
|
|
129
|
+
let result = await demoServiceClient.Echo({
|
|
130
|
+
body: 'Hello world!',
|
|
131
|
+
})
|
|
132
|
+
console.log('success: output', result.body)
|
|
133
|
+
|
|
134
|
+
const clientRequestStream = new Observable<EchoMsg>(subscriber => {
|
|
135
|
+
subscriber.next({body: 'Hello world from streaming request.'})
|
|
136
|
+
subscriber.complete()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
console.log('Calling EchoClientStream: client -> server...')
|
|
140
|
+
result = await demoServiceClient.EchoClientStream(clientRequestStream)
|
|
141
|
+
console.log('success: output', result.body)
|
|
124
142
|
```
|
|
125
143
|
|
|
144
|
+
## WebSocket
|
|
126
145
|
|
|
127
|
-
|
|
146
|
+
One way to integrate Go and TypeScript is over a WebSocket:
|
|
128
147
|
|
|
129
148
|
```typescript
|
|
130
|
-
import { WebSocketConn } from '
|
|
131
|
-
import { EchoerClientImpl } from '
|
|
149
|
+
import { WebSocketConn } from 'srpc'
|
|
150
|
+
import { EchoerClientImpl } from 'srpc/echo'
|
|
132
151
|
|
|
133
152
|
const ws = new WebSocket('ws://localhost:5000/demo')
|
|
134
153
|
const channel = new WebSocketConn(ws)
|
|
@@ -139,15 +158,6 @@ const result = await demoServiceClient.Echo({
|
|
|
139
158
|
body: "Hello world!"
|
|
140
159
|
})
|
|
141
160
|
console.log('output', result.body)
|
|
142
|
-
|
|
143
|
-
const clientRequestStream = new Observable<EchoMsg>(subscriber => {
|
|
144
|
-
subscriber.next({body: 'Hello world from streaming request.'})
|
|
145
|
-
subscriber.complete()
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
console.log('Calling EchoClientStream: client -> server...')
|
|
149
|
-
result = await demoServiceClient.EchoClientStream(clientRequestStream)
|
|
150
|
-
console.log('success: output', result.body)
|
|
151
161
|
```
|
|
152
162
|
|
|
153
163
|
# Attribution
|
package/dist/srpc/client.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ import type { TsProtoRpc } from './ts-proto-rpc.js';
|
|
|
3
3
|
import type { OpenStreamFunc } from './stream.js';
|
|
4
4
|
export declare class Client implements TsProtoRpc {
|
|
5
5
|
private openConnFn;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
private _openConnFn?;
|
|
7
|
+
constructor(openConnFn?: OpenStreamFunc);
|
|
8
|
+
setOpenConnFn(openConnFn?: OpenStreamFunc): Promise<OpenStreamFunc>;
|
|
9
|
+
private initOpenConnFn;
|
|
8
10
|
request(service: string, method: string, data: Uint8Array): Promise<Uint8Array>;
|
|
9
11
|
clientStreamingRequest(service: string, method: string, data: Observable<Uint8Array>): Promise<Uint8Array>;
|
|
10
12
|
serverStreamingRequest(service: string, method: string, data: Uint8Array): Observable<Uint8Array>;
|
package/dist/srpc/client.js
CHANGED
|
@@ -19,15 +19,45 @@ function writeClientStream(call, data) {
|
|
|
19
19
|
}
|
|
20
20
|
// Client implements the ts-proto Rpc interface with the drpcproto protocol.
|
|
21
21
|
export class Client {
|
|
22
|
-
// openConnFn is
|
|
23
|
-
// called when starting RPC.
|
|
22
|
+
// openConnFn is a promise which contains the OpenStreamFunc.
|
|
24
23
|
openConnFn;
|
|
24
|
+
// _openConnFn resolves openConnFn.
|
|
25
|
+
_openConnFn;
|
|
25
26
|
constructor(openConnFn) {
|
|
26
|
-
this.openConnFn = openConnFn;
|
|
27
|
+
this.openConnFn = this.setOpenConnFn(openConnFn);
|
|
27
28
|
}
|
|
28
29
|
// setOpenConnFn updates the openConnFn for the Client.
|
|
29
30
|
setOpenConnFn(openConnFn) {
|
|
30
|
-
this.
|
|
31
|
+
if (this._openConnFn) {
|
|
32
|
+
if (openConnFn) {
|
|
33
|
+
this._openConnFn(openConnFn);
|
|
34
|
+
this._openConnFn = undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
if (openConnFn) {
|
|
39
|
+
this.openConnFn = Promise.resolve(openConnFn);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
this.initOpenConnFn();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return this.openConnFn;
|
|
46
|
+
}
|
|
47
|
+
// initOpenConnFn creates the empty Promise for openConnFn.
|
|
48
|
+
initOpenConnFn() {
|
|
49
|
+
const openPromise = new Promise((resolve, reject) => {
|
|
50
|
+
this._openConnFn = (conn, err) => {
|
|
51
|
+
if (err) {
|
|
52
|
+
reject(err);
|
|
53
|
+
}
|
|
54
|
+
else if (conn) {
|
|
55
|
+
resolve(conn);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
this.openConnFn = openPromise;
|
|
60
|
+
return this.openConnFn;
|
|
31
61
|
}
|
|
32
62
|
// request starts a non-streaming request.
|
|
33
63
|
async request(service, method, data) {
|
|
@@ -105,7 +135,8 @@ export class Client {
|
|
|
105
135
|
// throws any error starting the rpc call
|
|
106
136
|
// if data == null and data.length == 0, sends a separate data packet.
|
|
107
137
|
async startRpc(rpcService, rpcMethod, data) {
|
|
108
|
-
const
|
|
138
|
+
const openConnFn = await this.openConnFn;
|
|
139
|
+
const conn = await openConnFn();
|
|
109
140
|
const call = new ClientRPC(rpcService, rpcMethod);
|
|
110
141
|
pipe(conn, parseLengthPrefixTransform(), decodePacketSource, call, encodePacketSource, prependLengthPrefixTransform(), conn);
|
|
111
142
|
await call.writeCallStart(data || undefined);
|
package/dist/srpc/index.d.ts
CHANGED
|
@@ -4,6 +4,5 @@ export { Server } from './server.js';
|
|
|
4
4
|
export { Conn, ConnParams } from './conn.js';
|
|
5
5
|
export { Handler, InvokeFn, createHandler, createInvokeFn } from './handler.js';
|
|
6
6
|
export { Mux, createMux } from './mux.js';
|
|
7
|
-
export { WebSocketConn } from './websocket.js';
|
|
8
7
|
export { BroadcastChannelIterable, newBroadcastChannelIterable, BroadcastChannelConn, } from './broadcast-channel.js';
|
|
9
8
|
export { MessagePortIterable, newMessagePortIterable, MessagePortConn, } from './message-port.js';
|
package/dist/srpc/index.js
CHANGED
|
@@ -3,6 +3,5 @@ export { Server } from './server.js';
|
|
|
3
3
|
export { Conn } from './conn.js';
|
|
4
4
|
export { createHandler, createInvokeFn } from './handler.js';
|
|
5
5
|
export { createMux } from './mux.js';
|
|
6
|
-
export { WebSocketConn } from './websocket.js';
|
|
7
6
|
export { BroadcastChannelIterable, newBroadcastChannelIterable, BroadcastChannelConn, } from './broadcast-channel.js';
|
|
8
7
|
export { MessagePortIterable, newMessagePortIterable, MessagePortConn, } from './message-port.js';
|
package/package.json
CHANGED
package/srpc/client.ts
CHANGED
|
@@ -29,17 +29,45 @@ function writeClientStream(call: ClientRPC, data: Observable<Uint8Array>) {
|
|
|
29
29
|
|
|
30
30
|
// Client implements the ts-proto Rpc interface with the drpcproto protocol.
|
|
31
31
|
export class Client implements TsProtoRpc {
|
|
32
|
-
// openConnFn is
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
// openConnFn is a promise which contains the OpenStreamFunc.
|
|
33
|
+
private openConnFn: Promise<OpenStreamFunc>
|
|
34
|
+
// _openConnFn resolves openConnFn.
|
|
35
|
+
private _openConnFn?: (conn?: OpenStreamFunc, err?: Error) => void
|
|
35
36
|
|
|
36
|
-
constructor(openConnFn
|
|
37
|
-
this.openConnFn = openConnFn
|
|
37
|
+
constructor(openConnFn?: OpenStreamFunc) {
|
|
38
|
+
this.openConnFn = this.setOpenConnFn(openConnFn)
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
// setOpenConnFn updates the openConnFn for the Client.
|
|
41
|
-
public setOpenConnFn(openConnFn
|
|
42
|
-
this.
|
|
42
|
+
public setOpenConnFn(openConnFn?: OpenStreamFunc): Promise<OpenStreamFunc> {
|
|
43
|
+
if (this._openConnFn) {
|
|
44
|
+
if (openConnFn) {
|
|
45
|
+
this._openConnFn(openConnFn)
|
|
46
|
+
this._openConnFn = undefined
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
if (openConnFn) {
|
|
50
|
+
this.openConnFn = Promise.resolve(openConnFn)
|
|
51
|
+
} else {
|
|
52
|
+
this.initOpenConnFn()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return this.openConnFn
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// initOpenConnFn creates the empty Promise for openConnFn.
|
|
59
|
+
private initOpenConnFn(): Promise<OpenStreamFunc> {
|
|
60
|
+
const openPromise = new Promise<OpenStreamFunc>((resolve, reject) => {
|
|
61
|
+
this._openConnFn = (conn?: OpenStreamFunc, err?: Error) => {
|
|
62
|
+
if (err) {
|
|
63
|
+
reject(err)
|
|
64
|
+
} else if (conn) {
|
|
65
|
+
resolve(conn)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
this.openConnFn = openPromise
|
|
70
|
+
return this.openConnFn
|
|
43
71
|
}
|
|
44
72
|
|
|
45
73
|
// request starts a non-streaming request.
|
|
@@ -140,7 +168,8 @@ export class Client implements TsProtoRpc {
|
|
|
140
168
|
rpcMethod: string,
|
|
141
169
|
data: Uint8Array | null
|
|
142
170
|
): Promise<ClientRPC> {
|
|
143
|
-
const
|
|
171
|
+
const openConnFn = await this.openConnFn
|
|
172
|
+
const conn = await openConnFn()
|
|
144
173
|
const call = new ClientRPC(rpcService, rpcMethod)
|
|
145
174
|
pipe(
|
|
146
175
|
conn,
|
package/srpc/index.ts
CHANGED
|
@@ -4,7 +4,6 @@ export { Server } from './server.js'
|
|
|
4
4
|
export { Conn, ConnParams } from './conn.js'
|
|
5
5
|
export { Handler, InvokeFn, createHandler, createInvokeFn } from './handler.js'
|
|
6
6
|
export { Mux, createMux } from './mux.js'
|
|
7
|
-
export { WebSocketConn } from './websocket.js'
|
|
8
7
|
export {
|
|
9
8
|
BroadcastChannelIterable,
|
|
10
9
|
newBroadcastChannelIterable,
|
package/srpc/server-http.go
CHANGED
|
@@ -57,7 +57,7 @@ func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
57
57
|
return
|
|
58
58
|
}
|
|
59
59
|
go func() {
|
|
60
|
-
err := s.srpc.
|
|
60
|
+
err := s.srpc.HandleStream(ctx, strm)
|
|
61
61
|
_ = err
|
|
62
62
|
// TODO: handle / log error?
|
|
63
63
|
// err != nil && err != io.EOF && err != context.Canceled
|
package/srpc/server-pipe.go
CHANGED
|
@@ -7,12 +7,12 @@ import (
|
|
|
7
7
|
|
|
8
8
|
// NewServerPipe constructs a open stream func which creates an in-memory Pipe
|
|
9
9
|
// Stream with the given Server. Starts read pumps for both. Starts the
|
|
10
|
-
//
|
|
10
|
+
// HandleStream function on the server in a separate goroutine.
|
|
11
11
|
func NewServerPipe(server *Server) OpenStreamFunc {
|
|
12
12
|
return func(ctx context.Context, msgHandler func(pkt *Packet) error) (Writer, error) {
|
|
13
13
|
srvPipe, clientPipe := net.Pipe()
|
|
14
14
|
go func() {
|
|
15
|
-
_ = server.
|
|
15
|
+
_ = server.HandleStream(ctx, srvPipe)
|
|
16
16
|
}()
|
|
17
17
|
clientPrw := NewPacketReadWriter(clientPipe, msgHandler)
|
|
18
18
|
go func() {
|
package/srpc/server.go
CHANGED
|
@@ -3,6 +3,8 @@ package srpc
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
"io"
|
|
6
|
+
|
|
7
|
+
"github.com/libp2p/go-libp2p-core/network"
|
|
6
8
|
)
|
|
7
9
|
|
|
8
10
|
// Server handles incoming RPC streams with a mux.
|
|
@@ -18,8 +20,8 @@ func NewServer(mux Mux) *Server {
|
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
//
|
|
22
|
-
func (s *Server)
|
|
23
|
+
// HandleStream handles an incoming ReadWriteCloser stream.
|
|
24
|
+
func (s *Server) HandleStream(ctx context.Context, rwc io.ReadWriteCloser) error {
|
|
23
25
|
subCtx, subCtxCancel := context.WithCancel(ctx)
|
|
24
26
|
defer subCtxCancel()
|
|
25
27
|
serverRPC := NewServerRPC(subCtx, s.mux)
|
|
@@ -27,3 +29,28 @@ func (s *Server) HandleConn(ctx context.Context, rwc io.ReadWriteCloser) error {
|
|
|
27
29
|
serverRPC.SetWriter(prw)
|
|
28
30
|
return prw.ReadPump()
|
|
29
31
|
}
|
|
32
|
+
|
|
33
|
+
// AcceptMuxedConn runs a loop which calls Accept on a muxer to handle streams.
|
|
34
|
+
//
|
|
35
|
+
// Starts HandleStream in a separate goroutine to handle the stream.
|
|
36
|
+
// Returns context.Canceled or io.EOF when the loop is complete / closed.
|
|
37
|
+
func (s *Server) AcceptMuxedConn(ctx context.Context, mplex network.MuxedConn) error {
|
|
38
|
+
for {
|
|
39
|
+
select {
|
|
40
|
+
case <-ctx.Done():
|
|
41
|
+
return context.Canceled
|
|
42
|
+
default:
|
|
43
|
+
if mplex.IsClosed() {
|
|
44
|
+
return io.EOF
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
muxedStream, err := mplex.AcceptStream()
|
|
49
|
+
if err != nil {
|
|
50
|
+
return err
|
|
51
|
+
}
|
|
52
|
+
go func() {
|
|
53
|
+
_ = s.HandleStream(ctx, muxedStream)
|
|
54
|
+
}()
|
|
55
|
+
}
|
|
56
|
+
}
|