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.
- package/README.md +1 -3
- package/cmd/protoc-gen-es-starpc/typescript.ts +11 -6
- package/dist/cmd/protoc-gen-es-starpc/typescript.js +6 -5
- package/dist/echo/echo.pb.d.ts +1 -1
- package/dist/echo/echo.pb.js +3 -3
- package/dist/integration/cross-language/ts-client.js +84 -11
- package/dist/integration/cross-language/ts-server.js +100 -4
- package/dist/mock/index.d.ts +2 -2
- package/dist/mock/index.js +2 -2
- package/dist/mock/mock.pb.d.ts +1 -1
- package/dist/mock/mock.pb.js +3 -3
- package/dist/rpcstream/receipt.test.d.ts +1 -0
- package/dist/rpcstream/receipt.test.js +41 -0
- package/dist/rpcstream/rpcstream.pb.d.ts +1 -1
- package/dist/rpcstream/rpcstream.pb.js +14 -8
- package/dist/srpc/call-receipt.d.ts +17 -0
- package/dist/srpc/call-receipt.js +106 -0
- package/dist/srpc/call-receipt.test.d.ts +1 -0
- package/dist/srpc/call-receipt.test.js +375 -0
- package/dist/srpc/client.d.ts +3 -1
- package/dist/srpc/client.js +20 -0
- package/dist/srpc/common-rpc.d.ts +13 -3
- package/dist/srpc/common-rpc.js +87 -10
- package/dist/srpc/handler.d.ts +2 -1
- package/dist/srpc/index.d.ts +4 -0
- package/dist/srpc/index.js +3 -0
- package/dist/srpc/invoker.d.ts +2 -1
- package/dist/srpc/invoker.js +2 -2
- package/dist/srpc/rpcproto.pb.d.ts +45 -1
- package/dist/srpc/rpcproto.pb.js +60 -7
- package/dist/srpc/server-invocation.d.ts +17 -0
- package/dist/srpc/server-invocation.js +37 -0
- package/dist/srpc/server-rpc.js +3 -1
- package/echo/echo.pb.go +1 -1
- package/echo/echo.pb.ts +6 -5
- package/echo/echo_srpc.pb.cpp +1 -1
- package/echo/echo_srpc.pb.go +1 -1
- package/echo/echo_srpc.pb.hpp +1 -1
- package/echo/echo_srpc.pb.rs +1 -1
- package/go.mod +13 -12
- package/go.sum +24 -24
- package/integration/cross-language/go-client/main.go +137 -5
- package/integration/cross-language/go-server/fixture-owner_test.go +127 -0
- package/integration/cross-language/go-server/main.go +169 -4
- package/integration/cross-language/run.bash +117 -13
- package/integration/cross-language/ts-client.ts +94 -13
- package/integration/cross-language/ts-server.ts +115 -7
- package/mock/index.ts +2 -2
- package/mock/mock.pb.go +1 -1
- package/mock/mock.pb.ts +6 -5
- package/mock/mock_srpc.pb.cpp +1 -1
- package/mock/mock_srpc.pb.go +1 -1
- package/mock/mock_srpc.pb.hpp +1 -1
- package/mock/mock_srpc.pb.rs +1 -1
- package/package.json +19 -26
- package/srpc/accept.go +1 -1
- package/srpc/call-receipt-e2e_test.go +111 -0
- package/srpc/call-receipt.go +112 -0
- package/srpc/call-receipt.test.ts +441 -0
- package/srpc/call-receipt.ts +131 -0
- package/srpc/call-receipt_test.go +536 -0
- package/srpc/client-rpc.go +1 -8
- package/srpc/client.ts +29 -2
- package/srpc/common-rpc.go +114 -29
- package/srpc/common-rpc.ts +104 -13
- package/srpc/handler.ts +2 -0
- package/srpc/index.ts +4 -0
- package/srpc/invoker.ts +10 -5
- package/srpc/msg-stream.go +8 -0
- package/srpc/muxed-conn.go +9 -4
- package/srpc/muxed-conn_test.go +79 -0
- package/srpc/muxed-yamux.go +1 -1
- package/srpc/rpcproto.pb.cc +15 -4
- package/srpc/rpcproto.pb.go +97 -1
- package/srpc/rpcproto.pb.h +59 -0
- package/srpc/rpcproto.pb.rs +45 -0
- package/srpc/rpcproto.pb.ts +90 -27
- package/srpc/rpcproto.proto +16 -0
- package/srpc/schema-ownership_test.go +115 -0
- package/srpc/server-invocation.go +40 -0
- package/srpc/server-invocation.ts +76 -0
- package/srpc/server-rpc.go +1 -1
- package/srpc/server-rpc.ts +6 -2
- package/srpc/stream-yamux.go +1 -1
- package/srpc/stream.go +20 -0
- 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/
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
2
|
-
export * from
|
|
1
|
+
export * from './mock.pb.js'
|
|
2
|
+
export * from './mock_srpc.pb.js'
|
package/mock/mock.pb.go
CHANGED
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
|
|
6
|
-
import { createMessageType
|
|
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
|
-
|
|
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
|
-
]
|
|
28
|
+
] satisfies readonly PartialFieldInfo[],
|
|
28
29
|
packedByDefault: true,
|
|
29
30
|
})
|
package/mock/mock_srpc.pb.cpp
CHANGED
package/mock/mock_srpc.pb.go
CHANGED
package/mock/mock_srpc.pb.hpp
CHANGED
package/mock/mock_srpc.pb.rs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starpc",
|
|
3
|
-
"version": "0.
|
|
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:
|
|
72
|
-
"format:config": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
98
|
-
"release:minor": "
|
|
99
|
-
"release:version": "
|
|
100
|
-
"release:version:minor": "
|
|
101
|
-
"release:
|
|
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
|
-
"
|
|
108
|
-
"./{srpc,echo,e2e,integration,rpcstream,cmd}
|
|
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
|
-
"@
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
137
|
+
"@aptre/protobuf-es-lite": "1.1.1"
|
|
145
138
|
},
|
|
146
139
|
"overrides": {
|
|
147
|
-
"@aptre/protobuf-es-lite": "1.1.
|
|
140
|
+
"@aptre/protobuf-es-lite": "1.1.1"
|
|
148
141
|
}
|
|
149
142
|
}
|
package/srpc/accept.go
CHANGED
|
@@ -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)
|