starpc 0.49.1 → 0.49.3
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/echo/echo_srpc.pb.d.ts +7 -7
- package/dist/mock/mock_srpc.pb.d.ts +2 -2
- package/dist/srpc/channel.d.ts +2 -0
- package/dist/srpc/channel.js +31 -7
- package/dist/srpc/client.js +7 -3
- package/dist/srpc/packet.d.ts +8 -8
- package/dist/srpc/server.d.ts +2 -3
- package/dist/srpc/server.test.js +22 -2
- package/go.mod +8 -8
- package/go.sum +14 -14
- package/integration/cross-language/go-client/main.go +1 -1
- package/integration/cross-language/run.bash +1 -1
- package/integration/integration.go +2 -0
- package/package.json +13 -16
- package/srpc/channel.ts +32 -7
- package/srpc/client.ts +6 -2
- package/srpc/server-http_js.go +1 -1
- package/srpc/server.test.ts +44 -2
- package/srpc/server.ts +2 -3
- package/integration/tsconfig.json +0 -11
|
@@ -9,7 +9,7 @@ import { MessageStream, ProtoRpc } from 'starpc';
|
|
|
9
9
|
* @generated from service echo.Echoer
|
|
10
10
|
*/
|
|
11
11
|
export declare const EchoerDefinition: {
|
|
12
|
-
readonly typeName:
|
|
12
|
+
readonly typeName: 'echo.Echoer';
|
|
13
13
|
readonly methods: {
|
|
14
14
|
/**
|
|
15
15
|
* Echo returns the given message.
|
|
@@ -17,7 +17,7 @@ export declare const EchoerDefinition: {
|
|
|
17
17
|
* @generated from rpc echo.Echoer.Echo
|
|
18
18
|
*/
|
|
19
19
|
readonly Echo: {
|
|
20
|
-
readonly name:
|
|
20
|
+
readonly name: 'Echo';
|
|
21
21
|
readonly I: import("@aptre/protobuf-es-lite").MessageType<EchoMsg>;
|
|
22
22
|
readonly O: import("@aptre/protobuf-es-lite").MessageType<EchoMsg>;
|
|
23
23
|
readonly kind: MethodKind.Unary;
|
|
@@ -28,7 +28,7 @@ export declare const EchoerDefinition: {
|
|
|
28
28
|
* @generated from rpc echo.Echoer.EchoServerStream
|
|
29
29
|
*/
|
|
30
30
|
readonly EchoServerStream: {
|
|
31
|
-
readonly name:
|
|
31
|
+
readonly name: 'EchoServerStream';
|
|
32
32
|
readonly I: import("@aptre/protobuf-es-lite").MessageType<EchoMsg>;
|
|
33
33
|
readonly O: import("@aptre/protobuf-es-lite").MessageType<EchoMsg>;
|
|
34
34
|
readonly kind: MethodKind.ServerStreaming;
|
|
@@ -39,7 +39,7 @@ export declare const EchoerDefinition: {
|
|
|
39
39
|
* @generated from rpc echo.Echoer.EchoClientStream
|
|
40
40
|
*/
|
|
41
41
|
readonly EchoClientStream: {
|
|
42
|
-
readonly name:
|
|
42
|
+
readonly name: 'EchoClientStream';
|
|
43
43
|
readonly I: import("@aptre/protobuf-es-lite").MessageType<EchoMsg>;
|
|
44
44
|
readonly O: import("@aptre/protobuf-es-lite").MessageType<EchoMsg>;
|
|
45
45
|
readonly kind: MethodKind.ClientStreaming;
|
|
@@ -50,7 +50,7 @@ export declare const EchoerDefinition: {
|
|
|
50
50
|
* @generated from rpc echo.Echoer.EchoBidiStream
|
|
51
51
|
*/
|
|
52
52
|
readonly EchoBidiStream: {
|
|
53
|
-
readonly name:
|
|
53
|
+
readonly name: 'EchoBidiStream';
|
|
54
54
|
readonly I: import("@aptre/protobuf-es-lite").MessageType<EchoMsg>;
|
|
55
55
|
readonly O: import("@aptre/protobuf-es-lite").MessageType<EchoMsg>;
|
|
56
56
|
readonly kind: MethodKind.BiDiStreaming;
|
|
@@ -61,7 +61,7 @@ export declare const EchoerDefinition: {
|
|
|
61
61
|
* @generated from rpc echo.Echoer.RpcStream
|
|
62
62
|
*/
|
|
63
63
|
readonly RpcStream: {
|
|
64
|
-
readonly name:
|
|
64
|
+
readonly name: 'RpcStream';
|
|
65
65
|
readonly I: import("@aptre/protobuf-es-lite").MessageType<RpcStreamPacket>;
|
|
66
66
|
readonly O: import("@aptre/protobuf-es-lite").MessageType<RpcStreamPacket>;
|
|
67
67
|
readonly kind: MethodKind.BiDiStreaming;
|
|
@@ -72,7 +72,7 @@ export declare const EchoerDefinition: {
|
|
|
72
72
|
* @generated from rpc echo.Echoer.DoNothing
|
|
73
73
|
*/
|
|
74
74
|
readonly DoNothing: {
|
|
75
|
-
readonly name:
|
|
75
|
+
readonly name: 'DoNothing';
|
|
76
76
|
readonly I: import("@aptre/protobuf-es-lite").MessageType<Empty>;
|
|
77
77
|
readonly O: import("@aptre/protobuf-es-lite").MessageType<Empty>;
|
|
78
78
|
readonly kind: MethodKind.Unary;
|
|
@@ -7,7 +7,7 @@ import { ProtoRpc } from 'starpc';
|
|
|
7
7
|
* @generated from service e2e.mock.Mock
|
|
8
8
|
*/
|
|
9
9
|
export declare const MockDefinition: {
|
|
10
|
-
readonly typeName:
|
|
10
|
+
readonly typeName: 'e2e.mock.Mock';
|
|
11
11
|
readonly methods: {
|
|
12
12
|
/**
|
|
13
13
|
* MockRequest runs a mock unary request.
|
|
@@ -15,7 +15,7 @@ export declare const MockDefinition: {
|
|
|
15
15
|
* @generated from rpc e2e.mock.Mock.MockRequest
|
|
16
16
|
*/
|
|
17
17
|
readonly MockRequest: {
|
|
18
|
-
readonly name:
|
|
18
|
+
readonly name: 'MockRequest';
|
|
19
19
|
readonly I: import("@aptre/protobuf-es-lite").MessageType<MockMsg>;
|
|
20
20
|
readonly O: import("@aptre/protobuf-es-lite").MessageType<MockMsg>;
|
|
21
21
|
readonly kind: MethodKind.Unary;
|
package/dist/srpc/channel.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export declare class ChannelStream<T = Uint8Array> implements Duplex<AsyncGenera
|
|
|
31
31
|
private _remoteAck?;
|
|
32
32
|
private keepAlive?;
|
|
33
33
|
private idleWatchdog?;
|
|
34
|
+
private closed;
|
|
34
35
|
get isAcked(): boolean;
|
|
35
36
|
get isOpen(): boolean;
|
|
36
37
|
get isIdlePaused(): boolean;
|
|
@@ -38,6 +39,7 @@ export declare class ChannelStream<T = Uint8Array> implements Duplex<AsyncGenera
|
|
|
38
39
|
private postMessage;
|
|
39
40
|
private idleElapsed;
|
|
40
41
|
private keepAliveElapsed;
|
|
42
|
+
private finish;
|
|
41
43
|
close(error?: Error): void;
|
|
42
44
|
pauseIdle(): void;
|
|
43
45
|
resumeIdle(): void;
|
package/dist/srpc/channel.js
CHANGED
|
@@ -37,6 +37,8 @@ export class ChannelStream {
|
|
|
37
37
|
keepAlive;
|
|
38
38
|
// idleWatchdog is the receive timeout watchdog.
|
|
39
39
|
idleWatchdog;
|
|
40
|
+
// closed indicates the local channel has been torn down.
|
|
41
|
+
closed = false;
|
|
40
42
|
// isAcked checks if the stream is acknowledged by the remote.
|
|
41
43
|
get isAcked() {
|
|
42
44
|
return this.remoteAck ?? false;
|
|
@@ -114,6 +116,9 @@ export class ChannelStream {
|
|
|
114
116
|
}
|
|
115
117
|
// postMessage writes a message to the stream.
|
|
116
118
|
postMessage(msg) {
|
|
119
|
+
if (this.closed) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
117
122
|
msg.from = this.localId;
|
|
118
123
|
if (this.channel instanceof MessagePort) {
|
|
119
124
|
this.channel.postMessage(msg);
|
|
@@ -139,23 +144,37 @@ export class ChannelStream {
|
|
|
139
144
|
this.postMessage({});
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
// finish tears down local stream state, optionally notifying the remote side.
|
|
148
|
+
finish(error, notifyRemote) {
|
|
149
|
+
if (this.closed) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (notifyRemote) {
|
|
153
|
+
try {
|
|
154
|
+
this.postMessage({ closed: true, error });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Ignore close races while tearing down the local channel.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
this.closed = true;
|
|
146
161
|
// close channels
|
|
147
162
|
if (this.channel instanceof MessagePort) {
|
|
163
|
+
this.channel.onmessage = null;
|
|
148
164
|
this.channel.close();
|
|
149
165
|
}
|
|
150
166
|
else {
|
|
167
|
+
this.channel.rx.onmessage = null;
|
|
151
168
|
this.channel.tx.close();
|
|
152
169
|
this.channel.rx.close();
|
|
153
170
|
}
|
|
154
171
|
if (!this.remoteOpen && this._remoteOpen) {
|
|
155
172
|
this._remoteOpen(error || new Error('closed'));
|
|
173
|
+
delete this._remoteOpen;
|
|
156
174
|
}
|
|
157
175
|
if (!this.remoteAck && this._remoteAck) {
|
|
158
176
|
this._remoteAck(error || new Error('closed'));
|
|
177
|
+
delete this._remoteAck;
|
|
159
178
|
}
|
|
160
179
|
if (this.idleWatchdog) {
|
|
161
180
|
this.idleWatchdog.clear();
|
|
@@ -167,6 +186,10 @@ export class ChannelStream {
|
|
|
167
186
|
}
|
|
168
187
|
this._source.end(error);
|
|
169
188
|
}
|
|
189
|
+
// close closes the broadcast channels.
|
|
190
|
+
close(error) {
|
|
191
|
+
this.finish(error, true);
|
|
192
|
+
}
|
|
170
193
|
// pauseIdle pauses the idle watchdog, preventing the stream from timing out.
|
|
171
194
|
// Use this when the remote is known to be inactive (e.g., browser tab hidden).
|
|
172
195
|
pauseIdle() {
|
|
@@ -236,10 +259,11 @@ export class ChannelStream {
|
|
|
236
259
|
this._source.push(data);
|
|
237
260
|
}
|
|
238
261
|
if (err) {
|
|
239
|
-
this.
|
|
262
|
+
this.finish(err, false);
|
|
263
|
+
return;
|
|
240
264
|
}
|
|
241
|
-
|
|
242
|
-
this.
|
|
265
|
+
if (closed) {
|
|
266
|
+
this.finish(undefined, false);
|
|
243
267
|
}
|
|
244
268
|
}
|
|
245
269
|
}
|
package/dist/srpc/client.js
CHANGED
|
@@ -81,13 +81,17 @@ export class Client {
|
|
|
81
81
|
const openStreamFn = await this.openStreamCtr.wait();
|
|
82
82
|
const stream = await openStreamFn();
|
|
83
83
|
const call = new ClientRPC(rpcService, rpcMethod);
|
|
84
|
-
|
|
84
|
+
const onAbort = () => {
|
|
85
85
|
call.writeCallCancel();
|
|
86
86
|
call.close(new Error(ERR_RPC_ABORT));
|
|
87
|
-
}
|
|
87
|
+
};
|
|
88
|
+
abortSignal?.addEventListener('abort', onAbort, { once: true });
|
|
88
89
|
pipe(stream, decodePacketSource, call, encodePacketSource, stream)
|
|
89
90
|
.catch((err) => call.close(err))
|
|
90
|
-
.then(() => call.close())
|
|
91
|
+
.then(() => call.close())
|
|
92
|
+
.finally(() => {
|
|
93
|
+
abortSignal?.removeEventListener('abort', onAbort);
|
|
94
|
+
});
|
|
91
95
|
await call.writeCallStart(data ?? undefined);
|
|
92
96
|
return call;
|
|
93
97
|
}
|
package/dist/srpc/packet.d.ts
CHANGED
|
@@ -3,14 +3,14 @@ import { Source, Transform } from 'it-stream-types';
|
|
|
3
3
|
import { Packet } from './rpcproto.pb.js';
|
|
4
4
|
export declare const decodePacketSource: import("./message.js").DecodeMessageTransform<Packet>;
|
|
5
5
|
export declare const encodePacketSource: import("./message.js").EncodeMessageTransform<Packet>;
|
|
6
|
-
export declare
|
|
7
|
-
|
|
8
|
-
bytes: number;
|
|
9
|
-
}
|
|
10
|
-
export declare
|
|
11
|
-
|
|
12
|
-
bytes: number;
|
|
13
|
-
}
|
|
6
|
+
export declare function uint32LEDecode(data: Uint8ArrayList): number;
|
|
7
|
+
export declare namespace uint32LEDecode {
|
|
8
|
+
var bytes: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function uint32LEEncode(value: number): Uint8ArrayList;
|
|
11
|
+
export declare namespace uint32LEEncode {
|
|
12
|
+
var bytes: number;
|
|
13
|
+
}
|
|
14
14
|
export declare function lengthPrefixEncode(source: Source<Uint8Array | Uint8ArrayList>, lengthEncoder: typeof uint32LEEncode): AsyncGenerator<Uint8ArrayList, void, unknown>;
|
|
15
15
|
export declare function lengthPrefixDecode(source: Source<Uint8Array | Uint8ArrayList>, lengthDecoder: typeof uint32LEDecode): AsyncGenerator<Uint8ArrayList, void, unknown>;
|
|
16
16
|
export declare function prependLengthPrefixTransform(lengthEncoder?: {
|
package/dist/srpc/server.d.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { LookupMethod } from './mux.js';
|
|
2
2
|
import { ServerRPC } from './server-rpc.js';
|
|
3
3
|
import type { StreamHandler } from './conn.js';
|
|
4
|
-
import { PacketStream } from './stream.js';
|
|
5
|
-
import { RpcStreamHandler } from '../rpcstream/rpcstream.js';
|
|
4
|
+
import { HandleStreamFunc, PacketStream } from './stream.js';
|
|
6
5
|
export declare class Server implements StreamHandler {
|
|
7
6
|
private lookupMethod;
|
|
8
7
|
constructor(lookupMethod: LookupMethod);
|
|
9
|
-
get rpcStreamHandler():
|
|
8
|
+
get rpcStreamHandler(): HandleStreamFunc;
|
|
10
9
|
startRpc(): ServerRPC;
|
|
11
10
|
handlePacketStream(stream: PacketStream): ServerRPC;
|
|
12
11
|
}
|
package/dist/srpc/server.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { describe, it, beforeEach } from 'vitest';
|
|
1
|
+
import { describe, it, beforeEach, expect, vi } from 'vitest';
|
|
2
2
|
import { pipe } from 'it-pipe';
|
|
3
3
|
import { createHandler, createMux, Server, Client, StreamConn, ChannelStream, combineUint8ArrayListTransform, } from '../srpc/index.js';
|
|
4
|
-
import { EchoerDefinition, EchoerServer, runClientTest } from '../echo/index.js';
|
|
4
|
+
import { EchoerDefinition, EchoerServer, EchoerServiceName, runClientTest, } from '../echo/index.js';
|
|
5
5
|
import { runAbortControllerTest, runRpcStreamTest, } from '../echo/client-test.js';
|
|
6
6
|
describe('srpc server', () => {
|
|
7
7
|
let client;
|
|
@@ -39,4 +39,24 @@ describe('srpc server', () => {
|
|
|
39
39
|
it('should pass rpc stream tests', async () => {
|
|
40
40
|
await runRpcStreamTest(client);
|
|
41
41
|
});
|
|
42
|
+
it('removes abort listeners after a request completes', async () => {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const removeEventListener = vi.spyOn(controller.signal, 'removeEventListener');
|
|
45
|
+
await client.request(EchoerServiceName, 'Echo', new TextEncoder().encode('hello'), controller.signal);
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
47
|
+
expect(removeEventListener).toHaveBeenCalledWith('abort', expect.any(Function));
|
|
48
|
+
});
|
|
49
|
+
it('tears down passive channel close state', async () => {
|
|
50
|
+
const { port1, port2 } = new MessageChannel();
|
|
51
|
+
const opts = { idleTimeoutMs: 1000, keepAliveMs: 1000 };
|
|
52
|
+
const active = new ChannelStream('active', port1, opts);
|
|
53
|
+
const passive = new ChannelStream('passive', port2, opts);
|
|
54
|
+
const next = passive.source[Symbol.asyncIterator]().next();
|
|
55
|
+
active.close();
|
|
56
|
+
await expect(next).resolves.toEqual({ done: true, value: undefined });
|
|
57
|
+
expect(passive.closed).toBe(true);
|
|
58
|
+
expect(passive.idleWatchdog).toBeUndefined();
|
|
59
|
+
expect(passive.keepAlive).toBeUndefined();
|
|
60
|
+
expect(port2.onmessage).toBe(null);
|
|
61
|
+
});
|
|
42
62
|
});
|
package/go.mod
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
module github.com/aperturerobotics/starpc
|
|
2
2
|
|
|
3
|
-
go 1.25
|
|
3
|
+
go 1.25.0
|
|
4
4
|
|
|
5
5
|
require (
|
|
6
|
-
github.com/aperturerobotics/common v0.32.
|
|
6
|
+
github.com/aperturerobotics/common v0.32.2 // latest
|
|
7
7
|
github.com/aperturerobotics/protobuf-go-lite v0.12.2 // latest
|
|
8
|
-
github.com/aperturerobotics/util v1.
|
|
8
|
+
github.com/aperturerobotics/util v1.33.0 // latest
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
require (
|
|
12
12
|
github.com/aperturerobotics/abseil-cpp v0.0.0-20260131110040-4bb56e2f9017 // aperture-2
|
|
13
13
|
github.com/aperturerobotics/cli v1.1.0 // indirect
|
|
14
|
-
github.com/aperturerobotics/go-protoc-wasi v0.0.0-
|
|
14
|
+
github.com/aperturerobotics/go-protoc-wasi v0.0.0-20260329113540-600516012db3 // indirect
|
|
15
15
|
github.com/aperturerobotics/json-iterator-lite v1.0.1-0.20251104042408-0c9eb8a3f726 // indirect
|
|
16
16
|
github.com/aperturerobotics/protobuf v0.0.0-20260203024654-8201686529c4 // wasi
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
require (
|
|
20
|
-
github.com/aperturerobotics/go-websocket v1.8.15-0.
|
|
20
|
+
github.com/aperturerobotics/go-websocket v1.8.15-0.20260329113544-74dbfb8f11c6 // master
|
|
21
21
|
github.com/libp2p/go-yamux/v4 v4.0.2 // latest
|
|
22
22
|
github.com/pkg/errors v0.9.1 // latest
|
|
23
23
|
github.com/sirupsen/logrus v1.9.5-0.20260309202648-9f0600962f75 // latest
|
|
@@ -25,10 +25,10 @@ require (
|
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
require (
|
|
28
|
-
github.com/aperturerobotics/go-protoc-gen-prost v0.0.0-
|
|
28
|
+
github.com/aperturerobotics/go-protoc-gen-prost v0.0.0-20260329113538-218ccd8f20e0 // indirect
|
|
29
29
|
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
|
30
30
|
github.com/tetratelabs/wazero v1.11.0 // indirect
|
|
31
31
|
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
|
32
|
-
golang.org/x/mod v0.
|
|
33
|
-
golang.org/x/sys v0.
|
|
32
|
+
golang.org/x/mod v0.34.0 // indirect
|
|
33
|
+
golang.org/x/sys v0.42.0 // indirect
|
|
34
34
|
)
|
package/go.sum
CHANGED
|
@@ -2,22 +2,22 @@ github.com/aperturerobotics/abseil-cpp v0.0.0-20260131110040-4bb56e2f9017 h1:3U7
|
|
|
2
2
|
github.com/aperturerobotics/abseil-cpp v0.0.0-20260131110040-4bb56e2f9017/go.mod h1:lNSJTKECIUFAnfeSqy01kXYTYe1BHubW7198jNX3nEw=
|
|
3
3
|
github.com/aperturerobotics/cli v1.1.0 h1:7a+YRC+EY3npAnTzhHV5gLCiw91KS0Ts3XwLILGOsT8=
|
|
4
4
|
github.com/aperturerobotics/cli v1.1.0/go.mod h1:M7BFP9wow5ytTzMyJQOOO991fGfsUqdTI7gGEsHfTQ8=
|
|
5
|
-
github.com/aperturerobotics/common v0.32.
|
|
6
|
-
github.com/aperturerobotics/common v0.32.
|
|
7
|
-
github.com/aperturerobotics/go-protoc-gen-prost v0.0.0-
|
|
8
|
-
github.com/aperturerobotics/go-protoc-gen-prost v0.0.0-
|
|
9
|
-
github.com/aperturerobotics/go-protoc-wasi v0.0.0-
|
|
10
|
-
github.com/aperturerobotics/go-protoc-wasi v0.0.0-
|
|
11
|
-
github.com/aperturerobotics/go-websocket v1.8.15-0.
|
|
12
|
-
github.com/aperturerobotics/go-websocket v1.8.15-0.
|
|
5
|
+
github.com/aperturerobotics/common v0.32.2 h1:YAZfvBphNxGTF7x+O/e7UIFw8cCf5Pog6ptw7Y+PjTU=
|
|
6
|
+
github.com/aperturerobotics/common v0.32.2/go.mod h1:6glNxt6cEYab10WAVT4tOGhwgvDG2fnUMSyKuKZg02Y=
|
|
7
|
+
github.com/aperturerobotics/go-protoc-gen-prost v0.0.0-20260329113538-218ccd8f20e0 h1:6/3RSSlPEQ6LeidslB1ZCJkxW+MnfYDkvdWMDklDXw4=
|
|
8
|
+
github.com/aperturerobotics/go-protoc-gen-prost v0.0.0-20260329113538-218ccd8f20e0/go.mod h1:OBb/beWmr/pDIZAUfi86j/4tBh2v5ctTxKMqSnh9c/4=
|
|
9
|
+
github.com/aperturerobotics/go-protoc-wasi v0.0.0-20260329113540-600516012db3 h1:lp+V8RYcBwTX1p81swkpZn5fhw1wn2xLorzETIxRyZQ=
|
|
10
|
+
github.com/aperturerobotics/go-protoc-wasi v0.0.0-20260329113540-600516012db3/go.mod h1:vEq8i7EKb32+KXGtIEZjjhNns+BdsL2dUMw4uhy3578=
|
|
11
|
+
github.com/aperturerobotics/go-websocket v1.8.15-0.20260329113544-74dbfb8f11c6 h1:Utc1F7jdCc6/HrwwIikJFXt/hXxkWIWETLp/CsG6Gl0=
|
|
12
|
+
github.com/aperturerobotics/go-websocket v1.8.15-0.20260329113544-74dbfb8f11c6/go.mod h1:9KnSGuqxSXbdB/Oi0I6vvfPLkclfJwMGAQaDDGVgGow=
|
|
13
13
|
github.com/aperturerobotics/json-iterator-lite v1.0.1-0.20251104042408-0c9eb8a3f726 h1:4B1F0DzuqPzb6WqgCjWaqDD7JU9RDsevQG5OP0DFBgs=
|
|
14
14
|
github.com/aperturerobotics/json-iterator-lite v1.0.1-0.20251104042408-0c9eb8a3f726/go.mod h1:SvGGBv3OVxUyqO0ZxA/nvs6z3cg7NIbZ64TnbV2OISo=
|
|
15
15
|
github.com/aperturerobotics/protobuf v0.0.0-20260203024654-8201686529c4 h1:4Dy3BAHh2kgVdHAqtlwcFsgY0kAwUe2m3rfFcaGwGQg=
|
|
16
16
|
github.com/aperturerobotics/protobuf v0.0.0-20260203024654-8201686529c4/go.mod h1:tMgO7y6SJo/d9ZcvrpNqIQtdYT9de+QmYaHOZ4KnhOg=
|
|
17
17
|
github.com/aperturerobotics/protobuf-go-lite v0.12.2 h1:ujwTKgpIYAVsv8VljB6PPMY5SI8i8m/NlIHAkkuxQcU=
|
|
18
18
|
github.com/aperturerobotics/protobuf-go-lite v0.12.2/go.mod h1:lGH3s5ArCTXKI4wJdlNpaybUtwSjfAG0vdWjxOfMcF8=
|
|
19
|
-
github.com/aperturerobotics/util v1.
|
|
20
|
-
github.com/aperturerobotics/util v1.
|
|
19
|
+
github.com/aperturerobotics/util v1.33.0 h1:l7Aql7rlFZaGPRS+lzFC7h0zuLE0WyR3nPVXgCYMW88=
|
|
20
|
+
github.com/aperturerobotics/util v1.33.0/go.mod h1:FOKm51ZpgLsRszA4e7mjvqrt6J6Pju5GjSJg1Qz4Ouo=
|
|
21
21
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
22
22
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
23
23
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
@@ -38,10 +38,10 @@ github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbw
|
|
|
38
38
|
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
|
|
39
39
|
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
|
|
40
40
|
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
|
41
|
-
golang.org/x/mod v0.
|
|
42
|
-
golang.org/x/mod v0.
|
|
43
|
-
golang.org/x/sys v0.
|
|
44
|
-
golang.org/x/sys v0.
|
|
41
|
+
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
|
42
|
+
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
|
43
|
+
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
|
44
|
+
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
45
45
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
|
46
46
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
|
47
47
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
@@ -21,7 +21,7 @@ func main() {
|
|
|
21
21
|
addr := os.Args[1]
|
|
22
22
|
|
|
23
23
|
openStream := func(ctx context.Context, msgHandler srpc.PacketDataHandler, closeHandler srpc.CloseHandler) (srpc.PacketWriter, error) {
|
|
24
|
-
conn, err := net.Dial("tcp", addr)
|
|
24
|
+
conn, err := net.Dial("tcp", addr) //nolint:gosec
|
|
25
25
|
if err != nil {
|
|
26
26
|
return nil, err
|
|
27
27
|
}
|
|
@@ -55,7 +55,7 @@ echo "Building TypeScript server/client..."
|
|
|
55
55
|
--outfile="$SCRIPT_DIR/ts-client.mjs"
|
|
56
56
|
|
|
57
57
|
echo "Building Rust server/client..."
|
|
58
|
-
cargo build --release --bin integration-server --bin integration-client
|
|
58
|
+
cargo build --release -p echo-example --bin integration-server --bin integration-client
|
|
59
59
|
|
|
60
60
|
echo "Vendoring Go dependencies (needed for C++ build)..."
|
|
61
61
|
go mod vendor
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starpc",
|
|
3
|
-
"version": "0.49.
|
|
3
|
+
"version": "0.49.3",
|
|
4
4
|
"description": "Streaming protobuf RPC service protocol over any two-way channel.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"clean": "rimraf ./dist",
|
|
65
|
-
"build": "bun run clean &&
|
|
65
|
+
"build": "bun run clean && tsgo --outDir ./dist/",
|
|
66
66
|
"check": "bun run typecheck",
|
|
67
67
|
"typecheck": "tsgo --noEmit",
|
|
68
68
|
"deps": "depcheck --ignores 'bufferutil,utf-8-validate,rimraf,starpc,@aptre/protobuf-es-lite,tsx'",
|
|
@@ -109,23 +109,23 @@
|
|
|
109
109
|
},
|
|
110
110
|
"devDependencies": {
|
|
111
111
|
"@eslint/js": "^10.0.0",
|
|
112
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
113
|
-
"@typescript-eslint/parser": "^8.
|
|
112
|
+
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
|
113
|
+
"@typescript-eslint/parser": "^8.57.2",
|
|
114
114
|
"depcheck": "^1.4.6",
|
|
115
|
-
"esbuild": "^0.27.
|
|
116
|
-
"eslint": "^10.0
|
|
115
|
+
"esbuild": "^0.27.4",
|
|
116
|
+
"eslint": "^10.1.0",
|
|
117
117
|
"eslint-config-prettier": "^10.0.0",
|
|
118
118
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
119
|
-
"globals": "^17.
|
|
120
|
-
"happy-dom": "^20.
|
|
119
|
+
"globals": "^17.4.0",
|
|
120
|
+
"happy-dom": "^20.8.9",
|
|
121
121
|
"husky": "^9.1.7",
|
|
122
|
-
"lint-staged": "^16.
|
|
122
|
+
"lint-staged": "^16.4.0",
|
|
123
123
|
"prettier": "^3.8.1",
|
|
124
124
|
"rimraf": "^6.1.3",
|
|
125
125
|
"tsx": "^4.20.4",
|
|
126
|
-
"typescript": "^
|
|
127
|
-
"@typescript/native-preview": "^7.0.0-dev.
|
|
128
|
-
"vitest": "^4.
|
|
126
|
+
"typescript": "^6.0.0",
|
|
127
|
+
"@typescript/native-preview": "^7.0.0-dev.20260330.1",
|
|
128
|
+
"vitest": "^4.1.2"
|
|
129
129
|
},
|
|
130
130
|
"dependencies": {
|
|
131
131
|
"@aptre/it-ws": "^1.1.2",
|
|
@@ -139,9 +139,6 @@
|
|
|
139
139
|
"it-pushable": "^3.2.3",
|
|
140
140
|
"it-stream-types": "^2.0.2",
|
|
141
141
|
"uint8arraylist": "^2.4.7",
|
|
142
|
-
"ws": "^8.
|
|
143
|
-
},
|
|
144
|
-
"resolutions": {
|
|
145
|
-
"@aptre/protobuf-es-lite": "1.0.2"
|
|
142
|
+
"ws": "^8.20.0"
|
|
146
143
|
}
|
|
147
144
|
}
|
package/srpc/channel.ts
CHANGED
|
@@ -80,6 +80,8 @@ export class ChannelStream<T = Uint8Array> implements Duplex<
|
|
|
80
80
|
private keepAlive?: Watchdog
|
|
81
81
|
// idleWatchdog is the receive timeout watchdog.
|
|
82
82
|
private idleWatchdog?: Watchdog
|
|
83
|
+
// closed indicates the local channel has been torn down.
|
|
84
|
+
private closed = false
|
|
83
85
|
|
|
84
86
|
// isAcked checks if the stream is acknowledged by the remote.
|
|
85
87
|
public get isAcked() {
|
|
@@ -168,6 +170,9 @@ export class ChannelStream<T = Uint8Array> implements Duplex<
|
|
|
168
170
|
|
|
169
171
|
// postMessage writes a message to the stream.
|
|
170
172
|
private postMessage(msg: Partial<ChannelStreamMessage<T>>) {
|
|
173
|
+
if (this.closed) {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
171
176
|
msg.from = this.localId
|
|
172
177
|
if (this.channel instanceof MessagePort) {
|
|
173
178
|
this.channel.postMessage(msg)
|
|
@@ -195,22 +200,35 @@ export class ChannelStream<T = Uint8Array> implements Duplex<
|
|
|
195
200
|
}
|
|
196
201
|
}
|
|
197
202
|
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
// finish tears down local stream state, optionally notifying the remote side.
|
|
204
|
+
private finish(error?: Error, notifyRemote?: boolean) {
|
|
205
|
+
if (this.closed) {
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
if (notifyRemote) {
|
|
209
|
+
try {
|
|
210
|
+
this.postMessage({ closed: true, error })
|
|
211
|
+
} catch {
|
|
212
|
+
// Ignore close races while tearing down the local channel.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
this.closed = true
|
|
202
216
|
// close channels
|
|
203
217
|
if (this.channel instanceof MessagePort) {
|
|
218
|
+
this.channel.onmessage = null
|
|
204
219
|
this.channel.close()
|
|
205
220
|
} else {
|
|
221
|
+
this.channel.rx.onmessage = null
|
|
206
222
|
this.channel.tx.close()
|
|
207
223
|
this.channel.rx.close()
|
|
208
224
|
}
|
|
209
225
|
if (!this.remoteOpen && this._remoteOpen) {
|
|
210
226
|
this._remoteOpen(error || new Error('closed'))
|
|
227
|
+
delete this._remoteOpen
|
|
211
228
|
}
|
|
212
229
|
if (!this.remoteAck && this._remoteAck) {
|
|
213
230
|
this._remoteAck(error || new Error('closed'))
|
|
231
|
+
delete this._remoteAck
|
|
214
232
|
}
|
|
215
233
|
if (this.idleWatchdog) {
|
|
216
234
|
this.idleWatchdog.clear()
|
|
@@ -223,6 +241,11 @@ export class ChannelStream<T = Uint8Array> implements Duplex<
|
|
|
223
241
|
this._source.end(error)
|
|
224
242
|
}
|
|
225
243
|
|
|
244
|
+
// close closes the broadcast channels.
|
|
245
|
+
public close(error?: Error) {
|
|
246
|
+
this.finish(error, true)
|
|
247
|
+
}
|
|
248
|
+
|
|
226
249
|
// pauseIdle pauses the idle watchdog, preventing the stream from timing out.
|
|
227
250
|
// Use this when the remote is known to be inactive (e.g., browser tab hidden).
|
|
228
251
|
public pauseIdle() {
|
|
@@ -298,9 +321,11 @@ export class ChannelStream<T = Uint8Array> implements Duplex<
|
|
|
298
321
|
this._source.push(data)
|
|
299
322
|
}
|
|
300
323
|
if (err) {
|
|
301
|
-
this.
|
|
302
|
-
|
|
303
|
-
|
|
324
|
+
this.finish(err, false)
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
if (closed) {
|
|
328
|
+
this.finish(undefined, false)
|
|
304
329
|
}
|
|
305
330
|
}
|
|
306
331
|
}
|
package/srpc/client.ts
CHANGED
|
@@ -116,13 +116,17 @@ export class Client implements ProtoRpc {
|
|
|
116
116
|
const openStreamFn = await this.openStreamCtr.wait()
|
|
117
117
|
const stream = await openStreamFn()
|
|
118
118
|
const call = new ClientRPC(rpcService, rpcMethod)
|
|
119
|
-
|
|
119
|
+
const onAbort = () => {
|
|
120
120
|
call.writeCallCancel()
|
|
121
121
|
call.close(new Error(ERR_RPC_ABORT))
|
|
122
|
-
}
|
|
122
|
+
}
|
|
123
|
+
abortSignal?.addEventListener('abort', onAbort, { once: true })
|
|
123
124
|
pipe(stream, decodePacketSource, call, encodePacketSource, stream)
|
|
124
125
|
.catch((err) => call.close(err))
|
|
125
126
|
.then(() => call.close())
|
|
127
|
+
.finally(() => {
|
|
128
|
+
abortSignal?.removeEventListener('abort', onAbort)
|
|
129
|
+
})
|
|
126
130
|
await call.writeCallStart(data ?? undefined)
|
|
127
131
|
return call
|
|
128
132
|
}
|
package/srpc/server-http_js.go
CHANGED
|
@@ -10,7 +10,7 @@ import "errors"
|
|
|
10
10
|
type HTTPServer struct{}
|
|
11
11
|
|
|
12
12
|
// NewHTTPServer builds a http server / handler.
|
|
13
|
-
func NewHTTPServer(mux Mux, path string) (*HTTPServer, error) {
|
|
13
|
+
func NewHTTPServer(mux Mux, path string, websocketOpts any) (*HTTPServer, error) {
|
|
14
14
|
return nil, errors.New("srpc: http server not implemented on js")
|
|
15
15
|
}
|
|
16
16
|
|
package/srpc/server.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, beforeEach } from 'vitest'
|
|
1
|
+
import { describe, it, beforeEach, expect, vi } from 'vitest'
|
|
2
2
|
import { pipe } from 'it-pipe'
|
|
3
3
|
import {
|
|
4
4
|
createHandler,
|
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
combineUint8ArrayListTransform,
|
|
11
11
|
ChannelStreamOpts,
|
|
12
12
|
} from '../srpc/index.js'
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
EchoerDefinition,
|
|
15
|
+
EchoerServer,
|
|
16
|
+
EchoerServiceName,
|
|
17
|
+
runClientTest,
|
|
18
|
+
} from '../echo/index.js'
|
|
14
19
|
import {
|
|
15
20
|
runAbortControllerTest,
|
|
16
21
|
runRpcStreamTest,
|
|
@@ -71,4 +76,41 @@ describe('srpc server', () => {
|
|
|
71
76
|
it('should pass rpc stream tests', async () => {
|
|
72
77
|
await runRpcStreamTest(client)
|
|
73
78
|
})
|
|
79
|
+
|
|
80
|
+
it('removes abort listeners after a request completes', async () => {
|
|
81
|
+
const controller = new AbortController()
|
|
82
|
+
const removeEventListener = vi.spyOn(
|
|
83
|
+
controller.signal,
|
|
84
|
+
'removeEventListener',
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
await client.request(
|
|
88
|
+
EchoerServiceName,
|
|
89
|
+
'Echo',
|
|
90
|
+
new TextEncoder().encode('hello'),
|
|
91
|
+
controller.signal,
|
|
92
|
+
)
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
94
|
+
|
|
95
|
+
expect(removeEventListener).toHaveBeenCalledWith(
|
|
96
|
+
'abort',
|
|
97
|
+
expect.any(Function),
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('tears down passive channel close state', async () => {
|
|
102
|
+
const { port1, port2 } = new MessageChannel()
|
|
103
|
+
const opts: ChannelStreamOpts = { idleTimeoutMs: 1000, keepAliveMs: 1000 }
|
|
104
|
+
const active = new ChannelStream('active', port1, opts)
|
|
105
|
+
const passive = new ChannelStream('passive', port2, opts)
|
|
106
|
+
const next = passive.source[Symbol.asyncIterator]().next()
|
|
107
|
+
|
|
108
|
+
active.close()
|
|
109
|
+
await expect(next).resolves.toEqual({ done: true, value: undefined })
|
|
110
|
+
|
|
111
|
+
expect((passive as any).closed).toBe(true)
|
|
112
|
+
expect((passive as any).idleWatchdog).toBeUndefined()
|
|
113
|
+
expect((passive as any).keepAlive).toBeUndefined()
|
|
114
|
+
expect(port2.onmessage).toBe(null)
|
|
115
|
+
})
|
|
74
116
|
})
|
package/srpc/server.ts
CHANGED
|
@@ -4,8 +4,7 @@ import { LookupMethod } from './mux.js'
|
|
|
4
4
|
import { ServerRPC } from './server-rpc.js'
|
|
5
5
|
import { decodePacketSource, encodePacketSource } from './packet.js'
|
|
6
6
|
import type { StreamHandler } from './conn.js'
|
|
7
|
-
import { PacketStream } from './stream.js'
|
|
8
|
-
import { RpcStreamHandler } from '../rpcstream/rpcstream.js'
|
|
7
|
+
import { HandleStreamFunc, PacketStream } from './stream.js'
|
|
9
8
|
|
|
10
9
|
// Server implements the SRPC server in TypeScript with a Mux.
|
|
11
10
|
export class Server implements StreamHandler {
|
|
@@ -18,7 +17,7 @@ export class Server implements StreamHandler {
|
|
|
18
17
|
|
|
19
18
|
// rpcStreamHandler implements the RpcStreamHandler interface.
|
|
20
19
|
// uses handlePacketDuplex (expects 1 buf = 1 Packet)
|
|
21
|
-
public get rpcStreamHandler():
|
|
20
|
+
public get rpcStreamHandler(): HandleStreamFunc {
|
|
22
21
|
return async (stream: PacketStream) => {
|
|
23
22
|
const rpc = this.startRpc()
|
|
24
23
|
return pipe(stream, decodePacketSource, rpc, encodePacketSource, stream)
|