starpc 0.49.18 → 0.50.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 (86) 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 +100 -4
  8. package/dist/mock/index.d.ts +2 -2
  9. package/dist/mock/index.js +2 -2
  10. package/dist/mock/mock.pb.d.ts +1 -1
  11. package/dist/mock/mock.pb.js +3 -3
  12. package/dist/rpcstream/receipt.test.d.ts +1 -0
  13. package/dist/rpcstream/receipt.test.js +41 -0
  14. package/dist/rpcstream/rpcstream.pb.d.ts +1 -1
  15. package/dist/rpcstream/rpcstream.pb.js +14 -8
  16. package/dist/srpc/call-receipt.d.ts +17 -0
  17. package/dist/srpc/call-receipt.js +106 -0
  18. package/dist/srpc/call-receipt.test.d.ts +1 -0
  19. package/dist/srpc/call-receipt.test.js +375 -0
  20. package/dist/srpc/client.d.ts +3 -1
  21. package/dist/srpc/client.js +20 -0
  22. package/dist/srpc/common-rpc.d.ts +13 -3
  23. package/dist/srpc/common-rpc.js +87 -10
  24. package/dist/srpc/handler.d.ts +2 -1
  25. package/dist/srpc/index.d.ts +4 -0
  26. package/dist/srpc/index.js +3 -0
  27. package/dist/srpc/invoker.d.ts +2 -1
  28. package/dist/srpc/invoker.js +2 -2
  29. package/dist/srpc/rpcproto.pb.d.ts +45 -1
  30. package/dist/srpc/rpcproto.pb.js +60 -7
  31. package/dist/srpc/server-invocation.d.ts +17 -0
  32. package/dist/srpc/server-invocation.js +37 -0
  33. package/dist/srpc/server-rpc.js +3 -1
  34. package/echo/echo.pb.go +1 -1
  35. package/echo/echo.pb.ts +6 -5
  36. package/echo/echo_srpc.pb.cpp +1 -1
  37. package/echo/echo_srpc.pb.go +1 -1
  38. package/echo/echo_srpc.pb.hpp +1 -1
  39. package/echo/echo_srpc.pb.rs +1 -1
  40. package/go.mod +13 -12
  41. package/go.sum +24 -24
  42. package/integration/cross-language/go-client/main.go +137 -5
  43. package/integration/cross-language/go-server/fixture-owner_test.go +127 -0
  44. package/integration/cross-language/go-server/main.go +169 -4
  45. package/integration/cross-language/run.bash +117 -13
  46. package/integration/cross-language/ts-client.ts +94 -13
  47. package/integration/cross-language/ts-server.ts +115 -7
  48. package/mock/index.ts +2 -2
  49. package/mock/mock.pb.go +1 -1
  50. package/mock/mock.pb.ts +6 -5
  51. package/mock/mock_srpc.pb.cpp +1 -1
  52. package/mock/mock_srpc.pb.go +1 -1
  53. package/mock/mock_srpc.pb.hpp +1 -1
  54. package/mock/mock_srpc.pb.rs +1 -1
  55. package/package.json +19 -26
  56. package/srpc/accept.go +1 -1
  57. package/srpc/call-receipt-e2e_test.go +111 -0
  58. package/srpc/call-receipt.go +112 -0
  59. package/srpc/call-receipt.test.ts +441 -0
  60. package/srpc/call-receipt.ts +131 -0
  61. package/srpc/call-receipt_test.go +536 -0
  62. package/srpc/client-rpc.go +1 -8
  63. package/srpc/client.ts +29 -2
  64. package/srpc/common-rpc.go +114 -29
  65. package/srpc/common-rpc.ts +104 -13
  66. package/srpc/handler.ts +2 -0
  67. package/srpc/index.ts +4 -0
  68. package/srpc/invoker.ts +10 -5
  69. package/srpc/msg-stream.go +8 -0
  70. package/srpc/muxed-conn.go +9 -4
  71. package/srpc/muxed-conn_test.go +79 -0
  72. package/srpc/muxed-yamux.go +1 -1
  73. package/srpc/rpcproto.pb.cc +15 -4
  74. package/srpc/rpcproto.pb.go +97 -1
  75. package/srpc/rpcproto.pb.h +59 -0
  76. package/srpc/rpcproto.pb.rs +45 -0
  77. package/srpc/rpcproto.pb.ts +90 -27
  78. package/srpc/rpcproto.proto +16 -0
  79. package/srpc/schema-ownership_test.go +115 -0
  80. package/srpc/server-invocation.go +40 -0
  81. package/srpc/server-invocation.ts +76 -0
  82. package/srpc/server-rpc.go +1 -1
  83. package/srpc/server-rpc.ts +6 -2
  84. package/srpc/stream-yamux.go +1 -1
  85. package/srpc/stream.go +20 -0
  86. package/srpc/websocket.go +1 -1
