starpc 0.50.0 → 0.51.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 (81) hide show
  1. package/dist/integration/cross-language/ts-client.js +5 -79
  2. package/dist/integration/cross-language/ts-server.js +4 -100
  3. package/dist/srpc/client.d.ts +1 -3
  4. package/dist/srpc/client.js +2 -22
  5. package/dist/srpc/common-rpc.d.ts +1 -9
  6. package/dist/srpc/common-rpc.js +4 -62
  7. package/dist/srpc/handler.d.ts +1 -2
  8. package/dist/srpc/index.d.ts +0 -4
  9. package/dist/srpc/index.js +0 -3
  10. package/dist/srpc/invoker.d.ts +1 -2
  11. package/dist/srpc/rpcproto.pb.d.ts +0 -44
  12. package/dist/srpc/rpcproto.pb.js +0 -53
  13. package/dist/srpc/server-rpc.js +1 -3
  14. package/dist/srpc/server.test.js +35 -1
  15. package/echo/echo.go +8 -8
  16. package/echo/echo.pb.go +12 -36
  17. package/echo/echo_srpc.pb.cpp +1 -1
  18. package/echo/echo_srpc.pb.go +1 -1
  19. package/echo/echo_srpc.pb.hpp +1 -1
  20. package/echo/echo_srpc.pb.rs +1 -1
  21. package/echo/server.go +1 -1
  22. package/go.mod +1 -1
  23. package/go.sum +2 -0
  24. package/integration/cross-language/go-client/main.go +4 -142
  25. package/integration/cross-language/go-server/main.go +2 -170
  26. package/integration/cross-language/run.bash +5 -105
  27. package/integration/cross-language/ts-client.ts +5 -90
  28. package/integration/cross-language/ts-server.ts +4 -112
  29. package/mock/mock.go +1 -1
  30. package/mock/mock.pb.go +12 -36
  31. package/mock/mock_srpc.pb.cpp +1 -1
  32. package/mock/mock_srpc.pb.go +1 -1
  33. package/mock/mock_srpc.pb.hpp +1 -1
  34. package/mock/mock_srpc.pb.rs +1 -1
  35. package/package.json +1 -1
  36. package/srpc/client-prefix.go +1 -1
  37. package/srpc/client-set.go +1 -1
  38. package/srpc/client.go +1 -1
  39. package/srpc/client.ts +4 -31
  40. package/srpc/common-rpc.go +15 -69
  41. package/srpc/common-rpc.ts +4 -77
  42. package/srpc/common-rpc_test.go +81 -0
  43. package/srpc/errors.go +26 -1
  44. package/srpc/handler.ts +1 -2
  45. package/srpc/index.ts +0 -4
  46. package/srpc/invoker-prefix.go +1 -1
  47. package/srpc/invoker.go +2 -2
  48. package/srpc/invoker.ts +5 -9
  49. package/srpc/message.go +1 -1
  50. package/srpc/msg-stream.go +1 -9
  51. package/srpc/mux-verbose.go +1 -1
  52. package/srpc/mux.go +1 -1
  53. package/srpc/rpcproto.pb.cc +4 -15
  54. package/srpc/rpcproto.pb.go +102 -419
  55. package/srpc/rpcproto.pb.h +0 -59
  56. package/srpc/rpcproto.pb.rs +0 -45
  57. package/srpc/rpcproto.pb.ts +0 -62
  58. package/srpc/rpcproto.proto +0 -16
  59. package/srpc/server-rpc.go +1 -1
  60. package/srpc/server-rpc.ts +1 -6
  61. package/srpc/server.test.ts +49 -1
  62. package/srpc/stream-pipe.go +1 -1
  63. package/srpc/stream-rwc.go +1 -1
  64. package/srpc/stream.go +0 -20
  65. package/dist/rpcstream/receipt.test.d.ts +0 -1
  66. package/dist/rpcstream/receipt.test.js +0 -41
  67. package/dist/srpc/call-receipt.d.ts +0 -17
  68. package/dist/srpc/call-receipt.js +0 -106
  69. package/dist/srpc/call-receipt.test.d.ts +0 -1
  70. package/dist/srpc/call-receipt.test.js +0 -375
  71. package/dist/srpc/server-invocation.d.ts +0 -17
  72. package/dist/srpc/server-invocation.js +0 -37
  73. package/integration/cross-language/go-server/fixture-owner_test.go +0 -127
  74. package/srpc/call-receipt-e2e_test.go +0 -111
  75. package/srpc/call-receipt.go +0 -112
  76. package/srpc/call-receipt.test.ts +0 -441
  77. package/srpc/call-receipt.ts +0 -131
  78. package/srpc/call-receipt_test.go +0 -536
  79. package/srpc/schema-ownership_test.go +0 -115
  80. package/srpc/server-invocation.go +0 -40
  81. package/srpc/server-invocation.ts +0 -76
