starpc 0.49.18 → 0.49.20

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 (69) hide show
  1. package/README.md +1 -3
  2. package/cmd/protoc-gen-es-starpc/typescript.ts +11 -6
  3. package/dist/cmd/protoc-gen-es-starpc/typescript.js +6 -5
  4. package/dist/echo/echo.pb.d.ts +1 -1
  5. package/dist/echo/echo.pb.js +3 -3
  6. package/dist/integration/cross-language/ts-client.js +84 -11
  7. package/dist/integration/cross-language/ts-server.js +84 -4
  8. package/dist/mock/mock.pb.d.ts +1 -1
  9. package/dist/mock/mock.pb.js +3 -3
  10. package/dist/rpcstream/rpcstream.pb.d.ts +1 -1
  11. package/dist/rpcstream/rpcstream.pb.js +14 -8
  12. package/dist/srpc/call-receipt.d.ts +17 -0
  13. package/dist/srpc/call-receipt.js +105 -0
  14. package/dist/srpc/call-receipt.test.d.ts +1 -0
  15. package/dist/srpc/call-receipt.test.js +374 -0
  16. package/dist/srpc/client.d.ts +3 -1
  17. package/dist/srpc/client.js +20 -0
  18. package/dist/srpc/common-rpc.d.ts +13 -1
  19. package/dist/srpc/common-rpc.js +86 -6
  20. package/dist/srpc/handler.d.ts +2 -1
  21. package/dist/srpc/index.d.ts +4 -0
  22. package/dist/srpc/index.js +2 -0
  23. package/dist/srpc/invoker.d.ts +2 -1
  24. package/dist/srpc/invoker.js +2 -2
  25. package/dist/srpc/rpcproto.pb.d.ts +1 -1
  26. package/dist/srpc/rpcproto.pb.js +7 -7
  27. package/dist/srpc/server-invocation.d.ts +17 -0
  28. package/dist/srpc/server-invocation.js +37 -0
  29. package/dist/srpc/server-rpc.js +3 -1
  30. package/echo/echo.pb.go +1 -1
  31. package/echo/echo.pb.ts +6 -5
  32. package/echo/echo_srpc.pb.cpp +1 -1
  33. package/echo/echo_srpc.pb.go +1 -1
  34. package/echo/echo_srpc.pb.hpp +1 -1
  35. package/echo/echo_srpc.pb.rs +1 -1
  36. package/integration/cross-language/go-client/main.go +137 -5
  37. package/integration/cross-language/go-server/main.go +146 -3
  38. package/integration/cross-language/run.bash +117 -13
  39. package/integration/cross-language/ts-client.ts +94 -13
  40. package/integration/cross-language/ts-server.ts +94 -6
  41. package/mock/mock.pb.go +1 -1
  42. package/mock/mock.pb.ts +6 -5
  43. package/mock/mock_srpc.pb.cpp +1 -1
  44. package/mock/mock_srpc.pb.go +1 -1
  45. package/mock/mock_srpc.pb.hpp +1 -1
  46. package/mock/mock_srpc.pb.rs +1 -1
  47. package/package.json +1 -1
  48. package/srpc/call-receipt-e2e_test.go +111 -0
  49. package/srpc/call-receipt.go +112 -0
  50. package/srpc/call-receipt.test.ts +438 -0
  51. package/srpc/call-receipt.ts +130 -0
  52. package/srpc/call-receipt_test.go +536 -0
  53. package/srpc/client-rpc.go +1 -8
  54. package/srpc/client.ts +29 -2
  55. package/srpc/common-rpc.go +114 -29
  56. package/srpc/common-rpc.ts +98 -8
  57. package/srpc/handler.ts +2 -0
  58. package/srpc/index.ts +4 -0
  59. package/srpc/invoker.ts +10 -5
  60. package/srpc/msg-stream.go +8 -0
  61. package/srpc/muxed-conn.go +8 -3
  62. package/srpc/muxed-conn_test.go +79 -0
  63. package/srpc/rpcproto.pb.go +1 -1
  64. package/srpc/rpcproto.pb.ts +28 -27
  65. package/srpc/server-invocation.go +58 -0
  66. package/srpc/server-invocation.ts +82 -0
  67. package/srpc/server-rpc.go +1 -1
  68. package/srpc/server-rpc.ts +6 -2
  69. package/srpc/stream.go +20 -0
