v1.3.6: stream reliability — server idle heartbeat + TS native reconnect
- handle.go: stream writer emits a blank-line heartbeat every 25s while
the business sends nothing; invisible to NDJSON parsers but resets
NAT/CDN idle timers (mobile CGNAT ~60s, Bunny ~120s)
- template_ts.go: split stream() into retry loop + streamOnce()
- dto accepts a factory re-evaluated per attempt (cursor resume)
- retry only on transport failures (status 4/5); opt-in via
StreamOptions.retry, exponential backoff with jitter, abortable
- idle watchdog (idleTimeoutMs) built on AbortSignal.timeout; no
setTimeout in generated code (simplicity gate)
- docs: README.zh.md version table for v1.3.6
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
@@ -127,13 +128,29 @@ func (f *Fun) handle(fastCtx *fasthttp.RequestCtx) {
|
||||
return
|
||||
}
|
||||
}
|
||||
for message := range streamCh {
|
||||
if !writeLine(message) {
|
||||
finish()
|
||||
return
|
||||
// 空闲心跳:业务长时间不 Send 时周期写一个空行。NDJSON 解析器
|
||||
// 跳过空行(业务无感知),字节能让 NAT/CDN 的空闲计时器归零。
|
||||
// 25s 需小于常见中间盒阈值(移动 CGNAT 约 60s、CDN 约 120s)。
|
||||
heartbeat := time.NewTicker(25 * time.Second)
|
||||
defer heartbeat.Stop()
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-streamCh:
|
||||
if !ok {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
if !writeLine(message) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
case <-heartbeat.C:
|
||||
if _, err := w.WriteString("\n"); err != nil || w.Flush() != nil {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
finish()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user