@@ -30,7 +30,6 @@
30
30
  #include "google/protobuf/message_lite.h"
31
31
  #include "google/protobuf/repeated_field.h" // IWYU pragma: export
32
32
  #include "google/protobuf/extension_set.h" // IWYU pragma: export
33
- #include "google/protobuf/generated_enum_reflection.h"
34
33
  #include "google/protobuf/unknown_field_set.h"
35
34
  // @@protoc_insertion_point(includes)
36
35
 
@@ -56,8 +55,6 @@ extern "C" {
56
55
  extern const ::google::protobuf::internal::DescriptorTable descriptor_table_github_2ecom_2faperturerobotics_2fstarpc_2fsrpc_2frpcproto_2eproto;
57
56
  } // extern "C"
58
57
  namespace srpc {
59
- enum TerminalKind : int;
60
- extern const uint32_t TerminalKind_internal_data_[];
61
58
  class CallData;
62
59
  struct CallDataDefaultTypeInternal;
63
60
  extern CallDataDefaultTypeInternal _CallData_default_instance_;
@@ -73,53 +70,10 @@ extern const ::google::protobuf::internal::ClassDataFull Packet_class_data_;
73
70
  } // namespace srpc
74
71
  namespace google {
75
72
  namespace protobuf {
76
- template <>
77
- internal::EnumTraitsT<::srpc::TerminalKind_internal_data_>
78
- internal::EnumTraitsImpl::value<::srpc::TerminalKind>;
79
73
  } // namespace protobuf
80
74
  } // namespace google
81
75
 