@@ -1,16 +1,33 @@
1
1
  import net from 'net'
2
+ import { closeSync, openSync, writeSync } from 'node:fs'
2
3
  import { pipe } from 'it-pipe'
3
4
  import { pushable } from 'it-pushable'
5
+ import type { Source } from 'it-stream-types'
6
+
4
7
  import { createMux, createHandler, Server } from '../../srpc/index.js'
5
8
  import {
6
9
  parseLengthPrefixTransform,
7
10
  prependLengthPrefixTransform,
8
11
  } from '../../srpc/packet.js'
9
12
  import { combineUint8ArrayListTransform } from '../../srpc/array-list.js'
10
- import { EchoerServer } from '../../echo/server.js'
13
+ import { EchoerServer, EchoMsg } from '../../echo/index.js'
11
14
  import { EchoerDefinition } from '../../echo/echo_srpc.pb.js'
15
+ import { Packet, TerminalKind } from '../../srpc/rpcproto.pb.js'
12
16
  import type { PacketStream } from '../../srpc/stream.js'
13
- import type { Source } from 'it-stream-types'
17
+
18
+ function emitReceiptEvent(line: string): void {
19
+ console.log(line)
20
+ const fifo = process.env.RECEIPT_EVENT_FIFO
21
+ if (!fifo) {
22
+ return
23
+ }
24
+ const fd = openSync(fifo, 'w')
25
+ try {
26
+ writeSync(fd, `${line}\n`)
27
+ } finally {
28
+ closeSync(fd)
29
+ }
30
+ }
14
31
 
15
32
  // tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
16
33
  // Each Uint8Array in source/sink is one packet (no length prefix).
@@ -35,14 +52,26 @@ function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
35
52
  source: socketSource(),
