Add realtime gateway and simulator bridge

This commit is contained in:
2026-03-27 21:06:17 +08:00
parent 0703fd47a2
commit 2c0fd4c549
36 changed files with 6852 additions and 1 deletions

View File

@@ -0,0 +1,262 @@
package channel
import (
"crypto/rand"
"encoding/hex"
"errors"
"sort"
"strconv"
"sync"
"time"
"realtime-gateway/internal/model"
)
const (
DeliveryModeCacheLatest = "cache_latest"
DeliveryModeDropIfNoConsumer = "drop_if_no_consumer"
)
var (
ErrChannelNotFound = errors.New("channel not found")
ErrChannelExpired = errors.New("channel expired")
ErrChannelUnauthorized = errors.New("channel token invalid")
ErrInvalidRole = errors.New("invalid channel role")
)
type CreateRequest struct {
Label string
DeliveryMode string
TTLSeconds int
}
type Snapshot struct {
ID string `json:"id"`
Label string `json:"label,omitempty"`
DeliveryMode string `json:"deliveryMode"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
ActiveProducers int `json:"activeProducers"`
ActiveConsumers int `json:"activeConsumers"`
ActiveControllers int `json:"activeControllers"`
}
type CreateResponse struct {
Snapshot Snapshot `json:"snapshot"`
ProducerToken string `json:"producerToken"`
ConsumerToken string `json:"consumerToken"`
ControllerToken string `json:"controllerToken"`
}
type Manager struct {
mu sync.RWMutex
defaultTTL time.Duration
channels map[string]*channelState
}
type channelState struct {
id string
label string
deliveryMode string
createdAt time.Time
expiresAt time.Time
producerToken string
consumerToken string
controllerToken string
activeProducers int
activeConsumers int
activeControllers int
}
func NewManager(defaultTTL time.Duration) *Manager {
if defaultTTL <= 0 {
defaultTTL = 8 * time.Hour
}
return &Manager{
defaultTTL: defaultTTL,
channels: make(map[string]*channelState),
}
}
func (m *Manager) Create(request CreateRequest) (CreateResponse, error) {
m.mu.Lock()
defer m.mu.Unlock()
now := time.Now()
deliveryMode := normalizeDeliveryMode(request.DeliveryMode)
ttl := m.defaultTTL
if request.TTLSeconds > 0 {
ttl = time.Duration(request.TTLSeconds) * time.Second
}
state := &channelState{
id: "ch-" + randomHex(6),
label: request.Label,
deliveryMode: deliveryMode,
createdAt: now,
expiresAt: now.Add(ttl),
producerToken: randomHex(16),
consumerToken: randomHex(16),
controllerToken: randomHex(16),
}
m.channels[state.id] = state
return CreateResponse{
Snapshot: snapshotOf(state),
ProducerToken: state.producerToken,
ConsumerToken: state.consumerToken,
ControllerToken: state.controllerToken,
}, nil
}
func (m *Manager) Join(channelID string, token string, role model.Role) (Snapshot, error) {
m.mu.RLock()
defer m.mu.RUnlock()
state, ok := m.channels[channelID]
if !ok {
return Snapshot{}, ErrChannelNotFound
}
if state.expiresAt.Before(time.Now()) {
return Snapshot{}, ErrChannelExpired
}
if !authorizeToken(state, role, token) {
return Snapshot{}, ErrChannelUnauthorized
}
return snapshotOf(state), nil
}
func (m *Manager) Bind(channelID string, role model.Role) error {
m.mu.Lock()
defer m.mu.Unlock()
state, ok := m.channels[channelID]
if !ok {
return ErrChannelNotFound
}
if state.expiresAt.Before(time.Now()) {
return ErrChannelExpired
}
switch role {
case model.RoleProducer:
state.activeProducers++
case model.RoleConsumer:
state.activeConsumers++
case model.RoleController:
state.activeControllers++
default:
return ErrInvalidRole
}
return nil
}
func (m *Manager) Unbind(channelID string, role model.Role) {
m.mu.Lock()
defer m.mu.Unlock()
state, ok := m.channels[channelID]
if !ok {
return
}
switch role {
case model.RoleProducer:
if state.activeProducers > 0 {
state.activeProducers--
}
case model.RoleConsumer:
if state.activeConsumers > 0 {
state.activeConsumers--
}
case model.RoleController:
if state.activeControllers > 0 {
state.activeControllers--
}
}
}
func (m *Manager) DeliveryMode(channelID string) string {
m.mu.RLock()
defer m.mu.RUnlock()
state, ok := m.channels[channelID]
if !ok {
return DeliveryModeCacheLatest
}
return state.deliveryMode
}
func (m *Manager) HasConsumers(channelID string) bool {
m.mu.RLock()
defer m.mu.RUnlock()
state, ok := m.channels[channelID]
if !ok {
return false
}
return state.activeConsumers > 0
}
func (m *Manager) List() []Snapshot {
m.mu.RLock()
defer m.mu.RUnlock()
now := time.Now()
items := make([]Snapshot, 0, len(m.channels))
for _, state := range m.channels {
if state.expiresAt.Before(now) {
continue
}
items = append(items, snapshotOf(state))
}
sort.Slice(items, func(i int, j int) bool {
return items[i].CreatedAt.After(items[j].CreatedAt)
})
return items
}
func normalizeDeliveryMode(value string) string {
switch value {
case DeliveryModeDropIfNoConsumer:
return DeliveryModeDropIfNoConsumer
default:
return DeliveryModeCacheLatest
}
}
func authorizeToken(state *channelState, role model.Role, token string) bool {
switch role {
case model.RoleProducer:
return state.producerToken == token
case model.RoleConsumer:
return state.consumerToken == token
case model.RoleController:
return state.controllerToken == token
default:
return false
}
}
func snapshotOf(state *channelState) Snapshot {
return Snapshot{
ID: state.id,
Label: state.label,
DeliveryMode: state.deliveryMode,
CreatedAt: state.createdAt,
ExpiresAt: state.expiresAt,
ActiveProducers: state.activeProducers,
ActiveConsumers: state.activeConsumers,
ActiveControllers: state.activeControllers,
}
}
func randomHex(size int) string {
if size <= 0 {
size = 8
}
buf := make([]byte, size)
if _, err := rand.Read(buf); err != nil {
return strconv.FormatInt(time.Now().UnixNano(), 16)
}
return hex.EncodeToString(buf)
}