starpc 0.50.0 → 0.52.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 (100) 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/echo_srpc.pb.d.ts +44 -1
  4. package/dist/echo/server.d.ts +9 -8
  5. package/dist/echo/server.js +6 -6
  6. package/dist/integration/cross-language/ts-client.js +5 -79
  7. package/dist/integration/cross-language/ts-server.js +4 -100
  8. package/dist/mock/mock_srpc.pb.d.ts +14 -1
  9. package/dist/srpc/channel.js +6 -3
  10. package/dist/srpc/channel.test.js +20 -1
  11. package/dist/srpc/client.d.ts +1 -3
  12. package/dist/srpc/client.js +2 -22
  13. package/dist/srpc/common-rpc.d.ts +1 -9
  14. package/dist/srpc/common-rpc.js +4 -62
  15. package/dist/srpc/handler.d.ts +12 -4
  16. package/dist/srpc/index.d.ts +2 -4
  17. package/dist/srpc/index.js +1 -3
  18. package/dist/srpc/invoker.d.ts +2 -2
  19. package/dist/srpc/invoker.js +2 -2
  20. package/dist/srpc/rpcproto.pb.d.ts +0 -44
  21. package/dist/srpc/rpcproto.pb.js +0 -53
  22. package/dist/srpc/server-context.d.ts +11 -0
  23. package/dist/srpc/server-context.js +28 -0
  24. package/dist/srpc/server-rpc.js +3 -3
  25. package/dist/srpc/server.test.js +65 -2
  26. package/dist/srpc/watchdog.test.js +1 -0
  27. package/echo/echo.go +8 -8
  28. package/echo/echo.pb.go +12 -36
  29. package/echo/echo_srpc.pb.cpp +1 -1
  30. package/echo/echo_srpc.pb.go +1 -1
  31. package/echo/echo_srpc.pb.hpp +1 -1
  32. package/echo/echo_srpc.pb.rs +1 -1
  33. package/echo/echo_srpc.pb.ts +74 -0
  34. package/echo/server.go +1 -1
  35. package/echo/server.ts +24 -5
  36. package/go.mod +1 -1
  37. package/go.sum +2 -0
  38. package/integration/cross-language/go-client/main.go +4 -142
  39. package/integration/cross-language/go-server/main.go +2 -170
  40. package/integration/cross-language/run.bash +5 -105
  41. package/integration/cross-language/ts-client.ts +5 -90
  42. package/integration/cross-language/ts-server.ts +4 -112
  43. package/mock/mock.go +1 -1
  44. package/mock/mock.pb.go +12 -36
  45. package/mock/mock_srpc.pb.cpp +1 -1
  46. package/mock/mock_srpc.pb.go +1 -1
  47. package/mock/mock_srpc.pb.hpp +1 -1
  48. package/mock/mock_srpc.pb.rs +1 -1
  49. package/mock/mock_srpc.pb.ts +19 -1
  50. package/package.json +2 -2
  51. package/srpc/channel.test.ts +21 -1
  52. package/srpc/channel.ts +7 -3
  53. package/srpc/client-prefix.go +1 -1
  54. package/srpc/client-set.go +1 -1
  55. package/srpc/client.go +1 -1
  56. package/srpc/client.ts +4 -31
  57. package/srpc/common-rpc.go +15 -69
  58. package/srpc/common-rpc.ts +4 -77
  59. package/srpc/common-rpc_test.go +81 -0
  60. package/srpc/errors.go +26 -1
  61. package/srpc/handler.ts +54 -5
  62. package/srpc/index.ts +7 -4
  63. package/srpc/invoker-prefix.go +1 -1
  64. package/srpc/invoker.go +2 -2
  65. package/srpc/invoker.ts +20 -7
  66. package/srpc/message.go +1 -1
  67. package/srpc/msg-stream.go +1 -9
  68. package/srpc/mux-verbose.go +1 -1
  69. package/srpc/mux.go +1 -1
  70. package/srpc/rpcproto.pb.cc +4 -15
  71. package/srpc/rpcproto.pb.go +102 -419
  72. package/srpc/rpcproto.pb.h +0 -59
  73. package/srpc/rpcproto.pb.rs +0 -45
  74. package/srpc/rpcproto.pb.ts +0 -62
  75. package/srpc/rpcproto.proto +0 -16
  76. package/srpc/server-context.ts +55 -0
  77. package/srpc/server-rpc.go +1 -1
  78. package/srpc/server-rpc.ts +4 -6
  79. package/srpc/server.test.ts +94 -1
  80. package/srpc/stream-pipe.go +1 -1
  81. package/srpc/stream-rwc.go +1 -1
  82. package/srpc/stream.go +0 -20
  83. package/srpc/watchdog.test.ts +1 -0
  84. package/dist/rpcstream/receipt.test.d.ts +0 -1
  85. package/dist/rpcstream/receipt.test.js +0 -41
  86. package/dist/srpc/call-receipt.d.ts +0 -17
  87. package/dist/srpc/call-receipt.js +0 -106
  88. package/dist/srpc/call-receipt.test.d.ts +0 -1
  89. package/dist/srpc/call-receipt.test.js +0 -375
  90. package/dist/srpc/server-invocation.d.ts +0 -17
  91. package/dist/srpc/server-invocation.js +0 -37
  92. package/integration/cross-language/go-server/fixture-owner_test.go +0 -127
  93. package/srpc/call-receipt-e2e_test.go +0 -111
  94. package/srpc/call-receipt.go +0 -112
  95. package/srpc/call-receipt.test.ts +0 -441
  96. package/srpc/call-receipt.ts +0 -131
  97. package/srpc/call-receipt_test.go +0 -536
  98. package/srpc/schema-ownership_test.go +0 -115
  99. package/srpc/server-invocation.go +0 -40
  100. 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.
