starpc 0.3.3 → 0.3.6

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 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: Server & Client
59
+ ## Go
60
60
 
61
- A basic example can be found in the [e2e test]:
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
- Also check out the [integration](./integration/integration.ts) test.
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
- Supports any AsyncIterable communication channel with an included implementation
102
- for WebSockets.
102
+ [e2e]: ./e2e/e2e.ts
103
+ [integration]: ./integration/integration.ts
103
104
 
104
- This repository uses protowrap, see the [Makefile](./Makefile).
105
-
106
- `WebSocketConn` uses [js-libp2p-mplex] to multiplex streams over the WebSocket.
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
- ### Server
111
+ This example demonstrates both the server and client:
111
112
 
112
113
  ```typescript
113
- import { WebSocketConn, Server, createMux } from '../srpc'
114
- import { EchoerClientImpl } from '../echo/echo'
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
- mux.register(TODO)
119
+ const echoer = new EchoerServer()
120
+ mux.register(createHandler(EchoerDefinition, echoer))
118
121
  const server = new Server(mux)
119
122
 
120
- // TODO: accept a WebSocket-like object.
121
- const ws = TODO
122
- const channel = new WebSocketConn(ws, server)
123
- // incoming streams will be handled by server.
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
- ### Client
146
+ One way to integrate Go and TypeScript is over a WebSocket:
128
147
 
129
148
  ```typescript
130
- import { WebSocketConn } from '../srpc'
131
- import { EchoerClientImpl } from '../echo/echo'
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/echo/echo.js CHANGED
@@ -47,7 +47,6 @@ export const EchoMsg = {
47
47
  },
48
48
  };