@@ -1,10 +1,11 @@
1
1
  // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false"
2
2
  // @generated from file github.com/aperturerobotics/starpc/srpc/rpcproto.proto (package srpc, syntax proto3)
3
3
  /* eslint-disable */
4
- import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite';
4
+ import { createMessageType } from '@aptre/protobuf-es-lite/message';
5
+ import { ScalarType } from '@aptre/protobuf-es-lite/scalar';
5
6
  export const protobufPackage = 'srpc';
6
- // CallStart contains the message type declaration for CallStart.
7
- export const CallStart = createMessageType({
7
+ export const CallStart =
8
+ /* @__PURE__ */ createMessageType({
8
9
  typeName: 'srpc.CallStart',
9
10
  fields: [
10
11
  { no: 1, name: 'rpc_service', kind: 'scalar', T: ScalarType.STRING },
@@ -14,8 +15,8 @@ export const CallStart = createMessageType({
14
15
  ],
15
16
  packedByDefault: true,
16
17
  });
17
- // CallData contains the message type declaration for CallData.
18
- export const CallData = createMessageType({
18
+ export const CallData =
19
+ /* @__PURE__ */ createMessageType({
19
20
  typeName: 'srpc.CallData',
20
21
  fields: [
21
22
  { no: 1, name: 'data', kind: 'scalar', T: ScalarType.BYTES },
@@ -25,8 +26,7 @@ export const CallData = createMessageType({
25
26
  ],
26
27
  packedByDefault: true,
27
28
  });
28
- // Packet contains the message type declaration for Packet.
29
- export const Packet = createMessageType({
29
+ export const Packet = /* @__PURE__ */ createMessageType({
30
30
  typeName: 'srpc.Packet',
31
31
  fields: [
32
32
  {
@@ -0,0 +1,17 @@
1
+ export type TerminalKind = 'committed' | 'canceled' | 'transportLost' | 'closed' | 'abandoned';
2
+ export declare class ServerInvocation implements AbortSignal {
3
+ readonly signal: AbortSignal;
4
+ private readonly waitFn;
5
+ constructor(signal: AbortSignal, waitFn: (ownerSignal: AbortSignal) => Promise<TerminalKind>);
6
+ get aborted(): boolean;
7
+ get onabort(): typeof this.signal.onabort;
8
+ set onabort(value: typeof this.signal.onabort);
9
+ get reason(): any;
10
+ throwIfAborted(): void;
11
+ addEventListener<K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
12
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
13
+ removeEventListener<K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
14
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
15
+ dispatchEvent(event: Event): boolean;
16
+ waitTerminal(ownerSignal: AbortSignal): Promise<TerminalKind>;
17
+ }
@@ -0,0 +1,37 @@
1
+ // ServerInvocation exposes the invocation signal and terminal wait.
2
+ export class ServerInvocation {
3
+ signal;
4
+ waitFn;
5
+ constructor(signal, waitFn) {
6
+ this.signal = signal;
7
+ this.waitFn = waitFn;
8
+ }
9
+ get aborted() {
10
+ return this.signal.aborted;
11
+ }
12
+ get onabort() {
13
+ return this.signal.onabort;
14
+ }
15
+ set onabort(value) {
16
+ this.signal.onabort = value;
17
+ }
18
+ get reason() {
19
+ return this.signal.reason;
20
+ }
21
+ throwIfAborted() {
22
+ this.signal.throwIfAborted();
23
+ }
24
+ addEventListener(type, listener, options) {
25
+ this.signal.addEventListener(type, listener, options);
26
+ }
27
+ removeEventListener(type, listener, options) {
28
+ this.signal.removeEventListener(type, listener, options);
29
+ }
30
+ dispatchEvent(event) {
31
+ return this.signal.dispatchEvent(event);
32
+ }
33
+ // waitTerminal waits for a remote terminal or owner-signal cancellation.
34
+ waitTerminal(ownerSignal) {
35
+ return this.waitFn(ownerSignal);
36
+ }
37
+ }
@@ -1,4 +1,5 @@
1
1
  import { CommonRPC } from './common-rpc.js';
2
+ import { ServerInvocation } from './server-invocation.js';
2
3
  // ServerRPC is an ongoing RPC from the server side.
3
4
  export class ServerRPC extends CommonRPC {
4
5
  // lookupMethod looks up the incoming RPC methods.
@@ -37,8 +38,9 @@ export class ServerRPC extends CommonRPC {
37
38
  // invokeRPC starts invoking the RPC handler.
38
39
  async invokeRPC(invokeFn) {
39
40
  const dataSink = this._createDataSink();
41
+ const invocation = new ServerInvocation(this.invocationSignal, (ownerSignal) => this.waitTerminal(ownerSignal));
40
42
  try {
41
- await invokeFn(this.rpcDataSource, dataSink);
43
+ await invokeFn(this.rpcDataSource, dataSink, invocation);
42
44
  }
43
45
  catch (err) {
44
46
  this.close(err);
package/echo/echo.pb.go CHANGED
@@ -1,5 +1,5 @@
1
1
  // Code generated by protoc-gen-go-lite. DO NOT EDIT.
2
- // protoc-gen-go-lite version: v0.12.1
2
+ // protoc-gen-go-lite version: v0.13.0
3
3
  // source: github.com/aperturerobotics/starpc/echo/echo.proto
4
4
 
5
5
  package echo
package/echo/echo.pb.ts CHANGED
@@ -2,8 +2,10 @@
2
2
  // @generated from file github.com/aperturerobotics/starpc/echo/echo.proto (package echo, syntax proto3)
3
3
  /* eslint-disable */
4
4
 
5
- import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite'
6
- import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite'
5
+ import type { MessageType } from '@aptre/protobuf-es-lite/message'
6
+ import { createMessageType } from '@aptre/protobuf-es-lite/message'
7
+ import { ScalarType } from '@aptre/protobuf-es-lite/scalar'
8
+ import type { PartialFieldInfo } from '@aptre/protobuf-es-lite/field'
7
9
 
8
10
  export const protobufPackage = 'echo'
9
11
 
@@ -19,11 +21,10 @@ export interface EchoMsg {
19
21
  body?: string
20
22
  }
21
23
 
22
- // EchoMsg contains the message type declaration for EchoMsg.
23
- export const EchoMsg: MessageType<EchoMsg> = createMessageType({
24
+ export const EchoMsg: MessageType<EchoMsg> = /* @__PURE__ */ createMessageType({
24
25
  typeName: 'echo.EchoMsg',
25
26
  fields: [
26
27
  { no: 1, name: 'body', kind: 'scalar', T: ScalarType.STRING },
27
- ] as readonly PartialFieldInfo[],
28
+ ] satisfies readonly PartialFieldInfo[],
28
29
  packedByDefault: true,
29
30
  })
@@ -1,7 +1,7 @@
1
1
  //go:build deps_only
2
2
 
3
3
  // Code generated by protoc-gen-starpc-cpp. DO NOT EDIT.
4
- // protoc-gen-starpc-cpp version: v0.46.1
4
+ // protoc-gen-starpc-cpp version: v0.49.7
5
5
  // source: github.com/aperturerobotics/starpc/echo/echo.proto
6
6
 
7
7
  #include "echo_srpc.pb.hpp"
@@ -1,5 +1,5 @@
1
1
  // Code generated by protoc-gen-srpc. DO NOT EDIT.
2
- // protoc-gen-srpc version: v0.46.1
2
+ // protoc-gen-srpc version: v0.49.7
3
3
  // source: github.com/aperturerobotics/starpc/echo/echo.proto
4
4
 
5
5
  package echo
@@ -1,7 +1,7 @@
1
1
  //go:build deps_only && cgo
2
2
 
3
3
  // Code generated by protoc-gen-starpc-cpp. DO NOT EDIT.
4
- // protoc-gen-starpc-cpp version: v0.46.1
4
+ // protoc-gen-starpc-cpp version: v0.49.7
5
5
  // source: github.com/aperturerobotics/starpc/echo/echo.proto
6
6
 
7
7
  #pragma once
@@ -1,5 +1,5 @@
1
1
  // Code generated by protoc-gen-starpc-rust. DO NOT EDIT.
2
- // protoc-gen-starpc-rust version: v0.46.1
2
+ // protoc-gen-starpc-rust version: v0.49.7
3
3
  // source: github.com/aperturerobotics/starpc/echo/echo.proto
4
4
 
5
5
  #[allow(unused_imports)]
@@ -2,6 +2,7 @@ package main
2
2
 
3
3
  import (
4
4
  "context"
5
+ "errors"
5
6
  "fmt"
6
7
  "io"
7
8
  "net"
@@ -14,14 +15,32 @@ import (
14
15
  const bodyTxt = "hello world via starpc cross-language e2e test"
15
16
 
16
17
  func main() {
17
- if len(os.Args) < 2 {
18
- fmt.Fprintf(os.Stderr, "usage: go-client <addr>\n")
18
+ receiptMode := len(os.Args) > 1 && os.Args[1] == "receipt"
19
+ receiptCase := ""
20
+ addrIndex := 1
21
+ if receiptMode {
22
+ if len(os.Args) < 3 {
23
+ fmt.Fprintf(os.Stderr, "usage: go-client receipt <case> <addr>\n")
24
+ os.Exit(1)
25
+ }
26
+ receiptCase = os.Args[2]
27
+ addrIndex = 3
28
+ switch receiptCase {
29
+ case "commit", "abort", "loss", "bare-close":
30
+ default:
31
+ fmt.Fprintf(os.Stderr, "unknown receipt case: %s\n", receiptCase)
32
+ os.Exit(1)
33
+ }
34
+ }
35
+ if len(os.Args) <= addrIndex {
36
+ fmt.Fprintf(os.Stderr, "usage: go-client [receipt <case>] <addr>\n")
19
37
  os.Exit(1)
20
38
  }
21
- addr := os.Args[1]
22
-
39
+ addr := os.Args[addrIndex]
40
+ var conn net.Conn
23
41
  openStream := func(ctx context.Context, msgHandler srpc.PacketDataHandler, closeHandler srpc.CloseHandler) (srpc.PacketWriter, error) {
24
- conn, err := net.Dial("tcp", addr) //nolint:gosec
42
+ var err error
43
+ conn, err = net.Dial("tcp", addr) //nolint:gosec
25
44
  if err != nil {
26
45
  return nil, err
27
46
  }
@@ -33,6 +52,14 @@ func main() {
33
52
  client := srpc.NewClient(openStream)
34
53
  echoClient := echo.NewSRPCEchoerClient(client)
35
54
  ctx := context.Background()
55
+ if receiptMode {
56
+ if err := testReceipt(ctx, client, &conn, receiptCase); err != nil {
57
+ fmt.Fprintf(os.Stderr, "receipt test failed: %v\n", err)
58
+ os.Exit(1)
59
+ }
60
+ fmt.Println("All tests passed.")
61
+ return
62
+ }
36
63
 
37
64
  if err := testUnary(ctx, echoClient); err != nil {
38
65
  fmt.Fprintf(os.Stderr, "unary test failed: %v\n", err)
@@ -70,6 +97,79 @@ func testUnary(ctx context.Context, client echo.SRPCEchoerClient) error {
70
97
  return nil
71
98
  }
72
99
 
100
+ func testReceipt(
101
+ ctx context.Context,
102
+ client srpc.Client,
103
+ conn *net.Conn,
104
+ receiptCase string,
105
+ ) error {
106
+ fmt.Printf("Testing held unary receipt (%s)...\n", receiptCase)
107
+ out := new(echo.EchoMsg)
108
+ receipt, err := srpc.ExecCallReceipt(
109
+ ctx, client, echo.SRPCEchoerServiceID, "Echo",
110
+ &echo.EchoMsg{Body: bodyTxt}, out,
111
+ )
112
+ if err != nil {
113
+ return fmt.Errorf("receipt call: %w", err)
114
+ }
115
+ if out.GetBody() != bodyTxt {
116
+ return fmt.Errorf("expected %q got %q", bodyTxt, out.GetBody())
117
+ }
118
+
119
+ switch receiptCase {
120
+ case "commit":
121
+ if err := receipt.Commit(); err != nil {
122
+ return fmt.Errorf("receipt commit: %w", err)
123
+ }
124
+ case "abort":
125
+ if err := receipt.Abort(); err != nil {
126
+ return fmt.Errorf("receipt abort: %w", err)
127
+ }
128
+ case "loss":
129
+ if *conn == nil {
130
+ return errors.New("receipt connection is nil")
131
+ }
132
+ tcpConn, ok := (*conn).(*net.TCPConn)
133
+ if !ok {
134
+ return errors.New("receipt connection is not TCP")
135
+ }
136
+ if err := tcpConn.SetLinger(0); err != nil {
137
+ return fmt.Errorf("set receipt loss linger: %w", err)
138
+ }
139
+ if err := tcpConn.Close(); err != nil {
140
+ return fmt.Errorf("close lost connection: %w", err)
141
+ }
142
+ if err := receipt.Commit(); err == nil {
143
+ return errors.New("loss receipt commit unexpectedly succeeded")
144
+ }
145
+ case "bare-close":
146
+ if *conn == nil {
147
+ return errors.New("receipt connection is nil")
148
+ }
149
+ tcpConn, ok := (*conn).(*net.TCPConn)
150
+ if !ok {
151
+ return errors.New("receipt connection is not TCP")
152
+ }
153
+ if err := tcpConn.CloseWrite(); err != nil {
154
+ return fmt.Errorf("close receipt write side: %w", err)
155
+ }
156
+ if err := receipt.Commit(); err == nil {
157
+ return errors.New("bare-close receipt commit unexpectedly succeeded")
158
+ }
159
+ default:
160
+ return fmt.Errorf("unknown receipt case: %s", receiptCase)
161
+ }
162
+
163
+ if err := emitReceiptEvent(
164
+ fmt.Sprintf("CLIENT_RECEIPT_RESOLVED %s", receiptTerminalName(receiptCase)),
165
+ ); err != nil {
166
+ return fmt.Errorf("record receipt resolution: %w", err)
167
+ }
168
+
169
+ fmt.Println(" PASSED")
170
+ return nil
171
+ }
172
+
73
173
  func testServerStream(ctx context.Context, client echo.SRPCEchoerClient) error {
74
174
  fmt.Println("Testing ServerStream RPC...")
75
175
  strm, err := client.EchoServerStream(ctx, &echo.EchoMsg{Body: bodyTxt})
@@ -149,3 +249,35 @@ func testBidiStream(ctx context.Context, client echo.SRPCEchoerClient) error {
149
249
  fmt.Println(" PASSED")
150
250
  return nil
151
251
  }
252
+
253
+ func receiptTerminalName(receiptCase string) string {
254
+ switch receiptCase {
255
+ case "commit":
256
+ return "committed"
257
+ case "abort":
258
+ return "canceled"
259
+ case "loss":
260
+ return "transportLost"
261
+ case "bare-close":
262
+ return "closed"
263
+ default:
264
+ return "unknown"
265
+ }
266
+ }
267
+
268
+ func emitReceiptEvent(line string) error {
269
+ fmt.Println(line)
270
+ fifo := os.Getenv("RECEIPT_EVENT_FIFO")
271
+ if fifo == "" {
272
+ return nil
273
+ }
274
+ file, err := os.OpenFile(fifo, os.O_WRONLY, 0) //nolint:gosec // FIFO path is created by the local integration runner.
275
+ if err != nil {
276
+ return err
277
+ }
278
+ if _, err := file.WriteString(line + "\n"); err != nil {
279
+ _ = file.Close()
280
+ return err
281
+ }
282
+ return file.Close()
283
+ }
@@ -6,6 +6,7 @@ import (
6
6
  "net"
7
7
  "os"
8
8
  "os/signal"
9
+ "sync"
9
10
 
10
11
  "github.com/aperturerobotics/starpc/echo"
11
12
  "github.com/aperturerobotics/starpc/srpc"
@@ -15,14 +16,73 @@ func main() {
15
16
  ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
16
17
  defer cancel()
17
18
 
19
+ receiptMode := len(os.Args) > 1 && os.Args[1] == "receipt"
20
+ receiptCase := ""
21
+ if receiptMode {
22
+ if len(os.Args) < 3 {
23
+ fmt.Fprintln(os.Stderr, "usage: go-server receipt <case>")
24
+ os.Exit(1)
25
+ }
26
+ receiptCase = os.Args[2]
27
+ switch receiptCase {
28
+ case "commit", "abort", "loss", "bare-close":
29
+ default:
30
+ fmt.Fprintf(os.Stderr, "unknown receipt case: %s\n", receiptCase)
31
+ os.Exit(1)
32
+ }
33
+ }
18
34
  mux := srpc.NewMux()
19
35
  echoServer := echo.NewEchoServer(mux)
20
36
  if err := echo.SRPCRegisterEchoer(mux, echoServer); err != nil {
21
37
  fmt.Fprintf(os.Stderr, "register error: %v\n", err)
22
38
  os.Exit(1)
23
39
  }
24
- server := srpc.NewServer(mux)
25
-
40
+ var receiptDone <-chan struct{}
41
+ var finishReceipt func()
42
+ if receiptMode {
43
+ done := make(chan struct{})
44
+ var doneOnce sync.Once
45
+ receiptDone = done
46
+ finishReceipt = func() {
47
+ doneOnce.Do(func() {
48
+ close(done)
49
+ })
50
+ }
51
+ }
52
+ var invoker srpc.Invoker = mux
53
+ if receiptMode {
54
+ invoker = srpc.InvokerFunc(func(
55
+ serviceID, methodID string,
56
+ strm srpc.Stream,
57
+ ) (bool, error) {
58
+ handled, err := mux.InvokeMethod(serviceID, methodID, strm)
59
+ if err != nil || !handled {
60
+ return handled, err
61
+ }
62
+ invocation, ok := srpc.GetServerInvocation(strm.Context())
63
+ if !ok {
64
+ return true, context.Canceled
65
+ }
66
+ kind, waitErr := invocation.WaitTerminal(context.Background())
67
+ markerErr := emitReceiptEvent(
68
+ fmt.Sprintf("SERVER_RECEIPT_TERMINAL %s", terminalName(kind)),
69
+ )
70
+ if waitErr != nil {
71
+ return true, waitErr
72
+ }
73
+ if markerErr != nil {
74
+ return true, markerErr
75
+ }
76
+ if kind != srpc.TerminalCommitted {
77
+ finishReceipt()
78
+ }
79
+ return true, nil
80
+ })
81
+ }
82
+ var server *srpc.Server
83
+ if !receiptMode {
84
+ server = srpc.NewServer(invoker)
85
+ }
26
86
  ln, err := net.Listen("tcp", "127.0.0.1:0")
27
87
  if err != nil {
28
88
  fmt.Fprintf(os.Stderr, "listen error: %v\n", err)
@@ -30,6 +90,13 @@ func main() {
30
90
  }
31
91
  defer ln.Close()
32
92
 
93
+ if receiptMode {
94
+ go func() {
95
+ <-receiptDone
96
+ _ = ln.Close()
97
+ }()
98
+ }
99
+
33
100
  fmt.Printf("LISTENING %s\n", ln.Addr().String())
34
101
 
35
102
  go func() {
@@ -42,6 +109,82 @@ func main() {
42
109
  if err != nil {
43
110
  return
44
111
  }
45
- go server.HandleStream(ctx, conn)
112
+ if receiptMode {
113
+ go handleReceiptStream(ctx, conn, invoker, finishReceipt)
114
+ } else {
115
+ go server.HandleStream(ctx, conn)
116
+ }
117
+ }
118
+ }
119
+
120
+ func handleReceiptStream(
121
+ ctx context.Context,
122
+ conn net.Conn,
123
+ invoker srpc.Invoker,
124
+ finishReceipt func(),
125
+ ) {
126
+ prw := srpc.NewPacketReadWriter(conn)
127
+ writer := &receiptPacketWriter{
128
+ inner: prw,
129
+ finishReceipt: finishReceipt,
130
+ }
131
+ rpc := srpc.NewServerRPC(ctx, invoker, writer)
132
+ prw.ReadPump(rpc.HandlePacketData, rpc.HandleStreamClose)
133
+ }
134
+
135
+ type receiptPacketWriter struct {
136
+ inner srpc.PacketWriter
137
+ finishReceipt func()
138
+ }
139
+
140
+ func (w *receiptPacketWriter) WritePacket(pkt *srpc.Packet) error {
141
+ if err := w.inner.WritePacket(pkt); err != nil {
142
+ return err
143
+ }
144
+ data := pkt.GetCallData()
145
+ if data != nil && data.GetComplete() && data.GetError() == "" {
146
+ if err := emitReceiptEvent("SERVER_RECEIPT_ACK committed"); err != nil {
147
+ return err
148
+ }
149
+ w.finishReceipt()
150
+ }
151
+ return nil
152
+ }
153
+
154
+ func (w *receiptPacketWriter) Close() error {
155
+ return w.inner.Close()
156
+ }
157
+
158
+ func terminalName(kind srpc.TerminalKind) string {
159
+ switch kind {
160
+ case srpc.TerminalCommitted:
161
+ return "committed"
162
+ case srpc.TerminalCanceled:
163
+ return "canceled"
164
+ case srpc.TerminalLost:
165
+ return "transportLost"
166
+ case srpc.TerminalClosed:
167
+ return "closed"
168
+ case srpc.TerminalAbandoned:
169
+ return "abandoned"
170
+ default:
171
+ return "unknown"
172
+ }
173
+ }
174
+
175
+ func emitReceiptEvent(line string) error {
176
+ fmt.Println(line)
177
+ fifo := os.Getenv("RECEIPT_EVENT_FIFO")
178
+ if fifo == "" {
179
+ return nil
180
+ }
181
+ file, err := os.OpenFile(fifo, os.O_WRONLY, 0) //nolint:gosec // FIFO path is created by the local integration runner.
182
+ if err != nil {
183
+ return err
184
+ }
185
+ if _, err := file.WriteString(line + "\n"); err != nil {
186
+ _ = file.Close()
187
+ return err
46
188
  }
189
+ return file.Close()
47
190
  }