starpc 0.0.1 → 0.1.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/LICENSE +19 -0
- package/Makefile +140 -0
- package/README.md +128 -26
- package/dist/echo/echo.d.ts +59 -0
- package/dist/echo/echo.js +85 -0
- package/dist/srpc/broadcast-channel.d.ts +16 -0
- package/dist/srpc/broadcast-channel.js +60 -0
- package/dist/srpc/client-rpc.d.ts +31 -0
- package/dist/srpc/client-rpc.js +176 -0
- package/dist/srpc/client.d.ts +12 -0
- package/dist/srpc/client.js +129 -0
- package/dist/srpc/conn.d.ts +14 -0
- package/dist/srpc/conn.js +38 -0
- package/dist/srpc/index.d.ts +3 -0
- package/dist/srpc/index.js +2 -0
- package/dist/srpc/packet.d.ts +9 -0
- package/dist/srpc/packet.js +89 -0
- package/dist/srpc/rpcproto.d.ts +194 -0
- package/dist/srpc/rpcproto.js +322 -0
- package/dist/srpc/stream.d.ts +5 -0
- package/dist/srpc/stream.js +1 -0
- package/dist/srpc/ts-proto-rpc.d.ts +7 -0
- package/dist/srpc/ts-proto-rpc.js +1 -0
- package/dist/srpc/websocket.d.ts +7 -0
- package/dist/srpc/websocket.js +18 -0
- package/e2e/e2e.go +1 -0
- package/e2e/e2e_test.go +158 -0
- package/echo/echo.go +1 -0
- package/echo/echo.pb.go +165 -0
- package/echo/echo.proto +19 -0
- package/echo/echo.ts +191 -0
- package/echo/echo_srpc.pb.go +333 -0
- package/echo/echo_vtproto.pb.go +271 -0
- package/echo/server.go +73 -0
- package/go.mod +50 -0
- package/go.sum +210 -0
- package/integration/integration.bash +25 -0
- package/integration/integration.go +30 -0
- package/integration/integration.ts +54 -0
- package/integration/tsconfig.json +11 -0
- package/package.json +77 -9
- package/patches/@libp2p+mplex+1.2.1.patch +22 -0
- package/srpc/broadcast-channel.ts +72 -0
- package/srpc/client-rpc.go +163 -0
- package/srpc/client-rpc.ts +197 -0
- package/srpc/client.go +96 -0
- package/srpc/client.ts +182 -0
- package/srpc/conn.go +7 -0
- package/srpc/conn.ts +49 -0
- package/srpc/errors.go +20 -0
- package/srpc/handler.go +13 -0
- package/srpc/index.ts +3 -0
- package/srpc/message.go +7 -0
- package/srpc/mux.go +76 -0
- package/srpc/packet-rw.go +102 -0
- package/srpc/packet.go +71 -0
- package/srpc/packet.ts +105 -0
- package/srpc/rpc-stream.go +76 -0
- package/srpc/rpcproto.pb.go +455 -0
- package/srpc/rpcproto.proto +46 -0
- package/srpc/rpcproto.ts +467 -0
- package/srpc/rpcproto_vtproto.pb.go +1094 -0
- package/srpc/server-http.go +66 -0
- package/srpc/server-pipe.go +26 -0
- package/srpc/server-rpc.go +160 -0
- package/srpc/server.go +29 -0
- package/srpc/stream-pipe.go +86 -0
- package/srpc/stream.go +24 -0
- package/srpc/stream.ts +11 -0
- package/srpc/ts-proto-rpc.ts +29 -0
- package/srpc/websocket.go +68 -0
- package/srpc/websocket.ts +22 -0
- package/srpc/writer.go +9 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { WebSocketConn } from '../dist/srpc/websocket.js'
|
|
2
|
+
import { EchoerClientImpl, EchoMsg } from '../dist/echo/echo.js'
|
|
3
|
+
import WebSocket from 'isomorphic-ws'
|
|
4
|
+
import { Observable } from 'rxjs'
|
|
5
|
+
|
|
6
|
+
async function runRPC() {
|
|
7
|
+
const addr = 'ws://localhost:5000/demo'
|
|
8
|
+
console.log(`Connecting to ${addr}`)
|
|
9
|
+
const ws = new WebSocket(addr)
|
|
10
|
+
const channel = new WebSocketConn(ws)
|
|
11
|
+
const client = channel.buildClient()
|
|
12
|
+
const demoServiceClient = new EchoerClientImpl(client)
|
|
13
|
+
|
|
14
|
+
console.log('Calling Echo: unary call...')
|
|
15
|
+
let result = await demoServiceClient.Echo({
|
|
16
|
+
body: "Hello world!"
|
|
17
|
+
})
|
|
18
|
+
console.log('success: output', result.body)
|
|
19
|
+
|
|
20
|
+
// observable for client requests
|
|
21
|
+
const clientRequestStream = new Observable<EchoMsg>(subscriber => {
|
|
22
|
+
subscriber.next({body: 'Hello world from streaming request.'})
|
|
23
|
+
subscriber.complete()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
console.log('Calling EchoClientStream: client -> server...')
|
|
27
|
+
result = await demoServiceClient.EchoClientStream(clientRequestStream)
|
|
28
|
+
console.log('success: output', result.body)
|
|
29
|
+
|
|
30
|
+
console.log('Calling EchoServerStream: server -> client...')
|
|
31
|
+
const serverStream = demoServiceClient.EchoServerStream({
|
|
32
|
+
body: 'Hello world from server to client streaming request.',
|
|
33
|
+
})
|
|
34
|
+
await new Promise<void>((resolve, reject) => {
|
|
35
|
+
serverStream.subscribe({
|
|
36
|
+
next(result) {
|
|
37
|
+
console.log('server: output', result.body)
|
|
38
|
+
},
|
|
39
|
+
complete() {
|
|
40
|
+
resolve()
|
|
41
|
+
},
|
|
42
|
+
error(err: Error) {
|
|
43
|
+
reject(err)
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
runRPC().then(() => {
|
|
50
|
+
process.exit(0)
|
|
51
|
+
}).catch((err) => {
|
|
52
|
+
console.error(err)
|
|
53
|
+
process.exit(1)
|
|
54
|
+
})
|
package/package.json
CHANGED
|
@@ -1,15 +1,83 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starpc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Streaming protobuf RPC service protocol over any two-way channel.",
|
|
5
|
-
"
|
|
6
|
-
"license": "Apache-2.0",
|
|
5
|
+
"license": "MIT",
|
|
7
6
|
"author": {
|
|
8
|
-
"name": "
|
|
9
|
-
"email": "
|
|
10
|
-
"url": "http://
|
|
7
|
+
"name": "Aperture Robotics LLC.",
|
|
8
|
+
"email": "support@aperture.us",
|
|
9
|
+
"url": "http://aperture.us"
|
|
11
10
|
},
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
"contributors": [
|
|
12
|
+
{
|
|
13
|
+
"name": "Christian Stewart",
|
|
14
|
+
"email": "christian@aperture.us",
|
|
15
|
+
"url": "http://github.com/paralin"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"main": "dist/srpc/index.js",
|
|
19
|
+
"types": "./dist/srpc/index.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"!**/*.tsbuildinfo",
|
|
22
|
+
"Makefile",
|
|
23
|
+
"dist/echo",
|
|
24
|
+
"dist/srpc",
|
|
25
|
+
"e2e",
|
|
26
|
+
"echo",
|
|
27
|
+
"go.mod",
|
|
28
|
+
"go.sum",
|
|
29
|
+
"integration",
|
|
30
|
+
"patches",
|
|
31
|
+
"srpc"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"url": "git@github.com:aperturerobotics/starpc.git"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc --project tsconfig.build.json --outDir ./dist/",
|
|
38
|
+
"check": "tsc",
|
|
39
|
+
"codegen": "npm run gen",
|
|
40
|
+
"ci": "npm run build && npm run lint:js && npm run lint:go",
|
|
41
|
+
"format": "prettier --write './srpc/**/(*.ts|*.tsx|*.js|*.html|*.css)'",
|
|
42
|
+
"gen": "make genproto",
|
|
43
|
+
"test": "make test && npm run check",
|
|
44
|
+
"test:integration": "make integration",
|
|
45
|
+
"lint": "npm run lint:go && npm run lint:js",
|
|
46
|
+
"lint:go": "make lint",
|
|
47
|
+
"lint:js": "eslint -c .eslintrc.js --ext .ts ./{srpc,echo}/**/*.ts",
|
|
48
|
+
"postinstall": "patch-package",
|
|
49
|
+
"precommit": "npm run format"
|
|
50
|
+
},
|
|
51
|
+
"prettier": {
|
|
52
|
+
"semi": false,
|
|
53
|
+
"singleQuote": true
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/varint": "^6.0.0",
|
|
57
|
+
"@typescript-eslint/eslint-plugin": "^5.27.1",
|
|
58
|
+
"@typescript-eslint/parser": "^5.27.1",
|
|
59
|
+
"esbuild": "^0.14.43",
|
|
60
|
+
"eslint": "^8.17.0",
|
|
61
|
+
"eslint-config-prettier": "^8.5.0",
|
|
62
|
+
"eslint-config-standard-with-typescript": "^21.0.1",
|
|
63
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
64
|
+
"husky": "^8.0.1",
|
|
65
|
+
"prettier": "^2.7.0",
|
|
66
|
+
"ts-proto": "^1.115.4",
|
|
67
|
+
"typescript": "^4.7.3"
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"@libp2p/mplex": "^1.2.1",
|
|
71
|
+
"event-iterator": "^2.0.0",
|
|
72
|
+
"isomorphic-ws": "^4.0.1",
|
|
73
|
+
"it-length-prefixed": "^7.0.1",
|
|
74
|
+
"it-pipe": "^2.0.3",
|
|
75
|
+
"it-pushable": "^3.0.0",
|
|
76
|
+
"it-stream-types": "^1.0.4",
|
|
77
|
+
"it-ws": "^5.0.2",
|
|
78
|
+
"protobufjs": "^6.11.3",
|
|
79
|
+
"patch-package": "^6.4.7",
|
|
80
|
+
"rxjs": "^7.5.5",
|
|
81
|
+
"ws": "^8.8.0"
|
|
82
|
+
}
|
|
15
83
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
diff --git a/node_modules/@libp2p/mplex/dist/src/index.d.ts b/node_modules/@libp2p/mplex/dist/src/index.d.ts
|
|
2
|
+
index 9814d45..ada275f 100644
|
|
3
|
+
--- a/node_modules/@libp2p/mplex/dist/src/index.d.ts
|
|
4
|
+
+++ b/node_modules/@libp2p/mplex/dist/src/index.d.ts
|
|
5
|
+
@@ -1,6 +1,7 @@
|
|
6
|
+
import type { Components } from '@libp2p/interfaces/components';
|
|
7
|
+
import type { StreamMuxerFactory, StreamMuxerInit } from '@libp2p/interfaces/stream-muxer';
|
|
8
|
+
import { MplexStreamMuxer } from './mplex.js';
|
|
9
|
+
+export { MplexStreamMuxer } from './mplex.js';
|
|
10
|
+
export interface MplexInit {
|
|
11
|
+
/**
|
|
12
|
+
* The maximum size of message that can be sent in one go in bytes.
|
|
13
|
+
diff --git a/node_modules/@libp2p/mplex/dist/src/index.js b/node_modules/@libp2p/mplex/dist/src/index.js
|
|
14
|
+
index 2c4cf01..4359734 100644
|
|
15
|
+
--- a/node_modules/@libp2p/mplex/dist/src/index.js
|
|
16
|
+
+++ b/node_modules/@libp2p/mplex/dist/src/index.js
|
|
17
|
+
@@ -1,4 +1,5 @@
|
|
18
|
+
import { MplexStreamMuxer } from './mplex.js';
|
|
19
|
+
+export { MplexStreamMuxer } from './mplex.js';
|
|
20
|
+
export class Mplex {
|
|
21
|
+
constructor(init = {}) {
|
|
22
|
+
this.protocol = '/mplex/6.7.0';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Duplex, Sink } from 'it-stream-types'
|
|
2
|
+
import { Conn } from './conn'
|
|
3
|
+
import { EventIterator } from 'event-iterator'
|
|
4
|
+
import { pipe } from 'it-pipe'
|
|
5
|
+
|
|
6
|
+
// BroadcastChannelIterable is a AsyncIterable wrapper for BroadcastChannel.
|
|
7
|
+
export class BroadcastChannelIterable<T> implements Duplex<T> {
|
|
8
|
+
// channel is the broadcast channel
|
|
9
|
+
public readonly channel: BroadcastChannel
|
|
10
|
+
// sink is the sink for incoming messages.
|
|
11
|
+
public sink: Sink<T>
|
|
12
|
+
// source is the source for outgoing messages.
|
|
13
|
+
public source: AsyncIterable<T>
|
|
14
|
+
|
|
15
|
+
constructor(channel: BroadcastChannel) {
|
|
16
|
+
this.channel = channel
|
|
17
|
+
this.sink = this._createSink()
|
|
18
|
+
this.source = this._createSource()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// _createSink initializes the sink field.
|
|
22
|
+
private _createSink(): Sink<T> {
|
|
23
|
+
return async (source) => {
|
|
24
|
+
for await (const msg of source) {
|
|
25
|
+
this.channel.postMessage(msg)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// _createSource initializes the source field.
|
|
31
|
+
private _createSource() {
|
|
32
|
+
return new EventIterator<T>((queue) => {
|
|
33
|
+
const messageListener = (ev: MessageEvent<T>) => {
|
|
34
|
+
if (ev.data) {
|
|
35
|
+
queue.push(ev.data)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
this.channel.addEventListener('message', messageListener)
|
|
39
|
+
|
|
40
|
+
return () => {
|
|
41
|
+
this.channel.removeEventListener('message', messageListener)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// newBroadcastChannelIterable constructs a BroadcastChannelIterable with a channel name.
|
|
48
|
+
export function newBroadcastChannelIterable<T>(
|
|
49
|
+
name: string
|
|
50
|
+
): BroadcastChannelIterable<T> {
|
|
51
|
+
const channel = new BroadcastChannel(name)
|
|
52
|
+
return new BroadcastChannelIterable<T>(channel)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// BroadcastChannelConn implements a connection with a BroadcastChannel.
|
|
56
|
+
//
|
|
57
|
+
// expects Uint8Array objects over the BroadcastChannel.
|
|
58
|
+
export class BroadcastChannelConn extends Conn {
|
|
59
|
+
// channel is the broadcast channel iterable
|
|
60
|
+
private channel: BroadcastChannelIterable<Uint8Array>
|
|
61
|
+
|
|
62
|
+
constructor(channel: BroadcastChannel) {
|
|
63
|
+
super()
|
|
64
|
+
this.channel = new BroadcastChannelIterable<Uint8Array>(channel)
|
|
65
|
+
pipe(this, this.channel, this)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// getBroadcastChannel returns the BroadcastChannel.
|
|
69
|
+
public getBroadcastChannel(): BroadcastChannel {
|
|
70
|
+
return this.channel.channel
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
package srpc
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
|
|
6
|
+
"github.com/pkg/errors"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// ClientRPC represents the client side of an on-going RPC call message stream.
|
|
10
|
+
// Not concurrency safe: use a mutex if calling concurrently.
|
|
11
|
+
type ClientRPC struct {
|
|
12
|
+
// ctx is the context, canceled when the rpc ends.
|
|
13
|
+
ctx context.Context
|
|
14
|
+
// ctxCancel is called when the rpc ends.
|
|
15
|
+
ctxCancel context.CancelFunc
|
|
16
|
+
// writer is the writer to write messages to
|
|
17
|
+
writer Writer
|
|
18
|
+
// service is the rpc service
|
|
19
|
+
service string
|
|
20
|
+
// method is the rpc method
|
|
21
|
+
method string
|
|
22
|
+
// dataCh contains queued data packets.
|
|
23
|
+
// closed when the client closes the channel.
|
|
24
|
+
dataCh chan []byte
|
|
25
|
+
// dataChClosed is a flag set after dataCh is closed.
|
|
26
|
+
// controlled by HandlePacket.
|
|
27
|
+
dataChClosed bool
|
|
28
|
+
// serverErr is an error set by the client.
|
|
29
|
+
// before dataCh is closed, managed by HandlePacket.
|
|
30
|
+
// immutable after dataCh is closed.
|
|
31
|
+
serverErr error
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// NewClientRPC constructs a new ClientRPC session and writes CallStart.
|
|
35
|
+
// the writer will be closed when the ClientRPC completes.
|
|
36
|
+
// service and method must be specified.
|
|
37
|
+
// must call Start after creating the RPC object.
|
|
38
|
+
func NewClientRPC(ctx context.Context, service, method string) *ClientRPC {
|
|
39
|
+
rpc := &ClientRPC{
|
|
40
|
+
service: service,
|
|
41
|
+
method: method,
|
|
42
|
+
dataCh: make(chan []byte, 5),
|
|
43
|
+
}
|
|
44
|
+
rpc.ctx, rpc.ctxCancel = context.WithCancel(ctx)
|
|
45
|
+
return rpc
|
|
46
|
+
}
|
|
47
|
+
|
|
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
|
+
// must only be called once!
|
|
51
|
+
func (r *ClientRPC) Start(writer Writer, firstMsg []byte) error {
|
|
52
|
+
select {
|
|
53
|
+
case <-r.ctx.Done():
|
|
54
|
+
r.Close()
|
|
55
|
+
return context.Canceled
|
|
56
|
+
default:
|
|
57
|
+
}
|
|
58
|
+
r.writer = writer
|
|
59
|
+
pkt := NewCallStartPacket(r.service, r.method, firstMsg)
|
|
60
|
+
if err := writer.WritePacket(pkt); err != nil {
|
|
61
|
+
r.Close()
|
|
62
|
+
return err
|
|
63
|
+
}
|
|
64
|
+
return nil
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ReadAll reads all returned Data packets and returns any error.
|
|
68
|
+
// intended for use with unary rpcs.
|
|
69
|
+
func (r *ClientRPC) ReadAll() ([][]byte, error) {
|
|
70
|
+
msgs := make([][]byte, 0, 1)
|
|
71
|
+
for {
|
|
72
|
+
select {
|
|
73
|
+
case <-r.ctx.Done():
|
|
74
|
+
return msgs, context.Canceled
|
|
75
|
+
case data, ok := <-r.dataCh:
|
|
76
|
+
if !ok {
|
|
77
|
+
return msgs, r.serverErr
|
|
78
|
+
}
|
|
79
|
+
msgs = append(msgs, data)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Context is canceled when the ClientRPC is no longer valid.
|
|
85
|
+
func (r *ClientRPC) Context() context.Context {
|
|
86
|
+
return r.ctx
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// HandlePacketData handles an incoming unparsed message packet.
|
|
90
|
+
// Not concurrency safe: use a mutex if calling concurrently.
|
|
91
|
+
func (r *ClientRPC) HandlePacketData(data []byte) error {
|
|
92
|
+
pkt := &Packet{}
|
|
93
|
+
if err := pkt.UnmarshalVT(data); err != nil {
|
|
94
|
+
return err
|
|
95
|
+
}
|
|
96
|
+
return r.HandlePacket(pkt)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// HandlePacket handles an incoming parsed message packet.
|
|
100
|
+
// Not concurrency safe: use a mutex if calling concurrently.
|
|
101
|
+
func (r *ClientRPC) HandlePacket(msg *Packet) error {
|
|
102
|
+
if err := msg.Validate(); err != nil {
|
|
103
|
+
return err
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
switch b := msg.GetBody().(type) {
|
|
107
|
+
case *Packet_CallStart:
|
|
108
|
+
return r.HandleCallStart(b.CallStart)
|
|
109
|
+
case *Packet_CallData:
|
|
110
|
+
return r.HandleCallData(b.CallData)
|
|
111
|
+
case *Packet_CallStartResp:
|
|
112
|
+
return r.HandleCallStartResp(b.CallStartResp)
|
|
113
|
+
default:
|
|
114
|
+
return nil
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// HandleCallStart handles the call start packet.
|
|
119
|
+
func (r *ClientRPC) HandleCallStart(pkt *CallStart) error {
|
|
120
|
+
// server-to-client calls not supported
|
|
121
|
+
return errors.Wrap(ErrUnrecognizedPacket, "call start packet unexpected")
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// HandleCallData handles the call data packet.
|
|
125
|
+
func (r *ClientRPC) HandleCallData(pkt *CallData) error {
|
|
126
|
+
if r.dataChClosed {
|
|
127
|
+
return ErrCompleted
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if data := pkt.GetData(); len(data) != 0 {
|
|
131
|
+
select {
|
|
132
|
+
case <-r.ctx.Done():
|
|
133
|
+
return context.Canceled
|
|
134
|
+
case r.dataCh <- data:
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
complete := pkt.GetComplete()
|
|
139
|
+
if err := pkt.GetError(); len(err) != 0 {
|
|
140
|
+
complete = true
|
|
141
|
+
r.serverErr = errors.New(err)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if complete {
|
|
145
|
+
r.dataChClosed = true
|
|
146
|
+
close(r.dataCh)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return nil
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// HandleCallStartResp handles the CallStartResp packet.
|
|
153
|
+
func (r *ClientRPC) HandleCallStartResp(resp *CallStartResp) error {
|
|
154
|
+
// client-side calls not supported
|
|
155
|
+
return errors.Wrap(ErrUnrecognizedPacket, "call start resp packet unexpected")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Close releases any resources held by the ClientRPC.
|
|
159
|
+
// not concurrency safe with HandlePacket.
|
|
160
|
+
func (r *ClientRPC) Close() {
|
|
161
|
+
r.ctxCancel()
|
|
162
|
+
_ = r.writer.Close()
|
|
163
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { CallData, CallStart, CallStartResp } from './rpcproto'
|
|
2
|
+
import { Packet } from './rpcproto'
|
|
3
|
+
import type { Sink } from 'it-stream-types'
|
|
4
|
+
import { pushable } from 'it-pushable'
|
|
5
|
+
|
|
6
|
+
// DataCb is a callback to handle incoming RPC messages.
|
|
7
|
+
// Returns true if more data is expected, false otherwise.
|
|
8
|
+
// If returns undefined, assumes more data is expected.
|
|
9
|
+
export type DataCb = (data: Uint8Array) => Promise<boolean | void>
|
|
10
|
+
|
|
11
|
+
// ClientRPC is an ongoing RPC from the client side.
|
|
12
|
+
export class ClientRPC {
|
|
13
|
+
// sink is the data sink for incoming messages.
|
|
14
|
+
public sink: Sink<Packet>
|
|
15
|
+
// source is the packet source for outgoing Packets.
|
|
16
|
+
public source: AsyncIterable<Packet>
|
|
17
|
+
// _source is used to write to the source.
|
|
18
|
+
private readonly _source: {
|
|
19
|
+
push: (val: Packet) => void
|
|
20
|
+
end: (err?: Error) => void
|
|
21
|
+
}
|
|
22
|
+
// service is the rpc service
|
|
23
|
+
private service: string
|
|
24
|
+
// method is the rpc method
|
|
25
|
+
private method: string
|
|
26
|
+
// dataCb is called with any incoming data.
|
|
27
|
+
private dataCb?: DataCb
|
|
28
|
+
// started is resolved when the request starts.
|
|
29
|
+
private started: Promise<void>
|
|
30
|
+
// onStarted is called by the message handler when the request starts.
|
|
31
|
+
private onStarted?: (err?: Error) => void
|
|
32
|
+
// complete is resolved when the request completes.
|
|
33
|
+
// rejected with an error if the call encountered any error.
|
|
34
|
+
private complete: Promise<void>
|
|
35
|
+
// onComplete is called by the message handler when the call completes.
|
|
36
|
+
private onComplete?: (err?: Error) => void
|
|
37
|
+
// closed indicates close has been called
|
|
38
|
+
private closed: boolean
|
|
39
|
+
|
|
40
|
+
constructor(service: string, method: string, dataCb: DataCb | null) {
|
|
41
|
+
this.closed = false
|
|
42
|
+
this.sink = this._createSink()
|
|
43
|
+
const sourcev = this._createSource()
|
|
44
|
+
this.source = sourcev
|
|
45
|
+
this._source = sourcev
|
|
46
|
+
this.service = service
|
|
47
|
+
this.method = method
|
|
48
|
+
if (dataCb) {
|
|
49
|
+
this.dataCb = dataCb
|
|
50
|
+
}
|
|
51
|
+
this.started = new Promise<void>((resolveStarted, rejectStarted) => {
|
|
52
|
+
this.onStarted = (err?: Error) => {
|
|
53
|
+
if (err) {
|
|
54
|
+
rejectStarted(err)
|
|
55
|
+
} else {
|
|
56
|
+
resolveStarted()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
this.complete = new Promise<void>((resolveComplete, rejectComplete) => {
|
|
61
|
+
this.onComplete = (err?: Error) => {
|
|
62
|
+
this.closed = true
|
|
63
|
+
if (err) {
|
|
64
|
+
rejectComplete(err)
|
|
65
|
+
} else {
|
|
66
|
+
resolveComplete()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// waitStarted returns the started promise.
|
|
73
|
+
public waitStarted(): Promise<void> {
|
|
74
|
+
return this.started
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// waitComplete returns the complete promise.
|
|
78
|
+
public waitComplete(): Promise<void> {
|
|
79
|
+
return this.complete
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// writeCallStart writes the call start packet.
|
|
83
|
+
public async writeCallStart(data?: Uint8Array) {
|
|
84
|
+
const callStart: CallStart = {
|
|
85
|
+
rpcService: this.service,
|
|
86
|
+
rpcMethod: this.method,
|
|
87
|
+
data: data || new Uint8Array(0),
|
|
88
|
+
}
|
|
89
|
+
await this.writePacket({
|
|
90
|
+
body: {
|
|
91
|
+
$case: 'callStart',
|
|
92
|
+
callStart,
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// writeCallData writes the call data packet.
|
|
98
|
+
public async writeCallData(data: Uint8Array, complete?: boolean, error?: string) {
|
|
99
|
+
const callData: CallData = {
|
|
100
|
+
data,
|
|
101
|
+
complete: complete || false,
|
|
102
|
+
error: error || "",
|
|
103
|
+
}
|
|
104
|
+
await this.writePacket({
|
|
105
|
+
body: {
|
|
106
|
+
$case: 'callData',
|
|
107
|
+
callData,
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// writePacket writes a packet to the stream.
|
|
113
|
+
private async writePacket(packet: Packet) {
|
|
114
|
+
this._source.push(packet)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// handleMessage handles an incoming encoded Packet.
|
|
118
|
+
//
|
|
119
|
+
// note: may throw an error if the message was unexpected or invalid.
|
|
120
|
+
public async handleMessage(message: Uint8Array) {
|
|
121
|
+
return this.handlePacket(Packet.decode(message))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// handlePacket handles an incoming packet.
|
|
125
|
+
public async handlePacket(packet: Partial<Packet>) {
|
|
126
|
+
switch (packet?.body?.$case) {
|
|
127
|
+
case 'callStart':
|
|
128
|
+
return this.handleCallStart(packet.body.callStart)
|
|
129
|
+
case 'callStartResp':
|
|
130
|
+
return this.handleCallStartResp(packet.body.callStartResp)
|
|
131
|
+
case 'callData':
|
|
132
|
+
return this.handleCallData(packet.body.callData)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// handleCallStart handles a CallStart packet.
|
|
137
|
+
public async handleCallStart(packet: Partial<CallStart>) {
|
|
138
|
+
// we do not implement server -> client RPCs.
|
|
139
|
+
throw new Error(`unexpected server to client rpc: ${packet.rpcService}/${packet.rpcMethod}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// handleCallStartResp handles a CallStartResp packet.
|
|
143
|
+
public async handleCallStartResp(packet: Partial<CallStartResp>) {
|
|
144
|
+
if (packet.error && packet.error.length) {
|
|
145
|
+
const err = new Error(packet.error)
|
|
146
|
+
this.onStarted!(err)
|
|
147
|
+
this.onComplete!(err)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// handleCallData handles a CallData packet.
|
|
152
|
+
public async handleCallData(packet: Partial<CallData>) {
|
|
153
|
+
const data = packet.data
|
|
154
|
+
if (this.dataCb && data?.length) {
|
|
155
|
+
await this.dataCb(data)
|
|
156
|
+
}
|
|
157
|
+
if (packet.error && packet.error.length) {
|
|
158
|
+
this.onComplete!(new Error(packet.error))
|
|
159
|
+
} else if (packet.complete) {
|
|
160
|
+
this.onComplete!()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// close closes the active call if not already completed.
|
|
165
|
+
public async close(err?: Error) {
|
|
166
|
+
if (!this.closed) {
|
|
167
|
+
await this.writeCallData(new Uint8Array(0), true, err ? err.message : "")
|
|
168
|
+
}
|
|
169
|
+
if (!err) {
|
|
170
|
+
err = new Error('call closed')
|
|
171
|
+
}
|
|
172
|
+
this.onComplete!(err)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// _createSink initializes the sink field.
|
|
176
|
+
private _createSink(): Sink<Packet> {
|
|
177
|
+
return async (source) => {
|
|
178
|
+
try {
|
|
179
|
+
for await (const msg of source) {
|
|
180
|
+
await this.handlePacket(msg)
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
this.close(err as Error)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// _createSource initializes the source field.
|
|
189
|
+
private _createSource() {
|
|
190
|
+
return pushable<Packet>({
|
|
191
|
+
objectMode: true,
|
|
192
|
+
onEnd: (err?: Error): void => {
|
|
193
|
+
this.onComplete!(err)
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
}
|