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.
Files changed (47) hide show
  1. package/dist/e2e/e2e.js +6 -5
  2. package/dist/rpcstream/rpcstream.d.ts +5 -5
  3. package/dist/rpcstream/rpcstream.js +1 -1
  4. package/dist/srpc/broadcast-channel.d.ts +9 -13
  5. package/dist/srpc/broadcast-channel.js +64 -30
  6. package/dist/srpc/client.js +11 -4
  7. package/dist/srpc/common-rpc.d.ts +4 -2
  8. package/dist/srpc/common-rpc.js +19 -7
  9. package/dist/srpc/conn.d.ts +16 -12
  10. package/dist/srpc/conn.js +35 -34
  11. package/dist/srpc/handler.d.ts +1 -4
  12. package/dist/srpc/handler.js +1 -67
  13. package/dist/srpc/index.d.ts +7 -7
  14. package/dist/srpc/index.js +5 -5
  15. package/dist/srpc/invoker.d.ts +4 -0
  16. package/dist/srpc/invoker.js +66 -0
  17. package/dist/srpc/message-port.d.ts +7 -9
  18. package/dist/srpc/message-port.js +56 -13
  19. package/dist/srpc/pushable.d.ts +1 -1
  20. package/dist/srpc/pushable.js +2 -14
  21. package/dist/srpc/server-rpc.js +1 -0
  22. package/dist/srpc/server.d.ts +2 -6
  23. package/dist/srpc/server.js +4 -17
  24. package/dist/srpc/stream.d.ts +5 -3
  25. package/dist/srpc/stream.js +16 -1
  26. package/dist/srpc/websocket.d.ts +2 -2
  27. package/dist/srpc/websocket.js +2 -2
  28. package/e2e/e2e.ts +8 -5
  29. package/package.json +1 -1
  30. package/srpc/broadcast-channel.ts +78 -47
  31. package/srpc/client.ts +10 -3
  32. package/srpc/common-rpc.go +1 -10
  33. package/srpc/common-rpc.ts +23 -8
  34. package/srpc/conn.ts +54 -58
  35. package/srpc/handler.ts +2 -92
  36. package/srpc/index.ts +18 -7
  37. package/srpc/invoker.ts +92 -0
  38. package/srpc/message-port.ts +71 -21
  39. package/srpc/open-stream-ctr.ts +2 -2
  40. package/srpc/pushable.ts +5 -17
  41. package/srpc/server-rpc.ts +1 -0
  42. package/srpc/server.ts +5 -37
  43. package/srpc/stream.ts +32 -7
  44. package/srpc/websocket.ts +2 -2
  45. package/dist/srpc/conn-stream.d.ts +0 -8
  46. package/dist/srpc/conn-stream.js +0 -16
  47. 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 { ConnParams } from './conn.js';
1
+ import type { Duplex, Source } from 'it-stream-types';
2
+ import { StreamConn, StreamConnParams } from './conn.js';
3
3
  import { Server } from './server';
4
- import { Stream } from './stream.js';
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 messagePort;
18
- constructor(port: MessagePort, server?: Server, connParams?: ConnParams);
19
- getMessagePort(): MessagePort;
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 { StreamConn } from './conn-stream.js';
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
- for await (const msg of source) {
18
- this.port.postMessage(msg);
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
- if (ev.data) {
27
- queue.push(ev.data);
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
- for await (const value of iterator) {
36
- yield value;
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(messagePort, server, connParams);
51
- this.messagePort = messagePort;
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
- // getMessagePort returns the MessagePort.
54
- getMessagePort() {
55
- return this.messagePort.port;
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
  }
@@ -1,4 +1,4 @@
1
1
  import { Pushable } from 'it-pushable';
2
- import { Source, Sink } from 'it-stream-types';
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>>;
@@ -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
- processPacket(pkt, target);
19
+ target.push(pkt);
21
20
  }
22
21
  }
23
22
  else {
24
- // Handle Iterable
25
23
  for (const pkt of source) {
26
- processPacket(pkt, target);
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
- }
@@ -50,6 +50,7 @@ export class ServerRPC extends CommonRPC {
50
50
  await this.writeCallData(msg);
51
51
  }
52
52
  await this.writeCallData(undefined, true);
53
+ this.close();
53
54
  }
54
55
  catch (err) {
55
56
  this.close(err);
@@ -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 { Stream } from './stream.js';
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
- handleFragmentStream(stream: Stream): ServerRPC;
14
- handlePacketStream(stream: Stream): ServerRPC;
15
- handlePacketDuplex(stream: Duplex<Source<Packet>>): ServerRPC;
11
+ handlePacketStream(stream: PacketStream): ServerRPC;
16
12
  }
@@ -1,7 +1,6 @@
1
1
  import { pipe } from 'it-pipe';
2
2
  import { ServerRPC } from './server-rpc.js';
3
- import { parseLengthPrefixTransform, prependLengthPrefixTransform, decodePacketSource, encodePacketSource, } from './packet.js';
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
- return rpc;
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
  }