82
76
  namespace srpc {
83
- enum TerminalKind : int {
84
- TERMINAL_KIND_UNSPECIFIED = 0,
85
- TERMINAL_KIND_COMMITTED = 1,
86
- TERMINAL_KIND_CANCELED = 2,
87
- TERMINAL_KIND_TRANSPORT_LOST = 3,
88
- TERMINAL_KIND_CLOSED = 4,
89
- TERMINAL_KIND_ABANDONED = 5,
90
- TerminalKind_INT_MIN_SENTINEL_DO_NOT_USE_ =
91
- ::std::numeric_limits<::int32_t>::min(),
92
- TerminalKind_INT_MAX_SENTINEL_DO_NOT_USE_ =
93
- ::std::numeric_limits<::int32_t>::max(),
94
- };
95
-
96
- extern const uint32_t TerminalKind_internal_data_[];
97
- inline constexpr TerminalKind TerminalKind_MIN =
98
- static_cast<TerminalKind>(0);
99
- inline constexpr TerminalKind TerminalKind_MAX =
100
- static_cast<TerminalKind>(5);
101
- inline bool TerminalKind_IsValid(int value) {
102
- return 0 <= value && value <= 5;
103
- }
104
- inline constexpr int TerminalKind_ARRAYSIZE = 5 + 1;
105
- const ::google::protobuf::EnumDescriptor* PROTOBUF_NONNULL TerminalKind_descriptor();
106
- template <typename T>
107
- const ::std::string& TerminalKind_Name(T value) {
108
- static_assert(::std::is_same<T, TerminalKind>::value ||
109
- ::std::is_integral<T>::value,
110
- "Incorrect type passed to TerminalKind_Name().");
111
- return TerminalKind_Name(static_cast<TerminalKind>(value));
112
- }
113
- template <>
114
- inline const ::std::string& TerminalKind_Name(TerminalKind value) {
115
- return ::google::protobuf::internal::NameOfDenseEnum<TerminalKind_descriptor, 0, 5>(
116
- static_cast<int>(value));
117
- }
118
- inline bool TerminalKind_Parse(
119
- ::absl::string_view name, TerminalKind* PROTOBUF_NONNULL value) {
120
- return ::google::protobuf::internal::ParseNamedEnum<TerminalKind>(TerminalKind_descriptor(), name,
121
- value);
122
- }
123
77
 
124
78
  // ===================================================================
125
79
 
@@ -1490,19 +1444,6 @@ inline void CallData::set_allocated_error(::std::string* PROTOBUF_NULLABLE value
1490
1444
  } // namespace srpc
1491
1445
 
1492
1446
 
1493
- namespace google {
1494
- namespace protobuf {
1495
-
1496
- template <>
1497
- struct is_proto_enum<::srpc::TerminalKind> : std::true_type {};
1498
- template <>
1499
- inline const EnumDescriptor* PROTOBUF_NONNULL GetEnumDescriptor<::srpc::TerminalKind>() {
1500
- return ::srpc::TerminalKind_descriptor();
1501
- }
1502
-
1503
- } // namespace protobuf
1504
- } // namespace google
1505
-
1506
1447
  // @@protoc_insertion_point(global_scope)
1507
1448
 
1508
1449
  #include "google/protobuf/port_undef.inc"
@@ -59,49 +59,4 @@ pub struct CallData {
59
59
  #[prost(string, tag="4")]
60
60
  pub error: ::prost::alloc::string::String,
61
61
  }
62
- /// TerminalKind is the terminal state of a held unary invocation.
63
- #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
64
- #[repr(i32)]
65
- pub enum TerminalKind {
66
- /// TERMINAL_KIND_UNSPECIFIED is not a valid terminal state.
67
- Unspecified = 0,
68
- /// TERMINAL_KIND_COMMITTED identifies an explicit remote CallData completion.
69
- Committed = 1,
70
- /// TERMINAL_KIND_CANCELED identifies a remote CallCancel packet.
71
- Canceled = 2,
72
- /// TERMINAL_KIND_TRANSPORT_LOST identifies a remote error or transport failure.
73
- TransportLost = 3,
74
- /// TERMINAL_KIND_CLOSED identifies a bare remote close without completion.
75
- Closed = 4,
76
- /// TERMINAL_KIND_ABANDONED identifies owner-context expiry without a remote terminal.
77
- Abandoned = 5,
78
- }
79
- impl TerminalKind {
80
- /// String value of the enum field names used in the ProtoBuf definition.
81
- ///
82
- /// The values are not transformed in any way and thus are considered stable
83
- /// (if the ProtoBuf definition does not change) and safe for programmatic use.
84
- pub fn as_str_name(&self) -> &'static str {
85
- match self {
86
- Self::Unspecified => "TERMINAL_KIND_UNSPECIFIED",
87
- Self::Committed => "TERMINAL_KIND_COMMITTED",
88
- Self::Canceled => "TERMINAL_KIND_CANCELED",
89
- Self::TransportLost => "TERMINAL_KIND_TRANSPORT_LOST",
90
- Self::Closed => "TERMINAL_KIND_CLOSED",
91
- Self::Abandoned => "TERMINAL_KIND_ABANDONED",
92
- }
93
- }
94
- /// Creates an enum from field names used in the ProtoBuf definition.
95
- pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
96
- match value {
97
- "TERMINAL_KIND_UNSPECIFIED" => Some(Self::Unspecified),
98
- "TERMINAL_KIND_COMMITTED" => Some(Self::Committed),
99
- "TERMINAL_KIND_CANCELED" => Some(Self::Canceled),
100
- "TERMINAL_KIND_TRANSPORT_LOST" => Some(Self::TransportLost),
101
- "TERMINAL_KIND_CLOSED" => Some(Self::Closed),
102
- "TERMINAL_KIND_ABANDONED" => Some(Self::Abandoned),
103
- _ => None,
104
- }
105
- }
106
- }
107
62
  // @@protoc_insertion_point(module)
@@ -2,7 +2,6 @@
2
2
  // @generated from file github.com/aperturerobotics/starpc/srpc/rpcproto.proto (package srpc, syntax proto3)
3
3
  /* eslint-disable */
4
4
 
5
- import { createEnumType } from '@aptre/protobuf-es-lite/enum'
6
5
  import type { MessageType } from '@aptre/protobuf-es-lite/message'
7
6
  import { createMessageType } from '@aptre/protobuf-es-lite/message'
8
7
  import { ScalarType } from '@aptre/protobuf-es-lite/scalar'
@@ -10,67 +9,6 @@ import type { PartialFieldInfo } from '@aptre/protobuf-es-lite/field'
10
9
 
11
10
  export const protobufPackage = 'srpc'
12
11
 
