v1.4.0: generic Stream[T] — typed stream messages, drop (T, *Stream, error)

- Stream[T]: Send(v T) type-safe; message type recovered from the
  instantiated Send signature (reflect has no type-args API)
- method signatures reduced to (error), (T, error), (*Stream[T], error);
  the (T, *Stream, error) first-message form is gone — send it via Send
- return T and stream message T now run checkType at bind time
  (Stream[any] remains the explicit untyped escape hatch)
- generated clients: TS onMessage: (data: T); Go <-chan T
- fix vacuous panic assertions (fmt.Sprint(nil) -> "<nil>") in
  hardening/route test catch helpers

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-09-19 15:39:41 +08:00
co-authored by Devin
parent 81d36f827e
commit 84c11c6fde
15 changed files with 184 additions and 117 deletions
+56 -3
View File
@@ -185,7 +185,11 @@ func TestStartedGuardAndShutdown(t *testing.T) {
}
catch := func(fn func()) (msg string) {
defer func() { msg = fmt.Sprint(recover()) }()
defer func() {
if r := recover(); r != nil {
msg = fmt.Sprint(r)
}
}()
fn()
return ""
}
@@ -248,8 +252,8 @@ func (boomMarshaler) MarshalJSON() ([]byte, error) { panic("boom-json") }
type PanicStreamSvc struct{}
func (s *PanicStreamSvc) Go() (*Stream, error) {
st := &Stream{}
func (s *PanicStreamSvc) Go() (*Stream[any], error) {
st := &Stream[any]{}
go func() {
_ = st.Send(boomMarshaler{})
st.Close()
@@ -341,3 +345,52 @@ if (!requestBody.includes('"id":42')) throw new Error("small int broken: " + req
t.Fatalf("bigint round trip failed: %v\n%s", err, output)
}
}
// ---- 返回类型 T 与 DTO 同受类型系统约束(含 Stream[T] 的消息类型) ----
type BadRetOnly struct{}
func (*BadRetOnly) Ok() (string, error) { return "ok", nil }
type mapRetSvc struct{}
func (*mapRetSvc) M() (map[string]any, error) { return nil, nil }
type anyRetSvc struct{}
func (*anyRetSvc) M() (any, error) { return nil, nil }
type badStreamSvc struct{}
func (*badStreamSvc) M() (*Stream[map[string]any], error) { return nil, nil }
type anyStreamSvc struct{}
func (*anyStreamSvc) M() (*Stream[any], error) { return &Stream[any]{}, nil }
func TestReturnTypeCheckedAtBind(t *testing.T) {
panics := func(fn func()) (p bool) {
defer func() { p = recover() != nil }()
fn()
return false
}
if panics(func() { _ = New().BindService(&BadRetOnly{}) }) {
t.Fatal("normal service must not panic")
}
for _, tc := range []struct {
name string
svc any
}{
{"map return", &mapRetSvc{}},
{"any return", &anyRetSvc{}},
{"Stream[map]", &badStreamSvc{}},
} {
if !panics(func() { _ = New().BindService(tc.svc) }) {
t.Fatalf("%s must panic at bind", tc.name)
}
}
// Stream[any] 显式任意消息流:放行
if panics(func() { _ = New().BindService(&anyStreamSvc{}) }) {
t.Fatal("Stream[any] should pass")
}
}