@@ -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 Stream<T = Uint8Array> = Duplex<AsyncGenerator<T>, Source<T>, Promise<void>>;
5
- export type OpenStreamFunc = () => Promise<Stream>;
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;
@@ -1 +1,16 @@
1
- export {};
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
+ }
@@ -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 { Conn } from './conn.js';
4
+ import { StreamConn } from './conn.js';
5
5
  import { Server } from './server.js';
6
- export declare class WebSocketConn extends Conn {
6
+ export declare class WebSocketConn extends StreamConn {
7
7
  private socket;
8
8
  constructor(socket: WebSocket, direction: Direction, server?: Server);
9
9
  getSocket(): WebSocket;
@@ -1,9 +1,9 @@
1
1
  import { pipe } from 'it-pipe';
2
2
  import duplex from '@aptre/it-ws/duplex';
3
- import { Conn } from './conn.js';
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 Conn {
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, Conn } from '../srpc'
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 Conn()
13
- const serverConn = new Conn(server, { direction: 'inbound' })
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,6 +1,6 @@
1
1
  {
2
2
  "name": "starpc",
3
- "version": "0.23.2",
3
+ "version": "0.24.0",
4
4
  "description": "Streaming protobuf RPC service protocol over any two-way channel.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -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 { ConnParams } from './conn.js'
4
+ import { StreamConn, StreamConnParams } from './conn.js'
5
5
  import { Server } from './server.js'
6
- import { StreamConn } from './conn-stream.js'
7
- import { Stream } from './stream.js'
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
- export class BroadcastChannelDuplex<T> implements Stream<T> {
11
- // readChannel is the incoming broadcast channel
12
- public readonly readChannel: BroadcastChannel
13
- // writeChannel is the outgoing broadcast channel
14
- public readonly writeChannel: BroadcastChannel
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(readChannel: BroadcastChannel, writeChannel: BroadcastChannel) {
21
- this.readChannel = readChannel
22
- this.writeChannel = writeChannel
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 broadcast channels.
33
+ // close closes the message port.
28
34
  public close() {
29
- this.readChannel.close()
30
- this.writeChannel.close()
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
- for await (const msg of source) {
37
- this.writeChannel.postMessage(msg)
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
- if (ev.data) {
47
- queue.push(ev.data)
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.readChannel.addEventListener('message', messageListener)
68
+ this.read.addEventListener('message', messageListener)
52
69
  return () => {
53
- this.readChannel.removeEventListener('message', messageListener)
70
+ this.read.removeEventListener('message', messageListener)
54
71
  }
55
72
  })
56
73
 
57
- for await (const value of iterator) {
58
- yield value
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
- // broadcastChannel is the broadcast channel iterable
79
- private broadcastChannel: BroadcastChannelDuplex<Uint8Array>
103
+ // duplex is the broadcast channel duplex.
104
+ public readonly duplex: BroadcastChannelDuplex<Uint8Array>
80
105
 
81
106
  constructor(
82
- readChannel: BroadcastChannel,
83
- writeChannel: BroadcastChannel,
107
+ duplex: BroadcastChannelDuplex<Uint8Array>,
84
108
  server?: Server,
85
- connParams?: ConnParams,
109
+ connParams?: StreamConnParams,
86
110
  ) {
87
- const broadcastChannel = new BroadcastChannelDuplex<Uint8Array>(
88
- readChannel,
89
- writeChannel,
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
- super(broadcastChannel, server, connParams)
92
- this.broadcastChannel = broadcastChannel
131
+ .catch((err) => this.close(err))
132
+ .then(() => this.close())
93
133
  }
94
134
 
95
- // getReadChannel returns the read BroadcastChannel.
96
- public getReadChannel(): BroadcastChannel {
97
- return this.broadcastChannel.readChannel
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
- return writeToPushable(call.rpcDataSource, serverData)
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
- serverData.end(err as Error)
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
  }
@@ -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.mtx.Lock()
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