package orbit import ( "context" "errors" "fmt" "net/http" "sync" "time" "github.com/coder/websocket" ) const eventSubprotocol = "orbit.events.v1" const eventMaxPending = 32 // EventConnection 管理单次连接、请求关联和有界收发。可并发调用Ack/Ping。 // 本类型不自动心跳/重连/调用处理器;高层消费请使用EventConsumer。 type EventConnection struct { conn *websocket.Conn ctx context.Context cancel context.CancelFunc welcome EventWelcome deliveries chan EventGrant done chan struct{} mu sync.Mutex err error pending map[string]chan eventFrame } // DialEvents 的ctx仅控制握手;成功后必须Close,取消握手ctx不会关闭已建立的连接。 func (c *Client) DialEvents(ctx context.Context, in EventRegistration) (*EventConnection, error) { if c.token == "" || !validID(in.SubscriptionID) || in.Capacity < 1 || in.Capacity > 32 { return nil, errors.New("authenticated client, valid subscription and capacity 1..32 are required") } bounded, stop := context.WithTimeout(ctx, 8*time.Second) defer stop() headers := http.Header{} headers.Set("Authorization", "Bearer "+c.token) conn, response, err := websocket.Dial(bounded, c.baseURL+"/v1/events/connect", &websocket.DialOptions{HTTPClient: c.http, HTTPHeader: headers, Subprotocols: []string{eventSubprotocol}, CompressionMode: websocket.CompressionDisabled}) if err != nil { if response != nil && response.StatusCode != http.StatusSwitchingProtocols { return nil, &HTTPError{StatusCode: response.StatusCode} } return nil, eventIOError(err) } ok := false defer func() { if !ok { _ = conn.CloseNow() } }() if conn.Subprotocol() != eventSubprotocol { return nil, ErrEventProtocol } conn.SetReadLimit(512 << 10) id, err := NewEventMessageID() if err != nil { return nil, err } body, _ := marshalWire(in) raw, _ := marshalWire(eventFrame{Version: "1", Type: "events.hello", MessageID: id, Body: body}) if err = conn.Write(bounded, websocket.MessageText, raw); err != nil { return nil, eventIOError(err) } kind, raw, err := conn.Read(bounded) if err != nil { return nil, eventIOError(err) } f, err := parseEventFrame(kind, raw) if err != nil || f.ReplyTo != id { return nil, ErrEventProtocol } if f.Type == "error" { return nil, f.Error } var welcome EventWelcome if f.Type != "events.welcome" || decodeEventValue(f.Body, &welcome) != nil || welcome.SessionID != f.SessionID || welcome.SubscriptionID != in.SubscriptionID || welcome.Capacity != in.Capacity || welcome.ServerTime.IsZero() || welcome.LeaseDurationMS != 30000 || welcome.HeartbeatIntervalMS != 15000 { return nil, ErrEventProtocol } lifetime, cancel := context.WithCancel(context.Background()) e := &EventConnection{conn: conn, ctx: lifetime, cancel: cancel, welcome: welcome, deliveries: make(chan EventGrant, in.Capacity), done: make(chan struct{}), pending: make(map[string]chan eventFrame)} ok = true go e.readLoop() return e, nil } func (e *EventConnection) Welcome() EventWelcome { return e.welcome } func (e *EventConnection) Done() <-chan struct{} { return e.done } func (e *EventConnection) Err() error { e.mu.Lock(); defer e.mu.Unlock(); return e.err } func (e *EventConnection) Close() error { e.fail(ErrEventClosed); <-e.done; return nil } func (e *EventConnection) fail(err error) { e.mu.Lock() if e.err != nil { e.mu.Unlock() return } e.err = err e.cancel() e.mu.Unlock() _ = e.conn.CloseNow() } func (e *EventConnection) readLoop() { defer close(e.done) for { ctx, stop := context.WithTimeout(e.ctx, 60*time.Second) kind, raw, err := e.conn.Read(ctx) stop() if err != nil { e.fail(eventIOError(err)) return } f, err := parseEventFrame(kind, raw) if err != nil || f.SessionID != e.welcome.SessionID { e.fail(ErrEventProtocol) return } if f.Type == "event.deliver" { var grant EventGrant if decodeEventValue(f.Body, &grant) != nil || !validEventGrant(grant, e.welcome.SubscriptionID) { e.fail(ErrEventProtocol) return } select { case e.deliveries <- grant: default: e.fail(ErrEventBackpressure) return } continue } if f.Type != "ack" && f.Type != "error" { e.fail(ErrEventProtocol) return } if f.Type == "error" && f.ReplyTo == "" { e.fail(f.Error) return } e.mu.Lock() waiter := e.pending[f.ReplyTo] e.mu.Unlock() if waiter == nil { e.fail(ErrEventProtocol) return } select { case waiter <- f: default: e.fail(ErrEventProtocol) return } if f.Error != nil && (f.Error.Code == "UNAUTHENTICATED" || f.Error.Code == "PERMISSION_DENIED" || f.Error.Code == "NOT_FOUND" || f.Error.Code == "UNAVAILABLE") { e.fail(f.Error) return } } } // Next 等待下一条交付。ctx到期只中断本次等待;未确认交付按服务端预算补发。 func (e *EventConnection) Next(ctx context.Context) (EventGrant, error) { if err := ctx.Err(); err != nil { return EventGrant{}, err } select { case <-ctx.Done(): return EventGrant{}, ctx.Err() case <-e.ctx.Done(): return EventGrant{}, e.Err() case g := <-e.deliveries: if err := e.Err(); err != nil { return EventGrant{}, err } return g, nil } } func (e *EventConnection) call(ctx context.Context, kind, id string, in, out any) error { if !validID(id) { return errors.New("invalid event message id") } if err := ctx.Err(); err != nil { return err } bounded, stop := context.WithTimeout(ctx, 10*time.Second) defer stop() waiter := make(chan eventFrame, 1) e.mu.Lock() if e.err != nil { err := e.err e.mu.Unlock() return err } if len(e.pending) >= eventMaxPending { e.mu.Unlock() return ErrEventBusy } if e.pending[id] != nil { e.mu.Unlock() return errors.New("event message id is already in flight") } e.pending[id] = waiter e.mu.Unlock() defer func() { e.mu.Lock(); delete(e.pending, id); e.mu.Unlock() }() body, err := marshalWire(in) if err != nil { return errors.New("invalid event request") } raw, err := marshalWire(eventFrame{Version: "1", Type: kind, MessageID: id, SessionID: e.welcome.SessionID, Body: body}) if err != nil || len(raw) > 512<<10 { return errors.New("event request exceeds frame limit") } if err = e.conn.Write(bounded, websocket.MessageText, raw); err != nil { err = eventIOError(err) e.fail(err) return err } var f eventFrame select { case f = <-waiter: case <-e.ctx.Done(): select { case f = <-waiter: default: return e.Err() } case <-bounded.Done(): e.fail(bounded.Err()) return bounded.Err() } if f.Error != nil { return f.Error } if decodeEventValue(f.Body, out) != nil { e.fail(ErrEventProtocol) return ErrEventProtocol } return nil } // Ack 仅在业务事务已提交或持久化去重命中后调用。重连可用原Proof重报,MessageID可新建。 // 超时/断线不证明ACK未提交;DELIVERY_LEASE_LOST表示证明已失效,不应撤销已经提交的业务。 func (e *EventConnection) Ack(ctx context.Context, id string, p EventProof) (EventAckResult, error) { var out EventAckResult if !validEventProof(p) || p.SubscriptionID != e.welcome.SubscriptionID { return out, errors.New("invalid event proof or subscription") } in := struct { DeliveryID string `json:"delivery_id"` Epoch uint64 `json:"epoch,string"` LeaseSecret string `json:"lease_secret"` }{p.DeliveryID, p.Epoch, p.LeaseSecret} err := e.call(ctx, "event.ack", id, in, &out) if err == nil && (out.DeliveryID != p.DeliveryID || out.EventID != p.EventID || out.Epoch != p.Epoch || out.AcknowledgedAt.IsZero()) { err = ErrEventProtocol e.fail(err) } return out, err } // Ping 重新核验凭据及订阅,并维持连接;不会续期交付租约。 func (e *EventConnection) Ping(ctx context.Context, id string) (EventPong, error) { var out EventPong err := e.call(ctx, "events.ping", id, struct{}{}, &out) if err == nil && out.ServerTime.IsZero() { err = ErrEventProtocol e.fail(err) } return out, err } func eventIOError(err error) error { if errors.Is(err, context.Canceled) { return context.Canceled } if errors.Is(err, context.DeadlineExceeded) { return context.DeadlineExceeded } switch websocket.CloseStatus(err) { case websocket.StatusProtocolError, websocket.StatusUnsupportedData, websocket.StatusInvalidFramePayloadData, websocket.StatusPolicyViolation, websocket.StatusMessageTooBig: return ErrEventProtocol } return ErrEventDisconnected } func (e *EventConnection) String() string { return fmt.Sprintf("EventConnection{SubscriptionID:%s SessionID:%s secrets:redacted}", e.welcome.SubscriptionID, e.welcome.SessionID) } func (e *EventConnection) GoString() string { return e.String() }