init push
This commit is contained in:
261
internal/realtime/helicopterfile/hub.go
Normal file
261
internal/realtime/helicopterfile/hub.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package helicopterfilerealtime
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultEventType = "helicopter_file_changed"
|
||||
DefaultSubscriberSize = 32
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
EventType string `json:"event_type"`
|
||||
HelicopterID string `json:"helicopter_id"`
|
||||
HelicopterFileID string `json:"helicopter_file_id"`
|
||||
FileID string `json:"file_id,omitempty"`
|
||||
Section string `json:"section,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
MimeType string `json:"mime_type,omitempty"`
|
||||
AttachmentPending bool `json:"attachment_pending"`
|
||||
Position int `json:"position,omitempty"`
|
||||
IsMandatory bool `json:"is_mandatory,omitempty"`
|
||||
UpdatedAt string `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
type Subscription struct {
|
||||
events <-chan Event
|
||||
done <-chan struct{}
|
||||
closeFn func()
|
||||
}
|
||||
|
||||
func (s *Subscription) Events() <-chan Event {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.events
|
||||
}
|
||||
|
||||
func (s *Subscription) Done() <-chan struct{} {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.done
|
||||
}
|
||||
|
||||
func (s *Subscription) Close() {
|
||||
if s == nil || s.closeFn == nil {
|
||||
return
|
||||
}
|
||||
s.closeFn()
|
||||
}
|
||||
|
||||
type subscriber struct {
|
||||
events chan Event
|
||||
done chan struct{}
|
||||
match func(Event) bool
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func (s *subscriber) close() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.closeOnce.Do(func() {
|
||||
close(s.done)
|
||||
})
|
||||
}
|
||||
|
||||
type Hub struct {
|
||||
mu sync.RWMutex
|
||||
subscribers map[string]map[uint64]*subscriber
|
||||
recentByUser map[string][]Event
|
||||
nextID uint64
|
||||
buffer int
|
||||
replayLimit int
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewHub(buffer int, logger *slog.Logger) *Hub {
|
||||
if buffer <= 0 {
|
||||
buffer = DefaultSubscriberSize
|
||||
}
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
return &Hub{
|
||||
subscribers: make(map[string]map[uint64]*subscriber),
|
||||
recentByUser: make(map[string][]Event),
|
||||
buffer: buffer,
|
||||
replayLimit: maxInt(buffer*4, DefaultSubscriberSize),
|
||||
logger: logger.With("component", "helicopter-file-hub"),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) Subscribe(userUUID string) (*Subscription, error) {
|
||||
return h.SubscribeWithFilter(userUUID, nil)
|
||||
}
|
||||
|
||||
func (h *Hub) SubscribeWithFilter(userUUID string, match func(Event) bool) (*Subscription, error) {
|
||||
if h == nil {
|
||||
return nil, errors.New("helicopter file hub is not configured")
|
||||
}
|
||||
userUUID = canonicalUserID(userUUID)
|
||||
if userUUID == "" {
|
||||
return nil, errors.New("user uuid is required")
|
||||
}
|
||||
|
||||
sub := &subscriber{
|
||||
events: make(chan Event, h.buffer),
|
||||
done: make(chan struct{}),
|
||||
match: match,
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
h.nextID++
|
||||
subID := h.nextID
|
||||
if h.subscribers[userUUID] == nil {
|
||||
h.subscribers[userUUID] = make(map[uint64]*subscriber)
|
||||
}
|
||||
h.subscribers[userUUID][subID] = sub
|
||||
backlog := append([]Event(nil), h.recentByUser[userUUID]...)
|
||||
h.mu.Unlock()
|
||||
|
||||
for i := range backlog {
|
||||
if sub.match != nil && !sub.match(backlog[i]) {
|
||||
continue
|
||||
}
|
||||
enqueueSubscriberEvent(sub, backlog[i])
|
||||
}
|
||||
|
||||
var closeOnce sync.Once
|
||||
closeFn := func() {
|
||||
closeOnce.Do(func() {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if byUser, ok := h.subscribers[userUUID]; ok {
|
||||
delete(byUser, subID)
|
||||
if len(byUser) == 0 {
|
||||
delete(h.subscribers, userUUID)
|
||||
}
|
||||
}
|
||||
sub.close()
|
||||
})
|
||||
}
|
||||
|
||||
return &Subscription{
|
||||
events: sub.events,
|
||||
done: sub.done,
|
||||
closeFn: closeFn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *Hub) PublishToUsers(userUUIDs []string, event Event) int {
|
||||
if h == nil {
|
||||
return 0
|
||||
}
|
||||
if strings.TrimSpace(event.EventType) == "" {
|
||||
event.EventType = DefaultEventType
|
||||
}
|
||||
if strings.TrimSpace(event.HelicopterID) == "" || strings.TrimSpace(event.HelicopterFileID) == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
uniqueUsers := make(map[string]struct{}, len(userUUIDs))
|
||||
for i := range userUUIDs {
|
||||
userID := canonicalUserID(userUUIDs[i])
|
||||
if userID == "" {
|
||||
continue
|
||||
}
|
||||
uniqueUsers[userID] = struct{}{}
|
||||
}
|
||||
if len(uniqueUsers) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
delivered := 0
|
||||
for userID := range uniqueUsers {
|
||||
h.mu.Lock()
|
||||
h.recentByUser[userID] = appendRecentEvents(h.recentByUser[userID], event, h.replayLimit)
|
||||
h.mu.Unlock()
|
||||
|
||||
h.mu.RLock()
|
||||
byUser := h.subscribers[userID]
|
||||
if len(byUser) == 0 {
|
||||
h.mu.RUnlock()
|
||||
continue
|
||||
}
|
||||
|
||||
for _, sub := range byUser {
|
||||
select {
|
||||
case <-sub.done:
|
||||
continue
|
||||
default:
|
||||
}
|
||||
if sub.match != nil && !sub.match(event) {
|
||||
continue
|
||||
}
|
||||
|
||||
if enqueueSubscriberEvent(sub, event) {
|
||||
delivered++
|
||||
}
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
}
|
||||
return delivered
|
||||
}
|
||||
|
||||
func canonicalUserID(raw string) string {
|
||||
return strings.ToLower(strings.TrimSpace(raw))
|
||||
}
|
||||
|
||||
func enqueueSubscriberEvent(sub *subscriber, event Event) bool {
|
||||
if sub == nil {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case <-sub.done:
|
||||
return false
|
||||
default:
|
||||
}
|
||||
|
||||
select {
|
||||
case sub.events <- event:
|
||||
return true
|
||||
default:
|
||||
select {
|
||||
case <-sub.events:
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case sub.events <- event:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appendRecentEvents(current []Event, event Event, limit int) []Event {
|
||||
if limit <= 0 {
|
||||
return current[:0]
|
||||
}
|
||||
current = append(current, event)
|
||||
if len(current) <= limit {
|
||||
return current
|
||||
}
|
||||
start := len(current) - limit
|
||||
copy(current, current[start:])
|
||||
return current[:limit]
|
||||
}
|
||||
|
||||
func maxInt(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user