sse

package
v0.50.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 14, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package sse 提供 net/http 形态的 SSE(Server-Sent Events)游标回放应答器: 游标窗口回放 + 「订阅先于回放」按事件 ID 去重 + 心跳注释帧,三件事 由一个 Responder 单点承担,宿主只注入取数与广播两处业务差异 (ReplayFunc 取游标窗口、Source 供实时帧),stdlib-only (pkg 零第三方依赖红线)。

定位与分工

本包是**应答器级**语义(响应头四件套、游标回放窗口、订阅/回放重叠 去重、心跳保活、慢消费者断流自愈),不是 Writer 级原语。oekit 矩阵 中 hertz/sse 已确立「SSE 是 oekit 的地盘」,但它绑定 Hertz RequestContext 且只有 Send/SendRaw——任何「DB 持久化事件 + 实时订阅」 的 net/http 事件流 API(挂件事件流、OpenAI 兼容流等)都还要各自手抄 这套应答器样板(下沉前两个消费点各约 60 行)。本包与 hertz/sse 并列: net/http 形态拿应答器,Hertz 形态拿 Writer 原语,互不替代。

订阅先于回放:无缝隙与去重边界

建流次序是本包的核心不变量:**先建立实时订阅,后回放游标窗口**。

  • 若先回放后订阅,「回放结束」与「订阅生效」之间存在缝隙——缝隙内 发生的实时帧永久丢失(DB 里已落、订阅里没有,谁也不会再发一遍)。 先订阅后回放则缝隙归零:订阅建立之后的每一帧实时面都会送达。

  • 代价是重叠:订阅建立后、回放进行中,宿主广播的实时帧与回放窗口 尾部可能是同一批事件。去重边界以**事件 ID** 为准:回放结束后, 凡 `ID <= 已回放最大游标` 的实时帧一律跳过(幂等跳帧)。因此:

    1. 事件 ID 必须由宿主**单调递增**分配(与 SSE `id:` 帧语义一致, 同时是断线重连的游标); 2. ReplayFunc 必须按 ID 升序返回、且只返回 `ID > after` 的事件; 3. 去重只保证「同一 ID 不写两次」,不保证「宿主重复广播不同 ID 的同一逻辑事件」——那是宿主事件模型的职责。

  • 客户端游标取 SSE 标准的 `Last-Event-ID` 请求头(EventSource 重连 自动携带);缺省视为 0(全量回放)。头在场但不是合法无符号整数 时按 400 拒绝(fail-closed:非法游标多半是客户端 bug,静默按 0 全量回放会放大成一次昂贵的误回放)。

心跳注释帧

空闲期每 DefaultHeartbeat(15s)写一帧 SSE 注释帧(冒号开头、 EventSource 客户端忽略的行,如 `: keepalive`,本包称之为「ping」形态 的保活帧),仅用于填满字节流。语义:HTTP 长连接上的反代/负载均衡 (Nginx 默认 60s、常见云 LB 30~60s)会对无字节流动的连接掐流, 注释帧让空闲流在客户端视角仍然安静、在链路视角一直活着。15s 的取值 理由:须低于常见中间层 30~60s 的空闲读超时并留出整数倍余量——按仓规 「跨区间取值须在包文档声明理由」在此声明。可用 WithHeartbeat 改间隔、WithHeartbeatComment 改注释文案(仍须是 冒号开头的单行注释,否则客户端会当事件解析)。

慢消费者自愈

每个订阅者有独立缓冲(WithSubscriberBuffer)。广播时缓冲满的订阅者 被判定为掉队者:其流被立即断开(客户端看到流结束),客户端按 SSE 标准带 `Last-Event-ID` 重连,漏掉的窗口由 ReplayFunc 回放补齐—— **丢弃帧必须以断流收场**(静默丢帧会破坏游标契约:客户端以为收全了), 丢帧-重连-补窗正是游标回放的自愈闭环。

用法

hub := sse.NewHub() // 实时面:进程内广播枢纽
replay := func(after uint64) []sse.Event { // 取数面:宿主 DB
    return db.EventsAfter(after)
}
mux.Handle("/events", sse.NewResponder(hub, replay))

// 事件落库成功后广播实时面:
hub.Emit(sse.Event{ID: next, Type: "run.step", Data: string(json)})

// 自带跨进程总线的宿主实现 Source 即可接入同一应答器:
// type MyBus struct{...}; func (b *MyBus) Subscribe() (<-chan sse.Event, func())

Index

Constants

View Source
const DefaultHeartbeat = 15 * time.Second

DefaultHeartbeat 心跳注释帧缺省间隔(15s,理由见包文档「心跳注释帧」)。

View Source
const DefaultSubscriberBuffer = 64

DefaultSubscriberBuffer 每订阅者缓冲的缺省帧数。

