starpc 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Makefile +1 -0
- package/README.md +24 -5
- package/dist/echo/client-test.d.ts +2 -0
- package/dist/echo/client-test.js +35 -0
- package/dist/echo/echo.d.ts +124 -0
- package/dist/echo/echo.js +42 -0
- package/dist/echo/index.d.ts +3 -0
- package/dist/echo/index.js +3 -0
- package/dist/echo/server.d.ts +8 -0
- package/dist/echo/server.js +50 -0
- package/dist/echo/sever.d.ts +0 -0
- package/dist/echo/sever.js +1 -0
- package/dist/srpc/broadcast-channel.d.ts +3 -2
- package/dist/srpc/broadcast-channel.js +2 -2
- package/dist/srpc/client-rpc.d.ts +4 -28
- package/dist/srpc/client-rpc.js +9 -152
- package/dist/srpc/client.d.ts +2 -2
- package/dist/srpc/client.js +56 -84
- package/dist/srpc/common-rpc.d.ts +24 -0
- package/dist/srpc/common-rpc.js +140 -0
- package/dist/srpc/conn.d.ts +8 -3
- package/dist/srpc/conn.js +19 -3
- package/dist/srpc/definition.d.ts +15 -0
- package/dist/srpc/definition.js +1 -0
- package/dist/srpc/handler.d.ts +24 -0
- package/dist/srpc/handler.js +123 -0
- package/dist/srpc/index.d.ts +7 -3
- package/dist/srpc/index.js +6 -2
- package/dist/srpc/message.d.ts +11 -0
- package/dist/srpc/message.js +32 -0
- package/dist/srpc/mux.d.ts +11 -0
- package/dist/srpc/mux.js +36 -0
- package/dist/srpc/packet.d.ts +4 -4
- package/dist/srpc/packet.js +38 -27
- package/dist/srpc/rpcproto.d.ts +18 -43
- package/dist/srpc/rpcproto.js +37 -78
- package/dist/srpc/server-rpc.d.ts +11 -0
- package/dist/srpc/server-rpc.js +55 -0
- package/dist/srpc/server.d.ts +14 -0
- package/dist/srpc/server.js +31 -0
- package/dist/srpc/websocket.d.ts +3 -2
- package/dist/srpc/websocket.js +4 -4
- package/e2e/README.md +10 -0
- package/e2e/e2e.ts +27 -0
- package/echo/client-test.ts +41 -0
- package/echo/echo.ts +45 -0
- package/echo/index.ts +3 -0
- package/echo/server.ts +57 -0
- package/echo/sever.ts +0 -0
- package/integration/integration.bash +1 -1
- package/integration/integration.ts +10 -42
- package/package.json +18 -17
- package/srpc/broadcast-channel.ts +8 -3
- package/srpc/client-rpc.go +1 -9
- package/srpc/client-rpc.ts +11 -175
- package/srpc/client.ts +58 -99
- package/srpc/common-rpc.ts +171 -0
- package/srpc/conn.ts +33 -5
- package/srpc/definition.ts +30 -0
- package/srpc/handler.ts +174 -0
- package/srpc/index.ts +7 -3
- package/srpc/message.ts +60 -0
- package/srpc/mux.ts +56 -0
- package/srpc/packet.go +4 -11
- package/srpc/packet.ts +44 -30
- package/srpc/rpcproto.pb.go +54 -118
- package/srpc/rpcproto.proto +7 -12
- package/srpc/rpcproto.ts +38 -101
- package/srpc/rpcproto_vtproto.pb.go +58 -210
- package/srpc/server-rpc.go +5 -10
- package/srpc/server-rpc.ts +65 -0
- package/srpc/server.ts +56 -0
- package/srpc/websocket.ts +6 -4
package/e2e/e2e.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { pipe } from 'it-pipe'
|
|
2
|
+
import { createHandler, createMux, Server, Client, Conn } from '../srpc'
|
|
3
|
+
import { EchoerDefinition, EchoerServer, runClientTest } from '../echo'
|
|
4
|
+
|
|
5
|
+
async function runRPC() {
|
|
6
|
+
const mux = createMux()
|
|
7
|
+
const echoer = new EchoerServer()
|
|
8
|
+
mux.register(createHandler(EchoerDefinition, echoer))
|
|
9
|
+
const server = new Server(mux)
|
|
10
|
+
|
|
11
|
+
const clientConn = new Conn()
|
|
12
|
+
const serverConn = new Conn(server)
|
|
13
|
+
pipe(clientConn, serverConn, clientConn)
|
|
14
|
+
const client = new Client(clientConn.buildOpenStreamFunc())
|
|
15
|
+
|
|
16
|
+
await runClientTest(client)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
runRPC()
|
|
20
|
+
.then(() => {
|
|
21
|
+
console.log('finished successfully')
|
|
22
|
+
})
|
|
23
|
+
.catch((err) => {
|
|
24
|
+
console.log('failed')
|
|
25
|
+
console.error(err)
|
|
26
|
+
process.exit(1)
|
|
27
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Client } from '../srpc/index.js'
|
|
2
|
+
import { EchoerClientImpl, EchoMsg } from './echo.js'
|
|
3
|
+
import { Observable } from 'rxjs'
|
|
4
|
+
|
|
5
|
+
export async function runClientTest(client: Client) {
|
|
6
|
+
const demoServiceClient = new EchoerClientImpl(client)
|
|
7
|
+
|
|
8
|
+
console.log('Calling Echo: unary call...')
|
|
9
|
+
let result = await demoServiceClient.Echo({
|
|
10
|
+
body: 'Hello world!',
|
|
11
|
+
})
|
|
12
|
+
console.log('success: output', result.body)
|
|
13
|
+
|
|
14
|
+
// observable for client requests
|
|
15
|
+
const clientRequestStream = new Observable<EchoMsg>((subscriber) => {
|
|
16
|
+
subscriber.next({ body: 'Hello world from streaming request.' })
|
|
17
|
+
subscriber.complete()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
console.log('Calling EchoClientStream: client -> server...')
|
|
21
|
+
result = await demoServiceClient.EchoClientStream(clientRequestStream)
|
|
22
|
+
console.log('success: output', result.body)
|
|
23
|
+
|
|
24
|
+
console.log('Calling EchoServerStream: server -> client...')
|
|
25
|
+
const serverStream = demoServiceClient.EchoServerStream({
|
|
26
|
+
body: 'Hello world from server to client streaming request.',
|
|
27
|
+
})
|
|
28
|
+
await new Promise<void>((resolve, reject) => {
|
|
29
|
+
serverStream.subscribe({
|
|
30
|
+
next(result) {
|
|
31
|
+
console.log('server: output', result.body)
|
|
32
|
+
},
|
|
33
|
+
complete() {
|
|
34
|
+
resolve()
|
|
35
|
+
},
|
|
36
|
+
error(err: Error) {
|
|
37
|
+
reject(err)
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
}
|
package/echo/echo.ts
CHANGED
|
@@ -125,6 +125,51 @@ export class EchoerClientImpl implements Echoer {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/** Echoer service returns the given message. */
|
|
129
|
+
export type EchoerDefinition = typeof EchoerDefinition
|
|
130
|
+
export const EchoerDefinition = {
|
|
131
|
+
name: 'Echoer',
|
|
132
|
+
fullName: 'echo.Echoer',
|
|
133
|
+
methods: {
|
|
134
|
+
/** Echo returns the given message. */
|
|
135
|
+
echo: {
|
|
136
|
+
name: 'Echo',
|
|
137
|
+
requestType: EchoMsg,
|
|
138
|
+
requestStream: false,
|
|
139
|
+
responseType: EchoMsg,
|
|
140
|
+
responseStream: false,
|
|
141
|
+
options: {},
|
|
142
|
+
},
|
|
143
|
+
/** EchoServerStream is an example of a server -> client one-way stream. */
|
|
144
|
+
echoServerStream: {
|
|
145
|
+
name: 'EchoServerStream',
|
|
146
|
+
requestType: EchoMsg,
|
|
147
|
+
requestStream: false,
|
|
148
|
+
responseType: EchoMsg,
|
|
149
|
+
responseStream: true,
|
|
150
|
+
options: {},
|
|
151
|
+
},
|
|
152
|
+
/** EchoClientStream is an example of client->server one-way stream. */
|
|
153
|
+
echoClientStream: {
|
|
154
|
+
name: 'EchoClientStream',
|
|
155
|
+
requestType: EchoMsg,
|
|
156
|
+
requestStream: true,
|
|
157
|
+
responseType: EchoMsg,
|
|
158
|
+
responseStream: false,
|
|
159
|
+
options: {},
|
|
160
|
+
},
|
|
161
|
+
/** EchoBidiStream is an example of a two-way stream. */
|
|
162
|
+
echoBidiStream: {
|
|
163
|
+
name: 'EchoBidiStream',
|
|
164
|
+
requestType: EchoMsg,
|
|
165
|
+
requestStream: true,
|
|
166
|
+
responseType: EchoMsg,
|
|
167
|
+
responseStream: true,
|
|
168
|
+
options: {},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
} as const
|
|
172
|
+
|
|
128
173
|
interface Rpc {
|
|
129
174
|
request(
|
|
130
175
|
service: string,
|
package/echo/index.ts
ADDED
package/echo/server.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Observable, from as observableFrom } from 'rxjs'
|
|
2
|
+
import { Echoer, EchoMsg } from './echo'
|
|
3
|
+
import { pushable, Pushable } from 'it-pushable'
|
|
4
|
+
|
|
5
|
+
// EchoServer implements the Echoer server.
|
|
6
|
+
export class EchoerServer implements Echoer {
|
|
7
|
+
public async Echo(request: EchoMsg): Promise<EchoMsg> {
|
|
8
|
+
return request
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
public EchoServerStream(request: EchoMsg): Observable<EchoMsg> {
|
|
12
|
+
// send 5 responses, with a 200ms delay for each
|
|
13
|
+
return observableFrom(
|
|
14
|
+
(async function* response(): AsyncIterable<EchoMsg> {
|
|
15
|
+
for (let i = 0; i < 5; i++) {
|
|
16
|
+
yield request
|
|
17
|
+
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
18
|
+
}
|
|
19
|
+
})()
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public EchoClientStream(request: Observable<EchoMsg>): Promise<EchoMsg> {
|
|
24
|
+
return new Promise<EchoMsg>((resolve, reject) => {
|
|
25
|
+
request.subscribe({
|
|
26
|
+
next(msg) {
|
|
27
|
+
resolve(msg)
|
|
28
|
+
},
|
|
29
|
+
error(err: any) {
|
|
30
|
+
reject(err)
|
|
31
|
+
},
|
|
32
|
+
complete() {
|
|
33
|
+
reject(new Error('none received'))
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public EchoBidiStream(request: Observable<EchoMsg>): Observable<EchoMsg> {
|
|
40
|
+
// build result observable
|
|
41
|
+
const pushResponse: Pushable<EchoMsg> = pushable({ objectMode: true })
|
|
42
|
+
const response = observableFrom(pushResponse)
|
|
43
|
+
pushResponse.push({ body: 'hello from server' })
|
|
44
|
+
request.subscribe({
|
|
45
|
+
next(msg) {
|
|
46
|
+
pushResponse.push(msg)
|
|
47
|
+
},
|
|
48
|
+
error(err) {
|
|
49
|
+
pushResponse.throw(err)
|
|
50
|
+
},
|
|
51
|
+
complete() {
|
|
52
|
+
pushResponse.end()
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
return response
|
|
56
|
+
}
|
|
57
|
+
}
|
package/echo/sever.ts
ADDED
|
File without changes
|
|
@@ -3,7 +3,7 @@ set -eo pipefail
|
|
|
3
3
|
|
|
4
4
|
echo "Compiling ts..."
|
|
5
5
|
# ../node_modules/.bin/tsc --out integration.js --project tsconfig.json
|
|
6
|
-
../node_modules/.bin/esbuild integration.ts --bundle --platform=node --outfile=integration.js
|
|
6
|
+
../node_modules/.bin/esbuild integration.ts --bundle --platform=node --outfile=integration.js
|
|
7
7
|
|
|
8
8
|
echo "Compiling go..."
|
|
9
9
|
go build -o integration -v ./
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { WebSocketConn } from '../
|
|
2
|
-
import {
|
|
1
|
+
import { WebSocketConn } from '../srpc'
|
|
2
|
+
import { runClientTest } from '../echo'
|
|
3
3
|
import WebSocket from 'isomorphic-ws'
|
|
4
|
-
import { Observable } from 'rxjs'
|
|
5
4
|
|
|
6
5
|
async function runRPC() {
|
|
7
6
|
const addr = 'ws://localhost:5000/demo'
|
|
@@ -9,46 +8,15 @@ async function runRPC() {
|
|
|
9
8
|
const ws = new WebSocket(addr)
|
|
10
9
|
const channel = new WebSocketConn(ws)
|
|
11
10
|
const client = channel.buildClient()
|
|
12
|
-
const demoServiceClient = new EchoerClientImpl(client)
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
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)
|
|
12
|
+
await runClientTest(client)
|
|
13
|
+
}
|
|
29
14
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
15
|
+
runRPC()
|
|
16
|
+
.then(() => {
|
|
17
|
+
process.exit(0)
|
|
33
18
|
})
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
console.log('server: output', result.body)
|
|
38
|
-
},
|
|
39
|
-
complete() {
|
|
40
|
-
resolve()
|
|
41
|
-
},
|
|
42
|
-
error(err: Error) {
|
|
43
|
-
reject(err)
|
|
44
|
-
},
|
|
45
|
-
})
|
|
19
|
+
.catch((err) => {
|
|
20
|
+
console.error(err)
|
|
21
|
+
process.exit(1)
|
|
46
22
|
})
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starpc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Streaming protobuf RPC service protocol over any two-way channel.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"url": "http://github.com/paralin"
|
|
16
16
|
}
|
|
17
17
|
],
|
|
18
|
-
"main": "dist/srpc/index.js",
|
|
18
|
+
"main": "./dist/srpc/index.js",
|
|
19
19
|
"types": "./dist/srpc/index.d.ts",
|
|
20
20
|
"files": [
|
|
21
21
|
"!**/*.tsbuildinfo",
|
|
@@ -34,13 +34,16 @@
|
|
|
34
34
|
"url": "git@github.com:aperturerobotics/starpc.git"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
-
"build": "tsc --project tsconfig.build.json --outDir ./dist/",
|
|
37
|
+
"build": "rimraf ./dist && tsc --project tsconfig.build.json --outDir ./dist/",
|
|
38
38
|
"check": "tsc",
|
|
39
|
+
"deps": "depcheck",
|
|
39
40
|
"codegen": "npm run gen",
|
|
40
41
|
"ci": "npm run build && npm run lint:js && npm run lint:go",
|
|
41
|
-
"format": "prettier --write './srpc/**/(*.ts|*.tsx|*.
|
|
42
|
+
"format": "prettier --write './{srpc,echo,e2e,integration}/**/(*.ts|*.tsx|*.html|*.css)'",
|
|
42
43
|
"gen": "make genproto",
|
|
43
|
-
"test": "
|
|
44
|
+
"test": "npm run test:js && npm run test:go",
|
|
45
|
+
"test:go": "make test",
|
|
46
|
+
"test:js": "npm run build && cd e2e && esbuild e2e.ts --sourcemap --outfile=e2e.js --bundle --platform=node && node ./e2e.js",
|
|
44
47
|
"test:integration": "make integration",
|
|
45
48
|
"integration": "npm run test:integration",
|
|
46
49
|
"lint": "npm run lint:go && npm run lint:js",
|
|
@@ -54,23 +57,21 @@
|
|
|
54
57
|
"singleQuote": true
|
|
55
58
|
},
|
|
56
59
|
"devDependencies": {
|
|
57
|
-
"@types/varint": "^6.0.0",
|
|
58
60
|
"@typescript-eslint/eslint-plugin": "^5.27.1",
|
|
59
61
|
"@typescript-eslint/parser": "^5.27.1",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
+
"depcheck": "^1.4.3",
|
|
63
|
+
"esbuild": "^0.14.46",
|
|
64
|
+
"eslint": "^8.18.0",
|
|
62
65
|
"eslint-config-prettier": "^8.5.0",
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"husky": "^8.0.1",
|
|
66
|
-
"prettier": "^2.7.0",
|
|
66
|
+
"prettier": "^2.7.1",
|
|
67
|
+
"rimraf": "^3.0.2",
|
|
67
68
|
"ts-proto": "^1.115.4",
|
|
68
|
-
"typescript": "^4.7.
|
|
69
|
+
"typescript": "^4.7.4"
|
|
69
70
|
},
|
|
70
71
|
"dependencies": {
|
|
71
|
-
"@libp2p/
|
|
72
|
-
"@libp2p/interface-
|
|
73
|
-
"@libp2p/mplex": "^
|
|
72
|
+
"@libp2p/interface-connection": "^2.0.0",
|
|
73
|
+
"@libp2p/interface-stream-muxer": "^1.0.2",
|
|
74
|
+
"@libp2p/mplex": "^3.0.0",
|
|
74
75
|
"event-iterator": "^2.0.0",
|
|
75
76
|
"isomorphic-ws": "^4.0.1",
|
|
76
77
|
"it-length-prefixed": "^7.0.1",
|
|
@@ -78,10 +79,10 @@
|
|
|
78
79
|
"it-pushable": "^3.0.0",
|
|
79
80
|
"it-stream-types": "^1.0.4",
|
|
80
81
|
"it-ws": "^5.0.2",
|
|
82
|
+
"long": "^5.2.0",
|
|
81
83
|
"patch-package": "^6.4.7",
|
|
82
84
|
"protobufjs": "^6.11.3",
|
|
83
85
|
"rxjs": "^7.5.5",
|
|
84
|
-
"ts-poet": "^4.13.0",
|
|
85
86
|
"ws": "^8.8.0"
|
|
86
87
|
}
|
|
87
88
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Duplex, Sink } from 'it-stream-types'
|
|
2
|
-
import { Conn } from './conn'
|
|
2
|
+
import { Conn, ConnParams } from './conn'
|
|
3
3
|
import { EventIterator } from 'event-iterator'
|
|
4
4
|
import { pipe } from 'it-pipe'
|
|
5
|
+
import { Server } from './server'
|
|
5
6
|
|
|
6
7
|
// BroadcastChannelIterable is a AsyncIterable wrapper for BroadcastChannel.
|
|
7
8
|
export class BroadcastChannelIterable<T> implements Duplex<T> {
|
|
@@ -59,8 +60,12 @@ export class BroadcastChannelConn extends Conn {
|
|
|
59
60
|
// channel is the broadcast channel iterable
|
|
60
61
|
private channel: BroadcastChannelIterable<Uint8Array>
|
|
61
62
|
|
|
62
|
-
constructor(
|
|
63
|
-
|
|
63
|
+
constructor(
|
|
64
|
+
channel: BroadcastChannel,
|
|
65
|
+
server?: Server,
|
|
66
|
+
connParams?: ConnParams
|
|
67
|
+
) {
|
|
68
|
+
super(server, connParams)
|
|
64
69
|
this.channel = new BroadcastChannelIterable<Uint8Array>(channel)
|
|
65
70
|
pipe(this, this.channel, this)
|
|
66
71
|
}
|
package/srpc/client-rpc.go
CHANGED
|
@@ -108,8 +108,6 @@ func (r *ClientRPC) HandlePacket(msg *Packet) error {
|
|
|
108
108
|
return r.HandleCallStart(b.CallStart)
|
|
109
109
|
case *Packet_CallData:
|
|
110
110
|
return r.HandleCallData(b.CallData)
|
|
111
|
-
case *Packet_CallStartResp:
|
|
112
|
-
return r.HandleCallStartResp(b.CallStartResp)
|
|
113
111
|
default:
|
|
114
112
|
return nil
|
|
115
113
|
}
|
|
@@ -127,7 +125,7 @@ func (r *ClientRPC) HandleCallData(pkt *CallData) error {
|
|
|
127
125
|
return ErrCompleted
|
|
128
126
|
}
|
|
129
127
|
|
|
130
|
-
if data := pkt.GetData(); len(data) != 0 {
|
|
128
|
+
if data := pkt.GetData(); len(data) != 0 || pkt.GetDataIsZero() {
|
|
131
129
|
select {
|
|
132
130
|
case <-r.ctx.Done():
|
|
133
131
|
return context.Canceled
|
|
@@ -149,12 +147,6 @@ func (r *ClientRPC) HandleCallData(pkt *CallData) error {
|
|
|
149
147
|
return nil
|
|
150
148
|
}
|
|
151
149
|
|
|
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
150
|
// Close releases any resources held by the ClientRPC.
|
|
159
151
|
// not concurrency safe with HandlePacket.
|
|
160
152
|
func (r *ClientRPC) Close() {
|
package/srpc/client-rpc.ts
CHANGED
|
@@ -1,90 +1,25 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
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>
|
|
1
|
+
import type { CallStart } from './rpcproto.js'
|
|
2
|
+
import { CommonRPC } from './common-rpc.js'
|
|
10
3
|
|
|
11
4
|
// ClientRPC is an ongoing RPC from the client side.
|
|
12
|
-
export class ClientRPC {
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
5
|
+
export class ClientRPC extends CommonRPC {
|
|
6
|
+
constructor(service: string, method: string) {
|
|
7
|
+
super()
|
|
46
8
|
this.service = service
|
|
47
9
|
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
10
|
}
|
|
81
11
|
|
|
82
12
|
// writeCallStart writes the call start packet.
|
|
13
|
+
// if data === undefined and data.length === 0 sends empty data packet.
|
|
83
14
|
public async writeCallStart(data?: Uint8Array) {
|
|
15
|
+
if (!this.service || !this.method) {
|
|
16
|
+
throw new Error('service and method must be set')
|
|
17
|
+
}
|
|
84
18
|
const callStart: CallStart = {
|
|
85
19
|
rpcService: this.service,
|
|
86
20
|
rpcMethod: this.method,
|
|
87
21
|
data: data || new Uint8Array(0),
|
|
22
|
+
dataIsZero: !!data && data.length === 0,
|
|
88
23
|
}
|
|
89
24
|
await this.writePacket({
|
|
90
25
|
body: {
|
|
@@ -94,110 +29,11 @@ export class ClientRPC {
|
|
|
94
29
|
})
|
|
95
30
|
}
|
|
96
31
|
|
|
97
|
-
// writeCallData writes the call data packet.
|
|
98
|
-
public async writeCallData(
|
|
99
|
-
data: Uint8Array,
|
|
100
|
-
complete?: boolean,
|
|
101
|
-
error?: string
|
|
102
|
-
) {
|
|
103
|
-
const callData: CallData = {
|
|
104
|
-
data,
|
|
105
|
-
complete: complete || false,
|
|
106
|
-
error: error || '',
|
|
107
|
-
}
|
|
108
|
-
await this.writePacket({
|
|
109
|
-
body: {
|
|
110
|
-
$case: 'callData',
|
|
111
|
-
callData,
|
|
112
|
-
},
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// writePacket writes a packet to the stream.
|
|
117
|
-
private async writePacket(packet: Packet) {
|
|
118
|
-
this._source.push(packet)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// handleMessage handles an incoming encoded Packet.
|
|
122
|
-
//
|
|
123
|
-
// note: may throw an error if the message was unexpected or invalid.
|
|
124
|
-
public async handleMessage(message: Uint8Array) {
|
|
125
|
-
return this.handlePacket(Packet.decode(message))
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// handlePacket handles an incoming packet.
|
|
129
|
-
public async handlePacket(packet: Partial<Packet>) {
|
|
130
|
-
switch (packet?.body?.$case) {
|
|
131
|
-
case 'callStart':
|
|
132
|
-
return this.handleCallStart(packet.body.callStart)
|
|
133
|
-
case 'callStartResp':
|
|
134
|
-
return this.handleCallStartResp(packet.body.callStartResp)
|
|
135
|
-
case 'callData':
|
|
136
|
-
return this.handleCallData(packet.body.callData)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
32
|
// handleCallStart handles a CallStart packet.
|
|
141
|
-
public async handleCallStart(packet: Partial<CallStart>) {
|
|
33
|
+
public override async handleCallStart(packet: Partial<CallStart>) {
|
|
142
34
|
// we do not implement server -> client RPCs.
|
|
143
35
|
throw new Error(
|
|
144
36
|
`unexpected server to client rpc: ${packet.rpcService}/${packet.rpcMethod}`
|
|
145
37
|
)
|
|
146
38
|
}
|
|
147
|
-
|
|
148
|
-
// handleCallStartResp handles a CallStartResp packet.
|
|
149
|
-
public async handleCallStartResp(packet: Partial<CallStartResp>) {
|
|
150
|
-
if (packet.error && packet.error.length) {
|
|
151
|
-
const err = new Error(packet.error)
|
|
152
|
-
this.onStarted!(err)
|
|
153
|
-
this.onComplete!(err)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// handleCallData handles a CallData packet.
|
|
158
|
-
public async handleCallData(packet: Partial<CallData>) {
|
|
159
|
-
const data = packet.data
|
|
160
|
-
if (this.dataCb && data?.length) {
|
|
161
|
-
await this.dataCb(data)
|
|
162
|
-
}
|
|
163
|
-
if (packet.error && packet.error.length) {
|
|
164
|
-
this.onComplete!(new Error(packet.error))
|
|
165
|
-
} else if (packet.complete) {
|
|
166
|
-
this.onComplete!()
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// close closes the active call if not already completed.
|
|
171
|
-
public async close(err?: Error) {
|
|
172
|
-
if (!this.closed) {
|
|
173
|
-
await this.writeCallData(new Uint8Array(0), true, err ? err.message : '')
|
|
174
|
-
}
|
|
175
|
-
if (!err) {
|
|
176
|
-
err = new Error('call closed')
|
|
177
|
-
}
|
|
178
|
-
this.onComplete!(err)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// _createSink initializes the sink field.
|
|
182
|
-
private _createSink(): Sink<Packet> {
|
|
183
|
-
return async (source) => {
|
|
184
|
-
try {
|
|
185
|
-
for await (const msg of source) {
|
|
186
|
-
await this.handlePacket(msg)
|
|
187
|
-
}
|
|
188
|
-
} catch (err) {
|
|
189
|
-
this.close(err as Error)
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// _createSource initializes the source field.
|
|
195
|
-
private _createSource() {
|
|
196
|
-
return pushable<Packet>({
|
|
197
|
-
objectMode: true,
|
|
198
|
-
onEnd: (err?: Error): void => {
|
|
199
|
-
this.onComplete!(err)
|
|
200
|
-
},
|
|
201
|
-
})
|
|
202
|
-
}
|
|
203
39
|
}
|