starpc 0.51.0 → 0.52.1

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 (93) hide show
  1. package/cmd/protoc-gen-es-starpc/typescript.ts +37 -0
  2. package/dist/cmd/protoc-gen-es-starpc/typescript.js +24 -0
  3. package/dist/echo/client-test.d.ts +1 -1
  4. package/dist/echo/client-test.js +110 -2
  5. package/dist/echo/echo_srpc.pb.d.ts +44 -1
  6. package/dist/echo/server.d.ts +9 -8
  7. package/dist/echo/server.js +6 -6
  8. package/dist/integration/cross-language/tcp-packet-stream.d.ts +3 -0
  9. package/dist/integration/cross-language/tcp-packet-stream.js +112 -0
  10. package/dist/integration/cross-language/tcp-packet-stream.test.d.ts +1 -0
  11. package/dist/integration/cross-language/tcp-packet-stream.test.js +121 -0
  12. package/dist/integration/cross-language/ts-client.js +50 -37
  13. package/dist/integration/cross-language/ts-server.js +1 -36
  14. package/dist/mock/mock_srpc.pb.d.ts +14 -1
  15. package/dist/rpcstream/rpcstream.d.ts +5 -1
  16. package/dist/rpcstream/rpcstream.js +75 -28
  17. package/dist/rpcstream/rpcstream.test.d.ts +1 -0
  18. package/dist/rpcstream/rpcstream.test.js +92 -0
  19. package/dist/srpc/channel.js +6 -3
  20. package/dist/srpc/channel.test.js +20 -1
  21. package/dist/srpc/client.js +18 -4
  22. package/dist/srpc/common-rpc.test.js +2 -0
  23. package/dist/srpc/handler.d.ts +12 -3
  24. package/dist/srpc/index.d.ts +2 -0
  25. package/dist/srpc/index.js +1 -0
  26. package/dist/srpc/invoker.d.ts +2 -1
  27. package/dist/srpc/invoker.js +2 -2
  28. package/dist/srpc/packet-codec.test.d.ts +1 -0
  29. package/dist/srpc/packet-codec.test.js +75 -0
  30. package/dist/srpc/packet.d.ts +1 -1
  31. package/dist/srpc/packet.js +11 -1
  32. package/dist/srpc/server-context.d.ts +11 -0
  33. package/dist/srpc/server-context.js +28 -0
  34. package/dist/srpc/server-rpc.js +3 -1
  35. package/dist/srpc/server.js +19 -6
  36. package/dist/srpc/server.test.js +78 -6
  37. package/dist/srpc/stream.d.ts +4 -1
  38. package/dist/srpc/stream.js +62 -3
  39. package/dist/srpc/stream.test.js +110 -1
  40. package/dist/srpc/termination.d.ts +27 -0
  41. package/dist/srpc/termination.js +56 -0
  42. package/dist/srpc/termination.test.d.ts +1 -0
  43. package/dist/srpc/termination.test.js +24 -0
  44. package/dist/srpc/watchdog.test.js +1 -0
  45. package/dist/testdata/packet-codec-vectors.json +64 -0
  46. package/echo/client-test.ts +124 -2
  47. package/echo/echo_pb2.py +40 -0
  48. package/echo/echo_pb2.pyi +13 -0
  49. package/echo/echo_srpc.pb.ts +74 -0
  50. package/echo/echo_srpc.py +306 -0
  51. package/echo/echo_srpc.pyi +85 -0
  52. package/echo/server.ts +24 -5
  53. package/go.mod +2 -2
  54. package/go.sum +14 -0
  55. package/integration/cross-language/go-client/main.go +79 -3
  56. package/integration/cross-language/python-client.py +146 -0
  57. package/integration/cross-language/python-server.py +140 -0
  58. package/integration/cross-language/run.bash +190 -65
  59. package/integration/cross-language/tcp-packet-stream.test.ts +154 -0
  60. package/integration/cross-language/tcp-packet-stream.ts +121 -0
  61. package/integration/cross-language/ts-client.ts +62 -40
  62. package/integration/cross-language/ts-server.ts +1 -45
  63. package/mock/mock_pb2.py +38 -0
  64. package/mock/mock_pb2.pyi +11 -0
  65. package/mock/mock_srpc.pb.ts +19 -1
  66. package/mock/mock_srpc.py +71 -0
  67. package/mock/mock_srpc.pyi +27 -0
  68. package/package.json +20 -6
  69. package/srpc/__init__.py +0 -0
  70. package/srpc/channel.test.ts +21 -1
  71. package/srpc/channel.ts +7 -3
  72. package/srpc/client.ts +20 -4
  73. package/srpc/codec.rs +6 -0
  74. package/srpc/common-rpc.test.ts +2 -0
  75. package/srpc/handler.ts +54 -4
  76. package/srpc/index.ts +7 -0
  77. package/srpc/invoker.ts +23 -6
  78. package/srpc/packet-codec-vectors_test.go +195 -0
  79. package/srpc/packet-codec.test.ts +139 -0
  80. package/srpc/packet-rw.go +9 -2
  81. package/srpc/packet.ts +15 -2
  82. package/srpc/py.typed +0 -0
  83. package/srpc/rpcproto_pb2.py +40 -0
  84. package/srpc/rpcproto_pb2.pyi +40 -0
  85. package/srpc/server-context.ts +55 -0
  86. package/srpc/server-rpc.ts +4 -1
  87. package/srpc/server.test.ts +100 -5
  88. package/srpc/server.ts +22 -6
  89. package/srpc/stream.test.ts +132 -1
  90. package/srpc/stream.ts +65 -9
  91. package/srpc/termination.test.ts +30 -0
  92. package/srpc/termination.ts +70 -0
  93. package/srpc/watchdog.test.ts +1 -0
