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
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { MethodDefinition } from './definition.js';
|
|
2
|
+
import { InvokeFn } from './handler.js';
|
|
3
|
+
export type MethodProto<R, O> = ((request: R) => Promise<O>) | ((request: R) => AsyncIterable<O>) | ((request: AsyncIterable<R>) => Promise<O>) | ((request: AsyncIterable<R>) => AsyncIterable<O>);
|
|
4
|
+
export declare function createInvokeFn<R, O>(methodInfo: MethodDefinition<R, O>, methodProto: MethodProto<R, O>): InvokeFn;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { pushable } from 'it-pushable';
|
|
2
|
+
import { pipe } from 'it-pipe';
|
|
3
|
+
import { buildDecodeMessageTransform, buildEncodeMessageTransform, } from './message.js';
|
|
4
|
+
import { writeToPushable } from './pushable.js';
|
|
5
|
+
// createInvokeFn builds an InvokeFn from a method definition and a function prototype.
|
|
6
|
+
export function createInvokeFn(methodInfo, methodProto) {
|
|
7
|
+
const requestDecode = buildDecodeMessageTransform(methodInfo.requestType);
|
|
8
|
+
return async (dataSource, dataSink) => {
|
|
9
|
+
// responseSink is a Sink for response messages.
|
|
10
|
+
const responseSink = pushable({
|
|
11
|
+
objectMode: true,
|
|
12
|
+
});
|
|
13
|
+
// pipe responseSink to dataSink.
|
|
14
|
+
pipe(responseSink, buildEncodeMessageTransform(methodInfo.responseType), dataSink);
|
|
15
|
+
// requestSource is a Source of decoded request messages.
|
|
16
|
+
const requestSource = pipe(dataSource, requestDecode);
|
|
17
|
+
// build the request argument.
|
|
18
|
+
let requestArg;
|
|
19
|
+
if (methodInfo.requestStream) {
|
|
20
|
+
// use the request source as the argument.
|
|
21
|
+
requestArg = requestSource;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// receive a single message for the argument.
|
|
25
|
+
for await (const msg of requestSource) {
|
|
26
|
+
requestArg = msg;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (!requestArg) {
|
|
31
|
+
throw new Error('request object was empty');
|
|
32
|
+
}
|
|
33
|
+
// Call the implementation.
|
|
34
|
+
try {
|
|
35
|
+
const responseObj = methodProto(requestArg);
|
|
36
|
+
if (!responseObj) {
|
|
37
|
+
throw new Error('return value was undefined');
|
|
38
|
+
}
|
|
39
|
+
if (methodInfo.responseStream) {
|
|
40
|
+
const response = responseObj;
|
|
41
|
+
return writeToPushable(response, responseSink);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const responsePromise = responseObj;
|
|
45
|
+
if (!responsePromise.then) {
|
|
46
|
+
throw new Error('expected return value to be a Promise');
|
|
47
|
+
}
|
|
48
|
+
const responseMsg = await responsePromise;
|
|
49
|
+
if (!responseMsg) {
|
|
50
|
+
throw new Error('expected non-empty response object');
|
|
51
|
+
}
|
|
52
|
+
responseSink.push(responseMsg);
|
|
53
|
+
responseSink.end();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
let asError = err;
|
|
58
|
+
if (!asError?.message) {
|
|
59
|
+
asError = new Error('error calling implementation: ' + err);
|
|
60
|
+
}
|
|
61
|
+
// mux will return the error to the rpc caller.
|
|
62
|
+
responseSink.end();
|
|
63
|
+
throw asError;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import type { Source } from 'it-stream-types';
|
|
2
|
-
import {
|
|
1
|
+
import type { Duplex, Source } from 'it-stream-types';
|
|
2
|
+
import { StreamConn, StreamConnParams } from './conn.js';
|
|
3
3
|
import { Server } from './server';
|
|
4
|
-
|
|
5
|
-
import { StreamConn } from './conn-stream.js';
|
|
6
|
-
export declare class MessagePortDuplex<T> implements Stream<T> {
|
|
4
|
+
export declare class MessagePortDuplex<T> implements Duplex<AsyncGenerator<T>, Source<T>, Promise<void>> {
|
|
7
5
|
readonly port: MessagePort;
|
|
8
6
|
sink: (source: Source<T>) => Promise<void>;
|
|
9
7
|
source: AsyncGenerator<T>;
|
|
@@ -14,8 +12,8 @@ export declare class MessagePortDuplex<T> implements Stream<T> {
|
|
|
14
12
|
}
|
|
15
13
|
export declare function newMessagePortDuplex<T>(port: MessagePort): MessagePortDuplex<T>;
|
|
16
14
|
export declare class MessagePortConn extends StreamConn {
|
|
17
|
-
private
|
|
18
|
-
constructor(port: MessagePort, server?: Server, connParams?:
|
|
19
|
-
|
|
20
|
-
close(): void;
|
|
15
|
+
private _messagePort;
|
|
16
|
+
constructor(port: MessagePort, server?: Server, connParams?: StreamConnParams);
|
|
17
|
+
get messagePort(): MessagePort;
|
|
18
|
+
close(err?: Error): void;
|
|
21
19
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { EventIterator } from 'event-iterator';
|
|
2
|
-
import {
|
|
2
|
+
import { pipe } from 'it-pipe';
|
|
3
|
+
import { StreamConn } from './conn.js';
|
|
4
|
+
import { combineUint8ArrayListTransform } from './array-list.js';
|
|
3
5
|
// MessagePortDuplex is a AsyncIterable wrapper for MessagePort.
|
|
6
|
+
//
|
|
7
|
+
// When the sink is closed, the message port will also be closed.
|
|
8
|
+
// Note: there is no way to know when a MessagePort is closed!
|
|
9
|
+
// You will need an additional keep-alive on top of MessagePortDuplex.
|
|
4
10
|
export class MessagePortDuplex {
|
|
5
11
|
constructor(port) {
|
|
6
12
|
this.port = port;
|
|
@@ -9,22 +15,34 @@ export class MessagePortDuplex {
|
|
|
9
15
|
}
|
|
10
16
|
// close closes the message port.
|
|
11
17
|
close() {
|
|
18
|
+
this.port.postMessage(null);
|
|
12
19
|
this.port.close();
|
|
13
20
|
}
|
|
14
21
|
// _createSink initializes the sink field.
|
|
15
22
|
_createSink() {
|
|
16
23
|
return async (source) => {
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
try {
|
|
25
|
+
for await (const msg of source) {
|
|
26
|
+
this.port.postMessage(msg);
|
|
27
|
+
}
|
|
19
28
|
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
this.close();
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
this.close();
|
|
20
34
|
};
|
|
21
35
|
}
|
|
22
36
|
// _createSource initializes the source field.
|
|
23
37
|
async *_createSource() {
|
|
24
38
|
const iterator = new EventIterator((queue) => {
|
|
25
39
|
const messageListener = (ev) => {
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
const data = ev.data;
|
|
41
|
+
if (data !== null) {
|
|
42
|
+
queue.push(data);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
queue.stop();
|
|
28
46
|
}
|
|
29
47
|
};
|
|
30
48
|
this.port.addEventListener('message', messageListener);
|
|
@@ -32,9 +50,16 @@ export class MessagePortDuplex {
|
|
|
32
50
|
this.port.removeEventListener('message', messageListener);
|
|
33
51
|
};
|
|
34
52
|
});
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
try {
|
|
54
|
+
for await (const value of iterator) {
|
|
55
|
+
yield value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
this.close();
|
|
60
|
+
throw err;
|
|
37
61
|
}
|
|
62
|
+
this.close();
|
|
38
63
|
}
|
|
39
64
|
}
|
|
40
65
|
// newMessagePortDuplex constructs a MessagePortDuplex with a channel name.
|
|
@@ -44,18 +69,36 @@ export function newMessagePortDuplex(port) {
|
|
|
44
69
|
// MessagePortConn implements a connection with a MessagePort.
|
|
45
70
|
//
|
|
46
71
|
// expects Uint8Array objects over the MessagePort.
|
|
72
|
+
// uses Yamux to mux streams over the port.
|
|
47
73
|
export class MessagePortConn extends StreamConn {
|
|
48
74
|
constructor(port, server, connParams) {
|
|
49
75
|
const messagePort = new MessagePortDuplex(port);
|
|
50
|
-
super(
|
|
51
|
-
|
|
76
|
+
super(server, {
|
|
77
|
+
...connParams,
|
|
78
|
+
yamuxParams: {
|
|
79
|
+
// There is no way to tell when a MessagePort is closed.
|
|
80
|
+
// We will send an undefined object through the MessagePort to indicate closed.
|
|
81
|
+
// We still need a way to detect when the connection is not cleanly terminated.
|
|
82
|
+
// Enable keep-alive to detect this on the other end.
|
|
83
|
+
enableKeepAlive: true,
|
|
84
|
+
keepAliveInterval: 1500,
|
|
85
|
+
...connParams?.yamuxParams,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
this._messagePort = messagePort;
|
|
89
|
+
pipe(messagePort, this,
|
|
90
|
+
// Uint8ArrayList usually cannot be sent over MessagePort, so we combine to a Uint8Array as part of the pipe.
|
|
91
|
+
combineUint8ArrayListTransform(), messagePort)
|
|
92
|
+
.catch((err) => this.close(err))
|
|
93
|
+
.then(() => this.close());
|
|
52
94
|
}
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
return this.
|
|
95
|
+
// messagePort returns the MessagePort.
|
|
96
|
+
get messagePort() {
|
|
97
|
+
return this._messagePort.port;
|
|
56
98
|
}
|
|
57
99
|
// close closes the message port.
|
|
58
|
-
close() {
|
|
100
|
+
close(err) {
|
|
101
|
+
super.close(err);
|
|
59
102
|
this.messagePort.close();
|
|
60
103
|
}
|
|
61
104
|
}
|
package/dist/srpc/pushable.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Pushable } from 'it-pushable';
|
|
2
|
-
import {
|
|
2
|
+
import { Sink, Source } from 'it-stream-types';
|
|
3
3
|
export declare function writeToPushable<T>(dataSource: AsyncIterable<T>, out: Pushable<T>): Promise<void>;
|
|
4
4
|
export declare function buildPushableSink<T>(target: Pushable<T>): Sink<Source<T>, Promise<void>>;
|
package/dist/srpc/pushable.js
CHANGED
|
@@ -15,15 +15,13 @@ export function buildPushableSink(target) {
|
|
|
15
15
|
return async (source) => {
|
|
16
16
|
try {
|
|
17
17
|
if (Symbol.asyncIterator in source) {
|
|
18
|
-
// Handle AsyncIterable
|
|
19
18
|
for await (const pkt of source) {
|
|
20
|
-
|
|
19
|
+
target.push(pkt);
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
22
|
else {
|
|
24
|
-
// Handle Iterable
|
|
25
23
|
for (const pkt of source) {
|
|
26
|
-
|
|
24
|
+
target.push(pkt);
|
|
27
25
|
}
|
|
28
26
|
}
|
|
29
27
|
target.end();
|
|
@@ -33,13 +31,3 @@ export function buildPushableSink(target) {
|
|
|
33
31
|
}
|
|
34
32
|
};
|
|
35
33
|
}
|
|
36
|
-
function processPacket(pkt, target) {
|
|
37
|
-
if (Array.isArray(pkt)) {
|
|
38
|
-
for (const p of pkt) {
|
|
39
|
-
target.push(p);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
target.push(pkt);
|
|
44
|
-
}
|
|
45
|
-
}
|
package/dist/srpc/server-rpc.js
CHANGED
package/dist/srpc/server.d.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
import { Duplex, Source } from 'it-stream-types';
|
|
2
1
|
import { LookupMethod } from './mux.js';
|
|
3
2
|
import { ServerRPC } from './server-rpc.js';
|
|
4
|
-
import { Packet } from './rpcproto.pb.js';
|
|
5
3
|
import { StreamHandler } from './conn.js';
|
|
6
|
-
import {
|
|
4
|
+
import { PacketStream } from './stream.js';
|
|
7
5
|
import { RpcStreamHandler } from '../rpcstream/rpcstream.js';
|
|
8
6
|
export declare class Server implements StreamHandler {
|
|
9
7
|
private lookupMethod;
|
|
10
8
|
constructor(lookupMethod: LookupMethod);
|
|
11
9
|
get rpcStreamHandler(): RpcStreamHandler;
|
|
12
10
|
startRpc(): ServerRPC;
|
|
13
|
-
|
|
14
|
-
handlePacketStream(stream: Stream): ServerRPC;
|
|
15
|
-
handlePacketDuplex(stream: Duplex<Source<Packet>>): ServerRPC;
|
|
11
|
+
handlePacketStream(stream: PacketStream): ServerRPC;
|
|
16
12
|
}
|
package/dist/srpc/server.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { pipe } from 'it-pipe';
|
|
2
2
|
import { ServerRPC } from './server-rpc.js';
|
|
3
|
-
import {
|
|
4
|
-
import { combineUint8ArrayListTransform } from './array-list.js';
|
|
3
|
+
import { decodePacketSource, encodePacketSource } from './packet.js';
|
|
5
4
|
// Server implements the SRPC server in TypeScript with a Mux.
|
|
6
5
|
export class Server {
|
|
7
6
|
constructor(lookupMethod) {
|
|
@@ -17,25 +16,13 @@ export class Server {
|
|
|
17
16
|
startRpc() {
|
|
18
17
|
return new ServerRPC(this.lookupMethod);
|
|
19
18
|
}
|
|
20
|
-
// handleFragmentStream handles an incoming stream.
|
|
21
|
-
// assumes that stream does not maintain packet framing.
|
|
22
|
-
// uses length-prefixed packets for packet framing.
|
|
23
|
-
handleFragmentStream(stream) {
|
|
24
|
-
const rpc = this.startRpc();
|
|
25
|
-
pipe(stream, parseLengthPrefixTransform(), combineUint8ArrayListTransform(), decodePacketSource, rpc, encodePacketSource, prependLengthPrefixTransform(), combineUint8ArrayListTransform(), stream);
|
|
26
|
-
return rpc;
|
|
27
|
-
}
|
|
28
19
|
// handlePacketStream handles an incoming Uint8Array duplex.
|
|
29
20
|
// the stream has one Uint8Array per packet w/o length prefix.
|
|
30
21
|
handlePacketStream(stream) {
|
|
31
22
|
const rpc = this.startRpc();
|
|
32
|
-
pipe(stream, decodePacketSource, rpc, encodePacketSource, stream)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// handlePacketDuplex handles an incoming Packet duplex.
|
|
36
|
-
handlePacketDuplex(stream) {
|
|
37
|
-
const rpc = this.startRpc();
|
|
38
|
-
pipe(stream, rpc, stream);
|
|
23
|
+
pipe(stream, decodePacketSource, rpc, encodePacketSource, stream)
|
|
24
|
+
.catch((err) => rpc.close(err))
|
|
25
|
+
.then(() => rpc.close());
|
|
39
26
|
return rpc;
|
|
40
27
|
}
|
|
41
28
|
}
|
package/dist/srpc/stream.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { Packet } from './rpcproto.pb.js';
|
|
2
1
|
import type { Duplex, Source } from 'it-stream-types';
|
|
2
|
+
import { Stream } from '@libp2p/interface';
|
|
3
|
+
import type { Packet } from './rpcproto.pb.js';
|
|
3
4
|
export type PacketHandler = (packet: Packet) => Promise<void>;
|
|
4
|
-
export type
|
|
5
|
-
export type OpenStreamFunc = () => Promise<
|
|
5
|
+
export type PacketStream = Duplex<AsyncGenerator<Uint8Array>, Source<Uint8Array>, Promise<void>>;
|
|
6
|
+
export type OpenStreamFunc = () => Promise<PacketStream>;
|
|
7
|
+
export declare function streamToPacketStream(stream: Stream): PacketStream;
|
package/dist/srpc/stream.js
CHANGED
|
@@ -1 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import { pipe } from 'it-pipe';
|
|
2
|
+
import { combineUint8ArrayListTransform } from './array-list.js';
|
|
3
|
+
import { parseLengthPrefixTransform, prependLengthPrefixTransform, } from './packet.js';
|
|
4
|
+
// streamToPacketStream converts a Stream into a PacketStream using length-prefix framing.
|
|
5
|
+
//
|
|
6
|
+
// The stream is closed when the source writing to the sink ends.
|
|
7
|
+
export function streamToPacketStream(stream) {
|
|
8
|
+
return {
|
|
9
|
+
source: pipe(stream, parseLengthPrefixTransform(), combineUint8ArrayListTransform()),
|
|
10
|
+
sink: async (source) => {
|
|
11
|
+
await pipe(source, prependLengthPrefixTransform(), stream)
|
|
12
|
+
.catch((err) => stream.close(err))
|
|
13
|
+
.then(() => stream.close());
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
package/dist/srpc/websocket.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/// <reference types="ws" />
|
|
2
2
|
import { Direction } from '@libp2p/interface';
|
|
3
3
|
import type WebSocket from '@aptre/it-ws/web-socket';
|
|
4
|
-
import {
|
|
4
|
+
import { StreamConn } from './conn.js';
|
|
5
5
|
import { Server } from './server.js';
|
|
6
|
-
export declare class WebSocketConn extends
|
|
6
|
+
export declare class WebSocketConn extends StreamConn {
|
|
7
7
|
private socket;
|
|
8
8
|
constructor(socket: WebSocket, direction: Direction, server?: Server);
|
|
9
9
|
getSocket(): WebSocket;
|
package/dist/srpc/websocket.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { pipe } from 'it-pipe';
|
|
2
2
|
import duplex from '@aptre/it-ws/duplex';
|
|
3
|
-
import {
|
|
3
|
+
import { StreamConn } from './conn.js';
|
|
4
4
|
import { combineUint8ArrayListTransform } from './array-list.js';
|
|
5
5
|
// WebSocketConn implements a connection with a WebSocket and optional Server.
|
|
6
|
-
export class WebSocketConn extends
|
|
6
|
+
export class WebSocketConn extends StreamConn {
|
|
7
7
|
constructor(socket, direction, server) {
|
|
8
8
|
super(server, { direction });
|
|
9
9
|
this.socket = socket;
|
package/e2e/e2e.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { pipe } from 'it-pipe'
|
|
2
|
-
import { createHandler, createMux, Server, Client,
|
|
2
|
+
import { createHandler, createMux, Server, Client, StreamConn } from '../srpc'
|
|
3
3
|
import { EchoerDefinition, EchoerServer, runClientTest } from '../echo'
|
|
4
|
-
import { runRpcStreamTest } from '../echo/client-test'
|
|
4
|
+
import { runAbortControllerTest, runRpcStreamTest } from '../echo/client-test'
|
|
5
5
|
|
|
6
6
|
async function runRPC() {
|
|
7
7
|
const mux = createMux()
|
|
@@ -9,13 +9,16 @@ async function runRPC() {
|
|
|
9
9
|
const echoer = new EchoerServer(server)
|
|
10
10
|
mux.register(createHandler(EchoerDefinition, echoer))
|
|
11
11
|
|
|
12
|
-
const clientConn = new
|
|
13
|
-
const serverConn = new
|
|
12
|
+
const clientConn = new StreamConn()
|
|
13
|
+
const serverConn = new StreamConn(server, { direction: 'inbound' })
|
|
14
|
+
|
|
14
15
|
pipe(clientConn, serverConn, clientConn)
|
|
16
|
+
|
|
15
17
|
const client = new Client(clientConn.buildOpenStreamFunc())
|
|
16
18
|
|
|
17
|
-
await runRpcStreamTest(client)
|
|
18
19
|
await runClientTest(client)
|
|
20
|
+
await runAbortControllerTest(client)
|
|
21
|
+
await runRpcStreamTest(client)
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
runRPC()
|
package/package.json
CHANGED
|
@@ -1,62 +1,86 @@
|
|
|
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
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { StreamConn, StreamConnParams } from './conn.js'
|
|
5
5
|
import { Server } from './server.js'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { combineUint8ArrayListTransform } from './array-list.js'
|
|
7
|
+
import { pipe } from 'it-pipe'
|
|
8
8
|
|
|
9
9
|
// BroadcastChannelDuplex is a AsyncIterable wrapper for BroadcastChannel.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
//
|
|
11
|
+
// When the sink is closed, the broadcast channel also be closed.
|
|
12
|
+
// Note: there is no way to know when a BroadcastChannel is closed!
|
|
13
|
+
// You will need an additional keep-alive on top of BroadcastChannelDuplex.
|
|
14
|
+
export class BroadcastChannelDuplex<T>
|
|
15
|
+
implements Duplex<AsyncGenerator<T>, Source<T>, Promise<void>>
|
|
16
|
+
{
|
|
17
|
+
// read is the read channel
|
|
18
|
+
public readonly read: BroadcastChannel
|
|
19
|
+
// write is the write channel
|
|
20
|
+
public readonly write: BroadcastChannel
|
|
15
21
|
// sink is the sink for incoming messages.
|
|
16
22
|
public sink: (source: Source<T>) => Promise<void>
|
|
17
23
|
// source is the source for outgoing messages.
|
|
18
24
|
public source: AsyncGenerator<T>
|
|
19
25
|
|
|
20
|
-
constructor(
|
|
21
|
-
this.
|
|
22
|
-
this.
|
|
26
|
+
constructor(read: BroadcastChannel, write: BroadcastChannel) {
|
|
27
|
+
this.read = read
|
|
28
|
+
this.write = write
|
|
23
29
|
this.sink = this._createSink()
|
|
24
30
|
this.source = this._createSource()
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
// close closes the
|
|
33
|
+
// close closes the message port.
|
|
28
34
|
public close() {
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
35
|
+
this.write.postMessage(null)
|
|
36
|
+
this.write.close()
|
|
37
|
+
this.read.close()
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
// _createSink initializes the sink field.
|
|
34
41
|
private _createSink(): (source: Source<T>) => Promise<void> {
|
|
35
42
|
return async (source) => {
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
try {
|
|
44
|
+
for await (const msg of source) {
|
|
45
|
+
this.write.postMessage(msg)
|
|
46
|
+
}
|
|
47
|
+
} catch (err: unknown) {
|
|
48
|
+
this.close()
|
|
49
|
+
throw err
|
|
38
50
|
}
|
|
51
|
+
|
|
52
|
+
this.close()
|
|
39
53
|
}
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
// _createSource initializes the source field.
|
|
43
57
|
private async *_createSource(): AsyncGenerator<T> {
|
|
44
58
|
const iterator = new EventIterator<T>((queue) => {
|
|
45
|
-
const messageListener = (ev: MessageEvent<T>) => {
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
const messageListener = (ev: MessageEvent<T | null>) => {
|
|
60
|
+
const data = ev.data
|
|
61
|
+
if (data !== null) {
|
|
62
|
+
queue.push(data)
|
|
63
|
+
} else {
|
|
64
|
+
queue.stop()
|
|
48
65
|
}
|
|
49
66
|
}
|
|
50
67
|
|
|
51
|
-
this.
|
|
68
|
+
this.read.addEventListener('message', messageListener)
|
|
52
69
|
return () => {
|
|
53
|
-
this.
|
|
70
|
+
this.read.removeEventListener('message', messageListener)
|
|
54
71
|
}
|
|
55
72
|
})
|
|
56
73
|
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
try {
|
|
75
|
+
for await (const value of iterator) {
|
|
76
|
+
yield value
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
this.close()
|
|
80
|
+
throw err
|
|
59
81
|
}
|
|
82
|
+
|
|
83
|
+
this.close()
|
|
60
84
|
}
|
|
61
85
|
}
|
|
62
86
|
|
|
@@ -74,36 +98,43 @@ export function newBroadcastChannelDuplex<T>(
|
|
|
74
98
|
// BroadcastChannelConn implements a connection with a BroadcastChannel.
|
|
75
99
|
//
|
|
76
100
|
// expects Uint8Array objects over the BroadcastChannel.
|
|
101
|
+
// uses Yamux to mux streams over the port.
|
|
77
102
|
export class BroadcastChannelConn extends StreamConn {
|
|
78
|
-
//
|
|
79
|
-
|
|
103
|
+
// duplex is the broadcast channel duplex.
|
|
104
|
+
public readonly duplex: BroadcastChannelDuplex<Uint8Array>
|
|
80
105
|
|
|
81
106
|
constructor(
|
|
82
|
-
|
|
83
|
-
writeChannel: BroadcastChannel,
|
|
107
|
+
duplex: BroadcastChannelDuplex<Uint8Array>,
|
|
84
108
|
server?: Server,
|
|
85
|
-
connParams?:
|
|
109
|
+
connParams?: StreamConnParams,
|
|
86
110
|
) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
111
|
+
super(server, {
|
|
112
|
+
...connParams,
|
|
113
|
+
yamuxParams: {
|
|
114
|
+
// There is no way to tell when a BroadcastChannel is closed.
|
|
115
|
+
// We will send an undefined object through the BroadcastChannel to indicate closed.
|
|
116
|
+
// We still need a way to detect when the connection is not cleanly terminated.
|
|
117
|
+
// Enable keep-alive to detect this on the other end.
|
|
118
|
+
enableKeepAlive: true,
|
|
119
|
+
keepAliveInterval: 1500,
|
|
120
|
+
...connParams?.yamuxParams,
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
this.duplex = duplex
|
|
124
|
+
pipe(
|
|
125
|
+
duplex,
|
|
126
|
+
this,
|
|
127
|
+
// Uint8ArrayList usually cannot be sent over BroadcastChannel, so we combine to a Uint8Array as part of the pipe.
|
|
128
|
+
combineUint8ArrayListTransform(),
|
|
129
|
+
duplex,
|
|
90
130
|
)
|
|
91
|
-
|
|
92
|
-
|
|
131
|
+
.catch((err) => this.close(err))
|
|
132
|
+
.then(() => this.close())
|
|
93
133
|
}
|
|
94
134
|
|
|
95
|
-
//
|
|
96
|
-
public
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// getWriteChannel returns the write BroadcastChannel.
|
|
101
|
-
public getWriteChannel(): BroadcastChannel {
|
|
102
|
-
return this.broadcastChannel.writeChannel
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// close closes the read and write channels.
|
|
106
|
-
public close() {
|
|
107
|
-
this.broadcastChannel.close()
|
|
135
|
+
// close closes the message port.
|
|
136
|
+
public override close(err?: Error) {
|
|
137
|
+
super.close(err)
|
|
138
|
+
this.duplex.close()
|
|
108
139
|
}
|
|
109
140
|
}
|
package/srpc/client.ts
CHANGED
|
@@ -68,7 +68,9 @@ export class Client implements TsProtoRpc {
|
|
|
68
68
|
const serverData: Pushable<Uint8Array> = pushable({ objectMode: true })
|
|
69
69
|
this.startRpc(service, method, data, abortSignal)
|
|
70
70
|
.then(async (call) => {
|
|
71
|
-
|
|
71
|
+
const result = writeToPushable(call.rpcDataSource, serverData)
|
|
72
|
+
result.finally(() => call.close())
|
|
73
|
+
return result
|
|
72
74
|
})
|
|
73
75
|
.catch((err) => serverData.end(err))
|
|
74
76
|
return serverData
|
|
@@ -84,14 +86,16 @@ export class Client implements TsProtoRpc {
|
|
|
84
86
|
const serverData: Pushable<Uint8Array> = pushable({ objectMode: true })
|
|
85
87
|
this.startRpc(service, method, null, abortSignal)
|
|
86
88
|
.then(async (call) => {
|
|
87
|
-
call.writeCallDataFromSource(data)
|
|
89
|
+
call.writeCallDataFromSource(data).catch((err) => call.close(err))
|
|
88
90
|
try {
|
|
89
91
|
for await (const message of call.rpcDataSource) {
|
|
90
92
|
serverData.push(message)
|
|
91
93
|
}
|
|
92
94
|
serverData.end()
|
|
95
|
+
call.close()
|
|
93
96
|
} catch (err) {
|
|
94
|
-
|
|
97
|
+
call.close(err as Error)
|
|
98
|
+
throw err
|
|
95
99
|
}
|
|
96
100
|
})
|
|
97
101
|
.catch((err) => serverData.end(err))
|
|
@@ -114,9 +118,12 @@ export class Client implements TsProtoRpc {
|
|
|
114
118
|
const stream = await openStreamFn()
|
|
115
119
|
const call = new ClientRPC(rpcService, rpcMethod)
|
|
116
120
|
abortSignal?.addEventListener('abort', () => {
|
|
121
|
+
call.writeCallCancel()
|
|
117
122
|
call.close(new Error(ERR_RPC_ABORT))
|
|
118
123
|
})
|
|
119
124
|
pipe(stream, decodePacketSource, call, encodePacketSource, stream)
|
|
125
|
+
.then(() => call.close())
|
|
126
|
+
.catch((err) => call.close(err))
|
|
120
127
|
await call.writeCallStart(data || undefined)
|
|
121
128
|
return call
|
|
122
129
|
}
|
package/srpc/common-rpc.go
CHANGED
|
@@ -131,16 +131,7 @@ func (c *commonRPC) HandleStreamClose(closeErr error) {
|
|
|
131
131
|
|
|
132
132
|
// HandleCallCancel handles the call cancel packet.
|
|
133
133
|
func (c *commonRPC) HandleCallCancel() error {
|
|
134
|
-
c.
|
|
135
|
-
defer c.mtx.Unlock()
|
|
136
|
-
if c.remoteErr != nil {
|
|
137
|
-
c.remoteErr = context.Canceled
|
|
138
|
-
}
|
|
139
|
-
c.dataClosed = true
|
|
140
|
-
if c.writer != nil {
|
|
141
|
-
_ = c.writer.Close()
|
|
142
|
-
}
|
|
143
|
-
c.bcast.Broadcast()
|
|
134
|
+
c.HandleStreamClose(context.Canceled)
|
|
144
135
|
return nil
|
|
145
136
|
}
|
|
146
137
|
|