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.
Files changed (73) hide show
  1. package/Makefile +1 -0
  2. package/README.md +24 -5
  3. package/dist/echo/client-test.d.ts +2 -0
  4. package/dist/echo/client-test.js +35 -0
  5. package/dist/echo/echo.d.ts +124 -0
  6. package/dist/echo/echo.js +42 -0
  7. package/dist/echo/index.d.ts +3 -0
  8. package/dist/echo/index.js +3 -0
  9. package/dist/echo/server.d.ts +8 -0
  10. package/dist/echo/server.js +50 -0
  11. package/dist/echo/sever.d.ts +0 -0
  12. package/dist/echo/sever.js +1 -0
  13. package/dist/srpc/broadcast-channel.d.ts +3 -2
  14. package/dist/srpc/broadcast-channel.js +2 -2
  15. package/dist/srpc/client-rpc.d.ts +4 -28
  16. package/dist/srpc/client-rpc.js +9 -152
  17. package/dist/srpc/client.d.ts +2 -2
  18. package/dist/srpc/client.js +56 -84
  19. package/dist/srpc/common-rpc.d.ts +24 -0
  20. package/dist/srpc/common-rpc.js +140 -0
  21. package/dist/srpc/conn.d.ts +8 -3
  22. package/dist/srpc/conn.js +19 -3
  23. package/dist/srpc/definition.d.ts +15 -0
  24. package/dist/srpc/definition.js +1 -0
  25. package/dist/srpc/handler.d.ts +24 -0
  26. package/dist/srpc/handler.js +123 -0
  27. package/dist/srpc/index.d.ts +7 -3
  28. package/dist/srpc/index.js +6 -2
  29. package/dist/srpc/message.d.ts +11 -0
  30. package/dist/srpc/message.js +32 -0
  31. package/dist/srpc/mux.d.ts +11 -0
  32. package/dist/srpc/mux.js +36 -0
  33. package/dist/srpc/packet.d.ts +4 -4
  34. package/dist/srpc/packet.js +38 -27
  35. package/dist/srpc/rpcproto.d.ts +18 -43
  36. package/dist/srpc/rpcproto.js +37 -78
  37. package/dist/srpc/server-rpc.d.ts +11 -0
  38. package/dist/srpc/server-rpc.js +55 -0
  39. package/dist/srpc/server.d.ts +14 -0
  40. package/dist/srpc/server.js +31 -0
  41. package/dist/srpc/websocket.d.ts +3 -2
  42. package/dist/srpc/websocket.js +4 -4
  43. package/e2e/README.md +10 -0
  44. package/e2e/e2e.ts +27 -0
  45. package/echo/client-test.ts +41 -0
  46. package/echo/echo.ts +45 -0
  47. package/echo/index.ts +3 -0
  48. package/echo/server.ts +57 -0
  49. package/echo/sever.ts +0 -0
  50. package/integration/integration.bash +1 -1
  51. package/integration/integration.ts +10 -42
  52. package/package.json +18 -17
  53. package/srpc/broadcast-channel.ts +8 -3
  54. package/srpc/client-rpc.go +1 -9
  55. package/srpc/client-rpc.ts +11 -175
  56. package/srpc/client.ts +58 -99
  57. package/srpc/common-rpc.ts +171 -0
  58. package/srpc/conn.ts +33 -5
  59. package/srpc/definition.ts +30 -0
  60. package/srpc/handler.ts +174 -0
  61. package/srpc/index.ts +7 -3
  62. package/srpc/message.ts +60 -0
  63. package/srpc/mux.ts +56 -0
  64. package/srpc/packet.go +4 -11
  65. package/srpc/packet.ts +44 -30
  66. package/srpc/rpcproto.pb.go +54 -118
  67. package/srpc/rpcproto.proto +7 -12
  68. package/srpc/rpcproto.ts +38 -101
  69. package/srpc/rpcproto_vtproto.pb.go +58 -210
  70. package/srpc/server-rpc.go +5 -10
  71. package/srpc/server-rpc.ts +65 -0
  72. package/srpc/server.ts +56 -0
  73. 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(new Uint8Array(0), true)
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
- return new Promise<Uint8Array>((resolve, reject) => {
82
- const dataCb = unaryDataCb(resolve)
83
- this.startRpc(service, method, data, dataCb)
84
- .then((call) => {
85
- waitCallComplete(call, resolve, reject)
86
- })
87
- .catch(reject)
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
- return new Promise<Uint8Array>((resolve, reject) => {
98
- const dataCb = unaryDataCb(resolve)
99
- this.startRpc(service, method, null, dataCb)
100
- .then((call) => {
101
- writeClientStream(call, data)
102
- waitCallComplete(call, resolve, reject)
103
- })
104
- .catch(reject)
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
- const dataCb: DataCb = async (
117
- data: Uint8Array
118
- ): Promise<boolean | void> => {
119
- // push the message to the observable
120
- pushServerData.push(data)
121
- // expect more messages
122
- return true
123
- }
124
- this.startRpc(service, method, data, dataCb)
125
- .then((call) => {
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
- const dataCb: DataCb = async (
148
- data: Uint8Array
149
- ): Promise<boolean | void> => {
150
- // push the message to the observable
151
- pushServerData.push(data)
152
- // expect more messages
153
- return true
154
- }
155
- this.startRpc(service, method, null, dataCb)
156
- .then((call) => {
157
- writeClientStream(call, data)
158
- call
159
- .waitComplete()
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, dataCb)
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 { StreamMuxer, StreamMuxerFactory } from '@libp2p/interface-stream-muxer'
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
- import type { Stream as SRPCStream } from './stream'
6
- import { Client } from './client'
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
- strm.abort(new Error('server -> client streams not implemented'))
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
+ }
@@ -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 { Client } from './client'
2
- export type { OpenStreamFunc } from './stream'
3
- export { WebSocketConn } from './websocket'
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'