13
- /**
14
- * TerminalKind is the terminal state of a held unary invocation.
15
- *
16
- * @generated from enum srpc.TerminalKind
17
- */
18
- export enum TerminalKind {
19
- /**
20
- * TERMINAL_KIND_UNSPECIFIED is not a valid terminal state.
21
- *
22
- * @generated from enum value: TERMINAL_KIND_UNSPECIFIED = 0;
23
- */
24
- UNSPECIFIED = 0,
25
-
26
- /**
27
- * TERMINAL_KIND_COMMITTED identifies an explicit remote CallData completion.
28
- *
29
- * @generated from enum value: TERMINAL_KIND_COMMITTED = 1;
30
- */
31
- COMMITTED = 1,
32
-
33
- /**
34
- * TERMINAL_KIND_CANCELED identifies a remote CallCancel packet.
35
- *
36
- * @generated from enum value: TERMINAL_KIND_CANCELED = 2;
37
- */
38
- CANCELED = 2,
39
-
40
- /**
41
- * TERMINAL_KIND_TRANSPORT_LOST identifies a remote error or transport failure.
42
- *
43
- * @generated from enum value: TERMINAL_KIND_TRANSPORT_LOST = 3;
44
- */
45
- TRANSPORT_LOST = 3,
46
-
47
- /**
48
- * TERMINAL_KIND_CLOSED identifies a bare remote close without completion.
49
- *
50
- * @generated from enum value: TERMINAL_KIND_CLOSED = 4;
51
- */
52
- CLOSED = 4,
53
-
54
- /**
55
- * TERMINAL_KIND_ABANDONED identifies owner-context expiry without a remote terminal.
56
- *
57
- * @generated from enum value: TERMINAL_KIND_ABANDONED = 5;
58
- */
59
- ABANDONED = 5,
60
- }
61
-
62
- export const TerminalKind_Enum = /* @__PURE__ */ createEnumType(
63
- 'srpc.TerminalKind',
64
- [
65
- [0, 'TERMINAL_KIND_UNSPECIFIED'],
66
- [1, 'TERMINAL_KIND_COMMITTED'],
67
- [2, 'TERMINAL_KIND_CANCELED'],
68
- [3, 'TERMINAL_KIND_TRANSPORT_LOST'],
69
- [4, 'TERMINAL_KIND_CLOSED'],
70
- [5, 'TERMINAL_KIND_ABANDONED'],
71
- ],
72
- )
73
-
74
12
  /**
75
13
  * CallStart requests starting a new RPC call.
76
14
  *
@@ -1,22 +1,6 @@
1
1
  syntax = "proto3";
2
2
  package srpc;
3
3
 
4
- // TerminalKind is the terminal state of a held unary invocation.
5
- enum TerminalKind {
6
- // TERMINAL_KIND_UNSPECIFIED is not a valid terminal state.
7
- TERMINAL_KIND_UNSPECIFIED = 0;
8
- // TERMINAL_KIND_COMMITTED identifies an explicit remote CallData completion.
9
- TERMINAL_KIND_COMMITTED = 1;
10
- // TERMINAL_KIND_CANCELED identifies a remote CallCancel packet.
11
- TERMINAL_KIND_CANCELED = 2;
12
- // TERMINAL_KIND_TRANSPORT_LOST identifies a remote error or transport failure.
13
- TERMINAL_KIND_TRANSPORT_LOST = 3;
14
- // TERMINAL_KIND_CLOSED identifies a bare remote close without completion.
15
- TERMINAL_KIND_CLOSED = 4;
16
- // TERMINAL_KIND_ABANDONED identifies owner-context expiry without a remote terminal.
17
- TERMINAL_KIND_ABANDONED = 5;
18
- }
19
-
20
4
  // Packet is a message sent over a srpc packet connection.
21
5
  message Packet {
22
6
  // Body is the packet body.
@@ -98,7 +98,7 @@ func (r *ServerRPC) HandleCallStart(pkt *CallStart) error {
98
98
  // invokeRPC invokes the RPC after CallStart is received.
99
99
  func (r *ServerRPC) invokeRPC(serviceID, methodID string) {
100
100
  // on the server side, the writer is closed by invokeRPC.
101
- strm := NewMsgStream(withServerInvocation(r.ctx, r), r, r.cancelContext)
101
+ strm := NewMsgStream(r.ctx, r, r.cancelContext)
102
102
  ok, err := r.invoker.InvokeMethod(serviceID, methodID, strm)
103
103
  if err == nil && !ok {
104
104
  err = ErrUnimplemented
@@ -3,7 +3,6 @@ import type { Sink, Source } from 'it-stream-types'
3
3
  import type { CallData, CallStart } from './rpcproto.pb.js'
4
4
  import { CommonRPC } from './common-rpc.js'
5
5
  import { InvokeFn } from './handler.js'
6
- import { ServerInvocation } from './server-invocation.js'
7
6
  import { LookupMethod } from './mux.js'
8
7
 
9
8
  // ServerRPC is an ongoing RPC from the server side.
@@ -47,12 +46,8 @@ export class ServerRPC extends CommonRPC {
47
46
  // invokeRPC starts invoking the RPC handler.
48
47
  private async invokeRPC(invokeFn: InvokeFn) {
49
48
  const dataSink = this._createDataSink()
50
- const invocation = new ServerInvocation(
51
- this.invocationSignal,
52
- (ownerSignal) => this.waitTerminal(ownerSignal),
53
- )
54
49
  try {
55
- await invokeFn(this.rpcDataSource, dataSink, invocation)
50
+ await invokeFn(this.rpcDataSource, dataSink, this.invocationSignal)
56
51
  } catch (err) {
57
52
  this.close(err as Error)
58
53
  }
@@ -15,13 +15,14 @@ import {
15
15
  EchoerDefinition,
16
16
  EchoerServer,
17
17
  EchoerServiceName,
18
+ EchoMsg,
18
19
  runClientTest,
19
20
  } from '../echo/index.js'
20
21
  import {
21
22
  runAbortControllerTest,
22
23
  runRpcStreamTest,
23
24
  } from '../echo/client-test.js'
24
-
25
+ import type { EchoMsg as EchoMsgType } from '../echo/echo.pb.js'
25
26
  describe('srpc server', () => {
26
27
  let client: Client
27
28
 
@@ -77,6 +78,37 @@ describe('srpc server', () => {
77
78
  it('should pass rpc stream tests', async () => {
78
79
  await runRpcStreamTest(client)
79
80
  })
81
+ it('passes the exact invocation signal after async request decode', async () => {
82
+ const controller = new AbortController()
83
+ let observedSignal: AbortSignal | undefined
84
+ const handler = createHandler(EchoerDefinition, {
85
+ Echo: async (request: EchoMsgType, signal?: AbortSignal) => {
86
+ observedSignal = signal
87
+ return request
88
+ },
89
+ })
90
+ const invokeFn = await handler.lookupMethod(EchoerServiceName, 'Echo')
91
+ if (!invokeFn) {
92
+ throw new Error('Echo method was not found')
93
+ }
94
+ const request = EchoMsg.create({ body: 'signal identity' })
95
+ const drained = Promise.withResolvers<void>()
96
+ await invokeFn(
97
+ (async function* () {
98
+ await Promise.resolve()
99
+ yield EchoMsg.toBinary(request)
100
+ })(),
101
+ async (source) => {
102
+ for await (const _data of source) {
103
+ // Drain the encoded response so the invocation pipeline completes.
104
+ }
105
+ drained.resolve()
106
+ },
107
+ controller.signal,
108
+ )
109
+ await drained.promise
110
+ expect(observedSignal).toBe(controller.signal)
111
+ })
80
112
 
81
113
  it('keeps detached server-streaming responses open after request source completes', async () => {
82
114
  const mux = createMux()
@@ -249,6 +281,22 @@ describe('srpc server', () => {
249
281
  )
250
282
  })
251
283
 
284
+ it('settles a late abort after the call has already closed', async () => {
285
+ const controller = new AbortController()
286
+ vi.spyOn(controller.signal, 'removeEventListener').mockImplementation(
287
+ () => undefined,
288
+ )
289
+
290
+ await client.request(
291
+ EchoerServiceName,
292
+ 'Echo',
293
+ new TextEncoder().encode('late abort'),
294
+ controller.signal,
295
+ )
296
+ controller.abort()
297
+ await Promise.resolve()
298
+ })
299
+
252
300
  it('tears down passive channel close state', async () => {
253
301
  const { port1, port2 } = new MessageChannel()
254
302
  const opts: ChannelStreamOpts = { idleTimeoutMs: 1000, keepAliveMs: 1000 }
@@ -85,4 +85,4 @@ func (p *pipeStream) closeRemote() {
85
85
  }
86
86
 
87
87
  // _ is a type assertion
88
- var _ Stream = ((*pipeStream)(nil))
88
+ var _ Stream = (*pipeStream)(nil)
@@ -82,4 +82,4 @@ func (s *StreamRwc) Write(p []byte) (n int, err error) {
82
82
  }
83
83
 
84
84
  // _ is a type assertion
85
- var _ io.ReadWriteCloser = ((*StreamRwc)(nil))
85
+ var _ io.ReadWriteCloser = (*StreamRwc)(nil)
package/srpc/stream.go CHANGED
@@ -23,10 +23,6 @@ type Stream interface {
23
23
  Close() error
24
24
  }
25
25
 
26
- type receiptTerminalStream interface {
27
- receiptTerminalKind() (TerminalKind, bool)
28
- }
29
-
30
26
  // StreamRecv is a stream that can receive typed messages.
31
27
  //
32
28
  // T is the response type.
@@ -73,14 +69,6 @@ func (s *streamWithClose) Close() error {
73
69
  return err2
74
70
  }
75
71
 
76
- func (s *streamWithClose) receiptTerminalKind() (TerminalKind, bool) {
77
- receipt, ok := s.Stream.(receiptTerminalStream)
78
- if !ok {
79
- return 0, false
80
- }
81
- return receipt.receiptTerminalKind()
82
- }
83
-
84
72
  // streamWithContext is a Stream with a wrapped Context function.
85
73
  type streamWithContext struct {
86
74
  Stream
@@ -97,14 +85,6 @@ func (s *streamWithContext) Context() context.Context {
97
85
  return s.ctx
98
86
  }
99
87
 
100
- func (s *streamWithContext) receiptTerminalKind() (TerminalKind, bool) {
101
- receipt, ok := s.Stream.(receiptTerminalStream)
102
- if !ok {
103
- return 0, false
104
- }
105
- return receipt.receiptTerminalKind()
106
- }
107
-
108
88
  // _ is a type assertion
109
89
  var (
110
90
  _ Stream = (*streamWithClose)(nil)
@@ -1 +0,0 @@
1
- export {};
@@ -1,41 +0,0 @@
1
- /// <reference lib="es2024.promise" />
2
- import { describe, expect, it } from 'vitest';
3
- import { Client } from '../srpc/client.js';
4
- import { Server } from '../srpc/server.js';
5
- import { TerminalKind } from '../srpc/rpcproto.pb.js';
6
- import { buildRpcStreamOpenStream, handleRpcStream } from './rpcstream.js';
7
- const response = new Uint8Array([4, 5, 6]);
8
- // This guard mirrors the Go rpcstream/receipt_test.go nested tunnel: an
9
- // ExecCallReceipt-equivalent held unary call runs through a real handleRpcStream
10
- // tunnel. The inner generated handler sends its single response, then the
11
- // wrapping invocation synchronously holds on waitTerminal. The first response
12
- // must be client-visible while the exact invocation stays open for commit.
13
- describe('rpcstream held receipt tunnel', () => {
14
- it('delivers the first inner response while the invocation stays held', async () => {
15
- const terminal = Promise.withResolvers();
16
- const innerServer = new Server(async () => {
17
- return async (_source, dataSink, invocation) => {
18
- await dataSink((async function* () {
19
- yield response;
20
- if (!invocation) {
21
- throw new Error('missing invocation');
22
- }
23
- terminal.resolve(await invocation.waitTerminal(new AbortController().signal));
24
- })());
25
- };
26
- });
27
- const caller = (packetTx) => handleRpcStream(packetTx[Symbol.asyncIterator](), async () => innerServer.rpcStreamHandler);
28
- const client = new Client(buildRpcStreamOpenStream('component-a', caller));
29
- const held = await client.requestWithReceipt('test.Service', 'Unary', response);
30
- // The first inner response is client-visible while the invocation is held.
31
- expect(held.response).toEqual(response);
32
- // The inner invocation must still be open: no terminal observed yet.
33
- const raced = await Promise.race([
34
- terminal.promise.then(() => 'terminal'),
35
- Promise.resolve('pending'),
36
- ]);
37
- expect(raced).toBe('pending');
38
- await expect(held.receipt.commit()).resolves.toBeUndefined();
39
- await expect(terminal.promise).resolves.toBe(TerminalKind.COMMITTED);
40
- });
41
- });
@@ -1,17 +0,0 @@
1
- import { ClientRPC } from './client-rpc.js';
2
- export interface ReceiptRpc {
3
- requestWithReceipt(service: string, method: string, data: Uint8Array, abortSignal?: AbortSignal): Promise<HeldCall>;
4
- }
5
- export interface HeldCall {
6
- readonly response: Uint8Array;
7
- readonly receipt: CallReceipt;
8
- }
9
- export declare class CallReceipt {
10
- #private;
11
- constructor(call: ClientRPC, iterator: AsyncIterator<Uint8Array>);
12
- get done(): Promise<void>;
13
- get settled(): boolean;
14
- commit(): Promise<void>;
15
- abort(reason?: Error): Promise<void>;
16
- [Symbol.asyncDispose](): Promise<void>;
17
- }
@@ -1,106 +0,0 @@
1
- /// <reference lib="es2024.promise" />
2
- import { ERR_RPC_ABORT } from './errors.js';
3
- import { TerminalKind } from './rpcproto.pb.js';
4
- // CallReceipt holds a unary call until it is committed or aborted.
5
- export class CallReceipt {
6
- #call;
7
- #iterator;
8
- #terminalPromise;
9
- #terminal;
10
- #done;
11
- #requestCommitted = false;
12
- #resolveDone;
13
- #rejectDone;
14
- constructor(call, iterator) {
15
- this.#call = call;
16
- this.#iterator = iterator;
17
- const { promise, resolve, reject } = Promise.withResolvers();
18
- this.#done = promise;
19
- this.#resolveDone = resolve;
20
- this.#rejectDone = reject;
21
- this.#done.catch(() => undefined);
22
- this.#terminalPromise = this.#observeTerminal();
23
- this.#terminalPromise.catch(() => undefined);
24
- }
25
- async #observeTerminal() {
26
- try {
27
- const result = await this.#iterator.next();
28
- if (!result.done) {
29
- throw new Error('unexpected trailing response data');
30
- }
31
- const terminal = this.#call.getTerminalKind();
32
- if (terminal !== TerminalKind.COMMITTED ||
33
- !this.#requestCommitted ||
34
- this.#terminal !== 'committed') {
35
- throw new Error('receipt closed before commit');
36
- }
37
- return result;
38
- }
39
- catch (err) {
40
- this.#rejectDone(err);
41
- const error = err instanceof Error ? err : new Error('receipt terminal failed');
42
- try {
43
- await this.#call.close(error);
44
- }
45
- catch {
46
- // The primary terminal error is already recorded on the receipt.
47
- }
48
- throw err;
49
- }
50
- }
51
- // done resolves after committed close and rejects on terminal failure.
52
- get done() {
53
- return this.#done;
54
- }
55
- // settled reports whether a terminal transition has been claimed.
56
- get settled() {
57
- return this.#terminal !== undefined;
58
- }
59
- // commit sends request completion and waits for server finalization.
60
- async commit() {
61
- if (this.#terminal === 'aborted') {
62
- throw new Error(ERR_RPC_ABORT);
63
- }
64
- if (this.#terminal === 'committed') {
65
- return this.#done;
66
- }
67
- this.#terminal = 'committed';
68
- this.#requestCommitted = true;
69
- try {
70
- await this.#call.writeCallData(undefined, true);
71
- await this.#terminalPromise;
72
- await this.#call.close();
73
- this.#resolveDone();
74
- }
75
- catch (err) {
76
- await this.#call.close(err instanceof Error ? err : new Error('receipt commit failed'));
77
- this.#rejectDone(err);
78
- throw err;
79
- }
80
- }
81
- // abort sends request cancellation and never rejects.
82
- async abort(reason) {
83
- if (this.#terminal !== undefined) {
84
- return;
85
- }
86
- this.#terminal = 'aborted';
87
- let terminalError = reason;
88
- try {
89
- await this.#call.writeCallCancel(true);
90
- }
91
- catch (err) {
92
- terminalError ??= err;
93
- }
94
- try {
95
- await this.#call.close();
96
- }
97
- catch (err) {
98
- terminalError ??= err;
99
- }
100
- this.#rejectDone(terminalError ?? new Error(ERR_RPC_ABORT));
101
- }
102
- // asyncDispose aborts a receipt that has not reached a terminal.
103
- async [Symbol.asyncDispose]() {
104
- await this.abort();
105
- }
106
- }
@@ -1 +0,0 @@
1
- export {};