starpc 0.49.20 → 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/dist/integration/cross-language/ts-server.js +22 -6
- package/dist/mock/index.d.ts +2 -2
- package/dist/mock/index.js +2 -2
- package/dist/rpcstream/receipt.test.d.ts +1 -0
- package/dist/rpcstream/receipt.test.js +41 -0
- package/dist/srpc/call-receipt.js +2 -1
- package/dist/srpc/call-receipt.test.js +15 -14
- package/dist/srpc/common-rpc.d.ts +1 -3
- package/dist/srpc/common-rpc.js +10 -13
- package/dist/srpc/index.d.ts +1 -1
- package/dist/srpc/index.js +1 -0
- package/dist/srpc/rpcproto.pb.d.ts +44 -0
- package/dist/srpc/rpcproto.pb.js +53 -0
- package/dist/srpc/server-invocation.d.ts +1 -1
- package/go.mod +13 -12
- package/go.sum +24 -24
- package/integration/cross-language/go-server/fixture-owner_test.go +127 -0
- package/integration/cross-language/go-server/main.go +64 -42
- package/integration/cross-language/run.bash +2 -2
- package/integration/cross-language/ts-server.ts +27 -7
- package/mock/index.ts +2 -2
- package/package.json +19 -26
- package/srpc/accept.go +1 -1
- package/srpc/call-receipt-e2e_test.go +2 -2
- package/srpc/call-receipt.go +1 -1
- package/srpc/call-receipt.test.ts +19 -16
- package/srpc/call-receipt.ts +2 -1
- package/srpc/call-receipt_test.go +19 -19
- package/srpc/common-rpc.go +6 -6
- package/srpc/common-rpc.ts +17 -16
- package/srpc/index.ts +1 -1
- package/srpc/muxed-conn.go +1 -1
- package/srpc/muxed-yamux.go +1 -1
- package/srpc/rpcproto.pb.cc +15 -4
- package/srpc/rpcproto.pb.go +96 -0
- package/srpc/rpcproto.pb.h +59 -0
- package/srpc/rpcproto.pb.rs +45 -0
- package/srpc/rpcproto.pb.ts +62 -0
- package/srpc/rpcproto.proto +16 -0
- package/srpc/schema-ownership_test.go +115 -0
- package/srpc/server-invocation.go +1 -19
- package/srpc/server-invocation.ts +1 -7
- package/srpc/stream-yamux.go +1 -1
- package/srpc/websocket.go +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"go/ast"
|
|
5
|
+
"go/parser"
|
|
6
|
+
"go/token"
|
|
7
|
+
"os"
|
|
8
|
+
"strconv"
|
|
9
|
+
"testing"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// forbiddenFixtureCalls names the pipeline constructors that rebuild a second
|
|
13
|
+
// srpc server inside a fixture. The receipt post-mortem removed a hand-built
|
|
14
|
+
// ReadPump pipeline; the fixture must drive one srpc.Server through one
|
|
15
|
+
// Server.HandleStream and observe framed packets by decorating net.Conn.
|
|
16
|
+
var forbiddenFixtureCalls = []string{"NewServerRPC", "NewPacketReadWriter", "ReadPump"}
|
|
17
|
+
|
|
18
|
+
// fixtureOwnerViolations returns the ways a cross-language Go fixture source
|
|
19
|
+
// diverges from the single production server owner.
|
|
20
|
+
func fixtureOwnerViolations(src string) []string {
|
|
21
|
+
fset := token.NewFileSet()
|
|
22
|
+
file, err := parser.ParseFile(fset, "fixture.go", src, 0)
|
|
23
|
+
if err != nil {
|
|
24
|
+
return []string{"parse error: " + err.Error()}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
seen := map[string]bool{}
|
|
28
|
+
handleStreamCalls := 0
|
|
29
|
+
newServerCalls := 0
|
|
30
|
+
ast.Inspect(file, func(n ast.Node) bool {
|
|
31
|
+
call, ok := n.(*ast.CallExpr)
|
|
32
|
+
if !ok {
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
sel, ok := call.Fun.(*ast.SelectorExpr)
|
|
36
|
+
if !ok {
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
switch sel.Sel.Name {
|
|
40
|
+
case "HandleStream":
|
|
41
|
+
handleStreamCalls++
|
|
42
|
+
case "NewServer":
|
|
43
|
+
newServerCalls++
|
|
44
|
+
case "NewServerRPC", "NewPacketReadWriter", "ReadPump":
|
|
45
|
+
seen[sel.Sel.Name] = true
|
|
46
|
+
}
|
|
47
|
+
return true
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
var violations []string
|
|
51
|
+
for _, name := range forbiddenFixtureCalls {
|
|
52
|
+
if seen[name] {
|
|
53
|
+
violations = append(violations, "reconstructs server pipeline via "+name)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if newServerCalls != 1 {
|
|
57
|
+
violations = append(violations, "expected exactly one srpc.NewServer, found "+strconv.Itoa(newServerCalls))
|
|
58
|
+
}
|
|
59
|
+
if handleStreamCalls != 1 {
|
|
60
|
+
violations = append(violations, "expected exactly one Server.HandleStream entrypoint, found "+strconv.Itoa(handleStreamCalls))
|
|
61
|
+
}
|
|
62
|
+
if !decoratesNetConn(file) {
|
|
63
|
+
violations = append(violations, "no fixture type decorates net.Conn")
|
|
64
|
+
}
|
|
65
|
+
return violations
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// decoratesNetConn reports whether the source embeds net.Conn in a struct, the
|
|
69
|
+
// only sanctioned way for fixture instrumentation to observe framed packets.
|
|
70
|
+
func decoratesNetConn(file *ast.File) bool {
|
|
71
|
+
found := false
|
|
72
|
+
ast.Inspect(file, func(n ast.Node) bool {
|
|
73
|
+
st, ok := n.(*ast.StructType)
|
|
74
|
+
if !ok {
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
for _, field := range st.Fields.List {
|
|
78
|
+
if len(field.Names) != 0 {
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
sel, ok := field.Type.(*ast.SelectorExpr)
|
|
82
|
+
if !ok {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
pkg, ok := sel.X.(*ast.Ident)
|
|
86
|
+
if ok && pkg.Name == "net" && sel.Sel.Name == "Conn" {
|
|
87
|
+
found = true
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return true
|
|
91
|
+
})
|
|
92
|
+
return found
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// TestFixtureUsesSingleServerOwner proves the shipped fixture drives one
|
|
96
|
+
// srpc.Server through one Server.HandleStream and decorates net.Conn.
|
|
97
|
+
func TestFixtureUsesSingleServerOwner(t *testing.T) {
|
|
98
|
+
src, err := os.ReadFile("main.go")
|
|
99
|
+
if err != nil {
|
|
100
|
+
t.Fatalf("read main.go: %v", err)
|
|
101
|
+
}
|
|
102
|
+
if violations := fixtureOwnerViolations(string(src)); len(violations) != 0 {
|
|
103
|
+
t.Fatalf("fixture diverged from single server owner: %v", violations)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// TestFixtureGuardDetectsSecondPipeline proves the guard fires when a fixture
|
|
108
|
+
// rebuilds a second RPC server pipeline.
|
|
109
|
+
func TestFixtureGuardDetectsSecondPipeline(t *testing.T) {
|
|
110
|
+
bad := `package main
|
|
111
|
+
|
|
112
|
+
import (
|
|
113
|
+
"net"
|
|
114
|
+
|
|
115
|
+
"github.com/aperturerobotics/starpc/srpc"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
func run(conn net.Conn) {
|
|
119
|
+
prw := srpc.NewPacketReadWriter(conn)
|
|
120
|
+
rpc := srpc.NewServerRPC(nil, nil)
|
|
121
|
+
_ = rpc.ReadPump(prw)
|
|
122
|
+
}
|
|
123
|
+
`
|
|
124
|
+
if violations := fixtureOwnerViolations(bad); len(violations) == 0 {
|
|
125
|
+
t.Fatal("guard failed to flag a second server pipeline")
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -2,11 +2,13 @@ package main
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
+
"encoding/binary"
|
|
5
6
|
"fmt"
|
|
6
7
|
"net"
|
|
7
8
|
"os"
|
|
8
9
|
"os/signal"
|
|
9
10
|
"sync"
|
|
11
|
+
"sync/atomic"
|
|
10
12
|
|
|
11
13
|
"github.com/aperturerobotics/starpc/echo"
|
|
12
14
|
"github.com/aperturerobotics/starpc/srpc"
|
|
@@ -39,6 +41,7 @@ func main() {
|
|
|
39
41
|
}
|
|
40
42
|
var receiptDone <-chan struct{}
|
|
41
43
|
var finishReceipt func()
|
|
44
|
+
var receiptCommitted atomic.Bool
|
|
42
45
|
if receiptMode {
|
|
43
46
|
done := make(chan struct{})
|
|
44
47
|
var doneOnce sync.Once
|
|
@@ -73,16 +76,16 @@ func main() {
|
|
|
73
76
|
if markerErr != nil {
|
|
74
77
|
return true, markerErr
|
|
75
78
|
}
|
|
76
|
-
if kind
|
|
79
|
+
if kind == srpc.TerminalKind_TERMINAL_KIND_COMMITTED {
|
|
80
|
+
receiptCommitted.Store(true)
|
|
81
|
+
}
|
|
82
|
+
if kind != srpc.TerminalKind_TERMINAL_KIND_COMMITTED {
|
|
77
83
|
finishReceipt()
|
|
78
84
|
}
|
|
79
85
|
return true, nil
|
|
80
86
|
})
|
|
81
87
|
}
|
|
82
|
-
|
|
83
|
-
if !receiptMode {
|
|
84
|
-
server = srpc.NewServer(invoker)
|
|
85
|
-
}
|
|
88
|
+
server := srpc.NewServer(invoker)
|
|
86
89
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
87
90
|
if err != nil {
|
|
88
91
|
fmt.Fprintf(os.Stderr, "listen error: %v\n", err)
|
|
@@ -101,7 +104,7 @@ func main() {
|
|
|
101
104
|
|
|
102
105
|
go func() {
|
|
103
106
|
<-ctx.Done()
|
|
104
|
-
ln.Close()
|
|
107
|
+
_ = ln.Close()
|
|
105
108
|
}()
|
|
106
109
|
|
|
107
110
|
for {
|
|
@@ -109,63 +112,82 @@ func main() {
|
|
|
109
112
|
if err != nil {
|
|
110
113
|
return
|
|
111
114
|
}
|
|
115
|
+
var stream net.Conn
|
|
116
|
+
stream = conn
|
|
112
117
|
if receiptMode {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
stream = &receiptConn{
|
|
119
|
+
Conn: conn,
|
|
120
|
+
finishReceipt: finishReceipt,
|
|
121
|
+
committed: &receiptCommitted,
|
|
122
|
+
}
|
|
116
123
|
}
|
|
124
|
+
go server.HandleStream(ctx, stream)
|
|
117
125
|
}
|
|
118
126
|
}
|
|
119
127
|
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
128
|
+
type receiptConn struct {
|
|
129
|
+
net.Conn
|
|
137
130
|
finishReceipt func()
|
|
131
|
+
committed *atomic.Bool
|
|
132
|
+
inspect []byte
|
|
133
|
+
ackPending bool
|
|
138
134
|
}
|
|
139
135
|
|
|
140
|
-
func (
|
|
141
|
-
if
|
|
142
|
-
|
|
136
|
+
func (c *receiptConn) Write(p []byte) (int, error) {
|
|
137
|
+
if !c.ackPending {
|
|
138
|
+
if err := c.observeReceiptPackets(p); err != nil {
|
|
139
|
+
return 0, err
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
n, err := c.Conn.Write(p)
|
|
143
|
+
if err != nil {
|
|
144
|
+
return n, err
|
|
143
145
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
if c.ackPending && n == len(p) {
|
|
147
|
+
c.finishReceipt()
|
|
148
|
+
c.ackPending = false
|
|
149
|
+
}
|
|
150
|
+
return n, nil
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func (c *receiptConn) observeReceiptPackets(p []byte) error {
|
|
154
|
+
c.inspect = append(c.inspect, p...)
|
|
155
|
+
for len(c.inspect) >= 4 {
|
|
156
|
+
size := int(binary.LittleEndian.Uint32(c.inspect[:4]))
|
|
157
|
+
if len(c.inspect) < 4+size {
|
|
158
|
+
break
|
|
159
|
+
}
|
|
160
|
+
pkt := &srpc.Packet{}
|
|
161
|
+
if err := pkt.UnmarshalVT(c.inspect[4 : 4+size]); err != nil {
|
|
162
|
+
return err
|
|
163
|
+
}
|
|
164
|
+
c.inspect = c.inspect[4+size:]
|
|
165
|
+
data := pkt.GetCallData()
|
|
166
|
+
if !c.committed.Load() {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
if data == nil || !data.GetComplete() || data.GetError() != "" {
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
if err := emitReceiptEvent("SERVER_RECEIPT_ACK_WRITE committed"); err != nil {
|
|
147
173
|
return err
|
|
148
174
|
}
|
|
149
|
-
|
|
175
|
+
c.ackPending = true
|
|
150
176
|
}
|
|
151
177
|
return nil
|
|
152
178
|
}
|
|
153
179
|
|
|
154
|
-
func (w *receiptPacketWriter) Close() error {
|
|
155
|
-
return w.inner.Close()
|
|
156
|
-
}
|
|
157
|
-
|
|
158
180
|
func terminalName(kind srpc.TerminalKind) string {
|
|
159
181
|
switch kind {
|
|
160
|
-
case srpc.
|
|
182
|
+
case srpc.TerminalKind_TERMINAL_KIND_COMMITTED:
|
|
161
183
|
return "committed"
|
|
162
|
-
case srpc.
|
|
184
|
+
case srpc.TerminalKind_TERMINAL_KIND_CANCELED:
|
|
163
185
|
return "canceled"
|
|
164
|
-
case srpc.
|
|
186
|
+
case srpc.TerminalKind_TERMINAL_KIND_TRANSPORT_LOST:
|
|
165
187
|
return "transportLost"
|
|
166
|
-
case srpc.
|
|
188
|
+
case srpc.TerminalKind_TERMINAL_KIND_CLOSED:
|
|
167
189
|
return "closed"
|
|
168
|
-
case srpc.
|
|
190
|
+
case srpc.TerminalKind_TERMINAL_KIND_ABANDONED:
|
|
169
191
|
return "abandoned"
|
|
170
192
|
default:
|
|
171
193
|
return "unknown"
|
|
@@ -118,14 +118,14 @@ check_receipt_events() {
|
|
|
118
118
|
local event_log="$2"
|
|
119
119
|
awk -v expected="$expected" '
|
|
120
120
|
$0 == "SERVER_RECEIPT_TERMINAL " expected { terminal = NR; next }
|
|
121
|
-
$0 == "
|
|
121
|
+
$0 == "SERVER_RECEIPT_ACK_WRITE " expected { ack_write = NR; next }
|
|
122
122
|
$0 == "CLIENT_RECEIPT_RESOLVED " expected { client = NR; next }
|
|
123
123
|
END {
|
|
124
124
|
if (!terminal || !client) {
|
|
125
125
|
exit 1
|
|
126
126
|
}
|
|
127
127
|
if (expected == "committed" &&
|
|
128
|
-
(!
|
|
128
|
+
(!ack_write || terminal >= ack_write || ack_write >= client)) {
|
|
129
129
|
exit 1
|
|
130
130
|
}
|
|
131
131
|
}
|
|
@@ -2,6 +2,8 @@ import net from 'net'
|
|
|
2
2
|
import { closeSync, openSync, writeSync } from 'node:fs'
|
|
3
3
|
import { pipe } from 'it-pipe'
|
|
4
4
|
import { pushable } from 'it-pushable'
|
|
5
|
+
import type { Source } from 'it-stream-types'
|
|
6
|
+
|
|
5
7
|
import { createMux, createHandler, Server } from '../../srpc/index.js'
|
|
6
8
|
import {
|
|
7
9
|
parseLengthPrefixTransform,
|
|
@@ -10,9 +12,9 @@ import {
|
|
|
10
12
|
import { combineUint8ArrayListTransform } from '../../srpc/array-list.js'
|
|
11
13
|
import { EchoerServer, EchoMsg } from '../../echo/index.js'
|
|
12
14
|
import { EchoerDefinition } from '../../echo/echo_srpc.pb.js'
|
|
13
|
-
import { Packet } from '../../srpc/rpcproto.pb.js'
|
|
15
|
+
import { Packet, TerminalKind } from '../../srpc/rpcproto.pb.js'
|
|
14
16
|
import type { PacketStream } from '../../srpc/stream.js'
|
|
15
|
-
|
|
17
|
+
|
|
16
18
|
function emitReceiptEvent(line: string): void {
|
|
17
19
|
console.log(line)
|
|
18
20
|
const fifo = process.env.RECEIPT_EVENT_FIFO
|
|
@@ -57,15 +59,15 @@ function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
|
|
|
57
59
|
packet.body?.case === 'callData' &&
|
|
58
60
|
packet.body.value.complete &&
|
|
59
61
|
!packet.body.value.error
|
|
62
|
+
if (receiptCompletion) {
|
|
63
|
+
emitReceiptEvent('SERVER_RECEIPT_ACK_WRITE committed')
|
|
64
|
+
}
|
|
60
65
|
const writeDone = new Promise<void>((resolve, reject) => {
|
|
61
66
|
socket.write(data, (err) => {
|
|
62
67
|
if (err) reject(err)
|
|
63
68
|
else resolve()
|
|
64
69
|
})
|
|
65
70
|
})
|
|
66
|
-
if (receiptCompletion) {
|
|
67
|
-
emitReceiptEvent('SERVER_RECEIPT_ACK committed')
|
|
68
|
-
}
|
|
69
71
|
await writeDone
|
|
70
72
|
if (receiptCompletion) {
|
|
71
73
|
finishReceiptServer()
|
|
@@ -121,8 +123,8 @@ if (receiptMode) {
|
|
|
121
123
|
const terminal = await invocation.waitTerminal(
|
|
122
124
|
new AbortController().signal,
|
|
123
125
|
)
|
|
124
|
-
emitReceiptEvent(`SERVER_RECEIPT_TERMINAL ${terminal}`)
|
|
125
|
-
if (terminal !==
|
|
126
|
+
emitReceiptEvent(`SERVER_RECEIPT_TERMINAL ${terminalName(terminal)}`)
|
|
127
|
+
if (terminal !== TerminalKind.COMMITTED) {
|
|
126
128
|
finishReceiptServer()
|
|
127
129
|
}
|
|
128
130
|
})(),
|
|
@@ -130,6 +132,24 @@ if (receiptMode) {
|
|
|
130
132
|
}
|
|
131
133
|
})
|
|
132
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
|
+
|
|
133
153
|
const server = new Server(mux.lookupMethod)
|
|
134
154
|
if (!receiptMode) {
|
|
135
155
|
const echoer = new EchoerServer(server)
|
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/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
|
@@ -5,7 +5,7 @@ import (
|
|
|
5
5
|
"net"
|
|
6
6
|
"testing"
|
|
7
7
|
|
|
8
|
-
yamux "github.com/libp2p/go-yamux/
|
|
8
|
+
yamux "github.com/libp2p/go-yamux/v5"
|
|
9
9
|
|
|
10
10
|
"github.com/aperturerobotics/starpc/echo"
|
|
11
11
|
"github.com/aperturerobotics/starpc/srpc"
|
|
@@ -105,7 +105,7 @@ func TestCallReceiptGoGo(t *testing.T) {
|
|
|
105
105
|
t.Fatal("generated handler context lost server invocation")
|
|
106
106
|
}
|
|
107
107
|
kind := <-terminal
|
|
108
|
-
if kind != srpc.
|
|
108
|
+
if kind != srpc.TerminalKind_TERMINAL_KIND_COMMITTED {
|
|
109
109
|
t.Fatalf("terminal = %v, want committed", kind)
|
|
110
110
|
}
|
|
111
111
|
}
|
package/srpc/call-receipt.go
CHANGED
|
@@ -62,7 +62,7 @@ func (r *CallReceipt) Commit() error {
|
|
|
62
62
|
receipt, ok := r.strm.(receiptTerminalStream)
|
|
63
63
|
if ok {
|
|
64
64
|
terminal, terminalOK := receipt.receiptTerminalKind()
|
|
65
|
-
if terminalOK && terminal ==
|
|
65
|
+
if terminalOK && terminal == TerminalKind_TERMINAL_KIND_COMMITTED {
|
|
66
66
|
return nil
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -9,9 +9,9 @@ import { Client } from './client.js'
|
|
|
9
9
|
import { ClientRPC } from './client-rpc.js'
|
|
10
10
|
import { Server } from './server.js'
|
|
11
11
|
import { CallReceipt } from './call-receipt.js'
|
|
12
|
-
import { Packet } from './rpcproto.pb.js'
|
|
12
|
+
import { Packet, TerminalKind } from './rpcproto.pb.js'
|
|
13
13
|
import { ServerRPC } from './server-rpc.js'
|
|
14
|
-
import { ServerInvocation
|
|
14
|
+
import { ServerInvocation } from './server-invocation.js'
|
|
15
15
|
|
|
16
16
|
const response = new Uint8Array([1, 2, 3])
|
|
17
17
|
|
|
@@ -68,7 +68,7 @@ describe('held unary receipt', () => {
|
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
it('commits through a real TypeScript server', async () => {
|
|
71
|
-
const terminal = Promise.withResolvers<
|
|
71
|
+
const terminal = Promise.withResolvers<TerminalKind>()
|
|
72
72
|
const server = new Server(async () => {
|
|
73
73
|
return async (_source, dataSink, invocation) => {
|
|
74
74
|
await dataSink(
|
|
@@ -113,7 +113,7 @@ describe('held unary receipt', () => {
|
|
|
113
113
|
response,
|
|
114
114
|
)
|
|
115
115
|
await expect(held.receipt.commit()).resolves.toBeUndefined()
|
|
116
|
-
await expect(terminal.promise).resolves.toBe(
|
|
116
|
+
await expect(terminal.promise).resolves.toBe(TerminalKind.COMMITTED)
|
|
117
117
|
})
|
|
118
118
|
|
|
119
119
|
it('rejects commit after a bare remote close', async () => {
|
|
@@ -292,12 +292,12 @@ describe('held unary receipt', () => {
|
|
|
292
292
|
|
|
293
293
|
describe('server invocation terminal', () => {
|
|
294
294
|
it.each([
|
|
295
|
-
['explicit completion',
|
|
296
|
-
['cancel',
|
|
297
|
-
['transport loss',
|
|
298
|
-
['remote error packet',
|
|
299
|
-
['bare close',
|
|
300
|
-
['remote error packet with completion',
|
|
295
|
+
['explicit completion', TerminalKind.COMMITTED],
|
|
296
|
+
['cancel', TerminalKind.CANCELED],
|
|
297
|
+
['transport loss', TerminalKind.TRANSPORT_LOST],
|
|
298
|
+
['remote error packet', TerminalKind.TRANSPORT_LOST],
|
|
299
|
+
['bare close', TerminalKind.CLOSED],
|
|
300
|
+
['remote error packet with completion', TerminalKind.TRANSPORT_LOST],
|
|
301
301
|
] as const)('%s is classified distinctly', async (_name, expected) => {
|
|
302
302
|
const captured = Promise.withResolvers<ServerInvocation>()
|
|
303
303
|
const owner = new AbortController()
|
|
@@ -318,11 +318,11 @@ describe('server invocation terminal', () => {
|
|
|
318
318
|
dataIsZero: true,
|
|
319
319
|
})
|
|
320
320
|
const invocation = await captured.promise
|
|
321
|
-
if (expected ===
|
|
321
|
+
if (expected === TerminalKind.COMMITTED) {
|
|
322
322
|
await rpc.handleCallData({ complete: true })
|
|
323
|
-
} else if (expected ===
|
|
323
|
+
} else if (expected === TerminalKind.CANCELED) {
|
|
324
324
|
await rpc.handleCallCancel()
|
|
325
|
-
} else if (expected ===
|
|
325
|
+
} else if (expected === TerminalKind.TRANSPORT_LOST) {
|
|
326
326
|
if (
|
|
327
327
|
_name === 'remote error packet' ||
|
|
328
328
|
_name === 'remote error packet with completion'
|
|
@@ -346,7 +346,10 @@ describe('server invocation terminal', () => {
|
|
|
346
346
|
}
|
|
347
347
|
expect(rpcState.remoteCompleted).toBe(false)
|
|
348
348
|
}
|
|
349
|
-
if (
|
|
349
|
+
if (
|
|
350
|
+
expected === TerminalKind.CLOSED ||
|
|
351
|
+
expected === TerminalKind.TRANSPORT_LOST
|
|
352
|
+
) {
|
|
350
353
|
expect(invocation.signal.aborted).toBe(true)
|
|
351
354
|
}
|
|
352
355
|
})
|
|
@@ -379,7 +382,7 @@ describe('server invocation terminal', () => {
|
|
|
379
382
|
await rpc.handleCallCancel()
|
|
380
383
|
}
|
|
381
384
|
await expect(invocation.waitTerminal(owner.signal)).resolves.toBe(
|
|
382
|
-
|
|
385
|
+
TerminalKind.COMMITTED,
|
|
383
386
|
)
|
|
384
387
|
owner.abort()
|
|
385
388
|
},
|
|
@@ -406,7 +409,7 @@ describe('server invocation terminal', () => {
|
|
|
406
409
|
const invocation = await captured.promise
|
|
407
410
|
owner.abort()
|
|
408
411
|
await expect(invocation.waitTerminal(owner.signal)).resolves.toBe(
|
|
409
|
-
|
|
412
|
+
TerminalKind.ABANDONED,
|
|
410
413
|
)
|
|
411
414
|
})
|
|
412
415
|
})
|
package/srpc/call-receipt.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference lib="es2024.promise" />
|
|
2
2
|
import { ERR_RPC_ABORT } from './errors.js'
|
|
3
3
|
import { ClientRPC } from './client-rpc.js'
|
|
4
|
+
import { TerminalKind } from './rpcproto.pb.js'
|
|
4
5
|
|
|
5
6
|
// ReceiptRpc exposes held unary calls without widening ProtoRpc.
|
|
6
7
|
export interface ReceiptRpc {
|
|
@@ -49,7 +50,7 @@ export class CallReceipt {
|
|
|
49
50
|
}
|
|
50
51
|
const terminal = this.#call.getTerminalKind()
|
|
51
52
|
if (
|
|
52
|
-
terminal !==
|
|
53
|
+
terminal !== TerminalKind.COMMITTED ||
|
|
53
54
|
!this.#requestCommitted ||
|
|
54
55
|
this.#terminal !== 'committed'
|
|
55
56
|
) {
|