294 lines
5.4 KiB
Go
294 lines
5.4 KiB
Go
package nixmsg
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type pendingReq struct {
|
|
rid string
|
|
ch chan respFrame
|
|
isSend bool
|
|
}
|
|
|
|
type sendItem struct {
|
|
frame map[string]any
|
|
payload []byte
|
|
id string
|
|
sendAtMs *int64
|
|
result chan sendOutcome
|
|
inflight bool
|
|
rateN int
|
|
epoch uint64
|
|
abandoned bool
|
|
}
|
|
|
|
type sendOutcome struct {
|
|
res SendResult
|
|
err error
|
|
}
|
|
|
|
type dedupState int
|
|
|
|
const (
|
|
dedupDelivered dedupState = iota + 1
|
|
dedupAcked
|
|
dedupRevoked
|
|
)
|
|
|
|
type dedupEntry struct {
|
|
state dedupState
|
|
from string
|
|
id string
|
|
}
|
|
|
|
type queuedFrame struct {
|
|
payload []byte
|
|
kind string
|
|
}
|
|
|
|
const eventQueueCap = 10000
|
|
|
|
// Client NixMsg 客户端。
|
|
type Client struct {
|
|
opts Options
|
|
endpointID string
|
|
url string
|
|
credKind string
|
|
transport transport
|
|
backoff *reconnectBackoff
|
|
|
|
mu sync.Mutex
|
|
state ConnectionState
|
|
handshook bool
|
|
limits HandshakeLimits
|
|
clockSkew int64
|
|
session string
|
|
stopReconnect bool
|
|
closed bool
|
|
lastStopCode string
|
|
lastStopErr error
|
|
|
|
ridSeq atomic.Uint64
|
|
pending map[string]*pendingReq
|
|
sendQ []*sendItem
|
|
inflight int
|
|
|
|
store *lruCache
|
|
|
|
onSession func(token string)
|
|
onMessage func(msg Message) error
|
|
onReceipt func(r Receipt)
|
|
onRevoked func(e RevokedEvent)
|
|
onPresence func(e PresenceEvent)
|
|
onGroupEvent func(e GroupEvent)
|
|
onConnection func(ev ConnectionEvent)
|
|
|
|
helloSentAt time.Time
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
|
|
incoming chan queuedFrame
|
|
cbQ chan func()
|
|
connLost chan struct{}
|
|
|
|
watchIDs []string
|
|
watchAll bool
|
|
watchSet bool
|
|
}
|
|
|
|
// New 创建客户端(尚未连接)。
|
|
func New() *Client {
|
|
return &Client{
|
|
pending: make(map[string]*pendingReq),
|
|
store: newLRU(10000),
|
|
state: StateOffline,
|
|
backoff: newReconnectBackoff(),
|
|
}
|
|
}
|
|
|
|
// OnSession 会话令牌回调。
|
|
func (c *Client) OnSession(h func(token string)) { c.onSession = h }
|
|
|
|
// OnMessage 消息回调。自动模式下返回 error 则不发 ack 并删去重记录。
|
|
func (c *Client) OnMessage(h func(msg Message) error) { c.onMessage = h }
|
|
|
|
// OnReceipt 回执回调。
|
|
func (c *Client) OnReceipt(h func(r Receipt)) { c.onReceipt = h }
|
|
|
|
// OnRevoked 撤回/作废回调。
|
|
func (c *Client) OnRevoked(h func(e RevokedEvent)) { c.onRevoked = h }
|
|
|
|
// OnPresence 上下线回调。
|
|
func (c *Client) OnPresence(h func(e PresenceEvent)) { c.onPresence = h }
|
|
|
|
// OnGroupEvent 群事件回调。
|
|
func (c *Client) OnGroupEvent(h func(e GroupEvent)) { c.onGroupEvent = h }
|
|
|
|
// OnConnection 连接状态回调。
|
|
func (c *Client) OnConnection(h func(ev ConnectionEvent)) { c.onConnection = h }
|
|
|
|
func newMessageID() string {
|
|
id, err := uuid.NewV7()
|
|
if err != nil {
|
|
return uuid.NewString()
|
|
}
|
|
return id.String()
|
|
}
|
|
|
|
func (c *Client) nextRID() string {
|
|
return fmt.Sprintf("%d", c.ridSeq.Add(1))
|
|
}
|
|
|
|
type respFrame struct {
|
|
OK bool `json:"ok"`
|
|
Data json.RawMessage `json:"data"`
|
|
Error *struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"error"`
|
|
}
|
|
|
|
func (c *Client) dispatch(fn func()) {
|
|
if fn == nil {
|
|
return
|
|
}
|
|
c.mu.Lock()
|
|
c.dispatchLocked(fn)
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
func (c *Client) dispatchLocked(fn func()) {
|
|
if fn == nil {
|
|
return
|
|
}
|
|
ch := c.cbQ
|
|
ctx := c.ctx
|
|
if ch == nil {
|
|
go fn()
|
|
return
|
|
}
|
|
select {
|
|
case ch <- fn:
|
|
default:
|
|
go func() {
|
|
if ctx == nil {
|
|
ch <- fn
|
|
return
|
|
}
|
|
select {
|
|
case ch <- fn:
|
|
case <-ctx.Done():
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func (c *Client) cbLoop(ctx context.Context) {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case fn := <-c.cbQ:
|
|
if fn != nil {
|
|
fn()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) stopErrLocked() error {
|
|
if c.lastStopErr != nil {
|
|
return c.lastStopErr
|
|
}
|
|
if c.lastStopCode != "" {
|
|
return apiErr(c.lastStopCode, "已停止重连")
|
|
}
|
|
return apiErr(CodeClosed, "已关闭")
|
|
}
|
|
|
|
func (c *Client) failPendingLocked(err error, sendsToo bool) {
|
|
for rid, p := range c.pending {
|
|
if p == nil {
|
|
continue
|
|
}
|
|
if p.isSend && !sendsToo {
|
|
continue
|
|
}
|
|
select {
|
|
case p.ch <- respFrame{OK: false, Error: &struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}{Code: errCode(err), Message: err.Error()}}:
|
|
default:
|
|
}
|
|
delete(c.pending, rid)
|
|
}
|
|
}
|
|
|
|
func errCode(err error) string {
|
|
if err == nil {
|
|
return CodeNotConnected
|
|
}
|
|
if ae, ok := err.(*APIError); ok {
|
|
return ae.Code
|
|
}
|
|
return CodeNotConnected
|
|
}
|
|
|
|
func (c *Client) closeConnLostLocked() {
|
|
if c.connLost != nil {
|
|
select {
|
|
case <-c.connLost:
|
|
default:
|
|
close(c.connLost)
|
|
}
|
|
c.connLost = nil
|
|
}
|
|
}
|
|
|
|
func (c *Client) regenerateSendLocked(it *sendItem) {
|
|
rid := c.nextRID()
|
|
it.frame["rid"] = rid
|
|
payload, err := marshalJSON(it.frame)
|
|
if err != nil {
|
|
return
|
|
}
|
|
it.payload = payload
|
|
}
|
|
|
|
func (c *Client) requeueInflightLocked() {
|
|
for _, it := range c.sendQ {
|
|
if !it.inflight {
|
|
continue
|
|
}
|
|
it.epoch++
|
|
it.inflight = false
|
|
rid, _ := it.frame["rid"].(string)
|
|
delete(c.pending, rid)
|
|
c.regenerateSendLocked(it)
|
|
}
|
|
c.inflight = 0
|
|
}
|
|
|
|
func (c *Client) onTransportOffline() {
|
|
c.mu.Lock()
|
|
c.handshook = false
|
|
c.closeConnLostLocked()
|
|
if c.stopReconnect || c.closed {
|
|
c.failPendingLocked(apiErr(CodeNotConnected, "未连接"), true)
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
c.requeueInflightLocked()
|
|
c.failPendingLocked(apiErr(CodeNotConnected, "未连接"), false)
|
|
c.setStateLocked(StateReconnecting, "")
|
|
c.mu.Unlock()
|
|
}
|