@@ -26,6 +26,7 @@ import {
26
26
  } from '@aptre/protobuf-es-lite/protoplugin/ecmascript'
27
27
 
28
28
  const MessageStream = createImportSymbol('MessageStream', 'starpc')
29
+ const ServerContext = createImportSymbol('ServerContext', 'starpc')
29
30
  const RuntimeMethodKind = createImportSymbol(
30
31
  'MethodKind',
31
32
  '@aptre/protobuf-es-lite',
@@ -128,6 +129,42 @@ function generateService(
128
129
  f.print("}");
129
130
  f.print();
130
131
 
132
+ // Generate the server implementation interface.
133
+ f.print(f.jsDoc(service));
134
+ f.print("export interface ", localName(service), "Handler {");
135
+ for (let i = 0; i < service.methods.length; i++) {
136
+ const method = service.methods[i];
137
+ f.print(f.jsDoc(method, " "));
138
+ if (method.methodKind === MethodKind.Unary) {
139
+ f.print(
140
+ " ", method.name,
141
+ "(request: ", method.input, ", abortSignal: AbortSignal, context: ", ServerContext, "): ",
142
+ "Promise<", method.output, ">;"
143
+ );
144
+ } else if (method.methodKind === MethodKind.ServerStreaming) {
145
+ f.print(
146
+ " ", method.name,
147
+ "(request: ", method.input, ", abortSignal: AbortSignal, context: ", ServerContext, "): ",
148
+ MessageStream, "<", method.output, ">;"
149
+ );
150
+ } else if (method.methodKind === MethodKind.ClientStreaming) {
151
+ f.print(
152
+ " ", method.name,
153
+ "(request: ", MessageStream, "<", method.input, ">, abortSignal: AbortSignal, context: ", ServerContext, "): ",
154
+ "Promise<", method.output, ">;"
155
+ );
156
+ } else if (method.methodKind === MethodKind.BiDiStreaming) {
157
+ f.print(
158
+ " ", method.name,
159
+ "(request: ", MessageStream, "<", method.input, ">, abortSignal: AbortSignal, context: ", ServerContext, "): ",
160
+ MessageStream, "<", method.output, ">;"
161
+ );
162
+ }
163
+ if (i < service.methods.length - 1) f.print();
164
+ }
165
+ f.print("}");
166
+ f.print();
167
+
131
168
 
132
169
  // Generate the service name constant
133
170
  f.print("export const ", localName(service), "ServiceName = ", localName(service), "Definition.typeName");
@@ -15,6 +15,7 @@
15
15
  import { MethodIdempotency, MethodKind, localName, } from '@aptre/protobuf-es-lite';
16
16
  import { createImportSymbol, } from '@aptre/protobuf-es-lite/protoplugin/ecmascript';
17
17
  const MessageStream = createImportSymbol('MessageStream', 'starpc');
18
+ const ServerContext = createImportSymbol('ServerContext', 'starpc');
18
19
  const RuntimeMethodKind = createImportSymbol('MethodKind', '@aptre/protobuf-es-lite');
19
20
  const RuntimeMethodIdempotency = createImportSymbol('MethodIdempotency', '@aptre/protobuf-es-lite');
20
21
  // const Message = createImportSymbol('Message', '@aptre/protobuf-es-lite')
@@ -74,6 +75,29 @@ function generateService(f, service) {
74
75
  }
75
76
  f.print("}");
76
77
  f.print();
78
+ // Generate the server implementation interface.
79
+ f.print(f.jsDoc(service));
80
+ f.print("export interface ", localName(service), "Handler {");
81
+ for (let i = 0; i < service.methods.length; i++) {
82
+ const method = service.methods[i];
83
+ f.print(f.jsDoc(method, " "));
84
+ if (method.methodKind === MethodKind.Unary) {
85
+ f.print(" ", method.name, "(request: ", method.input, ", abortSignal: AbortSignal, context: ", ServerContext, "): ", "Promise<", method.output, ">;");
86
+ }
87
+ else if (method.methodKind === MethodKind.ServerStreaming) {
88
+ f.print(" ", method.name, "(request: ", method.input, ", abortSignal: AbortSignal, context: ", ServerContext, "): ", MessageStream, "<", method.output, ">;");
89
+ }
90
+ else if (method.methodKind === MethodKind.ClientStreaming) {
91
+ f.print(" ", method.name, "(request: ", MessageStream, "<", method.input, ">, abortSignal: AbortSignal, context: ", ServerContext, "): ", "Promise<", method.output, ">;");
92
+ }
93
+ else if (method.methodKind === MethodKind.BiDiStreaming) {
94
+ f.print(" ", method.name, "(request: ", MessageStream, "<", method.input, ">, abortSignal: AbortSignal, context: ", ServerContext, "): ", MessageStream, "<", method.output, ">;");
95
+ }
96
+ if (i < service.methods.length - 1)
97
+ f.print();
98
+ }
99
+ f.print("}");
100
+ f.print();
77
101
  // Generate the service name constant
78
102
  f.print("export const ", localName(service), "ServiceName = ", localName(service), "Definition.typeName");
79
103
  f.print();
@@ -1,4 +1,4 @@
1
1
  import { Client } from '../srpc/index.js';
2
2
  export declare function runClientTest(client: Client): Promise<void>;
3
3
  export declare function runAbortControllerTest(client: Client): Promise<void>;
4
- export declare function runRpcStreamTest(client: Client): Promise<void>;
4
+ export declare function runRpcStreamTest(client: Client, release?: boolean): Promise<void>;
@@ -1,7 +1,7 @@
1
1
  import { Client, ERR_RPC_ABORT } from '../srpc/index.js';
2
2
  import { EchoerClient } from './echo_srpc.pb.js';
3
3
  import { pushable } from 'it-pushable';
4
- import { buildRpcStreamOpenStream } from '../rpcstream/rpcstream.js';
4
+ import { buildRpcStreamOpenStream, openRpcStream, } from '../rpcstream/rpcstream.js';
5
5
  export async function runClientTest(client) {
6
6
  const demoServiceClient = new EchoerClient(client);
7
7
  console.log('Calling Echo: unary call...');
@@ -77,8 +77,14 @@ export async function runAbortControllerTest(client) {
77
77
  }
78
78
  });
79
79
  }