Variables

This section is empty.

Functions

This section is empty.

Types

type Event

type Event struct {
	ID   uint64
	Type string // SSE `event:` 帧;空则不发该帧
	Data string // SSE `data:` 帧;含 \n 时按行拆成多条 data 行
}

Event 单个流事件。ID 是宿主分配的单调递增游标:既是 SSE `id:` 帧 (EventSource 重连时自动回传为 Last-Event-ID 头),也是「订阅先于 回放」的重叠去重键——见包文档「订阅先于回放」。

type Hub

type Hub struct {
	// contains filtered or unexported fields
}

Hub 进程内实时面广播枢纽:宿主在事件落库成功后调用 Hub.Emit, 应答器经 Hub.Subscribe(Source 实现)接收实时帧。

广播不阻塞:慢消费者(缓冲满)被判定掉队、其流立即断开,客户端带 Last-Event-ID 重连、由回放窗口补漏——见包文档「慢消费者自愈」。 Hub.Close 关闭全部在订流并使 Emit 变为无操作(服务优雅停机时让 SSE 长连接 orderly 结束)。

func NewHub

func NewHub(opts ...HubOption) *Hub

NewHub 建立广播枢纽。

func (*Hub) Close

func (h *Hub) Close(_ context.Context) error

Close 关闭全部在订流(客户端看到流结束、按游标重连)并使 Emit 变为 无操作;幂等(sync.Once)。

func (*Hub) Emit

func (h *Hub) Emit(e Event)

Emit 向全部在订流广播一帧。不阻塞、不等待:某订阅者缓冲满即断其流 (掉队自愈,见包文档);Hub 已 Close 后 Emit 是无操作。掉队断流须把 订阅者从在订表摘除(摘除与遍历同表,须写锁——广播整帧持写锁)。

func (*Hub) Subscribe

func (h *Hub) Subscribe() (<-chan Event, func())

Subscribe 实现 Source:建立订阅。通道缓冲 DefaultSubscriberBuffer (或 WithSubscriberBuffer 覆盖),订阅建立后的 Emit 都会送达 (缓冲满则断流)。

func (*Hub) Subscribers

func (h *Hub) Subscribers() int

Subscribers 当前在订流数(观测/测试面)。

type HubOption

type HubOption func(*Hub)

HubOption Hub 的构造选项(仓规禁裸 Option)。

func WithSubscriberBuffer

func WithSubscriberBuffer(n int) HubOption

WithSubscriberBuffer 设置每订阅者缓冲帧数(缺省 DefaultSubscriberBuffer)。 非正数 panic(构造期 fail-fast,仓规总则①)。

type ReplayFunc

type ReplayFunc func(after uint64) []Event

ReplayFunc 游标窗口取数:返回所有 ID 大于 after 的事件,**按 ID 升序** (契约见包文档「订阅先于回放」)。取数是宿主 DB,本包不带存储假设。

type Responder

type Responder struct {
	// contains filtered or unexported fields
}

Responder SSE 游标回放应答器:一条连接承担「响应头四件套 → 订阅先于 回放 → 按事件 ID 幂等跳帧 → 心跳注释帧保活 → 断开即退订」全套语义, 契约细节见包文档。

func NewResponder

func NewResponder(src Source, replay ReplayFunc, opts ...ResponderOption) *Responder

NewResponder 建立应答器。src 为实时面(Hub 或宿主自有 Source 实现), replay 为取数面;二者 nil panic(构造期 fail-fast,仓规总则①)。

func (*Responder) ServeHTTP

func (r *Responder) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP 应答一条 SSE 流。游标取 `Last-Event-ID` 请求头(EventSource 重连自动携带;缺省 0 = 全量回放;非法值 400)。

type ResponderOption

type ResponderOption func(*Responder)

ResponderOption Responder 的构造选项(仓规禁裸 Option)。

func WithHeartbeat

func WithHeartbeat(d time.Duration) ResponderOption

WithHeartbeat 覆盖心跳间隔(缺省 DefaultHeartbeat 15s)。非正数 panic(构造期 fail-fast,仓规总则①)。

func WithHeartbeatComment

func WithHeartbeatComment(comment string) ResponderOption

WithHeartbeatComment 覆盖心跳注释帧文案(缺省 `: keepalive`)。必须是 冒号开头的单行注释——SSE 里冒号开头才是客户端忽略的注释帧,非注释 形态会被 EventSource 当事件解析。非法值 panic(构造期 fail-fast)。

type Source

type Source interface {
	Subscribe() (<-chan Event, func())
}

Source 实时事件源抽象:订阅建立后发生的实时帧经返回的通道送达, 返回的取消函数退订(幂等)。宿主自带的跨进程总线实现本接口即可接入 NewResponder;进程内场景直接用 Hub(实现本接口)。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL