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/srpc/client.ts
CHANGED
|
@@ -1,36 +1,16 @@
|
|
|
1
|
-
import { Observable, from as observableFrom } from 'rxjs'
|
|
2
|
-
import type { TsProtoRpc } from './ts-proto-rpc'
|
|
3
|
-
import type { OpenStreamFunc } from './stream'
|
|
4
|
-
import { DataCb, ClientRPC } from './client-rpc'
|
|
5
1
|
import { pipe } from 'it-pipe'
|
|
6
2
|
import { pushable, Pushable } from 'it-pushable'
|
|
3
|
+
import { Observable, from as observableFrom } from 'rxjs'
|
|
4
|
+
|
|
5
|
+
import type { TsProtoRpc } from './ts-proto-rpc.js'
|
|
6
|
+
import type { OpenStreamFunc } from './stream.js'
|
|
7
|
+
import { ClientRPC } from './client-rpc.js'
|
|
7
8
|
import {
|
|
8
9
|
decodePacketSource,
|
|
9
10
|
encodePacketSource,
|
|
10
11
|
parseLengthPrefixTransform,
|
|
11
12
|
prependLengthPrefixTransform,
|
|
12
|
-
} from './packet'
|
|
13
|
-
|
|
14
|
-
// unaryDataCb builds a new unary request data callback.
|
|
15
|
-
function unaryDataCb(resolve: (data: Uint8Array) => void): DataCb {
|
|
16
|
-
return async (data: Uint8Array): Promise<boolean | void> => {
|
|
17
|
-
// resolve the promise
|
|
18
|
-
resolve(data)
|
|
19
|
-
// this is the last data we expect.
|
|
20
|
-
return false
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// streamingDataCb builds a new streaming request data callback.
|
|
25
|
-
/*
|
|
26
|
-
function streamingDataCb(resolve: (data: Uint8Array) => void): DataCb {
|
|
27
|
-
return async (
|
|
28
|
-
data: Uint8Array
|
|
29
|
-
): Promise<boolean | void> => {
|
|
30
|
-
// TODO
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
*/
|
|
13
|
+
} from './packet.js'
|
|
34
14
|
|
|
35
15
|
// writeClientStream registers the subscriber to write the client data stream.
|
|
36
16
|
function writeClientStream(call: ClientRPC, data: Observable<Uint8Array>) {
|
|
@@ -42,26 +22,11 @@ function writeClientStream(call: ClientRPC, data: Observable<Uint8Array>) {
|
|
|
42
22
|
call.close(err)
|
|
43
23
|
},
|
|
44
24
|
complete() {
|
|
45
|
-
call.writeCallData(
|
|
25
|
+
call.writeCallData(undefined, true)
|
|
46
26
|
},
|
|
47
27
|
})
|
|
48
28
|
}
|
|
49
29
|
|
|
50
|
-
// waitCallComplete handles the call complete promise.
|
|
51
|
-
function waitCallComplete(
|
|
52
|
-
call: ClientRPC,
|
|
53
|
-
resolve: (data: Uint8Array) => void,
|
|
54
|
-
reject: (err: Error) => void
|
|
55
|
-
) {
|
|
56
|
-
call
|
|
57
|
-
.waitComplete()
|
|
58
|
-
.catch(reject)
|
|
59
|
-
.finally(() => {
|
|
60
|
-
// ensure we resolve it if no data was ever returned.
|
|
61
|
-
resolve(new Uint8Array())
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
|
|
65
30
|
// Client implements the ts-proto Rpc interface with the drpcproto protocol.
|
|
66
31
|
export class Client implements TsProtoRpc {
|
|
67
32
|
// openConnFn is the open connection function.
|
|
@@ -78,31 +43,31 @@ export class Client implements TsProtoRpc {
|
|
|
78
43
|
method: string,
|
|
79
44
|
data: Uint8Array
|
|
80
45
|
): Promise<Uint8Array> {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
46
|
+
const call = await this.startRpc(service, method, data)
|
|
47
|
+
for await (const data of call.rpcDataSource) {
|
|
48
|
+
call.close()
|
|
49
|
+
return data
|
|
50
|
+
}
|
|
51
|
+
const err = new Error('empty response')
|
|
52
|
+
call.close(err)
|
|
53
|
+
throw err
|
|
89
54
|
}
|
|
90
55
|
|
|
91
56
|
// clientStreamingRequest starts a client side streaming request.
|
|
92
|
-
public clientStreamingRequest(
|
|
57
|
+
public async clientStreamingRequest(
|
|
93
58
|
service: string,
|
|
94
59
|
method: string,
|
|
95
60
|
data: Observable<Uint8Array>
|
|
96
61
|
): Promise<Uint8Array> {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
62
|
+
const call = await this.startRpc(service, method, null)
|
|
63
|
+
writeClientStream(call, data)
|
|
64
|
+
for await (const data of call.rpcDataSource) {
|
|
65
|
+
call.close()
|
|
66
|
+
return data
|
|
67
|
+
}
|
|
68
|
+
const err = new Error('empty response')
|
|
69
|
+
call.close(err)
|
|
70
|
+
throw err
|
|
106
71
|
}
|
|
107
72
|
|
|
108
73
|
// serverStreamingRequest starts a server-side streaming request.
|
|
@@ -113,24 +78,16 @@ export class Client implements TsProtoRpc {
|
|
|
113
78
|
): Observable<Uint8Array> {
|
|
114
79
|
const pushServerData: Pushable<Uint8Array> = pushable()
|
|
115
80
|
const serverData = observableFrom(pushServerData)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
call
|
|
127
|
-
.waitComplete()
|
|
128
|
-
.catch((err: Error) => {
|
|
129
|
-
pushServerData.throw(err)
|
|
130
|
-
})
|
|
131
|
-
.finally(() => {
|
|
132
|
-
pushServerData.end()
|
|
133
|
-
})
|
|
81
|
+
this.startRpc(service, method, data)
|
|
82
|
+
.then(async (call) => {
|
|
83
|
+
try {
|
|
84
|
+
for await (const data of call.rpcDataSource) {
|
|
85
|
+
pushServerData.push(data)
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
pushServerData.throw(err as Error)
|
|
89
|
+
}
|
|
90
|
+
pushServerData.end()
|
|
134
91
|
})
|
|
135
92
|
.catch(pushServerData.throw.bind(pushServerData))
|
|
136
93
|
return serverData
|
|
@@ -144,25 +101,27 @@ export class Client implements TsProtoRpc {
|
|
|
144
101
|
): Observable<Uint8Array> {
|
|
145
102
|
const pushServerData: Pushable<Uint8Array> = pushable()
|
|
146
103
|
const serverData = observableFrom(pushServerData)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
.catch((err: Error) => {
|
|
161
|
-
pushServerData.throw(err)
|
|
162
|
-
})
|
|
163
|
-
.finally(() => {
|
|
164
|
-
pushServerData.end()
|
|
104
|
+
this.startRpc(service, method, null)
|
|
105
|
+
.then(async (call) => {
|
|
106
|
+
try {
|
|
107
|
+
data.subscribe({
|
|
108
|
+
next(value) {
|
|
109
|
+
call.writeCallData(value)
|
|
110
|
+
},
|
|
111
|
+
error(err) {
|
|
112
|
+
call.close(err)
|
|
113
|
+
},
|
|
114
|
+
complete() {
|
|
115
|
+
call.close()
|
|
116
|
+
},
|
|
165
117
|
})
|
|
118
|
+
for await (const data of call.rpcDataSource) {
|
|
119
|
+
pushServerData.push(data)
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
pushServerData.throw(err as Error)
|
|
123
|
+
}
|
|
124
|
+
pushServerData.end()
|
|
166
125
|
})
|
|
167
126
|
.catch(pushServerData.throw.bind(pushServerData))
|
|
168
127
|
return serverData
|
|
@@ -170,14 +129,14 @@ export class Client implements TsProtoRpc {
|
|
|
170
129
|
|
|
171
130
|
// startRpc is a common utility function to begin a rpc call.
|
|
172
131
|
// throws any error starting the rpc call
|
|
132
|
+
// if data == null and data.length == 0, sends a separate data packet.
|
|
173
133
|
private async startRpc(
|
|
174
134
|
rpcService: string,
|
|
175
135
|
rpcMethod: string,
|
|
176
|
-
data: Uint8Array | null
|
|
177
|
-
dataCb: DataCb
|
|
136
|
+
data: Uint8Array | null
|
|
178
137
|
): Promise<ClientRPC> {
|
|
179
138
|
const conn = await this.openConnFn()
|
|
180
|
-
const call = new ClientRPC(rpcService, rpcMethod
|
|
139
|
+
const call = new ClientRPC(rpcService, rpcMethod)
|
|
181
140
|
pipe(
|
|
182
141
|
conn,
|
|
183
142
|
parseLengthPrefixTransform(),
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { Source, Sink } from 'it-stream-types'
|
|
2
|
+
import { pushable } from 'it-pushable'
|
|
3
|
+
|
|
4
|
+
import type { CallData, CallStart } from './rpcproto.js'
|
|
5
|
+
import { Packet } from './rpcproto.js'
|
|
6
|
+
|
|
7
|
+
// CommonRPC is common logic between server and client RPCs.
|
|
8
|
+
export class CommonRPC {
|
|
9
|
+
// sink is the data sink for incoming messages.
|
|
10
|
+
public sink: Sink<Packet>
|
|
11
|
+
// source is the packet source for outgoing Packets.
|
|
12
|
+
public source: AsyncIterable<Packet>
|
|
13
|
+
// _source is used to write to the source.
|
|
14
|
+
private readonly _source: {
|
|
15
|
+
push: (val: Packet) => void
|
|
16
|
+
end: (err?: Error) => void
|
|
17
|
+
}
|
|
18
|
+
// rpcDataSource emits incoming client RPC messages to the caller.
|
|
19
|
+
public readonly rpcDataSource: Source<Uint8Array>
|
|
20
|
+
// _rpcDataSource is used to write to the rpc message source.
|
|
21
|
+
private readonly _rpcDataSource: {
|
|
22
|
+
push: (val: Uint8Array) => void
|
|
23
|
+
end: (err?: Error) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// service is the rpc service
|
|
27
|
+
protected service?: string
|
|
28
|
+
// method is the rpc method
|
|
29
|
+
protected method?: string
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
this.sink = this._createSink()
|
|
33
|
+
|
|
34
|
+
const sourcev = this._createSource()
|
|
35
|
+
this.source = sourcev
|
|
36
|
+
this._source = sourcev
|
|
37
|
+
|
|
38
|
+
const rpcDataSource = this._createRpcDataSource()
|
|
39
|
+
this.rpcDataSource = rpcDataSource
|
|
40
|
+
this._rpcDataSource = rpcDataSource
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// writeCallData writes the call data packet.
|
|
44
|
+
public async writeCallData(
|
|
45
|
+
data?: Uint8Array,
|
|
46
|
+
complete?: boolean,
|
|
47
|
+
error?: string
|
|
48
|
+
) {
|
|
49
|
+
const callData: CallData = {
|
|
50
|
+
data: data || new Uint8Array(0),
|
|
51
|
+
dataIsZero: !!data && data.length === 0,
|
|
52
|
+
complete: complete || false,
|
|
53
|
+
error: error || '',
|
|
54
|
+
}
|
|
55
|
+
await this.writePacket({
|
|
56
|
+
body: {
|
|
57
|
+
$case: 'callData',
|
|
58
|
+
callData,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// writePacket writes a packet to the stream.
|
|
64
|
+
protected async writePacket(packet: Packet) {
|
|
65
|
+
this._source.push(packet)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// handleMessage handles an incoming encoded Packet.
|
|
69
|
+
//
|
|
70
|
+
// note: closes the stream if any error is thrown.
|
|
71
|
+
public async handleMessage(message: Uint8Array) {
|
|
72
|
+
return this.handlePacket(Packet.decode(message))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// handlePacket handles an incoming packet.
|
|
76
|
+
//
|
|
77
|
+
// note: closes the stream if any error is thrown.
|
|
78
|
+
public async handlePacket(packet: Partial<Packet>) {
|
|
79
|
+
try {
|
|
80
|
+
switch (packet?.body?.$case) {
|
|
81
|
+
case 'callStart':
|
|
82
|
+
await this.handleCallStart(packet.body.callStart)
|
|
83
|
+
break
|
|
84
|
+
case 'callData':
|
|
85
|
+
await this.handleCallData(packet.body.callData)
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
let asError = err as Error
|
|
90
|
+
if (!asError?.message) {
|
|
91
|
+
asError = new Error('error handling packet')
|
|
92
|
+
}
|
|
93
|
+
this.close(asError)
|
|
94
|
+
throw asError
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// handleCallStart handles a CallStart packet.
|
|
99
|
+
public async handleCallStart(packet: Partial<CallStart>) {
|
|
100
|
+
// no-op
|
|
101
|
+
throw new Error(
|
|
102
|
+
`unexpected call start: ${packet.rpcService}/${packet.rpcMethod}`
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// pushRpcData pushes incoming rpc data to the rpc data source.
|
|
107
|
+
protected pushRpcData(
|
|
108
|
+
data: Uint8Array | undefined,
|
|
109
|
+
dataIsZero: boolean | undefined
|
|
110
|
+
) {
|
|
111
|
+
if (dataIsZero) {
|
|
112
|
+
if (!data || data.length !== 0) {
|
|
113
|
+
data = new Uint8Array(0)
|
|
114
|
+
}
|
|
115
|
+
} else if (!data || data.length === 0) {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
this._rpcDataSource.push(data)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// handleCallData handles a CallData packet.
|
|
122
|
+
public async handleCallData(packet: Partial<CallData>) {
|
|
123
|
+
if (!this.service || !this.method) {
|
|
124
|
+
throw new Error('call start must be sent before call data')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.pushRpcData(packet.data, packet.dataIsZero)
|
|
128
|
+
if (packet.error) {
|
|
129
|
+
this._rpcDataSource.end(new Error(packet.error))
|
|
130
|
+
} else if (packet.complete) {
|
|
131
|
+
this._rpcDataSource.end()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// close marks the call as complete, optionally with an error.
|
|
136
|
+
public async close(err?: Error) {
|
|
137
|
+
this._rpcDataSource.end(err)
|
|
138
|
+
this._source.end(err)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// _createSink returns a value for the sink field.
|
|
142
|
+
private _createSink(): Sink<Packet> {
|
|
143
|
+
return async (source) => {
|
|
144
|
+
try {
|
|
145
|
+
for await (const msg of source) {
|
|
146
|
+
await this.handlePacket(msg)
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const anyErr = err as any
|
|
150
|
+
if (anyErr?.code !== 'ERR_MPLEX_STREAM_RESET') {
|
|
151
|
+
this.close(err as Error)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
this._rpcDataSource.end()
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// _createSource returns a value for the source field.
|
|
159
|
+
private _createSource() {
|
|
160
|
+
return pushable<Packet>({
|
|
161
|
+
objectMode: true,
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// _createRpcDataSource returns a value for the rpc data source field.
|
|
166
|
+
private _createRpcDataSource() {
|
|
167
|
+
return pushable<Uint8Array>({
|
|
168
|
+
objectMode: true,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
}
|
package/srpc/conn.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { Stream } from '@libp2p/interface-connection'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
StreamMuxer,
|
|
4
|
+
StreamMuxerFactory,
|
|
5
|
+
} from '@libp2p/interface-stream-muxer'
|
|
3
6
|
import type { Duplex } from 'it-stream-types'
|
|
4
7
|
import { Mplex } from '@libp2p/mplex'
|
|
5
|
-
|
|
6
|
-
import {
|
|
8
|
+
|
|
9
|
+
import type { OpenStreamFunc, Stream as SRPCStream } from './stream.js'
|
|
10
|
+
import { Client } from './client.js'
|
|
7
11
|
|
|
8
12
|
// ConnParams are parameters that can be passed to the Conn constructor.
|
|
9
13
|
export interface ConnParams {
|
|
@@ -11,12 +15,27 @@ export interface ConnParams {
|
|
|
11
15
|
muxerFactory?: StreamMuxerFactory
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
// StreamHandler handles incoming streams.
|
|
19
|
+
// Implemented by Server.
|
|
20
|
+
export interface StreamHandler {
|
|
21
|
+
// handleStream handles an incoming stream.
|
|
22
|
+
handleStream(strm: Duplex<Uint8Array>): Promise<void>
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
// Conn implements a generic connection with a two-way stream.
|
|
26
|
+
// Implements the client by opening streams with the remote.
|
|
27
|
+
// Implements the server by handling incoming streams.
|
|
28
|
+
// If the server is unset, rejects any incoming streams.
|
|
15
29
|
export class Conn implements Duplex<Uint8Array> {
|
|
16
30
|
// muxer is the mplex stream muxer.
|
|
17
31
|
private muxer: StreamMuxer
|
|
32
|
+
// server is the server side, if set.
|
|
33
|
+
private server?: StreamHandler
|
|
18
34
|
|
|
19
|
-
constructor(connParams?: ConnParams) {
|
|
35
|
+
constructor(server?: StreamHandler, connParams?: ConnParams) {
|
|
36
|
+
if (server) {
|
|
37
|
+
this.server = server
|
|
38
|
+
}
|
|
20
39
|
let muxerFactory = connParams?.muxerFactory
|
|
21
40
|
if (!muxerFactory) {
|
|
22
41
|
muxerFactory = new Mplex()
|
|
@@ -51,8 +70,17 @@ export class Conn implements Duplex<Uint8Array> {
|
|
|
51
70
|
return this.muxer.newStream()
|
|
52
71
|
}
|
|
53
72
|
|
|
73
|
+
// buildOpenStreamFunc returns openStream bound to this conn.
|
|
74
|
+
public buildOpenStreamFunc(): OpenStreamFunc {
|
|
75
|
+
return this.openStream.bind(this)
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
// handleIncomingStream handles an incoming stream.
|
|
55
79
|
private handleIncomingStream(strm: Stream) {
|
|
56
|
-
|
|
80
|
+
const server = this.server
|
|
81
|
+
if (!server) {
|
|
82
|
+
return strm.abort(new Error('server not implemented'))
|
|
83
|
+
}
|
|
84
|
+
server.handleStream(strm)
|
|
57
85
|
}
|
|
58
86
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { MessageDefinition } from './message'
|
|
2
|
+
|
|
3
|
+
// Definition describes the service definitions generated by ts-proto.
|
|
4
|
+
// use --ts_proto_opt=outputServices=default,outputServices=generic-definitions
|
|
5
|
+
export interface Definition {
|
|
6
|
+
// name is the name of the service.
|
|
7
|
+
// e.x.: Echo
|
|
8
|
+
name: string
|
|
9
|
+
// fullName is the fully qualified name of the service
|
|
10
|
+
// e.x.: echoer.Echoer
|
|
11
|
+
fullName: string
|
|
12
|
+
// methods is the set of RPC methods.
|
|
13
|
+
methods: { [id: string]: MethodDefinition<unknown, unknown> }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// MethodDefinition describes the method definitions generated by ts-proto.
|
|
17
|
+
// use --ts_proto_opt=outputServices=default,outputServices=generic-definitions
|
|
18
|
+
export interface MethodDefinition<RequestType, ResponseType> {
|
|
19
|
+
// name is the name and function name of the method.
|
|
20
|
+
// e.x.: Echo
|
|
21
|
+
name: string
|
|
22
|
+
// requestType is the object used for the request.
|
|
23
|
+
requestType: MessageDefinition<RequestType>
|
|
24
|
+
// requestStream indicates the request is a stream.
|
|
25
|
+
requestStream: boolean
|
|
26
|
+
// responseType is the object used for the response.
|
|
27
|
+
responseType: MessageDefinition<ResponseType>
|
|
28
|
+
// responseStream indicates the response is a stream.
|
|
29
|
+
responseStream: boolean
|
|
30
|
+
}
|
package/srpc/handler.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Sink, Source } from 'it-stream-types'
|
|
2
|
+
import { pipe } from 'it-pipe'
|
|
3
|
+
import { pushable } from 'it-pushable'
|
|
4
|
+
import { Observable, from as observableFrom } from 'rxjs'
|
|
5
|
+
|
|
6
|
+
import { Definition, MethodDefinition } from './definition.js'
|
|
7
|
+
import {
|
|
8
|
+
buildDecodeMessageTransform,
|
|
9
|
+
buildEncodeMessageTransform,
|
|
10
|
+
} from './message.js'
|
|
11
|
+
|
|
12
|
+
// InvokeFn describes an SRPC call method invoke function.
|
|
13
|
+
export type InvokeFn = (
|
|
14
|
+
dataSource: Source<Uint8Array>,
|
|
15
|
+
dataSink: Sink<Uint8Array>
|
|
16
|
+
) => Promise<void>
|
|
17
|
+
|
|
18
|
+
// Handler describes a SRPC call handler implementation.
|
|
19
|
+
export interface Handler {
|
|
20
|
+
// getServiceID returns the ID of the service.
|
|
21
|
+
getServiceID(): string
|
|
22
|
+
// getMethodIDs returns the IDs of the methods.
|
|
23
|
+
getMethodIDs(): string[]
|
|
24
|
+
// lookupMethod looks up the method matching the service & method ID.
|
|
25
|
+
// returns null if not found.
|
|
26
|
+
lookupMethod(serviceID: string, methodID: string): Promise<InvokeFn | null>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// MethodMap is a map from method id to invoke function.
|
|
30
|
+
export type MethodMap = { [name: string]: InvokeFn }
|
|
31
|
+
|
|
32
|
+
// StaticHandler is a handler with a definition and implementation.
|
|
33
|
+
export class StaticHandler implements Handler {
|
|
34
|
+
// service is the service id
|
|
35
|
+
private service: string
|
|
36
|
+
// methods is the map of method to invoke fn
|
|
37
|
+
private methods: MethodMap
|
|
38
|
+
|
|
39
|
+
constructor(serviceID: string, methods: MethodMap) {
|
|
40
|
+
this.service = serviceID
|
|
41
|
+
this.methods = methods
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// getServiceID returns the ID of the service.
|
|
45
|
+
public getServiceID(): string {
|
|
46
|
+
return this.service
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// getMethodIDs returns the IDs of the methods.
|
|
50
|
+
public getMethodIDs(): string[] {
|
|
51
|
+
return Object.keys(this.methods)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// lookupMethod looks up the method matching the service & method ID.
|
|
55
|
+
// returns null if not found.
|
|
56
|
+
public async lookupMethod(
|
|
57
|
+
serviceID: string,
|
|
58
|
+
methodID: string
|
|
59
|
+
): Promise<InvokeFn | null> {
|
|
60
|
+
if (serviceID && serviceID !== this.service) {
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
return this.methods[methodID] || null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// MethodProto is a function which matches one of the RPC signatures.
|
|
68
|
+
type MethodProto =
|
|
69
|
+
| ((request: unknown) => Promise<unknown>)
|
|
70
|
+
| ((request: unknown) => Observable<unknown>)
|
|
71
|
+
| ((request: Observable<unknown>) => Promise<unknown>)
|
|
72
|
+
| ((request: Observable<unknown>) => Observable<unknown>)
|
|
73
|
+
|
|
74
|
+
// createInvokeFn builds an InvokeFn from a method definition and a function prototype.
|
|
75
|
+
export function createInvokeFn(
|
|
76
|
+
methodInfo: MethodDefinition<unknown, unknown>,
|
|
77
|
+
methodProto: MethodProto
|
|
78
|
+
): InvokeFn {
|
|
79
|
+
const requestDecode = buildDecodeMessageTransform(methodInfo.requestType)
|
|
80
|
+
return async (dataSource: Source<Uint8Array>, dataSink: Sink<Uint8Array>) => {
|
|
81
|
+
// responseSink is a Sink for response messages.
|
|
82
|
+
const responseSink = pushable<unknown>({
|
|
83
|
+
objectMode: true,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// pipe responseSink to dataSink.
|
|
87
|
+
const responsePipe = pipe(
|
|
88
|
+
responseSink,
|
|
89
|
+
buildEncodeMessageTransform(methodInfo.responseType),
|
|
90
|
+
dataSink
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
// build the request argument.
|
|
94
|
+
let requestArg: any
|
|
95
|
+
if (methodInfo.requestStream) {
|
|
96
|
+
// requestSource is a Source of decoded request messages.
|
|
97
|
+
const requestSource = pipe(dataSource, requestDecode)
|
|
98
|
+
// convert the request data source into an Observable<T>
|
|
99
|
+
requestArg = observableFrom(requestSource)
|
|
100
|
+
} else {
|
|
101
|
+
// receive a single message for the argument.
|
|
102
|
+
const requestRx = requestDecode(dataSource)
|
|
103
|
+
for await (const msg of requestRx) {
|
|
104
|
+
requestArg = msg
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Call the implementation.
|
|
110
|
+
try {
|
|
111
|
+
const responseObj = methodProto(requestArg)
|
|
112
|
+
if (!responseObj) {
|
|
113
|
+
throw new Error('return value was undefined')
|
|
114
|
+
}
|
|
115
|
+
if (methodInfo.responseStream) {
|
|
116
|
+
const responseObs = responseObj as Observable<unknown>
|
|
117
|
+
if (!responseObs.subscribe) {
|
|
118
|
+
throw new Error('expected return value to be an Observable')
|
|
119
|
+
}
|
|
120
|
+
return new Promise<void>((resolve, reject) => {
|
|
121
|
+
responseObs.subscribe({
|
|
122
|
+
next(value) {
|
|
123
|
+
responseSink.push(value)
|
|
124
|
+
},
|
|
125
|
+
error: (err: any) => {
|
|
126
|
+
responseSink.throw(err)
|
|
127
|
+
reject(err)
|
|
128
|
+
},
|
|
129
|
+
complete: () => {
|
|
130
|
+
responseSink.end()
|
|
131
|
+
responsePipe.finally(() => {
|
|
132
|
+
resolve()
|
|
133
|
+
})
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
} else {
|
|
138
|
+
const responsePromise = responseObj as Promise<unknown>
|
|
139
|
+
if (!responsePromise.then) {
|
|
140
|
+
throw new Error('expected return value to be a Promise')
|
|
141
|
+
}
|
|
142
|
+
const responseMsg = await responsePromise
|
|
143
|
+
if (!responseMsg) {
|
|
144
|
+
throw new Error('expected non-empty response object')
|
|
145
|
+
}
|
|
146
|
+
responseSink.push(responseMsg)
|
|
147
|
+
responseSink.end()
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
let asError = err as Error
|
|
151
|
+
if (!asError?.message) {
|
|
152
|
+
asError = new Error('error calling implementation: ' + err)
|
|
153
|
+
}
|
|
154
|
+
// mux will return the error to the rpc caller.
|
|
155
|
+
throw asError
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// createHandler creates a handler from a definition and an implementation.
|
|
161
|
+
export function createHandler(definition: Definition, impl: any): Handler {
|
|
162
|
+
const methodMap: MethodMap = {}
|
|
163
|
+
for (const methodInfo of Object.values(definition.methods)) {
|
|
164
|
+
const methodName = methodInfo.name
|
|
165
|
+
let methodProto: MethodProto = impl[methodName]
|
|
166
|
+
if (!methodProto) {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
methodProto = methodProto.bind(impl)
|
|
170
|
+
methodMap[methodName] = createInvokeFn(methodInfo, methodProto)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return new StaticHandler(definition.fullName, methodMap)
|
|
174
|
+
}
|
package/srpc/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export
|
|
3
|
-
export {
|
|
1
|
+
export type { OpenStreamFunc } from './stream.js'
|
|
2
|
+
export { Client } from './client.js'
|
|
3
|
+
export { Server } from './server.js'
|
|
4
|
+
export { Conn } from './conn.js'
|
|
5
|
+
export { Handler, InvokeFn, createHandler, createInvokeFn } from './handler.js'
|
|
6
|
+
export { Mux, createMux } from './mux.js'
|
|
7
|
+
export { WebSocketConn } from './websocket.js'
|