80
+ function requireError(error, label, fragments) {
81
+ const message = error instanceof Error ? error.message : String(error);
82
+ if (!fragments.some((fragment) => message.includes(fragment))) {
83
+ throw new Error(`${label} returned unexpected error: ${message}`);
84
+ }
85
+ }
80
86
  // runRpcStreamTest tests a RPCStream.
81
- export async function runRpcStreamTest(client) {
87
+ export async function runRpcStreamTest(client, release = false) {
82
88
  console.log('Calling RpcStream to open a RPC stream client...');
83
89
  const service = new EchoerClient(client);
84
90
  const openStreamFn = buildRpcStreamOpenStream('test', service.RpcStream.bind(service));
@@ -89,4 +95,106 @@ export async function runRpcStreamTest(client) {
89
95
  console.log('rpc stream test: succeeded: response: ' + resp.body);
90
96
  console.log('Running client test over RPC stream...');
91
97
  await runClientTest(proxiedClient);
98
+ if (release) {
99
+ let unknownRejected = false;
100
+ try {
101
+ await openRpcStream('missing', service.RpcStream.bind(service), true);
102
+ }
103
+ catch (error) {
104
+ unknownRejected = true;
105
+ requireError(error, 'unknown component', ['unknown component: missing']);
106
+ }
107
+ if (!unknownRejected) {
108
+ throw new Error('unknown component unexpectedly succeeded');
109
+ }
110
+ }
111
+ let methodRejected = false;
112
+ try {
113
+ await proxiedClient.request('missing.Service', 'Missing', new Uint8Array());
114
+ }
115
+ catch (error) {
116
+ methodRejected = true;
117
+ requireError(error, 'unknown nested method', [
118
+ 'missing.Service',
119
+ 'unimplemented',
120
+ ]);
121
+ }
122
+ if (!methodRejected) {
123
+ throw new Error('unknown nested method unexpectedly succeeded');
124
+ }
125
+ if (release) {
126
+ const terminalService = new EchoerClient(proxiedClient);
127
+ let terminalFailed = false;
128
+ try {
129
+ await terminalService.Echo({ body: '__nested_error__' });
130
+ }
131
+ catch (error) {
132
+ terminalFailed = true;
133
+ requireError(error, 'terminal nested error', ['nested terminal error']);
134
+ }
135
+ if (!terminalFailed) {
136
+ throw new Error('terminal nested error unexpectedly succeeded');
137
+ }
138
+ }
139
+ if (release) {
140
+ const releaseClient = new Client(buildRpcStreamOpenStream('release', service.RpcStream.bind(service)));
141
+ let releaseFailed = false;
142
+ const releaseService = new EchoerClient(releaseClient);
143
+ try {
144
+ await releaseService.Echo({ body: '__nested_release__' });
145
+ }
146
+ catch (error) {
147
+ releaseFailed = true;
148
+ requireError(error, 'release during active call', [
149
+ 'closed before completion',
150
+ 'stream closed',
151
+ 'abort',
152
+ 'cancel',
153
+ ]);
154
+ }
155
+ if (!releaseFailed) {
156
+ throw new Error('release during active call unexpectedly succeeded');
157
+ }
158
+ const releaseStatus = await service.Echo({
159
+ body: '__nested_release_status__',
160
+ });
161
+ if (releaseStatus.body !== 'released') {
162
+ throw new Error(`release completion returned ${releaseStatus.body}`);
163
+ }
164
+ let releasedRejected = false;
165
+ try {
166
+ await releaseService.Echo({});
167
+ }
168
+ catch (error) {
169
+ releasedRejected = true;
170
+ requireError(error, 'released component', ['unknown component: release']);
171
+ }
172
+ if (!releasedRejected) {
173
+ throw new Error('released component unexpectedly remained available');
174
+ }
175
+ }
176
+ const cancelled = new AbortController();
177
+ const cancelledStream = proxiedClient.bidirectionalStreamingRequest('echo.Echoer', 'EchoBidiStream', (async function* () {
178
+ yield new Uint8Array();
179
+ await new Promise(() => undefined);
180
+ })(), cancelled.signal);
181
+ const cancelledIterator = cancelledStream[Symbol.asyncIterator]();
182
+ const firstNestedResponse = await cancelledIterator.next();
183
+ if (firstNestedResponse.done) {
184
+ throw new Error('nested cancellation call ended before its first response');
185
+ }
186
+ cancelled.abort();
187
+ let cancelRejected = false;
188
+ try {
189
+ while (!(await cancelledIterator.next()).done) {
190
+ // Drain until the abort reaches the nested call.
191
+ }
192
+ }
193
+ catch (error) {
194
+ cancelRejected = true;
195
+ requireError(error, 'nested cancellation', [ERR_RPC_ABORT]);
196
+ }
197
+ if (!cancelRejected) {
198
+ throw new Error('nested cancellation unexpectedly completed normally');
199
+ }
92
200
  }
@@ -2,7 +2,7 @@ import { EchoMsg } from './echo.pb.js';
2
2
  import { MethodKind } from '@aptre/protobuf-es-lite';
3
3
  import { RpcStreamPacket } from '../rpcstream/rpcstream.pb.js';
4
4
  import { Empty } from '@aptre/protobuf-es-lite/google/protobuf/empty';
5
- import { MessageStream, ProtoRpc } from 'starpc';
5
+ import { MessageStream, ProtoRpc, ServerContext } from 'starpc';
6
6
  /**
7
7
  * Echoer service returns the given message.
8
8
  *
@@ -122,6 +122,49 @@ export interface Echoer {
122
122
  */
123
123
  DoNothing(request: Empty, abortSignal?: AbortSignal): Promise<Empty>;
124
124
  }
125
+ /**
126
+ * Echoer service returns the given message.
127
+ *
128
+ * @generated from service echo.Echoer
129
+ */
130
+ export interface EchoerHandler {
131
+ /**
132
+ * Echo returns the given message.
133
+ *
134
+ * @generated from rpc echo.Echoer.Echo
135
+ */
136
+ Echo(request: EchoMsg, abortSignal: AbortSignal, context: ServerContext): Promise<EchoMsg>;
137
+ /**
138
+ * EchoServerStream is an example of a server -> client one-way stream.
139
+ *
140
+ * @generated from rpc echo.Echoer.EchoServerStream
141
+ */
142
+ EchoServerStream(request: EchoMsg, abortSignal: AbortSignal, context: ServerContext): MessageStream<EchoMsg>;
143
+ /**
144
+ * EchoClientStream is an example of client->server one-way stream.
145
+ *
146
+ * @generated from rpc echo.Echoer.EchoClientStream
147
+ */
148
+ EchoClientStream(request: MessageStream<EchoMsg>, abortSignal: AbortSignal, context: ServerContext): Promise<EchoMsg>;
149
+ /**
150
+ * EchoBidiStream is an example of a two-way stream.
151
+ *
152
+ * @generated from rpc echo.Echoer.EchoBidiStream
153
+ */
154
+ EchoBidiStream(request: MessageStream<EchoMsg>, abortSignal: AbortSignal, context: ServerContext): MessageStream<EchoMsg>;
155
+ /**
156
+ * RpcStream opens a nested rpc call stream.
157
+ *
158
+ * @generated from rpc echo.Echoer.RpcStream
159
+ */
160
+ RpcStream(request: MessageStream<RpcStreamPacket>, abortSignal: AbortSignal, context: ServerContext): MessageStream<RpcStreamPacket>;
161
+ /**
162
+ * DoNothing does nothing.
163
+ *
164
+ * @generated from rpc echo.Echoer.DoNothing
165
+ */
166
+ DoNothing(request: Empty, abortSignal: AbortSignal, context: ServerContext): Promise<Empty>;
167
+ }
125
168
  export declare const EchoerServiceName: "echo.Echoer";
126
169
  export declare class EchoerClient implements Echoer {
127
170
  private readonly rpc;
@@ -2,16 +2,17 @@ import { Message } from '@aptre/protobuf-es-lite';
2
2
  import { Empty } from '@aptre/protobuf-es-lite/google/protobuf/empty';
3
3
  import { EchoMsg } from './echo.pb.js';
4
4
  import { Server } from '../srpc/server.js';
5
+ import type { ServerContext } from '../srpc/server-context.js';
5
6
  import { RpcStreamPacket } from '../rpcstream/rpcstream.pb.js';
6
7
  import { MessageStream } from '../srpc/message.js';
7
- import { Echoer } from './echo_srpc.pb.js';
8
- export declare class EchoerServer implements Echoer {
8
+ import type { EchoerHandler } from './echo_srpc.pb.js';
9
+ export declare class EchoerServer implements EchoerHandler {
9
10
  private proxyServer?;
10
11
  constructor(proxyServer?: Server);
11
- Echo(request: EchoMsg): Promise<Message<EchoMsg>>;
12
- EchoServerStream(request: EchoMsg): MessageStream<EchoMsg>;
13
- EchoClientStream(request: MessageStream<EchoMsg>): Promise<Message<EchoMsg>>;
14
- EchoBidiStream(request: MessageStream<EchoMsg>): MessageStream<EchoMsg>;
15
- RpcStream(request: MessageStream<RpcStreamPacket>): MessageStream<RpcStreamPacket>;
16
- DoNothing(): Promise<Empty>;
12
+ Echo(request: EchoMsg, _abortSignal: AbortSignal, _context: ServerContext): Promise<Message<EchoMsg>>;
13
+ EchoServerStream(request: EchoMsg, _abortSignal: AbortSignal, _context: ServerContext): MessageStream<EchoMsg>;
14
+ EchoClientStream(request: MessageStream<EchoMsg>, _abortSignal: AbortSignal, _context: ServerContext): Promise<Message<EchoMsg>>;
15
+ EchoBidiStream(request: MessageStream<EchoMsg>, _abortSignal: AbortSignal, _context: ServerContext): MessageStream<EchoMsg>;
16
+ RpcStream(request: MessageStream<RpcStreamPacket>, _abortSignal: AbortSignal, _context: ServerContext): MessageStream<RpcStreamPacket>;
17
+ DoNothing(_request: Empty, _abortSignal: AbortSignal, _context: ServerContext): Promise<Empty>;
17
18
  }
@@ -8,16 +8,16 @@ export class EchoerServer {
8
8
  constructor(proxyServer) {
9
9
  this.proxyServer = proxyServer;
10
10
  }
11
- async Echo(request) {
11
+ async Echo(request, _abortSignal, _context) {
12
12
  return request;
13
13
  }
14
- async *EchoServerStream(request) {
14
+ async *EchoServerStream(request, _abortSignal, _context) {
15
15
  for (let i = 0; i < 5; i++) {
16
16
  yield request;
17
17
  await new Promise((resolve) => setTimeout(resolve, 200));
18
18
  }
19
19
  }
20
- async EchoClientStream(request) {
20
+ async EchoClientStream(request, _abortSignal, _context) {
21
21
  // return the first message sent by the client.
22
22
  const message = await first(request);
23
23
  if (!message) {
@@ -25,14 +25,14 @@ export class EchoerServer {
25
25
  }
26
26
  return message;
27
27
  }
28
- EchoBidiStream(request) {
28
+ EchoBidiStream(request, _abortSignal, _context) {
29
29
  // build result observable
30
30
  const result = messagePushable();
31
31
  result.push({ body: 'hello from server' });
32
32
  writeToPushable(request, result);
33
33
  return result;
34
34
  }
35
- RpcStream(request) {
35
+ RpcStream(request, _abortSignal, _context) {
36
36
  return handleRpcStream(request[Symbol.asyncIterator](), async () => {
37
37
  if (!this.proxyServer) {
38
38
  throw new Error('rpc stream proxy server not set');
@@ -40,7 +40,7 @@ export class EchoerServer {
40
40
  return this.proxyServer.rpcStreamHandler;
41
41
  });
42
42
  }
43
- async DoNothing() {
43
+ async DoNothing(_request, _abortSignal, _context) {
44
44
  return {};
45
45
  }
46
46
  }
@@ -0,0 +1,3 @@
1
+ import net from 'net';
2
+ import type { PacketStream } from '../../srpc/stream.js';
3
+ export declare function tcpSocketToPacketStream(socket: net.Socket): PacketStream;
@@ -0,0 +1,112 @@
1
+ import { pipe } from 'it-pipe';
2
+ import { pushable } from 'it-pushable';
3
+ import { combineUint8ArrayListTransform } from '../../srpc/array-list.js';
4
+ import { parseLengthPrefixTransform, prependLengthPrefixTransform, } from '../../srpc/packet.js';
5
+ import { closeIterator, sourceIterator, TerminationGate, } from '../../srpc/termination.js';
6
+ // tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
7
+ export function tcpSocketToPacketStream(socket) {
8
+ const sourceTermination = new TerminationGate();
9
+ const sinkTermination = new TerminationGate();
10
+ const bytes = pushable({ objectMode: true });
11
+ let bytesEnded = false;
12
+ const endBytes = (error) => {
13
+ if (bytesEnded)
14
+ return;
15
+ bytesEnded = true;
16
+ bytes.end(error);
17
+ };
18
+ const terminate = (error) => {
19
+ const first = sourceTermination.terminate(error);
20
+ sinkTermination.terminate(error);
21
+ endBytes(error);
22
+ return first;
23
+ };
24
+ socket.on('data', (data) => {
25
+ if (!bytesEnded)
26
+ bytes.push(new Uint8Array(data));
27
+ });
28
+ socket.on('end', () => endBytes());
29
+ socket.on('error', (error) => terminate(error));
30
+ socket.on('close', () => {
31
+ endBytes();
32
+ sinkTermination.terminate();
33
+ });
34
+ const close = async () => {
35
+ if (terminate())
36
+ socket.destroy();
37
+ };
38
+ const abort = (error) => {
39
+ if (terminate(error))
40
+ socket.destroy(error);
41
+ };
42
+ return {
43
+ close,
44
+ abort,
45
+ source: (async function* () {
46
+ const packets = pipe(bytes, parseLengthPrefixTransform(), combineUint8ArrayListTransform())[Symbol.asyncIterator]();
47
+ try {
48
+ while (true) {
49
+ const next = await sourceTermination.next(packets);
50
+ if ('terminated' in next) {
51
+ if (next.error)
52
+ throw next.error;
53
+ return;
54
+ }
55
+ if ('error' in next)
56
+ throw next.error;
57
+ if (next.result.done)
58
+ return;
59
+ yield next.result.value;
60
+ }
61
+ }
62
+ finally {
63
+ closeIterator(packets);
64
+ }
65
+ })(),
66
+ sink: async (source) => {
67
+ const iterator = sourceIterator(pipe(source, prependLengthPrefixTransform()));
68
+ try {
69
+ while (true) {
70
+ const next = await sinkTermination.next(iterator);
71
+ if ('terminated' in next) {
72
+ if (next.error)
73
+ throw next.error;
74
+ return;
75
+ }
76
+ if ('error' in next)
77
+ throw next.error;
78
+ if (next.result.done) {
79
+ socket.end();
80
+ return;
81
+ }
82
+ const data = next.result.value instanceof Uint8Array
83
+ ? next.result.value
84
+ : next.result.value.subarray();
85
+ const written = await sinkTermination.wait(new Promise((resolve, reject) => {
86
+ socket.write(data, (error) => {
87
+ if (error)
88
+ reject(error);
89
+ else
90
+ resolve();
91
+ });
92
+ }));
93
+ if ('terminated' in written) {
94
+ if (written.error)
95
+ throw written.error;
96
+ return;
97
+ }
98
+ if ('error' in written)
99
+ throw written.error;
100
+ }
101
+ }
102
+ catch (err) {
103
+ const error = err instanceof Error ? err : new Error(String(err));
104
+ abort(error);
105
+ throw error;
106
+ }
107
+ finally {
108
+ closeIterator(iterator);
109
+ }
110
+ },
111
+ };
112
+ }
@@ -0,0 +1,121 @@
1
+ import net from 'net';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import { pushable } from 'it-pushable';
4
+ import { tcpSocketToPacketStream } from './tcp-packet-stream.js';
5
+ const sockets = [];
6
+ const servers = [];
7
+ afterEach(() => {
8
+ for (const socket of sockets)
9
+ socket.destroy();
10
+ for (const server of servers)
11
+ server.close();
12
+ sockets.length = 0;
13
+ servers.length = 0;
14
+ });
15
+ describe('TCP PacketStream lifecycle', () => {
16
+ it('settles a blocked source when the peer ends cleanly', async () => {
17
+ const { local, peer } = await connectSockets();
18
+ const stream = tcpSocketToPacketStream(local);
19
+ const pending = stream.source.next();
20
+ peer.end();
21
+ await expect(settleBeforeTimeout(pending)).resolves.toEqual({
22
+ done: true,
23
+ value: undefined,
24
+ });
25
+ });
26
+ it('reports a truncated frame when the peer ends', async () => {
27
+ const { local, peer } = await connectSockets();
28
+ const stream = tcpSocketToPacketStream(local);
29
+ const pending = stream.source.next();
30
+ peer.end(Buffer.from([4, 0, 0, 0, 10, 1]));
31
+ await expect(settleBeforeTimeout(pending)).rejects.toThrow('truncated packet frame');
32
+ });
33
+ it('rejects a blocked source with the abort error', async () => {
34
+ const { local } = await connectSockets();
35
+ const stream = tcpSocketToPacketStream(local);
36
+ const pending = stream.source.next();
37
+ const error = new Error('stopped');
38
+ stream.abort(error);
39
+ await expect(settleBeforeTimeout(pending)).rejects.toBe(error);
40
+ });
41
+ it('settles a blocked sink when closed', async () => {
42
+ const { local } = await connectSockets();
43
+ const stream = tcpSocketToPacketStream(local);
44
+ const input = pushable({ objectMode: true });
45
+ const pending = stream.sink(input);
46
+ await stream.close();
47
+ await expect(pending).resolves.toBeUndefined();
48
+ });
49
+ it('settles an in-flight socket write when closed', async () => {
50
+ const { local } = await connectSockets();
51
+ const stream = tcpSocketToPacketStream(local);
52
+ const input = pushable({ objectMode: true });
53
+ const writeStarted = Promise.withResolvers();
54
+ vi.spyOn(local, 'write').mockImplementation(((_data, _callback) => {
55
+ writeStarted.resolve();
56
+ return true;
57
+ }));
58
+ input.push(new Uint8Array([1]));
59
+ const pending = stream.sink(input);
60
+ await writeStarted.promise;
61
+ await stream.close();
62
+ await expect(settleBeforeTimeout(pending)).resolves.toBeUndefined();
63
+ });
64
+ it('rejects a blocked sink with the abort error', async () => {
65
+ const { local } = await connectSockets();
66
+ const stream = tcpSocketToPacketStream(local);
67
+ const input = pushable({ objectMode: true });
68
+ const pending = stream.sink(input);
69
+ const error = new Error('stopped');
70
+ stream.abort(error);
71
+ await expect(pending).rejects.toBe(error);
72
+ });
73
+ it('does not write ready input after close', async () => {
74
+ const { local, peer } = await connectSockets();
75
+ const stream = tcpSocketToPacketStream(local);
76
+ const input = pushable({ objectMode: true });
77
+ const received = [];
78
+ peer.on('data', (data) => {
79
+ if (typeof data !== 'string')
80
+ received.push(data);
81
+ });
82
+ input.push(new Uint8Array([1]));
83
+ const pending = stream.sink(input);
84
+ await stream.close();
85
+ await pending;
86
+ await new Promise((resolve) => peer.once('close', () => resolve()));
87
+ expect(received).toEqual([]);
88
+ });
89
+ });
90
+ async function connectSockets() {
91
+ const accepted = Promise.withResolvers();
92
+ const server = net.createServer((socket) => accepted.resolve(socket));
93
+ servers.push(server);
94
+ await new Promise((resolve, reject) => {
95
+ server.once('error', reject);
96
+ server.listen(0, '127.0.0.1', resolve);
97
+ });
98
+ const address = server.address();
99
+ const local = net.connect(address.port, address.address);
100
+ await new Promise((resolve, reject) => {
101
+ local.once('connect', resolve);
102
+ local.once('error', reject);
103
+ });
104
+ const peer = await accepted.promise;
105
+ sockets.push(local, peer);
106
+ return { local, peer };
107
+ }
108
+ async function settleBeforeTimeout(promise) {
109
+ let timeout;
110
+ try {
111
+ return await Promise.race([
112
+ promise,
113
+ new Promise((_resolve, reject) => {
114
+ timeout = setTimeout(() => reject(new Error('source stayed blocked')), 500);
115
+ }),
116
+ ]);
117
+ }
118
+ finally {
119
+ clearTimeout(timeout);
120
+ }
121
+ }