starpc 0.23.2 → 0.24.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/dist/e2e/e2e.js +6 -5
- package/dist/rpcstream/rpcstream.d.ts +5 -5
- package/dist/rpcstream/rpcstream.js +1 -1
- package/dist/srpc/broadcast-channel.d.ts +9 -13
- package/dist/srpc/broadcast-channel.js +64 -30
- package/dist/srpc/client.js +11 -4
- package/dist/srpc/common-rpc.d.ts +4 -2
- package/dist/srpc/common-rpc.js +19 -7
- package/dist/srpc/conn.d.ts +16 -12
- package/dist/srpc/conn.js +35 -34
- package/dist/srpc/handler.d.ts +1 -4
- package/dist/srpc/handler.js +1 -67
- package/dist/srpc/index.d.ts +7 -7
- package/dist/srpc/index.js +5 -5
- package/dist/srpc/invoker.d.ts +4 -0
- package/dist/srpc/invoker.js +66 -0
- package/dist/srpc/message-port.d.ts +7 -9
- package/dist/srpc/message-port.js +56 -13
- package/dist/srpc/pushable.d.ts +1 -1
- package/dist/srpc/pushable.js +2 -14
- package/dist/srpc/server-rpc.js +1 -0
- package/dist/srpc/server.d.ts +2 -6
- package/dist/srpc/server.js +4 -17
- package/dist/srpc/stream.d.ts +5 -3
- package/dist/srpc/stream.js +16 -1
- package/dist/srpc/websocket.d.ts +2 -2
- package/dist/srpc/websocket.js +2 -2
- package/e2e/e2e.ts +8 -5
- package/package.json +1 -1
- package/srpc/broadcast-channel.ts +78 -47
- package/srpc/client.ts +10 -3
- package/srpc/common-rpc.go +1 -10
- package/srpc/common-rpc.ts +23 -8
- package/srpc/conn.ts +54 -58
- package/srpc/handler.ts +2 -92
- package/srpc/index.ts +18 -7
- package/srpc/invoker.ts +92 -0
- package/srpc/message-port.ts +71 -21
- package/srpc/open-stream-ctr.ts +2 -2
- package/srpc/pushable.ts +5 -17
- package/srpc/server-rpc.ts +1 -0
- package/srpc/server.ts +5 -37
- package/srpc/stream.ts +32 -7
- package/srpc/websocket.ts +2 -2
- package/dist/srpc/conn-stream.d.ts +0 -8
- package/dist/srpc/conn-stream.js +0 -16
- package/srpc/conn-stream.ts +0 -24
package/srpc/common-rpc.ts
CHANGED
|
@@ -3,13 +3,14 @@ import { pushable } from 'it-pushable'
|
|
|
3
3
|
|
|
4
4
|
import type { CallData, CallStart } from './rpcproto.pb.js'
|
|
5
5
|
import { Packet } from './rpcproto.pb.js'
|
|
6
|
+
import { ERR_RPC_ABORT } from './errors.js'
|
|
6
7
|
|
|
7
8
|
// CommonRPC is common logic between server and client RPCs.
|
|
8
9
|
export class CommonRPC {
|
|
9
10
|
// sink is the data sink for incoming messages.
|
|
10
|
-
public sink: Sink<Source<Packet>>
|
|
11
|
+
public readonly sink: Sink<Source<Packet>>
|
|
11
12
|
// source is the packet source for outgoing Packets.
|
|
12
|
-
public source: AsyncIterable<Packet>
|
|
13
|
+
public readonly source: AsyncIterable<Packet>
|
|
13
14
|
// rpcDataSource is the source for rpc packets.
|
|
14
15
|
public readonly rpcDataSource: AsyncIterable<Uint8Array>
|
|
15
16
|
|
|
@@ -95,6 +96,7 @@ export class CommonRPC {
|
|
|
95
96
|
//
|
|
96
97
|
// note: closes the stream if any error is thrown.
|
|
97
98
|
public async handlePacket(packet: Partial<Packet>) {
|
|
99
|
+
// console.log('handlePacket', packet)
|
|
98
100
|
try {
|
|
99
101
|
switch (packet?.body?.$case) {
|
|
100
102
|
case 'callStart':
|
|
@@ -103,6 +105,11 @@ export class CommonRPC {
|
|
|
103
105
|
case 'callData':
|
|
104
106
|
await this.handleCallData(packet.body.callData)
|
|
105
107
|
break
|
|
108
|
+
case 'callCancel':
|
|
109
|
+
if (packet.body.callCancel) {
|
|
110
|
+
await this.handleCallCancel()
|
|
111
|
+
}
|
|
112
|
+
break
|
|
106
113
|
}
|
|
107
114
|
} catch (err) {
|
|
108
115
|
let asError = err as Error
|
|
@@ -151,19 +158,28 @@ export class CommonRPC {
|
|
|
151
158
|
}
|
|
152
159
|
}
|
|
153
160
|
|
|
161
|
+
// handleCallCancel handles a CallCancel packet.
|
|
162
|
+
public async handleCallCancel() {
|
|
163
|
+
this.close(new Error(ERR_RPC_ABORT))
|
|
164
|
+
}
|
|
165
|
+
|
|
154
166
|
// close closes the call, optionally with an error.
|
|
155
167
|
public async close(err?: Error) {
|
|
156
168
|
if (this.closed) {
|
|
157
169
|
return
|
|
158
170
|
}
|
|
159
171
|
this.closed = true
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
// note: don't pass error to _source here.
|
|
173
|
+
if (err) {
|
|
162
174
|
await this.writeCallCancel()
|
|
163
|
-
} finally {
|
|
164
|
-
// note: don't pass error to _source here.
|
|
165
|
-
this._source.end()
|
|
166
175
|
}
|
|
176
|
+
this._source.end()
|
|
177
|
+
this._rpcDataSource.end(err)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// closeWrite closes the call for writing.
|
|
181
|
+
public closeWrite() {
|
|
182
|
+
this._source.end()
|
|
167
183
|
}
|
|
168
184
|
|
|
169
185
|
// _createSink returns a value for the sink field.
|
|
@@ -181,7 +197,6 @@ export class CommonRPC {
|
|
|
181
197
|
await this.handlePacket(msg)
|
|
182
198
|
}
|
|
183
199
|
}
|
|
184
|
-
this._rpcDataSource.end()
|
|
185
200
|
} catch (err) {
|
|
186
201
|
this.close(err as Error)
|
|
187
202
|
}
|
package/srpc/conn.ts
CHANGED
|
@@ -1,33 +1,31 @@
|
|
|
1
|
-
import { yamux } from '@chainsafe/libp2p-yamux'
|
|
1
|
+
import { YamuxMuxerInit, yamux } from '@chainsafe/libp2p-yamux'
|
|
2
2
|
import type {
|
|
3
3
|
Direction,
|
|
4
4
|
Stream,
|
|
5
5
|
StreamMuxer,
|
|
6
6
|
StreamMuxerFactory,
|
|
7
7
|
} from '@libp2p/interface'
|
|
8
|
-
import {
|
|
9
|
-
import type { Duplex, Source } from 'it-stream-types'
|
|
8
|
+
import type { Duplex } from 'it-stream-types'
|
|
10
9
|
import { Uint8ArrayList } from 'uint8arraylist'
|
|
11
|
-
import isPromise from 'is-promise'
|
|
12
|
-
import { pushable, Pushable } from 'it-pushable'
|
|
13
10
|
import { defaultLogger } from '@libp2p/logger'
|
|
14
11
|
|
|
15
|
-
import type { OpenStreamFunc, Stream as SRPCStream } from './stream.js'
|
|
16
|
-
import { Client } from './client.js'
|
|
17
|
-
import { combineUint8ArrayListTransform } from './array-list.js'
|
|
18
12
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
streamToPacketStream,
|
|
14
|
+
type OpenStreamFunc,
|
|
15
|
+
type PacketStream,
|
|
16
|
+
} from './stream.js'
|
|
17
|
+
import { Client } from './client.js'
|
|
23
18
|
|
|
24
|
-
// ConnParams are parameters that can be passed to the
|
|
25
|
-
export interface
|
|
19
|
+
// ConnParams are parameters that can be passed to the StreamConn constructor.
|
|
20
|
+
export interface StreamConnParams {
|
|
26
21
|
// muxerFactory overrides using the default yamux factory.
|
|
27
22
|
muxerFactory?: StreamMuxerFactory
|
|
28
23
|
// direction is the muxer connection direction.
|
|
29
24
|
// defaults to outbound (client).
|
|
30
25
|
direction?: Direction
|
|
26
|
+
// yamuxParams are parameters to pass to yamux.
|
|
27
|
+
// only used if muxerFactory is unset
|
|
28
|
+
yamuxParams?: YamuxMuxerInit
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
// StreamHandler handles incoming streams.
|
|
@@ -35,52 +33,35 @@ export interface ConnParams {
|
|
|
35
33
|
export interface StreamHandler {
|
|
36
34
|
// handlePacketStream handles an incoming Uint8Array duplex.
|
|
37
35
|
// the stream has one Uint8Array per packet w/o length prefix.
|
|
38
|
-
handlePacketStream(strm:
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// streamToSRPCStream converts a Stream to a SRPCStream.
|
|
42
|
-
// uses length-prefix for packet framing
|
|
43
|
-
export function streamToSRPCStream(
|
|
44
|
-
stream: Duplex<
|
|
45
|
-
AsyncIterable<Uint8ArrayList>,
|
|
46
|
-
Source<Uint8ArrayList | Uint8Array>,
|
|
47
|
-
Promise<void>
|
|
48
|
-
>,
|
|
49
|
-
): SRPCStream {
|
|
50
|
-
const pushSink: Pushable<Uint8Array> = pushable({ objectMode: true })
|
|
51
|
-
pipe(pushSink, prependLengthPrefixTransform(), stream.sink)
|
|
52
|
-
return {
|
|
53
|
-
source: pipe(
|
|
54
|
-
stream,
|
|
55
|
-
parseLengthPrefixTransform(),
|
|
56
|
-
combineUint8ArrayListTransform(),
|
|
57
|
-
),
|
|
58
|
-
sink: buildPushableSink(pushSink),
|
|
59
|
-
}
|
|
36
|
+
handlePacketStream(strm: PacketStream): void
|
|
60
37
|
}
|
|
61
38
|
|
|
62
|
-
//
|
|
39
|
+
// StreamConn implements a generic connection with a two-way stream.
|
|
40
|
+
// The stream is not expected to manage packet boundaries.
|
|
41
|
+
// Packets will be sent with uint32le length prefixes.
|
|
42
|
+
// Uses Yamux to manage streams over the connection.
|
|
43
|
+
//
|
|
63
44
|
// Implements the client by opening streams with the remote.
|
|
64
45
|
// Implements the server by handling incoming streams.
|
|
65
46
|
// If the server is unset, rejects any incoming streams.
|
|
66
|
-
export class
|
|
47
|
+
export class StreamConn
|
|
67
48
|
implements Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>>
|
|
68
49
|
{
|
|
69
50
|
// muxer is the stream muxer.
|
|
70
|
-
private
|
|
51
|
+
private _muxer: StreamMuxer
|
|
71
52
|
// server is the server side, if set.
|
|
72
|
-
private
|
|
53
|
+
private _server?: StreamHandler
|
|
73
54
|
|
|
74
|
-
constructor(server?: StreamHandler, connParams?:
|
|
55
|
+
constructor(server?: StreamHandler, connParams?: StreamConnParams) {
|
|
75
56
|
if (server) {
|
|
76
|
-
this.
|
|
57
|
+
this._server = server
|
|
77
58
|
}
|
|
78
59
|
const muxerFactory =
|
|
79
60
|
connParams?.muxerFactory ??
|
|
80
|
-
yamux({ enableKeepAlive: false })({
|
|
61
|
+
yamux({ enableKeepAlive: false, ...connParams?.yamuxParams })({
|
|
81
62
|
logger: defaultLogger(),
|
|
82
63
|
})
|
|
83
|
-
this.
|
|
64
|
+
this._muxer = muxerFactory.createStreamMuxer({
|
|
84
65
|
onIncomingStream: this.handleIncomingStream.bind(this),
|
|
85
66
|
direction: connParams?.direction || 'outbound',
|
|
86
67
|
})
|
|
@@ -88,17 +69,27 @@ export class Conn
|
|
|
88
69
|
|
|
89
70
|
// sink returns the message sink.
|
|
90
71
|
get sink() {
|
|
91
|
-
return this.
|
|
72
|
+
return this._muxer.sink
|
|
92
73
|
}
|
|
93
74
|
|
|
94
75
|
// source returns the outgoing message source.
|
|
95
76
|
get source() {
|
|
96
|
-
return this.
|
|
77
|
+
return this._muxer.source
|
|
97
78
|
}
|
|
98
79
|
|
|
99
80
|
// streams returns the set of all ongoing streams.
|
|
100
81
|
get streams() {
|
|
101
|
-
return this.
|
|
82
|
+
return this._muxer.streams
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// muxer returns the muxer
|
|
86
|
+
get muxer() {
|
|
87
|
+
return this._muxer
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// server returns the server, if any.
|
|
91
|
+
get server() {
|
|
92
|
+
return this._server
|
|
102
93
|
}
|
|
103
94
|
|
|
104
95
|
// buildClient builds a new client from the connection.
|
|
@@ -107,15 +98,9 @@ export class Conn
|
|
|
107
98
|
}
|
|
108
99
|
|
|
109
100
|
// openStream implements the client open stream function.
|
|
110
|
-
public async openStream(): Promise<
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
if (isPromise(streamPromise)) {
|
|
114
|
-
stream = await streamPromise
|
|
115
|
-
} else {
|
|
116
|
-
stream = streamPromise
|
|
117
|
-
}
|
|
118
|
-
return streamToSRPCStream(stream)
|
|
101
|
+
public async openStream(): Promise<PacketStream> {
|
|
102
|
+
const strm = await this.muxer.newStream()
|
|
103
|
+
return streamToPacketStream(strm)
|
|
119
104
|
}
|
|
120
105
|
|
|
121
106
|
// buildOpenStreamFunc returns openStream bound to this conn.
|
|
@@ -124,11 +109,22 @@ export class Conn
|
|
|
124
109
|
}
|
|
125
110
|
|
|
126
111
|
// handleIncomingStream handles an incoming stream.
|
|
127
|
-
|
|
112
|
+
//
|
|
113
|
+
// this is usually called by the muxer when streams arrive.
|
|
114
|
+
public handleIncomingStream(strm: Stream) {
|
|
128
115
|
const server = this.server
|
|
129
116
|
if (!server) {
|
|
130
117
|
return strm.abort(new Error('server not implemented'))
|
|
131
118
|
}
|
|
132
|
-
server.handlePacketStream(
|
|
119
|
+
server.handlePacketStream(streamToPacketStream(strm))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// close closes or aborts the muxer with an optional error.
|
|
123
|
+
public close(err?: Error) {
|
|
124
|
+
if (err) {
|
|
125
|
+
this.muxer.abort(err)
|
|
126
|
+
} else {
|
|
127
|
+
this.muxer.close()
|
|
128
|
+
}
|
|
133
129
|
}
|
|
134
130
|
}
|
package/srpc/handler.ts
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import type { Sink, Source } from 'it-stream-types'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { Definition, MethodDefinition } from './definition.js'
|
|
6
|
-
import {
|
|
7
|
-
buildDecodeMessageTransform,
|
|
8
|
-
buildEncodeMessageTransform,
|
|
9
|
-
} from './message.js'
|
|
10
|
-
import { writeToPushable } from './pushable.js'
|
|
2
|
+
import { Definition } from './definition.js'
|
|
3
|
+
import { MethodProto, createInvokeFn } from './invoker.js'
|
|
11
4
|
|
|
12
5
|
// InvokeFn describes an SRPC call method invoke function.
|
|
13
6
|
export type InvokeFn = (
|
|
@@ -64,89 +57,6 @@ export class StaticHandler implements Handler {
|
|
|
64
57
|
}
|
|
65
58
|
}
|
|
66
59
|
|
|
67
|
-
// MethodProto is a function which matches one of the RPC signatures.
|
|
68
|
-
type MethodProto<R, O> =
|
|
69
|
-
| ((request: R) => Promise<O>)
|
|
70
|
-
| ((request: R) => AsyncIterable<O>)
|
|
71
|
-
| ((request: AsyncIterable<R>) => Promise<O>)
|
|
72
|
-
| ((request: AsyncIterable<R>) => AsyncIterable<O>)
|
|
73
|
-
|
|
74
|
-
// createInvokeFn builds an InvokeFn from a method definition and a function prototype.
|
|
75
|
-
export function createInvokeFn<R, O>(
|
|
76
|
-
methodInfo: MethodDefinition<R, O>,
|
|
77
|
-
methodProto: MethodProto<R, O>,
|
|
78
|
-
): InvokeFn {
|
|
79
|
-
const requestDecode = buildDecodeMessageTransform<R>(methodInfo.requestType)
|
|
80
|
-
return async (
|
|
81
|
-
dataSource: Source<Uint8Array>,
|
|
82
|
-
dataSink: Sink<Source<Uint8Array>>,
|
|
83
|
-
) => {
|
|
84
|
-
// responseSink is a Sink for response messages.
|
|
85
|
-
const responseSink = pushable<O>({
|
|
86
|
-
objectMode: true,
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
// pipe responseSink to dataSink.
|
|
90
|
-
pipe(
|
|
91
|
-
responseSink,
|
|
92
|
-
buildEncodeMessageTransform(methodInfo.responseType),
|
|
93
|
-
dataSink,
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
// requestSource is a Source of decoded request messages.
|
|
97
|
-
const requestSource = pipe(dataSource, requestDecode)
|
|
98
|
-
|
|
99
|
-
// build the request argument.
|
|
100
|
-
let requestArg: any
|
|
101
|
-
if (methodInfo.requestStream) {
|
|
102
|
-
// use the request source as the argument.
|
|
103
|
-
requestArg = requestSource
|
|
104
|
-
} else {
|
|
105
|
-
// receive a single message for the argument.
|
|
106
|
-
for await (const msg of requestSource) {
|
|
107
|
-
if (msg) {
|
|
108
|
-
requestArg = msg
|
|
109
|
-
break
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (!requestArg) {
|
|
115
|
-
throw new Error('request object was empty')
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Call the implementation.
|
|
119
|
-
try {
|
|
120
|
-
const responseObj = methodProto(requestArg)
|
|
121
|
-
if (!responseObj) {
|
|
122
|
-
throw new Error('return value was undefined')
|
|
123
|
-
}
|
|
124
|
-
if (methodInfo.responseStream) {
|
|
125
|
-
const response = responseObj as AsyncIterable<O>
|
|
126
|
-
return writeToPushable(response, responseSink)
|
|
127
|
-
} else {
|
|
128
|
-
const responsePromise = responseObj as Promise<O>
|
|
129
|
-
if (!responsePromise.then) {
|
|
130
|
-
throw new Error('expected return value to be a Promise')
|
|
131
|
-
}
|
|
132
|
-
const responseMsg = await responsePromise
|
|
133
|
-
if (!responseMsg) {
|
|
134
|
-
throw new Error('expected non-empty response object')
|
|
135
|
-
}
|
|
136
|
-
responseSink.push(responseMsg)
|
|
137
|
-
responseSink.end()
|
|
138
|
-
}
|
|
139
|
-
} catch (err) {
|
|
140
|
-
let asError = err as Error
|
|
141
|
-
if (!asError?.message) {
|
|
142
|
-
asError = new Error('error calling implementation: ' + err)
|
|
143
|
-
}
|
|
144
|
-
// mux will return the error to the rpc caller.
|
|
145
|
-
throw asError
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
60
|
// createHandler creates a handler from a definition and an implementation.
|
|
151
61
|
// if serviceID is not set, uses the fullName of the service as the identifier.
|
|
152
62
|
export function createHandler(
|
package/srpc/index.ts
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
export { ERR_RPC_ABORT, isAbortError, castToError } from './errors.js'
|
|
2
2
|
export { Client } from './client.js'
|
|
3
3
|
export { Server } from './server.js'
|
|
4
|
-
export {
|
|
4
|
+
export { StreamConn, StreamConnParams, StreamHandler } from './conn.js'
|
|
5
5
|
export { WebSocketConn } from './websocket.js'
|
|
6
|
-
export {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
export type {
|
|
7
|
+
PacketHandler,
|
|
8
|
+
OpenStreamFunc,
|
|
9
|
+
PacketStream,
|
|
10
|
+
streamToPacketStream,
|
|
11
|
+
} from './stream.js'
|
|
12
|
+
export {
|
|
13
|
+
Handler,
|
|
14
|
+
InvokeFn,
|
|
15
|
+
MethodMap,
|
|
16
|
+
StaticHandler,
|
|
17
|
+
createHandler,
|
|
18
|
+
} from './handler.js'
|
|
19
|
+
export { MethodProto, createInvokeFn } from './invoker.js'
|
|
9
20
|
export { Packet, CallStart, CallData } from './rpcproto.pb.js'
|
|
10
|
-
export { Mux, StaticMux, createMux } from './mux.js'
|
|
21
|
+
export { Mux, StaticMux, LookupMethod, createMux } from './mux.js'
|
|
11
22
|
export {
|
|
12
23
|
BroadcastChannelDuplex,
|
|
13
|
-
newBroadcastChannelDuplex,
|
|
14
24
|
BroadcastChannelConn,
|
|
25
|
+
newBroadcastChannelDuplex,
|
|
15
26
|
} from './broadcast-channel.js'
|
|
16
27
|
export {
|
|
17
28
|
MessagePortDuplex,
|
|
18
|
-
newMessagePortDuplex,
|
|
19
29
|
MessagePortConn,
|
|
30
|
+
newMessagePortDuplex,
|
|
20
31
|
} from './message-port.js'
|
|
21
32
|
export {
|
|
22
33
|
MessageDefinition,
|
package/srpc/invoker.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Sink, Source } from 'it-stream-types'
|
|
2
|
+
import { pushable } from 'it-pushable'
|
|
3
|
+
import { pipe } from 'it-pipe'
|
|
4
|
+
import { MethodDefinition } from './definition.js'
|
|
5
|
+
import { InvokeFn } from './handler.js'
|
|
6
|
+
import {
|
|
7
|
+
buildDecodeMessageTransform,
|
|
8
|
+
buildEncodeMessageTransform,
|
|
9
|
+
} from './message.js'
|
|
10
|
+
import { writeToPushable } from './pushable.js'
|
|
11
|
+
|
|
12
|
+
// MethodProto is a function which matches one of the RPC signatures.
|
|
13
|
+
export type MethodProto<R, O> =
|
|
14
|
+
| ((request: R) => Promise<O>)
|
|
15
|
+
| ((request: R) => AsyncIterable<O>)
|
|
16
|
+
| ((request: AsyncIterable<R>) => Promise<O>)
|
|
17
|
+
| ((request: AsyncIterable<R>) => AsyncIterable<O>)
|
|
18
|
+
|
|
19
|
+
// createInvokeFn builds an InvokeFn from a method definition and a function prototype.
|
|
20
|
+
export function createInvokeFn<R, O>(
|
|
21
|
+
methodInfo: MethodDefinition<R, O>,
|
|
22
|
+
methodProto: MethodProto<R, O>,
|
|
23
|
+
): InvokeFn {
|
|
24
|
+
const requestDecode = buildDecodeMessageTransform<R>(methodInfo.requestType)
|
|
25
|
+
return async (
|
|
26
|
+
dataSource: Source<Uint8Array>,
|
|
27
|
+
dataSink: Sink<Source<Uint8Array>>,
|
|
28
|
+
) => {
|
|
29
|
+
// responseSink is a Sink for response messages.
|
|
30
|
+
const responseSink = pushable<O>({
|
|
31
|
+
objectMode: true,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// pipe responseSink to dataSink.
|
|
35
|
+
pipe(
|
|
36
|
+
responseSink,
|
|
37
|
+
buildEncodeMessageTransform(methodInfo.responseType),
|
|
38
|
+
dataSink,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// requestSource is a Source of decoded request messages.
|
|
42
|
+
const requestSource = pipe(dataSource, requestDecode)
|
|
43
|
+
|
|
44
|
+
// build the request argument.
|
|
45
|
+
let requestArg: any
|
|
46
|
+
if (methodInfo.requestStream) {
|
|
47
|
+
// use the request source as the argument.
|
|
48
|
+
requestArg = requestSource
|
|
49
|
+
} else {
|
|
50
|
+
// receive a single message for the argument.
|
|
51
|
+
for await (const msg of requestSource) {
|
|
52
|
+
requestArg = msg
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!requestArg) {
|
|
58
|
+
throw new Error('request object was empty')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Call the implementation.
|
|
62
|
+
try {
|
|
63
|
+
const responseObj = methodProto(requestArg)
|
|
64
|
+
if (!responseObj) {
|
|
65
|
+
throw new Error('return value was undefined')
|
|
66
|
+
}
|
|
67
|
+
if (methodInfo.responseStream) {
|
|
68
|
+
const response = responseObj as AsyncIterable<O>
|
|
69
|
+
return writeToPushable(response, responseSink)
|
|
70
|
+
} else {
|
|
71
|
+
const responsePromise = responseObj as Promise<O>
|
|
72
|
+
if (!responsePromise.then) {
|
|
73
|
+
throw new Error('expected return value to be a Promise')
|
|
74
|
+
}
|
|
75
|
+
const responseMsg = await responsePromise
|
|
76
|
+
if (!responseMsg) {
|
|
77
|
+
throw new Error('expected non-empty response object')
|
|
78
|
+
}
|
|
79
|
+
responseSink.push(responseMsg)
|
|
80
|
+
responseSink.end()
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
let asError = err as Error
|
|
84
|
+
if (!asError?.message) {
|
|
85
|
+
asError = new Error('error calling implementation: ' + err)
|
|
86
|
+
}
|
|
87
|
+
// mux will return the error to the rpc caller.
|
|
88
|
+
responseSink.end()
|
|
89
|
+
throw asError
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
package/srpc/message-port.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import type { Source } from 'it-stream-types'
|
|
1
|
+
import type { Duplex, Source } from 'it-stream-types'
|
|
2
2
|
import { EventIterator } from 'event-iterator'
|
|
3
|
+
import { pipe } from 'it-pipe'
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { StreamConn, StreamConnParams } from './conn.js'
|
|
5
6
|
import { Server } from './server'
|
|
6
|
-
import {
|
|
7
|
-
import { StreamConn } from './conn-stream.js'
|
|
7
|
+
import { combineUint8ArrayListTransform } from './array-list.js'
|
|
8
8
|
|
|
9
9
|
// MessagePortDuplex is a AsyncIterable wrapper for MessagePort.
|
|
10
|
-
|
|
10
|
+
//
|
|
11
|
+
// When the sink is closed, the message port will also be closed.
|
|
12
|
+
// Note: there is no way to know when a MessagePort is closed!
|
|
13
|
+
// You will need an additional keep-alive on top of MessagePortDuplex.
|
|
14
|
+
export class MessagePortDuplex<T>
|
|
15
|
+
implements Duplex<AsyncGenerator<T>, Source<T>, Promise<void>>
|
|
16
|
+
{
|
|
11
17
|
// port is the message port
|
|
12
18
|
public readonly port: MessagePort
|
|
13
19
|
// sink is the sink for incoming messages.
|
|
@@ -23,24 +29,35 @@ export class MessagePortDuplex<T> implements Stream<T> {
|
|
|
23
29
|
|
|
24
30
|
// close closes the message port.
|
|
25
31
|
public close() {
|
|
32
|
+
this.port.postMessage(null)
|
|
26
33
|
this.port.close()
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
// _createSink initializes the sink field.
|
|
30
37
|
private _createSink(): (source: Source<T>) => Promise<void> {
|
|
31
38
|
return async (source) => {
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
try {
|
|
40
|
+
for await (const msg of source) {
|
|
41
|
+
this.port.postMessage(msg)
|
|
42
|
+
}
|
|
43
|
+
} catch (err: unknown) {
|
|
44
|
+
this.close()
|
|
45
|
+
throw err
|
|
34
46
|
}
|
|
47
|
+
|
|
48
|
+
this.close()
|
|
35
49
|
}
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
// _createSource initializes the source field.
|
|
39
53
|
private async *_createSource(): AsyncGenerator<T> {
|
|
40
54
|
const iterator = new EventIterator<T>((queue) => {
|
|
41
|
-
const messageListener = (ev: MessageEvent<T>) => {
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
const messageListener = (ev: MessageEvent<T | null>) => {
|
|
56
|
+
const data = ev.data
|
|
57
|
+
if (data !== null) {
|
|
58
|
+
queue.push(data)
|
|
59
|
+
} else {
|
|
60
|
+
queue.stop()
|
|
44
61
|
}
|
|
45
62
|
}
|
|
46
63
|
|
|
@@ -50,9 +67,16 @@ export class MessagePortDuplex<T> implements Stream<T> {
|
|
|
50
67
|
}
|
|
51
68
|
})
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
|
|
70
|
+
try {
|
|
71
|
+
for await (const value of iterator) {
|
|
72
|
+
yield value
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
this.close()
|
|
76
|
+
throw err
|
|
55
77
|
}
|
|
78
|
+
|
|
79
|
+
this.close()
|
|
56
80
|
}
|
|
57
81
|
}
|
|
58
82
|
|
|
@@ -66,23 +90,49 @@ export function newMessagePortDuplex<T>(
|
|
|
66
90
|
// MessagePortConn implements a connection with a MessagePort.
|
|
67
91
|
//
|
|
68
92
|
// expects Uint8Array objects over the MessagePort.
|
|
93
|
+
// uses Yamux to mux streams over the port.
|
|
69
94
|
export class MessagePortConn extends StreamConn {
|
|
70
|
-
//
|
|
71
|
-
private
|
|
95
|
+
// _messagePort is the message port iterable.
|
|
96
|
+
private _messagePort: MessagePortDuplex<Uint8Array>
|
|
72
97
|
|
|
73
|
-
constructor(
|
|
98
|
+
constructor(
|
|
99
|
+
port: MessagePort,
|
|
100
|
+
server?: Server,
|
|
101
|
+
connParams?: StreamConnParams,
|
|
102
|
+
) {
|
|
74
103
|
const messagePort = new MessagePortDuplex<Uint8Array>(port)
|
|
75
|
-
super(
|
|
76
|
-
|
|
104
|
+
super(server, {
|
|
105
|
+
...connParams,
|
|
106
|
+
yamuxParams: {
|
|
107
|
+
// There is no way to tell when a MessagePort is closed.
|
|
108
|
+
// We will send an undefined object through the MessagePort to indicate closed.
|
|
109
|
+
// We still need a way to detect when the connection is not cleanly terminated.
|
|
110
|
+
// Enable keep-alive to detect this on the other end.
|
|
111
|
+
enableKeepAlive: true,
|
|
112
|
+
keepAliveInterval: 1500,
|
|
113
|
+
...connParams?.yamuxParams,
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
this._messagePort = messagePort
|
|
117
|
+
pipe(
|
|
118
|
+
messagePort,
|
|
119
|
+
this,
|
|
120
|
+
// Uint8ArrayList usually cannot be sent over MessagePort, so we combine to a Uint8Array as part of the pipe.
|
|
121
|
+
combineUint8ArrayListTransform(),
|
|
122
|
+
messagePort,
|
|
123
|
+
)
|
|
124
|
+
.catch((err) => this.close(err))
|
|
125
|
+
.then(() => this.close())
|
|
77
126
|
}
|
|
78
127
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
return this.
|
|
128
|
+
// messagePort returns the MessagePort.
|
|
129
|
+
get messagePort(): MessagePort {
|
|
130
|
+
return this._messagePort.port
|
|
82
131
|
}
|
|
83
132
|
|
|
84
133
|
// close closes the message port.
|
|
85
|
-
public close() {
|
|
134
|
+
public override close(err?: Error) {
|
|
135
|
+
super.close(err)
|
|
86
136
|
this.messagePort.close()
|
|
87
137
|
}
|
|
88
138
|
}
|
package/srpc/open-stream-ctr.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OpenStreamFunc
|
|
1
|
+
import { OpenStreamFunc } from './stream.js'
|
|
2
2
|
import { ValueCtr } from './value-ctr.js'
|
|
3
3
|
|
|
4
4
|
// OpenStreamCtr contains an OpenStream func which can be awaited.
|
|
@@ -9,7 +9,7 @@ export class OpenStreamCtr extends ValueCtr<OpenStreamFunc> {
|
|
|
9
9
|
|
|
10
10
|
// openStreamFunc returns an OpenStreamFunc which waits for the underlying OpenStreamFunc.
|
|
11
11
|
get openStreamFunc(): OpenStreamFunc {
|
|
12
|
-
return async ()
|
|
12
|
+
return async () => {
|
|
13
13
|
let openFn = this.value
|
|
14
14
|
if (!openFn) {
|
|
15
15
|
openFn = await this.wait()
|