Add realtime gateway and simulator bridge
This commit is contained in:
262
realtime-gateway/internal/channel/manager.go
Normal file
262
realtime-gateway/internal/channel/manager.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user