@@ -0,0 +1,55 @@
1
+ declare const contextKeyValue: unique symbol
2
+ const contextParent = Symbol('server context parent')
3
+ const contextKey = Symbol('server context key')
4
+ const contextStoredValue = Symbol('server context value')
5
+
6
+ // ContextKey identifies one typed server-context value.
7
+ export interface ContextKey<T> {
8
+ readonly [contextKeyValue]?: T
9
+ }
10
+
11
+ // ServerContext carries cancellation for one server invocation.
12
+ export interface ServerContext {
13
+ readonly signal: AbortSignal
14
+ }
15
+
16
+ type StoredServerContext = ServerContext & {
17
+ readonly [contextParent]?: ServerContext
18
+ readonly [contextKey]?: ContextKey<unknown>
19
+ readonly [contextStoredValue]?: unknown
20
+ }
21
+
22
+ // createContextKey constructs an identity key for one server-context value.
23
+ export function createContextKey<T>(): ContextKey<T> {
24
+ return {}
25
+ }
26
+
27
+ // withServerContextValue derives a context with one immutable typed value.
28
+ export function withServerContextValue<T>(
29
+ context: ServerContext,
30
+ key: ContextKey<T>,
31
+ value: T,
32
+ ): ServerContext {
33
+ const derived: StoredServerContext = {
34
+ signal: context.signal,
35
+ [contextParent]: context,
36
+ [contextKey]: key as ContextKey<unknown>,
37
+ [contextStoredValue]: value,
38
+ }
39
+ return derived
40
+ }
41
+
42
+ // serverContextValue retrieves the nearest value for a typed identity key.
43
+ export function serverContextValue<T>(
44
+ context: ServerContext,
45
+ key: ContextKey<T>,
46
+ ): T | undefined {
47
+ let current: StoredServerContext | undefined = context as StoredServerContext
48
+ while (current) {
49
+ if (current[contextKey] === key) {
50
+ return current[contextStoredValue] as T
51
+ }
52
+ current = current[contextParent] as StoredServerContext | undefined
53
+ }
54
+ return undefined
55
+ }
@@ -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,8 +3,8 @@ 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'
7
+ import type { ServerContext } from './server-context.js'
8
8
 
9
9
  // ServerRPC is an ongoing RPC from the server side.