49
49
  export class EchoerClientImpl {
50
- rpc;
51
50
  constructor(rpc) {
52
51
  this.rpc = rpc;
53
52
  this.Echo = this.Echo.bind(this);
@@ -2,14 +2,6 @@ import { EventIterator } from 'event-iterator';
2
2
  import { DuplexConn } from './conn-duplex.js';
3
3
  // BroadcastChannelIterable is a AsyncIterable wrapper for BroadcastChannel.
4
4
  export class BroadcastChannelIterable {
5
- // readChannel is the incoming broadcast channel
6
- readChannel;
7
- // writeChannel is the outgoing broadcast channel
8
- writeChannel;
9
- // sink is the sink for incoming messages.
10
- sink;
11
- // source is the source for outgoing messages.
12
- source;
13
5
  constructor(readChannel, writeChannel) {
14
6
  this.readChannel = readChannel;
15
7
  this.writeChannel = writeChannel;
@@ -52,8 +44,6 @@ export function newBroadcastChannelIterable(readName, writeName) {
52
44
  //
53
45
  // expects Uint8Array objects over the BroadcastChannel.
54
46
  export class BroadcastChannelConn extends DuplexConn {
55
- // broadcastChannel is the broadcast channel iterable
56
- broadcastChannel;
57
47
  constructor(readChannel, writeChannel, server, connParams) {
58
48
  const broadcastChannel = new BroadcastChannelIterable(readChannel, writeChannel);
59
49
  super(broadcastChannel, server, connParams);
@@ -19,10 +19,6 @@ 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 a promise which contains the OpenStreamFunc.
23
- openConnFn;
24
- // _openConnFn resolves openConnFn.
25
- _openConnFn;
26
22
  constructor(openConnFn) {
27
23
  this.openConnFn = this.setOpenConnFn(openConnFn);
28
24
  }
@@ -84,7 +80,7 @@ export class Client {
84
80
  }
85
81
  // serverStreamingRequest starts a server-side streaming request.
86
82
  serverStreamingRequest(service, method, data) {
87
- const pushServerData = pushable();
83
+ const pushServerData = pushable({ objectMode: true });
88
84
  const serverData = observableFrom(pushServerData);
89
85
  this.startRpc(service, method, data)
90
86
  .then(async (call) => {
@@ -103,7 +99,7 @@ export class Client {
103
99
  }
104
100
  // bidirectionalStreamingRequest starts a two-way streaming request.
105
101
  bidirectionalStreamingRequest(service, method, data) {
106
- const pushServerData = pushable();
102
+ const pushServerData = pushable({ objectMode: true });
107
103
  const serverData = observableFrom(pushServerData);
108
104
  this.startRpc(service, method, null)
109
105
  .then(async (call) => {
@@ -2,20 +2,6 @@ import { pushable } from 'it-pushable';
2
2
  import { Packet } from './rpcproto.js';
3
3
  // CommonRPC is common logic between server and client RPCs.
4
4
  export class CommonRPC {
5
- // sink is the data sink for incoming messages.
6
- sink;
7
- // source is the packet source for outgoing Packets.
8
- source;
9
- // _source is used to write to the source.
10
- _source;
11
- // rpcDataSource emits incoming client RPC messages to the caller.
12
- rpcDataSource;
13
- // _rpcDataSource is used to write to the rpc message source.
14
- _rpcDataSource;
15
- // service is the rpc service
16
- service;
17
- // method is the rpc method
18
- method;
19
5
  constructor() {
20
6
  this.sink = this._createSink();
21
7
  const sourcev = this._createSource();
@@ -4,12 +4,10 @@ import { Conn } from './conn.js';
4
4
  //
5
5
  // expects Uint8Array objects
6
6
  export class DuplexConn extends Conn {
7
- // channel is the iterable
8
- channel;
9
7
  constructor(duplex, server, connParams) {
10
8
  super(server, connParams);
11
9
  this.channel = duplex;
12
- pipe(this, this.channel, this);
10
+ pipe(this.channel, this, this.channel);
13
11
  }
14
12
  // getChannelDuplex returns the Duplex channel.
15
13
  getChannelDuplex() {
package/dist/srpc/conn.js CHANGED
@@ -5,10 +5,6 @@ import { Client } from './client.js';
5
5
  // Implements the server by handling incoming streams.
6
6
  // If the server is unset, rejects any incoming streams.
7
7
  export class Conn {
8
- // muxer is the mplex stream muxer.
9
- muxer;
10
- // server is the server side, if set.
11
- server;
12
8
  constructor(server, connParams) {
13
9
  if (server) {
14
10
  this.server = server;
@@ -4,10 +4,6 @@ import { from as observableFrom } from 'rxjs';
4
4
  import { buildDecodeMessageTransform, buildEncodeMessageTransform, } from './message.js';
5
5
  // StaticHandler is a handler with a definition and implementation.
6
6
  export class StaticHandler {
7
- // service is the service id
8
- service;
9
- // methods is the map of method to invoke fn
10
- methods;
11
7
  constructor(serviceID, methods) {
12
8
  this.service = serviceID;
13
9
  this.methods = methods;
@@ -38,23 +34,27 @@ export function createInvokeFn(methodInfo, methodProto) {
38
34
  objectMode: true,
39
35
  });
40
36
  // pipe responseSink to dataSink.
41
- const responsePipe = pipe(responseSink, buildEncodeMessageTransform(methodInfo.responseType), dataSink);
37
+ pipe(responseSink, buildEncodeMessageTransform(methodInfo.responseType), dataSink);
38
+ // requestSource is a Source of decoded request messages.
39
+ const requestSource = pipe(dataSource, requestDecode);
42
40
  // build the request argument.
43
41
  let requestArg;
44
42
  if (methodInfo.requestStream) {
45
- // requestSource is a Source of decoded request messages.
46
- const requestSource = pipe(dataSource, requestDecode);
47
43
  // convert the request data source into an Observable<T>
48
44
  requestArg = observableFrom(requestSource);
49
45
  }
50
46
  else {
51
47
  // receive a single message for the argument.
52
- const requestRx = requestDecode(dataSource);
53
- for await (const msg of requestRx) {
54
- requestArg = msg;
55
- break;
48
+ for await (const msg of requestSource) {
49
+ if (msg) {
50
+ requestArg = msg;
51
+ break;
52
+ }
56
53
  }
57
54
  }
55
+ if (!requestArg) {
56
+ throw new Error('request object was empty');
57
+ }
58
58
  // Call the implementation.
59
59
  try {
60
60
  const responseObj = methodProto(requestArg);
@@ -77,9 +77,7 @@ export function createInvokeFn(methodInfo, methodProto) {
77
77
  },
78
78
  complete: () => {
79
79
  responseSink.end();
80
- responsePipe.finally(() => {
81
- resolve();
82
- });
80
+ resolve();
83
81
  },
84
82
  });
85
83
  });
@@ -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';
@@ -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';
@@ -2,14 +2,6 @@ import { EventIterator } from 'event-iterator';
2
2
  import { DuplexConn } from './conn-duplex.js';
3
3
  // MessagePortIterable is a AsyncIterable wrapper for MessagePort.
4
4
  export class MessagePortIterable {
5
- // port is the message port
6
- port;
7
- // sink is the sink for incoming messages.
8
- sink;
9
- // source is the source for outgoing messages.
10
- source;
11
- // _source is the EventIterator for source.
12
- _source;
13
5
  constructor(port) {
14
6
  this.port = port;
15
7
  this.sink = this._createSink();
@@ -51,8 +43,6 @@ export function newMessagePortIterable(port) {
51
43
  //
52
44
  // expects Uint8Array objects over the MessagePort.
53
45
  export class MessagePortConn extends DuplexConn {
54
- // messagePort is the message port iterable.
55
- messagePort;
56
46
  constructor(port, server, connParams) {
57
47
  const messagePort = new MessagePortIterable(port);
58
48
  super(messagePort, server, connParams);
package/dist/srpc/mux.js CHANGED
@@ -5,8 +5,10 @@ export function createMux() {
5
5
  // StaticMux contains a in-memory mapping between service ID and handlers.
6
6
  // implements Mux
7
7
  export class StaticMux {
8
- // services contains a mapping from service id to handlers.
9
- services = {};
8
+ constructor() {
9
+ // services contains a mapping from service id to handlers.
10
+ this.services = {};
11
+ }
10
12
  register(handler) {
11
13
  const serviceID = handler?.getServiceID();
12
14
  if (!serviceID) {
@@ -1,8 +1,6 @@
1
1
  import { CommonRPC } from './common-rpc.js';
2
2
  // ServerRPC is an ongoing RPC from the server side.
3
3
  export class ServerRPC extends CommonRPC {
4
- // mux is used to handle incoming rpcs.
5
- mux;
6
4
  constructor(mux) {
7
5
  super();
8
6
  this.mux = mux;
@@ -3,8 +3,6 @@ import { ServerRPC } from './server-rpc.js';
3
3
  import { parseLengthPrefixTransform, prependLengthPrefixTransform, decodePacketSource, encodePacketSource, } from './packet.js';
4
4
  // Server implements the SRPC server in TypeScript with a Mux.
5
5
  export class Server {
6
- // mux is the mux used to handle requests.
7
- mux;
8
6
  constructor(mux) {
9
7
  this.mux = mux;
10
8
  }
@@ -4,15 +4,13 @@ import { Mplex } from '@libp2p/mplex';
4
4
  import { Conn } from './conn.js';
5
5
  // WebSocketConn implements a connection with a WebSocket and optional Server.
6
6
  export class WebSocketConn extends Conn {
7
- // socket is the web socket
8
- socket;
9
7
  constructor(socket, server) {
10
8
  super(server, {
11
9
  muxerFactory: new Mplex(),
12
10
  });
13
11
  this.socket = socket;
14
12
  const socketDuplex = duplex(socket);
15
- pipe(this.source, socketDuplex, this.sink);
13
+ pipe(socketDuplex, this, socketDuplex);
16
14
  }
17
15
  // getSocket returns the websocket.
18
16
  getSocket() {
package/e2e/debug.test ADDED
Binary file
package/e2e/e2e_test.go CHANGED
@@ -3,10 +3,13 @@ package e2e
3
3
  import (
4
4
  "context"
5
5
  "io"
6
+ "net"
6
7
  "testing"
7
8
 
8
9
  "github.com/aperturerobotics/starpc/echo"
9
10
  "github.com/aperturerobotics/starpc/srpc"
11
+ "github.com/libp2p/go-libp2p/p2p/muxer/mplex"
12
+ mp "github.com/libp2p/go-mplex"
10
13
  "github.com/pkg/errors"
11
14
  )
12
15
 
@@ -20,9 +23,24 @@ func RunE2E(t *testing.T, cb func(client echo.SRPCEchoerClient) error) {
20
23
  }
21
24
  server := srpc.NewServer(mux)
22
25
 
26
+ // note: also possible to do without mplex:
27
+ // openStream := srpc.NewServerPipe(server)
28
+ // client := srpc.NewClient(openStream)
29
+
23
30
  // construct the client
24
- openStream := srpc.NewServerPipe(server)
25
- client := srpc.NewClient(openStream)
31
+ clientPipe, serverPipe := net.Pipe()
32
+
33
+ ctx := context.Background()
34
+ go func() {
35
+ serverMp, _ := mp.NewMultiplex(serverPipe, false, nil)
36
+ _ = server.AcceptMuxedConn(ctx, mplex.NewMuxedConn(serverMp))
37
+ }()
38
+
39
+ clientMp, err := mp.NewMultiplex(clientPipe, true, nil)
40
+ if err != nil {
41
+ t.Fatal(err.Error())
42
+ }
43
+ client := srpc.NewClientWithMuxedConn(mplex.NewMuxedConn(clientMp))
26
44
 
27
45
  // construct the client rpc interface
28
46
  clientEcho := echo.NewSRPCEchoerClient(client)
package/go.mod CHANGED
@@ -9,8 +9,9 @@ require (
9
9
  )
10
10
 
11
11
  require (
12
- github.com/libp2p/go-libp2p v0.20.1
12
+ github.com/libp2p/go-libp2p v0.20.1-0.20220622205512-3cf611ad8c9c
13
13
  github.com/libp2p/go-libp2p-core v0.16.1
14
+ github.com/libp2p/go-mplex v0.7.0
14
15
  github.com/sirupsen/logrus v1.8.2-0.20220112234510-85981c045988
15
16
  )
16
17
 
@@ -25,7 +26,6 @@ require (
25
26
  github.com/klauspost/compress v1.15.1 // indirect
26
27
  github.com/klauspost/cpuid/v2 v2.0.12 // indirect
27
28
  github.com/libp2p/go-buffer-pool v0.0.2 // indirect
28
- github.com/libp2p/go-mplex v0.7.0 // indirect
29
29
  github.com/libp2p/go-openssl v0.0.7 // indirect
30
30
  github.com/mattn/go-isatty v0.0.14 // indirect
31
31
  github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect
package/go.sum CHANGED
@@ -65,8 +65,8 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
65
65
  github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
66
66
  github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs=
67
67
  github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM=
68
- github.com/libp2p/go-libp2p v0.20.1 h1:tCgC8yXtleyOg/mp+ZoCcA+aryAhueCfFmAVXURT/PM=
69
- github.com/libp2p/go-libp2p v0.20.1/go.mod h1:XgJHsOhEBVBXp/2Sj9bm/yEyD94uunAaP6oaegdcKks=
68
+ github.com/libp2p/go-libp2p v0.20.1-0.20220622205512-3cf611ad8c9c h1:BTR12zXVGQtdk9bT5GLOmdzJrhyFO6YUH3/e8lnkjQ0=
69
+ github.com/libp2p/go-libp2p v0.20.1-0.20220622205512-3cf611ad8c9c/go.mod h1:tqyre7jPbLfhzjQfpiFX6x7wj/sqt1074riPM0Xn/qI=
70
70
  github.com/libp2p/go-libp2p-core v0.16.1 h1:bWoiEBqVkpJ13hbv/f69tHODp86t6mvc4fBN4DkK73M=
71
71
  github.com/libp2p/go-libp2p-core v0.16.1/go.mod h1:O3i/7y+LqUb0N+qhzXjBjjpchgptWAVMG1Voegk7b4c=
72
72
  github.com/libp2p/go-libp2p-testing v0.9.2 h1:dCpODRtRaDZKF8HXT9qqqgON+OMEB423Knrgeod8j84=
@@ -1,6 +1,9 @@
1
1
  #!/bin/bash
2
2
  set -eo pipefail
3
3
 
4
+ unset GOOS
5
+ unset GOARCH
6
+
4
7
  echo "Compiling ts..."
5
8
  # ../node_modules/.bin/tsc --out integration.js --project tsconfig.json
6
9
  ../node_modules/.bin/esbuild integration.ts --bundle --platform=node --outfile=integration.js
@@ -1,4 +1,4 @@
1
- import { WebSocketConn } from '../srpc'
1
+ import { WebSocketConn } from '../srpc/websocket'
2
2
  import { runClientTest } from '../echo'
3
3
  import WebSocket from 'isomorphic-ws'
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starpc",
3
- "version": "0.3.3",
3
+ "version": "0.3.6",
4
4
  "description": "Streaming protobuf RPC service protocol over any two-way channel.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -46,9 +46,8 @@ func NewClientRPC(ctx context.Context, service, method string) *ClientRPC {
46
46
  }
47
47
 
48
48
  // Start sets the writer and writes the MsgSend message.
49
- // firstMsg can be nil, but is usually set if this is a non-streaming rpc.
50
49
  // must only be called once!
51
- func (r *ClientRPC) Start(writer Writer, firstMsg []byte) error {
50
+ func (r *ClientRPC) Start(writer Writer, writeFirstMsg bool, firstMsg []byte) error {
52
51
  select {
53
52
  case <-r.ctx.Done():
54
53
  r.Close()
@@ -56,7 +55,13 @@ func (r *ClientRPC) Start(writer Writer, firstMsg []byte) error {
56
55
  default:
57
56
  }
58
57
  r.writer = writer
59
- pkt := NewCallStartPacket(r.service, r.method, firstMsg)
58
+ var firstMsgEmpty bool
59
+ if writeFirstMsg {
60
+ firstMsgEmpty = len(firstMsg) == 0
61
+ } else {
62
+ firstMsg = nil
63
+ }
64
+ pkt := NewCallStartPacket(r.service, r.method, firstMsg, firstMsgEmpty)
60
65
  if err := writer.WritePacket(pkt); err != nil {
61
66
  r.Close()
62
67
  return err
package/srpc/client.go CHANGED
@@ -48,7 +48,7 @@ func (c *client) Invoke(rctx context.Context, service, method string, in, out Me
48
48
  if err != nil {
49
49
  return err
50
50
  }
51
- if err := clientRPC.Start(writer, firstMsg); err != nil {
51
+ if err := clientRPC.Start(writer, true, firstMsg); err != nil {
52
52
  return err
53
53
  }
54
54
  msgs, err := clientRPC.ReadAll()
@@ -85,7 +85,7 @@ func (c *client) NewStream(ctx context.Context, service, method string, firstMsg
85
85
  if err != nil {
86
86
  return nil, err
87
87
  }
88
- if err := clientRPC.Start(writer, firstMsgData); err != nil {
88
+ if err := clientRPC.Start(writer, firstMsg != nil, firstMsgData); err != nil {
89
89
  return nil, err
90
90
  }
91
91
 
package/srpc/client.ts CHANGED
@@ -109,7 +109,7 @@ export class Client implements TsProtoRpc {
109
109
  method: string,
110
110
  data: Uint8Array
111
111
  ): Observable<Uint8Array> {
112
- const pushServerData: Pushable<Uint8Array> = pushable()
112
+ const pushServerData: Pushable<Uint8Array> = pushable({ objectMode: true })
113
113
  const serverData = observableFrom(pushServerData)
114
114
  this.startRpc(service, method, data)
115
115
  .then(async (call) => {
@@ -132,7 +132,7 @@ export class Client implements TsProtoRpc {
132
132
  method: string,
133
133
  data: Observable<Uint8Array>
134
134
  ): Observable<Uint8Array> {
135
- const pushServerData: Pushable<Uint8Array> = pushable()
135
+ const pushServerData: Pushable<Uint8Array> = pushable({ objectMode: true })
136
136
  const serverData = observableFrom(pushServerData)
137
137
  this.startRpc(service, method, null)
138
138
  .then(async (call) => {
@@ -18,7 +18,7 @@ export class DuplexConn extends Conn {
18
18
  ) {
19
19
  super(server, connParams)
20
20
  this.channel = duplex
21
- pipe(this, this.channel, this)
21
+ pipe(this.channel, this, this.channel)
22
22
  }
23
23
 
24
24
  // getChannelDuplex returns the Duplex channel.
package/srpc/handler.ts CHANGED
@@ -84,28 +84,34 @@ export function createInvokeFn(
84
84
  })
85
85
 
86
86
  // pipe responseSink to dataSink.
87
- const responsePipe = pipe(
87
+ pipe(
88
88
  responseSink,
89
89
  buildEncodeMessageTransform(methodInfo.responseType),
90
90
  dataSink
91
91
  )
92
92
 
93
+ // requestSource is a Source of decoded request messages.
94
+ const requestSource = pipe(dataSource, requestDecode)
95
+
93
96
  // build the request argument.
94
97
  let requestArg: any
95
98
  if (methodInfo.requestStream) {
96
- // requestSource is a Source of decoded request messages.
97
- const requestSource = pipe(dataSource, requestDecode)
98
99
  // convert the request data source into an Observable<T>
99
100
  requestArg = observableFrom(requestSource)
100
101
  } else {
101
102
  // receive a single message for the argument.
102
- const requestRx = requestDecode(dataSource)
103
- for await (const msg of requestRx) {
104
- requestArg = msg
105
- break
103
+ for await (const msg of requestSource) {
104
+ if (msg) {
105
+ requestArg = msg
106
+ break
107
+ }
106
108
  }
107
109
  }
108
110
 
111
+ if (!requestArg) {
112
+ throw new Error('request object was empty')
113
+ }
114
+
109
115
  // Call the implementation.
110
116
  try {
111
117
  const responseObj = methodProto(requestArg)
@@ -128,9 +134,7 @@ export function createInvokeFn(
128
134
  },
129
135
  complete: () => {
130
136
  responseSink.end()
131
- responsePipe.finally(() => {
132
- resolve()
133
- })
137
+ resolve()
134
138
  },
135
139
  })
136
140
  })
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,
@@ -0,0 +1,31 @@
1
+ package srpc
2
+
3
+ import (
4
+ "context"
5
+
6
+ "github.com/libp2p/go-libp2p-core/network"
7
+ )
8
+
9
+ // NewClientWithMuxedConn constructs a new client with a MuxedConn.
10
+ func NewClientWithMuxedConn(conn network.MuxedConn) Client {
11
+ openStreamFn := NewOpenStreamWithMuxedConn(conn)
12
+ return NewClient(openStreamFn)
13
+ }
14
+
15
+ // NewOpenStreamWithMuxedConn constructs a OpenStream func with a MuxedConn.
16
+ func NewOpenStreamWithMuxedConn(conn network.MuxedConn) OpenStreamFunc {
17
+ return func(ctx context.Context, msgHandler func(pkt *Packet) error) (Writer, error) {
18
+ mstrm, err := conn.OpenStream(ctx)
19
+ if err != nil {
20
+ return nil, err
21
+ }
22
+ rw := NewPacketReadWriter(mstrm, msgHandler)
23
+ go func() {
24
+ err := rw.ReadPump()
25
+ if err != nil {
26
+ _ = rw.Close()
27
+ }
28
+ }()
29
+ return rw, nil
30
+ }
31
+ }
package/srpc/packet.go CHANGED
@@ -16,12 +16,13 @@ func (p *Packet) Validate() error {
16
16
  }
17
17
 
18
18
  // NewCallStartPacket constructs a new CallStart packet.
19
- func NewCallStartPacket(service, method string, data []byte) *Packet {
19
+ func NewCallStartPacket(service, method string, data []byte, dataIsZero bool) *Packet {
20
20
  return &Packet{Body: &Packet_CallStart{
21
21
  CallStart: &CallStart{
22
22
  RpcService: service,
23
23
  RpcMethod: method,
24
24
  Data: data,
25
+ DataIsZero: dataIsZero,
25
26
  },
26
27
  }}
27
28
  }
@@ -40,7 +41,7 @@ func (p *CallStart) Validate() error {
40
41
  }
41
42
 
42
43
  // NewCallDataPacket constructs a new CallData packet.
43
- func NewCallDataPacket(data []byte, complete bool, err error) *Packet {
44
+ func NewCallDataPacket(data []byte, dataIsZero bool, complete bool, err error) *Packet {
44
45
  var errStr string
45
46
  if err != nil {
46
47
  errStr = err.Error()
@@ -48,7 +49,7 @@ func NewCallDataPacket(data []byte, complete bool, err error) *Packet {
48
49
  return &Packet{Body: &Packet_CallData{
49
50
  CallData: &CallData{
50
51
  Data: data,
51
- DataIsZero: len(data) == 0 && !complete && err == nil,
52
+ DataIsZero: dataIsZero,
52
53
  Complete: err != nil || complete,
53
54
  Error: errStr,
54
55
  },
@@ -42,7 +42,7 @@ func (r *RPCStream) MsgSend(msg Message) error {
42
42
  if err != nil {
43
43
  return err
44
44
  }
45
- outPkt := NewCallDataPacket(msgData, false, nil)
45
+ outPkt := NewCallDataPacket(msgData, len(msgData) == 0, false, nil)
46
46
  return r.writer.WritePacket(outPkt)
47
47
  }
48
48
 
@@ -62,7 +62,7 @@ func (r *RPCStream) MsgRecv(msg Message) error {
62
62
 
63
63
  // CloseSend signals to the remote that we will no longer send any messages.
64
64
  func (r *RPCStream) CloseSend() error {
65
- outPkt := NewCallDataPacket(nil, true, nil)
65
+ outPkt := NewCallDataPacket(nil, false, true, nil)
66
66
  return r.writer.WritePacket(outPkt)
67
67
  }
68
68
 
@@ -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.HandleConn(ctx, strm)
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
@@ -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
- // HandleConn function on the server in a separate goroutine.
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.HandleConn(ctx, srvPipe)
15
+ _ = server.HandleStream(ctx, srvPipe)
16
16
  }()
17
17
  clientPrw := NewPacketReadWriter(clientPipe, msgHandler)
18
18
  go func() {
@@ -138,7 +138,7 @@ func (r *ServerRPC) invokeRPC() {
138
138
  if err == nil && !ok {
139
139
  err = ErrUnimplemented
140
140
  }
141
- outPkt := NewCallDataPacket(nil, true, err)
141
+ outPkt := NewCallDataPacket(nil, false, true, err)
142
142
  _ = r.writer.WritePacket(outPkt)
143
143
  r.ctxCancel()
144
144
  _ = r.writer.Close()
@@ -149,7 +149,7 @@ func (r *ServerRPC) invokeRPC() {
149
149
  func (r *ServerRPC) Close() {
150
150
  r.ctxCancel()
151
151
  if r.service == "" {
152
- // invokeRPC has not been called
152
+ // invokeRPC has not been called, otherwise it would call Close()
153
153
  _ = r.writer.Close()
154
154
  }
155
155
  }
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,12 +20,39 @@ func NewServer(mux Mux) *Server {
18
20
  }
19
21
  }
20
22
 
21
- // HandleConn handles an incoming ReadWriteCloser.
22
- func (s *Server) HandleConn(ctx context.Context, rwc io.ReadWriteCloser) error {
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)
26
28
  prw := NewPacketReadWriter(rwc, serverRPC.HandlePacket)
27
29
  serverRPC.SetWriter(prw)
28
- return prw.ReadPump()
30
+ err := prw.ReadPump()
31
+ _ = rwc.Close()
32
+ return err
33
+ }
34
+
35
+ // AcceptMuxedConn runs a loop which calls Accept on a muxer to handle streams.
36
+ //
37
+ // Starts HandleStream in a separate goroutine to handle the stream.
38
+ // Returns context.Canceled or io.EOF when the loop is complete / closed.
39
+ func (s *Server) AcceptMuxedConn(ctx context.Context, mplex network.MuxedConn) error {
40
+ for {
41
+ select {
42
+ case <-ctx.Done():
43
+ return context.Canceled
44
+ default:
45
+ if mplex.IsClosed() {
46
+ return io.EOF
47
+ }
48
+ }
49
+
50
+ muxedStream, err := mplex.AcceptStream()
51
+ if err != nil {
52
+ return err
53
+ }
54
+ go func() {
55
+ _ = s.HandleStream(ctx, muxedStream)
56
+ }()
57
+ }
29
58
  }
package/srpc/websocket.go CHANGED
@@ -63,6 +63,3 @@ func (w *WebSocketConn) OpenStream(ctx context.Context, msgHandler func(pkt *Pac
63
63
  func (w *WebSocketConn) Close() error {
64
64
  return w.conn.Close(websocket.StatusGoingAway, "conn closed")
65
65
  }
66
-
67
- // _ is a type assertion
68
- var _ Conn = ((*WebSocketConn)(nil))
package/srpc/websocket.ts CHANGED
@@ -17,7 +17,7 @@ export class WebSocketConn extends Conn {
17
17
  })
18
18
  this.socket = socket
19
19
  const socketDuplex = duplex(socket)
20
- pipe(this.source, socketDuplex, this.sink)
20
+ pipe(socketDuplex, this, socketDuplex)
21
21
  }
22
22
 
23
23
  // getSocket returns the websocket.
package/srpc/conn.go DELETED
@@ -1,7 +0,0 @@
1
- package srpc
2
-
3
- // Conn represents a connection to a remote.
4
- type Conn interface {
5
- // GetOpenStreamFunc returns the OpenStream func.
6
- GetOpenStreamFunc() OpenStreamFunc
7
- }