36
53
  sink: async (source: Source<Uint8Array>): Promise<void> => {
37
54
  for await (const chunk of pipe(source, prependLengthPrefixTransform())) {
38
- const data =
39
- chunk instanceof Uint8Array ? chunk : (chunk as any).subarray()
40
- await new Promise<void>((resolve, reject) => {
55
+ const data = chunk instanceof Uint8Array ? chunk : chunk.subarray()
56
+ const packet = Packet.fromBinary(data.subarray(4))
57
+ const receiptCompletion =
58
+ receiptMode &&
59
+ packet.body?.case === 'callData' &&
60
+ packet.body.value.complete &&
61
+ !packet.body.value.error
62
+ if (receiptCompletion) {
63
+ emitReceiptEvent('SERVER_RECEIPT_ACK_WRITE committed')
64
+ }
65
+ const writeDone = new Promise<void>((resolve, reject) => {
41
66
  socket.write(data, (err) => {
42
67
  if (err) reject(err)
43
68
  else resolve()
44
69
  })
45
70
  })
71
+ await writeDone
72
+ if (receiptCompletion) {
73
+ finishReceiptServer()
74
+ }
46
75
  }
47
76
  socket.end()
48
77
  },
@@ -50,9 +79,82 @@ function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
50
79
  }
51
80
 
52
81
  const mux = createMux()
82
+ const receiptMode = process.argv[2] === 'receipt'
83
+ const receiptCase = receiptMode ? process.argv[3] : undefined
84
+ if (
85
+ receiptMode &&
86
+ !['commit', 'abort', 'loss', 'bare-close'].includes(receiptCase ?? '')
87
+ ) {
88
+ console.error(`unknown receipt case: ${receiptCase}`)
89
+ process.exit(1)
90
+ }
91
+
92
+ const receiptServerDone = Promise.withResolvers<void>()
93
+ let receiptServerFinished = false
94
+ function finishReceiptServer(): void {
95
+ if (!receiptServerFinished) {
96
+ receiptServerFinished = true
97
+ receiptServerDone.resolve()
98
+ }
99
+ }
100
+
101
+ if (receiptMode) {
102
+ mux.registerLookupMethod(async (serviceID, methodID) => {
103
+ if (serviceID !== 'echo.Echoer' || methodID !== 'Echo') {
104
+ return null
105
+ }
106
+ return async (dataSource, dataSink, invocation) => {
107
+ let requestData: Uint8Array | undefined
108
+ for await (const data of dataSource) {
109
+ requestData = data
110
+ break
111
+ }
112
+ if (!requestData) {
113
+ throw new Error('receipt request was empty')
114
+ }
115
+ const requestMsg = EchoMsg.fromBinary(requestData)
116
+ await dataSink(
117
+ (async function* () {
118
+ yield EchoMsg.toBinary(requestMsg)
119
+ if (!invocation) {
120
+ throw new Error('receipt invocation was missing')
121
+ }
122
+ emitReceiptEvent('SERVER_RECEIPT_WAITING')
123
+ const terminal = await invocation.waitTerminal(
124
+ new AbortController().signal,
125
+ )
126
+ emitReceiptEvent(`SERVER_RECEIPT_TERMINAL ${terminalName(terminal)}`)
127
+ if (terminal !== TerminalKind.COMMITTED) {
128
+ finishReceiptServer()
129
+ }
130
+ })(),
131
+ )
132
+ }
133
+ })
134
+ }
135
+
136
+ function terminalName(terminal: TerminalKind): string {
137
+ switch (terminal) {
138
+ case TerminalKind.COMMITTED:
139
+ return 'committed'
140
+ case TerminalKind.CANCELED:
141
+ return 'canceled'
142
+ case TerminalKind.TRANSPORT_LOST:
143
+ return 'transportLost'
144
+ case TerminalKind.CLOSED:
145
+ return 'closed'
146
+ case TerminalKind.ABANDONED:
147
+ return 'abandoned'
148
+ default:
149
+ return 'unknown'
150
+ }
151
+ }
152
+
53
153
  const server = new Server(mux.lookupMethod)
54
- const echoer = new EchoerServer(server)
55
- mux.register(createHandler(EchoerDefinition, echoer))
154
+ if (!receiptMode) {
155
+ const echoer = new EchoerServer(server)
156
+ mux.register(createHandler(EchoerDefinition, echoer))
157
+ }
56
158
 
57
159
  const tcpServer = net.createServer((socket) => {
58
160
  const stream = tcpSocketToPacketStream(socket)
@@ -64,6 +166,12 @@ tcpServer.listen(0, '127.0.0.1', () => {
64
166
  console.log(`LISTENING ${addr.address}:${addr.port}`)
65
167
  })
66
168
 
169
+ if (receiptMode) {
170
+ void receiptServerDone.promise.then(() => {
171
+ tcpServer.close()
172
+ })
173
+ }
174
+
67
175
  process.on('SIGINT', () => {
68
176
  tcpServer.close()
69
177
  process.exit(0)
package/mock/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export * from "./mock.pb.js"
2
- export * from "./mock_srpc.pb.js"
1
+ export * from './mock.pb.js'
2
+ export * from './mock_srpc.pb.js'
package/mock/mock.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/mock/mock.proto
4
4
 
5
5
  package e2e_mock
package/mock/mock.pb.ts CHANGED
@@ -2,8 +2,10 @@
2
2
  // @generated from file github.com/aperturerobotics/starpc/mock/mock.proto (package e2e.mock, 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 = 'e2e.mock'
9
11
 
@@ -19,11 +21,10 @@ export interface MockMsg {
19
21
  body?: string
20
22
  }
21
23
 
22
- // MockMsg contains the message type declaration for MockMsg.
23
- export const MockMsg: MessageType<MockMsg> = createMessageType({
24
+ export const MockMsg: MessageType<MockMsg> = /* @__PURE__ */ createMessageType({
24
25
  typeName: 'e2e.mock.MockMsg',
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/mock/mock.proto
6
6
 
7
7
  #include "mock_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/mock/mock.proto
4
4
 
5
5
  package e2e_mock
@@ -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/mock/mock.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/mock/mock.proto
4
4
 
5
5
  #[allow(unused_imports)]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starpc",
3
- "version": "0.49.18",
3
+ "version": "0.50.0",
4
4
  "description": "Streaming protobuf RPC service protocol over any two-way channel.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -68,10 +68,10 @@
68
68
  "deps": "depcheck --ignores 'bufferutil,utf-8-validate,rimraf,starpc,@aptre/protobuf-es-lite,tsx'",
69
69
  "codegen": "bun run gen",
70
70
  "ci": "bun run build && bun run lint:js && bun run lint:go",
71
- "format": "bun run format:go && bun run format:js && bun run format:config",
72
- "format:config": "prettier --write tsconfig.json package.json",
71
+ "format": "bun run format:js && bun run format:go && bun run format:config",
72
+ "format:config": "oxfmt '*.json' '.github/**/*.json' '.github/**/*.yml' '.oxfmtrc.json' 'tsconfig.json'",
73
73
  "format:go": "bun run go:aptre -- format",
74
- "format:js": "prettier --write './{srpc,echo,e2e,integration,rpcstream,cmd}/**/(*.ts|*.tsx|*.html|*.css)'",
74
+ "format:js": "oxfmt './{srpc,echo,e2e,integration,rpcstream,cmd,mock,scripts}/**/*.{ts,tsx,js,mjs,html,css,scss}' '*.{ts,tsx,js,mjs}'",
75
75
  "gen": "bun run go:aptre -- generate && bun run format",
76
76
  "gen:force": "bun run go:aptre -- generate --force && bun run format",
77
77
  "test": "bun run test:js && bun run test:go",
@@ -90,46 +90,39 @@
90
90
  "integration:rust:cpp": "bash ./integration/cross-language/run.bash rust:cpp",
91
91
  "lint": "bun run lint:go && bun run lint:js",
92
92
  "lint:go": "bun run go:aptre -- lint",
93
- "lint:js": "eslint ./",
93
+ "lint:js": "oxlint .",
94
+ "lint:js:fix": "bun run lint:js -- --fix",
94
95
  "prepare": "husky && go mod vendor",
95
96
  "go:aptre": "go run -mod=mod github.com/aperturerobotics/common/cmd/aptre",
96
97
  "precommit": "lint-staged",
97
- "release": "npm run release:version && npm run release:commit",
98
- "release:minor": "npm run release:version:minor && npm run release:commit",
99
- "release:version": "npm version patch -m \"release: v%s\" --no-git-tag-version && npm run release:sync-cargo",
100
- "release:version:minor": "npm version minor -m \"release: v%s\" --no-git-tag-version && npm run release:sync-cargo",
101
- "release:sync-cargo": "node -e \"const v=require('./package.json').version,f='Cargo.toml',c=require('fs').readFileSync(f,'utf8');require('fs').writeFileSync(f,c.replace(/^version = \\\".*?\\\"/m,'version = \\\"'+v+'\\\"'))\"",
102
- "release:commit": "git reset && git add package.json Cargo.toml && git commit -s -m \"release: v$(node -p \"require('./package.json').version\")\" && git tag v$(node -p \"require('./package.json').version\")",
98
+ "release": "bun run release:version && bun run release:commit",
99
+ "release:minor": "bun run release:version:minor && bun run release:commit",
100
+ "release:version": "bun scripts/release-version.ts patch",
101
+ "release:version:minor": "bun scripts/release-version.ts minor",
102
+ "release:commit": "version=$(bun -e 'console.log(require(\"./package.json\").version)') && git reset && git add package.json Cargo.toml && git commit -s -m \"release: v$version\" && git tag \"v$version\"",
103
103
  "release:publish": "git push && git push --tags"
104
104
  },
105
105
  "preferUnplugged": true,
106
106
  "lint-staged": {
107
- "package.json": "prettier --config .prettierrc.yaml --write",
108
- "./{srpc,echo,e2e,integration,rpcstream,cmd}/**/(*.ts|*.tsx|*.html|*.css)": "prettier --config .prettierrc.yaml --write"
107
+ "*.{json,ts,tsx,js,mjs}": "oxfmt",
108
+ "./{srpc,echo,e2e,integration,rpcstream,cmd,mock,scripts}/**/*.{ts,tsx,js,mjs,html,css,scss}": "oxfmt"
109
109
  },
110
110
  "devDependencies": {
111
- "@eslint/js": "^10.0.0",
112
- "@typescript-eslint/eslint-plugin": "^8.59.0",
113
- "@typescript-eslint/parser": "^8.59.0",
111
+ "@typescript/native-preview": "^7.0.0-dev.20260422.1",
114
112
  "depcheck": "^1.4.6",
115
113
  "esbuild": "^0.28.0",
116
- "eslint": "^10.2.1",
117
- "eslint-config-prettier": "^10.0.0",
118
- "eslint-plugin-unused-imports": "^4.4.1",
119
- "globals": "^17.5.0",
120
114
  "happy-dom": "^20.9.0",
121
115
  "husky": "^9.1.7",
122
116
  "lint-staged": "^17.0.0",
123
- "prettier": "^3.8.3",
117
+ "oxfmt": "0.61.0",
118
+ "oxlint": "^1.76.0",
124
119
  "rimraf": "^6.1.3",
125
120
  "tsx": "^4.20.4",
126
- "typescript": "^6.0.3",
127
- "@typescript/native-preview": "^7.0.0-dev.20260422.1",
128
121
  "vitest": "^4.1.5"
129
122
  },
130
123
  "dependencies": {
131
124
  "@aptre/it-ws": "^1.1.2",
132
- "@aptre/protobuf-es-lite": "1.1.0",
125
+ "@aptre/protobuf-es-lite": "1.1.1",
133
126
  "@aptre/yamux": "^1.0.3",
134
127
  "event-iterator": "^2.0.0",
135
128
  "isomorphic-ws": "^5.0.0",
@@ -141,9 +134,9 @@
141
134
  "ws": "^8.20.0"
142
135
  },
143
136
  "resolutions": {
144
- "@aptre/protobuf-es-lite": "1.1.0"
137
+ "@aptre/protobuf-es-lite": "1.1.1"
145
138
  },
146
139
  "overrides": {
147
- "@aptre/protobuf-es-lite": "1.1.0"
140
+ "@aptre/protobuf-es-lite": "1.1.1"
148
141
  }
149
142
  }
package/srpc/accept.go CHANGED
@@ -4,7 +4,7 @@ import (
4
4
  "context"
5
5
  "net"
6
6
 
7
- "github.com/libp2p/go-yamux/v4"
7
+ "github.com/libp2p/go-yamux/v5"
8
8
  )
9
9
 
10
10
  // AcceptMuxedListener accepts incoming connections from a net.Listener.
@@ -0,0 +1,111 @@
1
+ package srpc_test
2
+
3
+ import (
4
+ "context"
5
+ "net"
6
+ "testing"
7
+
8
+ yamux "github.com/libp2p/go-yamux/v5"
9
+
10
+ "github.com/aperturerobotics/starpc/echo"
11
+ "github.com/aperturerobotics/starpc/srpc"
12
+ )
13
+
14
+ type receiptEchoServer struct {
15
+ echo.SRPCEchoerServer
16
+ invocationFound chan bool
17
+ }
18
+
19
+ func (s *receiptEchoServer) Echo(
20
+ ctx context.Context,
21
+ msg *echo.EchoMsg,
22
+ ) (*echo.EchoMsg, error) {
23
+ _, found := srpc.GetServerInvocation(ctx)
24
+ s.invocationFound <- found
25
+ return msg.CloneVT(), nil
26
+ }
27
+
28
+ func TestCallReceiptGoGo(t *testing.T) {
29
+ clientConn, serverConn := net.Pipe()
30
+ clientMux, err := srpc.NewMuxedConn(clientConn, true, nil)
31
+ if err != nil {
32
+ t.Fatalf("client mux: %v", err)
33
+ }
34
+ serverMux, err := srpc.NewMuxedConn(serverConn, false, nil)
35
+ if err != nil {
36
+ _ = clientMux.Close()
37
+ t.Fatalf("server mux: %v", err)
38
+ }
39
+
40
+ mux := srpc.NewMux()
41
+ echoServer := &receiptEchoServer{
42
+ SRPCEchoerServer: echo.NewEchoServer(mux),
43
+ invocationFound: make(chan bool, 1),
44
+ }
45
+ if err := echo.SRPCRegisterEchoer(mux, echoServer); err != nil {
46
+ t.Fatalf("register echo: %v", err)
47
+ }
48
+
49
+ terminal := make(chan srpc.TerminalKind, 1)
50
+ invoker := srpc.InvokerFunc(func(
51
+ serviceID, methodID string,
52
+ strm srpc.Stream,
53
+ ) (bool, error) {
54
+ handled, err := mux.InvokeMethod(serviceID, methodID, strm)
55
+ if err != nil || !handled {
56
+ return handled, err
57
+ }
58
+ invocation, ok := srpc.GetServerInvocation(strm.Context())
59
+ if !ok {
60
+ return true, context.Canceled
61
+ }
62
+ kind, err := invocation.WaitTerminal(context.Background())
63
+ if err == nil {
64
+ terminal <- kind
65
+ }
66
+ return true, err
67
+ })
68
+ server := srpc.NewServer(invoker)
69
+ serverCtx, serverCancel := context.WithCancel(context.Background())
70
+ serverErr := make(chan error, 1)
71
+ go func() {
72
+ serverErr <- server.AcceptMuxedConn(serverCtx, serverMux)
73
+ }()
74
+ t.Cleanup(func() {
75
+ if err := serverMux.Close(); err != nil {
76
+ t.Errorf("close server mux: %v", err)
77
+ }
78
+ acceptErr := <-serverErr
79
+ serverCancel()
80
+ if acceptErr != yamux.ErrSessionShutdown {
81
+ t.Errorf("server accept: %v, want session shutdown", acceptErr)
82
+ }
83
+ if err := clientMux.Close(); err != nil {
84
+ t.Errorf("close client mux: %v", err)
85
+ }
86
+ })
87
+
88
+ client := srpc.NewClientWithMuxedConn(clientMux)
89
+ out := new(echo.EchoMsg)
90
+ receipt, err := srpc.ExecCallReceipt(
91
+ context.Background(), client, echo.SRPCEchoerServiceID, "Echo",
92
+ &echo.EchoMsg{Body: "held"}, out,
93
+ )
94
+ if err != nil {
95
+ t.Fatalf("exec receipt: %v", err)
96
+ }
97
+ if err := receipt.Commit(); err != nil {
98
+ t.Fatalf("commit: %v", err)
99
+ }
100
+ if out.GetBody() != "held" {
101
+ t.Fatalf("response body = %q, want held", out.GetBody())
102
+ }
103
+ found := <-echoServer.invocationFound
104
+ if !found {
105
+ t.Fatal("generated handler context lost server invocation")
106
+ }
107
+ kind := <-terminal
108
+ if kind != srpc.TerminalKind_TERMINAL_KIND_COMMITTED {
109
+ t.Fatalf("terminal = %v, want committed", kind)
110
+ }
111
+ }
@@ -0,0 +1,112 @@
1
+ package srpc
2
+
3
+ import (
4
+ "context"
5
+ "io"
6
+ "sync/atomic"
7
+
8
+ "github.com/pkg/errors"
9
+ )
10
+
11
+ // ExecCallReceipt executes a unary call over cc.NewStream, reads the single
12
+ // response into out, and returns a held receipt. The caller must dispose it
13
+ // with Commit, Abort, or Close.
14
+ func ExecCallReceipt(
15
+ ctx context.Context,
16
+ cc Client,
17
+ service, method string,
18
+ in, out Message,
19
+ ) (*CallReceipt, error) {
20
+ strm, err := cc.NewStream(ctx, service, method, in)
21
+ if err != nil {
22
+ return nil, err
23
+ }
24
+ if err := strm.MsgRecv(out); err != nil {
25
+ _ = strm.Close()
26
+ return nil, err
27
+ }
28
+ return &CallReceipt{strm: strm}, nil
29
+ }
30
+
31
+ // CallReceipt holds a unary call until the server finalizes it.
32
+ type CallReceipt struct {
33
+ strm Stream
34
+ disposed atomic.Bool
35
+ }
36
+
37
+ // Context returns the context of the held call.
38
+ func (r *CallReceipt) Context() context.Context {
39
+ return r.strm.Context()
40
+ }
41
+
42
+ // Commit sends request completion and waits for the server completion
43
+ // acknowledgment before releasing the stream.
44
+ func (r *CallReceipt) Commit() error {
45
+ if r.disposed.Swap(true) {
46
+ return nil
47
+ }
48
+
49
+ if r.strm.Context().Err() != nil {
50
+ _ = r.strm.Close()
51
+ return context.Canceled
52
+ }
53
+ if err := r.strm.CloseSend(); err != nil {
54
+ _ = r.strm.Close()
55
+ return err
56
+ }
57
+
58
+ var done receiptDone
59
+ err := r.strm.MsgRecv(&done)
60
+ _ = r.strm.Close()
61
+ if err == io.EOF {
62
+ receipt, ok := r.strm.(receiptTerminalStream)
63
+ if ok {
64
+ terminal, terminalOK := receipt.receiptTerminalKind()
65
+ if terminalOK && terminal == TerminalKind_TERMINAL_KIND_COMMITTED {
66
+ return nil
67
+ }
68
+ }
69
+ return errors.New("missing trailing completion acknowledgment")
70
+ }
71
+ if err == nil {
72
+ return errors.New("unexpected trailing response data")
73
+ }
74
+ return err
75
+ }
76
+
77
+ // Abort sends request cancellation and releases the stream.
78
+ func (r *CallReceipt) Abort() error {
79
+ if r.disposed.Swap(true) {
80
+ return nil
81
+ }
82
+ return r.strm.Close()
83
+ }
84
+
85
+ // Close aborts the held call unless it has already reached a terminal.
86
+ func (r *CallReceipt) Close() error {
87
+ return r.Abort()
88
+ }
89
+
90
+ // receiptDone is a no-op message used to read the trailing completion packet.
91
+ type receiptDone struct{}
92
+
93
+ func (*receiptDone) SizeVT() int {
94
+ return 0
95
+ }
96
+
97
+ func (*receiptDone) MarshalToSizedBufferVT([]byte) (int, error) {
98
+ return 0, nil
99
+ }
100
+
101
+ func (*receiptDone) MarshalVT() ([]byte, error) {
102
+ return nil, nil
103
+ }
104
+
105
+ func (*receiptDone) UnmarshalVT([]byte) error {
106
+ return nil
107
+ }
108
+
109
+ func (*receiptDone) Reset() {}
110
+
111
+ // _ is a type assertion.
112
+ var _ Message = (*receiptDone)(nil)