10
10
  export class ServerRPC extends CommonRPC {
@@ -47,12 +47,10 @@ export class ServerRPC extends CommonRPC {
47
47
  // invokeRPC starts invoking the RPC handler.
48
48
  private async invokeRPC(invokeFn: InvokeFn) {
49
49
  const dataSink = this._createDataSink()
50
- const invocation = new ServerInvocation(
51
- this.invocationSignal,
52
- (ownerSignal) => this.waitTerminal(ownerSignal),
53
- )
54
50
  try {
55
- await invokeFn(this.rpcDataSource, dataSink, invocation)
51
+ await invokeFn(this.rpcDataSource, dataSink, {
52
+ signal: this.invocationSignal,
53
+ } satisfies ServerContext)
56
54
  } catch (err) {
57
55
  this.close(err as Error)
58
56
  }
@@ -10,18 +10,23 @@ import {
10
10
  combineUint8ArrayListTransform,
11
11
  ChannelStreamOpts,
12
12
  Packet,
13
+ createContextKey,
14
+ serverContextValue,
15
+ withServerContextValue,
16
+ type ServerContext,
13
17
  } from '../srpc/index.js'
14
18
  import {
15
19
  EchoerDefinition,
16
20
  EchoerServer,
17
21
  EchoerServiceName,
22
+ EchoMsg,
18
23
  runClientTest,
19
24
  } from '../echo/index.js'
20
25
  import {
21
26
  runAbortControllerTest,
22
27
  runRpcStreamTest,
23
28
  } from '../echo/client-test.js'
24
-
29
+ import type { EchoMsg as EchoMsgType } from '../echo/echo.pb.js'
25
30
  describe('srpc server', () => {
26
31
  let client: Client
27
32
 
@@ -77,6 +82,78 @@ describe('srpc server', () => {
77
82
  it('should pass rpc stream tests', async () => {
78
83
  await runRpcStreamTest(client)
79
84
  })
85
+ it('passes the exact invocation context after async request decode', async () => {
86
+ const controller = new AbortController()
87
+ const callerKey = createContextKey<string>()
88
+ let observedAbortSignal: AbortSignal | undefined
89
+ let observedContextSignal: AbortSignal | undefined
90
+ let observedCaller: string | undefined
91
+ const handler = createHandler(EchoerDefinition, {
92
+ Echo: async (
93
+ request: EchoMsgType,
94
+ abortSignal: AbortSignal,
95
+ context: ServerContext,
96
+ ) => {
97
+ observedAbortSignal = abortSignal
98
+ observedContextSignal = context.signal
99
+ observedCaller = serverContextValue(context, callerKey)
100
+ return request
101
+ },
102
+ })
103
+ const invokeFn = await handler.lookupMethod(EchoerServiceName, 'Echo')
104
+ if (!invokeFn) {
105
+ throw new Error('Echo method was not found')
106
+ }
107
+ const request = EchoMsg.create({ body: 'signal identity' })
108
+ const drained = Promise.withResolvers<void>()
109
+ await invokeFn(
110
+ (async function* () {
111
+ await Promise.resolve()
112
+ yield EchoMsg.toBinary(request)
113
+ })(),
114
+ async (source) => {
115
+ for await (const _data of source) {
116
+ // Drain the encoded response so the invocation pipeline completes.
117
+ }
118
+ drained.resolve()
119
+ },
120
+ withServerContextValue(
121
+ { signal: controller.signal },
122
+ callerKey,
123
+ 'caller-1',
124
+ ),
125
+ )
126
+ await drained.promise
127
+ expect(observedAbortSignal).toBe(controller.signal)
128
+ expect(observedContextSignal).toBe(controller.signal)
129
+ expect(observedCaller).toBe('caller-1')
130
+ })
131
+
132
+ it('keeps two-argument server handlers compatible', async () => {
133
+ const controller = new AbortController()
134
+ let observedSignal: AbortSignal | undefined
135
+ const handler = createHandler(EchoerDefinition, {
136
+ Echo: async (request: EchoMsgType, abortSignal?: AbortSignal) => {
137
+ observedSignal = abortSignal
138
+ return request
139
+ },
140
+ })
141
+ const invokeFn = await handler.lookupMethod(EchoerServiceName, 'Echo')
142
+ if (!invokeFn) throw new Error('Echo method was not found')
143
+ const request = EchoMsg.create({ body: 'legacy handler' })
144
+ await invokeFn(
145
+ (async function* () {
146
+ yield EchoMsg.toBinary(request)
147
+ })(),
148
+ async (source) => {
149
+ for await (const _data of source) {
150
+ // Drain the response.
151
+ }
152
+ },
153
+ { signal: controller.signal },
154
+ )
155
+ expect(observedSignal).toBe(controller.signal)
156
+ })
80
157
 
81
158
  it('keeps detached server-streaming responses open after request source completes', async () => {
82
159
  const mux = createMux()
@@ -249,6 +326,22 @@ describe('srpc server', () => {
249
326
  )
250
327
  })
251
328
 
329
+ it('settles a late abort after the call has already closed', async () => {
330
+ const controller = new AbortController()
331
+ vi.spyOn(controller.signal, 'removeEventListener').mockImplementation(
332
+ () => undefined,
333
+ )
334
+
335
+ await client.request(
336
+ EchoerServiceName,
337
+ 'Echo',
338
+ new TextEncoder().encode('late abort'),
339
+ controller.signal,
340
+ )
341
+ controller.abort()
342
+ await Promise.resolve()
343
+ })
344
+
252
345
  it('tears down passive channel close state', async () => {
253
346
  const { port1, port2 } = new MessageChannel()
254
347
  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)
@@ -7,6 +7,7 @@ describe('Watchdog', () => {
7
7
  })
8
8
 
9
9
  afterEach(() => {
10
+ vi.useRealTimers()
10
11
  vi.restoreAllMocks()
11
12
  })
12
13
 
@@ -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
- }